Skip to content

Commit fb85684

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

File tree

5 files changed

+284
-4
lines changed

5 files changed

+284
-4
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

Lib/test/test_inspect.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4521,7 +4521,7 @@ def test_builtins_have_signatures(self):
45214521
needs_semantic_update = {"round"}
45224522
no_signature |= needs_semantic_update
45234523
# These need *args support in Argument Clinic
4524-
needs_varargs = {"breakpoint", "min", "max", "print",
4524+
needs_varargs = {"breakpoint", "min", "max", "print", "mermaid",
45254525
"__build_class__"}
45264526
no_signature |= needs_varargs
45274527
# These simply weren't covered in the initial AC conversion

Objects/regions.c

Lines changed: 263 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,15 @@ static PyObject* stack_pop(stack* s){
558558
return object;
559559
}
560560

561+
// Returns a pointer to the top object without poping it.
562+
static PyObject* stack_peek(stack* s){
563+
if(s->head == NULL){
564+
return NULL;
565+
}
566+
567+
return s->head->object;
568+
}
569+
561570
static void stack_free(stack* s){
562571
while(s->head != NULL){
563572
PyObject* op = stack_pop(s);
@@ -571,12 +580,16 @@ static bool stack_empty(stack* s){
571580
return s->head == NULL;
572581
}
573582

574-
__attribute__((unused))
575-
static void stack_print(stack* s){
583+
static bool stack_contains(stack* s, PyObject* object){
576584
node* n = s->head;
577585
while(n != NULL){
586+
if (n->object == object) {
587+
return true;
588+
}
578589
n = n->next;
579590
}
591+
592+
return false;
580593
}
581594

582595
static bool is_c_wrapper(PyObject* obj){
@@ -746,7 +759,7 @@ static void invariant_reset_captured_list(void) {
746759
int _Py_CheckRegionInvariant(PyThreadState *tstate)
747760
{
748761
// Check if we should perform the region invariant check
749-
if(!invariant_do_region_check){
762+
if(!invariant_do_region_check || true){
750763
return 0;
751764
}
752765

@@ -1640,6 +1653,253 @@ static int try_close(PyRegionObject *root_bridge) {
16401653
return -1;
16411654
}
16421655

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

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)