Skip to content

Commit d40bd97

Browse files
committed
Add pyrona aware mermaid output
1 parent 349a598 commit d40bd97

File tree

4 files changed

+285
-3
lines changed

4 files changed

+285
-3
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ Tools/msi/obj
145145
Tools/ssl/amd64
146146
Tools/ssl/win32
147147
Tools/freeze/test/outdir
148+
/mermaid.md
148149

149150
# This is where my build ends up.
150151
debug/

Include/internal/pycore_regions.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ static inline void _Py_SET_REGION(PyObject *ob, Py_region_ptr_t region) {
4444
PyObject* _Py_MakeImmutable(PyObject* obj);
4545
#define Py_MakeImmutable(op) _Py_MakeImmutable(_PyObject_CAST(op))
4646

47+
PyObject* _Py_Mermaid(PyObject *args, PyObject *kwargs);
48+
4749
PyObject* _Py_InvariantSrcFailure(void);
4850
#define Py_InvariantSrcFailure() _Py_InvariantSrcFailure()
4951

Objects/regions.c

Lines changed: 265 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ static inline Py_region_ptr_with_tags_t Py_TAGGED_REGION(PyObject *ob) {
3737
#define IS_COWN_REGION(r) (REGION_PTR_CAST(r) == _Py_COWN)
3838
#define HAS_METADATA(r) (!IS_LOCAL_REGION(r) && !IS_IMMUTABLE_REGION(r) && !IS_COWN_REGION(r))
3939

40+
#define MERMAID_DRAW_LIMIT 100
41+
4042
typedef struct regionmetadata regionmetadata;
4143
typedef struct PyRegionObject PyRegionObject;
4244

@@ -558,6 +560,15 @@ static PyObject* stack_pop(stack* s){
558560
return object;
559561
}
560562

563+
// Returns a pointer to the top object without poping it.
564+
static PyObject* stack_peek(stack* s){
565+
if(s->head == NULL){
566+
return NULL;
567+
}
568+
569+
return s->head->object;
570+
}
571+
561572
static void stack_free(stack* s){
562573
while(s->head != NULL){
563574
PyObject* op = stack_pop(s);
@@ -571,12 +582,16 @@ static bool stack_empty(stack* s){
571582
return s->head == NULL;
572583
}
573584

574-
__attribute__((unused))
575-
static void stack_print(stack* s){
585+
static bool stack_contains(stack* s, PyObject* object){
576586
node* n = s->head;
577587
while(n != NULL){
588+
if (n->object == object) {
589+
return true;
590+
}
578591
n = n->next;
579592
}
593+
594+
return false;
580595
}
581596

582597
static bool is_c_wrapper(PyObject* obj){
@@ -746,7 +761,7 @@ static void invariant_reset_captured_list(void) {
746761
int _Py_CheckRegionInvariant(PyThreadState *tstate)
747762
{
748763
// Check if we should perform the region invariant check
749-
if(!invariant_do_region_check){
764+
if(!invariant_do_region_check || true){
750765
return 0;
751766
}
752767

@@ -1640,6 +1655,253 @@ static int try_close(PyRegionObject *root_bridge) {
16401655
return -1;
16411656
}
16421657

1658+
1659+
typedef struct drawmermaidvisitinfo {
1660+
// Nodes that have been seen before
1661+
stack* seen;
1662+
1663+
// The number max depth of subregions that should be drawn from
1664+
// this point on.
1665+
int reg_budget;
1666+
// The number max depth of immutable objects that should be drawn from
1667+
// this point on.
1668+
int imm_budget;
1669+
int obj_budget;
1670+
// The source object of the reference.
1671+
PyObject* src;
1672+
1673+
FILE *out;
1674+
} drawmermaidvisitinfo;
1675+
1676+
static int _draw_mermaid_visit(PyObject* target, void* info_void) {
1677+
drawmermaidvisitinfo *info = _Py_CAST(drawmermaidvisitinfo *, info_void);
1678+
1679+
// Self references don't look good in mermaid
1680+
if (target == info->src) {
1681+
return 0;
1682+
}
1683+
1684+
fprintf(info->out, "%p --> %p\n", info->src, target);
1685+
1686+
// Check if the target should be traversed
1687+
if (stack_contains(info->seen, target)) {
1688+
return 0;
1689+
}
1690+
1691+
// Mark the object as seen
1692+
if (stack_push(info->seen, target)) {
1693+
PyErr_NoMemory();
1694+
return -1;
1695+
}
1696+
1697+
info->obj_budget -= 1;
1698+
if (info->obj_budget <= 0) {
1699+
fprintf(info->out, "%p:::maxdepth\n", target);
1700+
return 0;
1701+
}
1702+
1703+
// c functions can't be traversed
1704+
if (is_c_wrapper(target) || PyFunction_Check(target)) {
1705+
fprintf(info->out, "%p:::maxdepth\n", target);
1706+
return 0;
1707+
}
1708+
1709+
// Handle immutable objects
1710+
if (Py_IsImmutable(target)) {
1711+
if (info->imm_budget == 0) {
1712+
fprintf(info->out, "%p:::maxdepth\n", target);
1713+
return 0;
1714+
}
1715+
1716+
info->imm_budget -= 1;
1717+
PyObject *old_src = info->src;
1718+
info->src = target;
1719+
int result = !visit_object(target, (visitproc)_draw_mermaid_visit, info);
1720+
info->src = old_src;
1721+
info->imm_budget += 1;
1722+
return result;
1723+
}
1724+
1725+
// Don't traverse cowns
1726+
if (Py_IsCown(target)) {
1727+
return 0;
1728+
}
1729+
1730+
int same_region = Py_REGION_DATA(target) == Py_REGION_DATA(info->src);
1731+
if (!same_region) {
1732+
if (info->reg_budget == 0) {
1733+
fprintf(info->out, "%p:::maxdepth\n", target);
1734+
return 0;
1735+
}
1736+
info->reg_budget -= 1;
1737+
}
1738+
1739+
PyObject *old_src = info->src;
1740+
info->src = target;
1741+
int result = !visit_object(target, (visitproc)_draw_mermaid_visit, info);
1742+
info->src = old_src;
1743+
1744+
if (!same_region) {
1745+
info->reg_budget += 1;
1746+
}
1747+
1748+
return result;
1749+
}
1750+
1751+
static PyObject *draw_mermaid(PyObject *obj, int reg_depth, int imm_depth, int draw_limit) {
1752+
// This is definitly not optimized for speed
1753+
PyObject *result = NULL;
1754+
stack *pending = NULL;
1755+
FILE *out = fopen("mermaid.md", "w");
1756+
fprintf(out,"Note that only reachable objects are draw!\n");
1757+
fprintf(out,"<div style='background: #fff'>\n\n");
1758+
fprintf(out,"```mermaid\n");
1759+
fprintf(
1760+
out,
1761+
"%%%%{init: {'theme': 'neutral', 'themeVariables': { 'fontSize': '16px' }}}%%%%\n"
1762+
);
1763+
fprintf(out,"graph TD\n");
1764+
1765+
drawmermaidvisitinfo info = {
1766+
.seen = stack_new(),
1767+
.reg_budget = reg_depth,
1768+
.imm_budget = imm_depth - 1,
1769+
.obj_budget = draw_limit,
1770+
.src = obj,
1771+
.out = out,
1772+
};
1773+
if (info.seen == NULL) {
1774+
PyErr_NoMemory();
1775+
goto cleanup;
1776+
}
1777+
1778+
// Mark the object as seen
1779+
if (stack_push(info.seen, obj)) {
1780+
PyErr_NoMemory();
1781+
goto cleanup;
1782+
}
1783+
1784+
// Traverse objects
1785+
if (!visit_object(obj, (visitproc)_draw_mermaid_visit, &info)) {
1786+
goto cleanup;
1787+
}
1788+
1789+
// Draw regions and add text to objects
1790+
pending = stack_new();
1791+
if (pending == NULL) {
1792+
PyErr_NoMemory();
1793+
goto cleanup;
1794+
}
1795+
while (!stack_empty(info.seen)) {
1796+
Py_region_ptr_t current_region = Py_REGION(stack_peek(info.seen));
1797+
1798+
if (HAS_METADATA(current_region)) {
1799+
const regionmetadata *md = REGION_DATA_CAST(current_region);
1800+
fprintf(out, "style %p fill:#fcfbdd\n", md);
1801+
if (md->name) {
1802+
fprintf(
1803+
out,
1804+
"subgraph %p['%s']\n",
1805+
md,
1806+
get_region_name(stack_peek(info.seen))
1807+
);
1808+
} else {
1809+
fprintf(out, "subgraph %p['%p']\n", md, md->bridge);
1810+
}
1811+
}
1812+
1813+
while (!stack_empty(info.seen)) {
1814+
PyObject *item = stack_pop(info.seen);
1815+
1816+
// Skip objects from other regions
1817+
if (Py_REGION(item) != current_region) {
1818+
if (stack_push(pending, item)) {
1819+
PyErr_NoMemory();
1820+
goto cleanup;
1821+
}
1822+
continue;
1823+
}
1824+
1825+
const char *info_text = "";
1826+
1827+
if (is_c_wrapper(item)) {
1828+
info_text = "<br>#91;C-Wrapper#93;";
1829+
} else if (PyFunction_Check(item)) {
1830+
info_text = "<br>#91;PyFunction#93;";
1831+
} else if (Py_IsCown(item)) {
1832+
info_text = "<br>#91;Cown#93;";
1833+
} else if (PyType_Check(item)) {
1834+
info_text = "<br>#91;Type#93;";
1835+
} else if (PyTuple_Check(item)) {
1836+
info_text = "<br>#91;Tuple#93;";
1837+
} else if (PyDict_Check(item)) {
1838+
info_text = "<br>#91;Dictionary#93;";
1839+
}
1840+
1841+
// Set text
1842+
if (Py_IsNone(item)) {
1843+
fprintf(out, " %p((None<br>%p<br>#40;immortal#41;))", item, item);
1844+
} else if (_Py_IsImmortal(item)) {
1845+
fprintf(out, " %p[%p<br>#40;immortal#41;%s]", item, item, info_text);
1846+
} else if (Py_is_bridge_object(item)) {
1847+
fprintf(out, " %p[\\%p<br>rc=%ld%s/]", item, item, Py_REFCNT(item), info_text);
1848+
} else {
1849+
fprintf(out, " %p[%p<br>rc=%ld%s]", item, item, Py_REFCNT(item), info_text);
1850+
}
1851+
1852+
// Add color
1853+
if (IS_LOCAL_REGION(current_region)) {
1854+
fprintf(out, ":::local");
1855+
} else if (IS_IMMUTABLE_REGION(current_region)) {
1856+
fprintf(out, ":::immutable");
1857+
}
1858+
fprintf(out, "\n");
1859+
}
1860+
1861+
if (HAS_METADATA(current_region)) {
1862+
fprintf(out, "end\n");
1863+
}
1864+
1865+
stack *tmp = info.seen;
1866+
info.seen = pending;
1867+
pending = tmp;
1868+
}
1869+
1870+
result = Py_None;
1871+
cleanup:
1872+
fprintf(out, "classDef local fill:#eefcdd\n");
1873+
fprintf(out, "classDef immutable fill:#94f7ff\n");
1874+
fprintf(out, "classDef maxdepth stroke-width:4px,stroke:#c00,stroke-dasharray: 10 5\n");
1875+
fprintf(out, "```\n");
1876+
fprintf(out, "</div>\n");
1877+
1878+
if (info.obj_budget <= 0) {
1879+
fprintf(out, "Stopped drawing after traversing %d objects\n", draw_limit);
1880+
}
1881+
fclose(out);
1882+
if (pending) {
1883+
stack_free(pending);
1884+
}
1885+
if (info.seen) {
1886+
stack_free(info.seen);
1887+
}
1888+
return result;
1889+
}
1890+
1891+
PyObject* _Py_Mermaid(PyObject *args, PyObject *kwargs) {
1892+
PyObject *obj = Py_None;
1893+
int reg_depth = 3; // The maximum region depth to draw
1894+
int imm_depth = 1; // The maximum depth of immutable objects to draw
1895+
int draw_limit = 50; // The maximum number of objects to traverse
1896+
static char *kwlist[] = {"object", "reg_depth", "imm_depth", "draw_limit", NULL};
1897+
1898+
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|iii", kwlist, &obj, &reg_depth, &imm_depth, &draw_limit)) {
1899+
return NULL;
1900+
}
1901+
1902+
return draw_mermaid(obj, reg_depth, imm_depth, draw_limit);
1903+
}
1904+
16431905
static void PyRegion_dealloc(PyRegionObject *self) {
16441906
// Name is immutable and not in our region.
16451907

Python/bltinmodule.c

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1891,6 +1891,22 @@ default keyword-only argument specifies an object to return if\n\
18911891
the provided iterable is empty.\n\
18921892
With two or more arguments, return the largest argument.");
18931893

1894+
/* AC: cannot convert yet, waiting for *args support */
1895+
static PyObject *
1896+
builtin_mermaid(PyObject *module, PyObject *args, PyObject *kwds)
1897+
{
1898+
return _Py_Mermaid(args, kwds);
1899+
}
1900+
1901+
PyDoc_STRVAR(mermaid_docs,
1902+
"mermaid(obj, *[, reg_depth=1, imm_depth=1, draw_limit=50]) -> value\n\
1903+
\n\
1904+
Generates a Mermaid diagram of all reachable objects. The output is\n\
1905+
written to a new `mermaid.md` file in the current working directory.\n\
1906+
Optional Arguments_\n\
1907+
* reg_depth: The depth of regions to show\n\
1908+
* imm_depth: The depth of immutable objects to show\n\
1909+
* draw_limit: The number of objects to traverse");
18941910

18951911
/*[clinic input]
18961912
oct as builtin_oct
@@ -3118,6 +3134,7 @@ static PyMethodDef builtin_methods[] = {
31183134
BUILTIN_MAKEIMMUTABLE_METHODDEF
31193135
BUILTIN_INVARIANTSRCFAILURE_METHODDEF
31203136
BUILTIN_INVARIANTTGTFAILURE_METHODDEF
3137+
{"mermaid", _PyCFunction_CAST(builtin_mermaid), METH_VARARGS | METH_KEYWORDS, mermaid_docs},
31213138
BUILTIN_ITER_METHODDEF
31223139
BUILTIN_AITER_METHODDEF
31233140
BUILTIN_LEN_METHODDEF

0 commit comments

Comments
 (0)