Skip to content

Commit e62e1ca

Browse files
authored
GH-126833: Dumps graphviz representation of executor graph. (GH-126880)
1 parent 5fc6bb2 commit e62e1ca

9 files changed

+230
-2
lines changed

Include/internal/pycore_global_objects_fini_generated.h

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,7 @@ struct _Py_global_strings {
618618
STRUCT_FOR_ID(origin)
619619
STRUCT_FOR_ID(out_fd)
620620
STRUCT_FOR_ID(outgoing)
621+
STRUCT_FOR_ID(outpath)
621622
STRUCT_FOR_ID(overlapped)
622623
STRUCT_FOR_ID(owner)
623624
STRUCT_FOR_ID(pages)

Include/internal/pycore_optimizer.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ typedef struct {
6060
};
6161
uint64_t operand0; // A cache entry
6262
uint64_t operand1;
63+
#ifdef Py_STATS
64+
uint64_t execution_count;
65+
#endif
6366
} _PyUOpInstruction;
6467

6568
typedef struct {
@@ -285,6 +288,8 @@ static inline int is_terminator(const _PyUOpInstruction *uop)
285288
);
286289
}
287290

291+
PyAPI_FUNC(int) _PyDumpExecutors(FILE *out);
292+
288293
#ifdef __cplusplus
289294
}
290295
#endif

Include/internal/pycore_runtime_init_generated.h

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_unicodeobject_generated.h

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Python/ceval.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,6 +1095,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
10951095
UOP_PAIR_INC(uopcode, lastuop);
10961096
#ifdef Py_STATS
10971097
trace_uop_execution_counter++;
1098+
((_PyUOpInstruction *)next_uop)[-1].execution_count++;
10981099
#endif
10991100

11001101
switch (uopcode) {

Python/clinic/sysmodule.c.h

Lines changed: 57 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Python/optimizer.c

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
#include "Python.h"
2+
13
#ifdef _Py_TIER2
24

3-
#include "Python.h"
45
#include "opcode.h"
56
#include "pycore_interp.h"
67
#include "pycore_backoff.h"
@@ -474,6 +475,9 @@ add_to_trace(
474475
trace[trace_length].target = target;
475476
trace[trace_length].oparg = oparg;
476477
trace[trace_length].operand0 = operand;
478+
#ifdef Py_STATS
479+
trace[trace_length].execution_count = 0;
480+
#endif
477481
return trace_length + 1;
478482
}
479483

@@ -983,6 +987,9 @@ static void make_exit(_PyUOpInstruction *inst, int opcode, int target)
983987
inst->operand0 = 0;
984988
inst->format = UOP_FORMAT_TARGET;
985989
inst->target = target;
990+
#ifdef Py_STATS
991+
inst->execution_count = 0;
992+
#endif
986993
}
987994

988995
/* Convert implicit exits, errors and deopts
@@ -1709,4 +1716,131 @@ _Py_Executors_InvalidateCold(PyInterpreterState *interp)
17091716
_Py_Executors_InvalidateAll(interp, 0);
17101717
}
17111718

1719+
static void
1720+
write_str(PyObject *str, FILE *out)
1721+
{
1722+
// Encode the Unicode object to the specified encoding
1723+
PyObject *encoded_obj = PyUnicode_AsEncodedString(str, "utf8", "strict");
1724+
if (encoded_obj == NULL) {
1725+
PyErr_Clear();
1726+
return;
1727+
}
1728+
const char *encoded_str = PyBytes_AsString(encoded_obj);
1729+
Py_ssize_t encoded_size = PyBytes_Size(encoded_obj);
1730+
fwrite(encoded_str, 1, encoded_size, out);
1731+
Py_DECREF(encoded_obj);
1732+
}
1733+
1734+
static int
1735+
find_line_number(PyCodeObject *code, _PyExecutorObject *executor)
1736+
{
1737+
int code_len = (int)Py_SIZE(code);
1738+
for (int i = 0; i < code_len; i++) {
1739+
_Py_CODEUNIT *instr = &_PyCode_CODE(code)[i];
1740+
int opcode = instr->op.code;
1741+
if (opcode == ENTER_EXECUTOR) {
1742+
_PyExecutorObject *exec = code->co_executors->executors[instr->op.arg];
1743+
if (exec == executor) {
1744+
return PyCode_Addr2Line(code, i*2);
1745+
}
1746+
}
1747+
i += _PyOpcode_Caches[_Py_GetBaseCodeUnit(code, i).op.code];
1748+
}
1749+
return -1;
1750+
}
1751+
1752+
/* Writes the node and outgoing edges for a single tracelet in graphviz format.
1753+
* Each tracelet is presented as a table of the uops it contains.
1754+
* If Py_STATS is enabled, execution counts are included.
1755+
*
1756+
* https://graphviz.readthedocs.io/en/stable/manual.html
1757+
* https://graphviz.org/gallery/
1758+
*/
1759+
static void
1760+
executor_to_gv(_PyExecutorObject *executor, FILE *out)
1761+
{
1762+
PyCodeObject *code = executor->vm_data.code;
1763+
fprintf(out, "executor_%p [\n", executor);
1764+
fprintf(out, " shape = none\n");
1765+
1766+
/* Write the HTML table for the uops */
1767+
fprintf(out, " label = <<table border=\"0\" cellspacing=\"0\">\n");
1768+
fprintf(out, " <tr><td port=\"start\" border=\"1\" ><b>Executor</b></td></tr>\n");
1769+
if (code == NULL) {
1770+
fprintf(out, " <tr><td border=\"1\" >No code object</td></tr>\n");
1771+
}
1772+
else {
1773+
fprintf(out, " <tr><td border=\"1\" >");
1774+
write_str(code->co_qualname, out);
1775+
int line = find_line_number(code, executor);
1776+
fprintf(out, ": %d</td></tr>\n", line);
1777+
}
1778+
for (uint32_t i = 0; i < executor->code_size; i++) {
1779+
/* Write row for uop.
1780+
* The `port` is a marker so that outgoing edges can
1781+
* be placed correctly. If a row is marked `port=17`,
1782+
* then the outgoing edge is `{EXEC_NAME}:17 -> {TARGET}`
1783+
* https://graphviz.readthedocs.io/en/stable/manual.html#node-ports-compass
1784+
*/
1785+
_PyUOpInstruction const *inst = &executor->trace[i];
1786+
const char *opname = _PyOpcode_uop_name[inst->opcode];
1787+
#ifdef Py_STATS
1788+
fprintf(out, " <tr><td port=\"i%d\" border=\"1\" >%s -- %" PRIu64 "</td></tr>\n", i, opname, inst->execution_count);
1789+
#else
1790+
fprintf(out, " <tr><td port=\"i%d\" border=\"1\" >%s</td></tr>\n", i, opname);
1791+
#endif
1792+
if (inst->opcode == _EXIT_TRACE || inst->opcode == _JUMP_TO_TOP) {
1793+
break;
1794+
}
1795+
}
1796+
fprintf(out, " </table>>\n");
1797+
fprintf(out, "]\n\n");
1798+
1799+
/* Write all the outgoing edges */
1800+
for (uint32_t i = 0; i < executor->code_size; i++) {
1801+
_PyUOpInstruction const *inst = &executor->trace[i];
1802+
uint16_t flags = _PyUop_Flags[inst->opcode];
1803+
_PyExitData *exit = NULL;
1804+
if (inst->opcode == _EXIT_TRACE) {
1805+
exit = (_PyExitData *)inst->operand0;
1806+
}
1807+
else if (flags & HAS_EXIT_FLAG) {
1808+
assert(inst->format == UOP_FORMAT_JUMP);
1809+
_PyUOpInstruction const *exit_inst = &executor->trace[inst->jump_target];
1810+
assert(exit_inst->opcode == _EXIT_TRACE);
1811+
exit = (_PyExitData *)exit_inst->operand0;
1812+
}
1813+
if (exit != NULL && exit->executor != NULL) {
1814+
fprintf(out, "executor_%p:i%d -> executor_%p:start\n", executor, i, exit->executor);
1815+
}
1816+
if (inst->opcode == _EXIT_TRACE || inst->opcode == _JUMP_TO_TOP) {
1817+
break;
1818+
}
1819+
}
1820+
}
1821+
1822+
/* Write the graph of all the live tracelets in graphviz format. */
1823+
int
1824+
_PyDumpExecutors(FILE *out)
1825+
{
1826+
fprintf(out, "digraph ideal {\n\n");
1827+
fprintf(out, " rankdir = \"LR\"\n\n");
1828+
PyInterpreterState *interp = PyInterpreterState_Get();
1829+
for (_PyExecutorObject *exec = interp->executor_list_head; exec != NULL;) {
1830+
executor_to_gv(exec, out);
1831+
exec = exec->vm_data.links.next;
1832+
}
1833+
fprintf(out, "}\n\n");
1834+
return 0;
1835+
}
1836+
1837+
#else
1838+
1839+
int
1840+
_PyDumpExecutors(FILE *out)
1841+
{
1842+
PyErr_SetString(PyExc_NotImplementedError, "No JIT available");
1843+
return -1;
1844+
}
1845+
17121846
#endif /* _Py_TIER2 */

Python/sysmodule.c

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2344,6 +2344,30 @@ sys_is_stack_trampoline_active_impl(PyObject *module)
23442344
Py_RETURN_FALSE;
23452345
}
23462346

2347+
/*[clinic input]
2348+
sys._dump_tracelets
2349+
2350+
outpath: object
2351+
2352+
Dump the graph of tracelets in graphviz format
2353+
[clinic start generated code]*/
2354+
2355+
static PyObject *
2356+
sys__dump_tracelets_impl(PyObject *module, PyObject *outpath)
2357+
/*[clinic end generated code: output=a7fe265e2bc3b674 input=5bff6880cd28ffd1]*/
2358+
{
2359+
FILE *out = _Py_fopen_obj(outpath, "wb");
2360+
if (out == NULL) {
2361+
return NULL;
2362+
}
2363+
int err = _PyDumpExecutors(out);
2364+
fclose(out);
2365+
if (err) {
2366+
return NULL;
2367+
}
2368+
Py_RETURN_NONE;
2369+
}
2370+
23472371

23482372
/*[clinic input]
23492373
sys._getframemodulename
@@ -2603,6 +2627,7 @@ static PyMethodDef sys_methods[] = {
26032627
#endif
26042628
SYS__GET_CPU_COUNT_CONFIG_METHODDEF
26052629
SYS__IS_GIL_ENABLED_METHODDEF
2630+
SYS__DUMP_TRACELETS_METHODDEF
26062631
{NULL, NULL} // sentinel
26072632
};
26082633

0 commit comments

Comments
 (0)