Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GH-126833: Dumps graphviz representation of executor graph. #126880

Merged
merged 13 commits into from
Dec 13, 2024
1 change: 1 addition & 0 deletions Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(origin)
STRUCT_FOR_ID(out_fd)
STRUCT_FOR_ID(outgoing)
STRUCT_FOR_ID(outpath)
STRUCT_FOR_ID(overlapped)
STRUCT_FOR_ID(owner)
STRUCT_FOR_ID(pages)
Expand Down
5 changes: 5 additions & 0 deletions Include/internal/pycore_optimizer.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ typedef struct {
};
uint64_t operand0; // A cache entry
uint64_t operand1;
#ifdef Py_STATS
uint64_t execution_count;
#endif
} _PyUOpInstruction;

typedef struct {
Expand Down Expand Up @@ -285,6 +288,8 @@ static inline int is_terminator(const _PyUOpInstruction *uop)
);
}

PyAPI_FUNC(int) _PyDumpExecutors(FILE *out);

#ifdef __cplusplus
}
#endif
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_runtime_init_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Include/internal/pycore_unicodeobject_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
UOP_PAIR_INC(uopcode, lastuop);
#ifdef Py_STATS
trace_uop_execution_counter++;
((_PyUOpInstruction *)next_uop)[-1].execution_count++;
#endif

switch (uopcode) {
Expand Down
58 changes: 57 additions & 1 deletion Python/clinic/sysmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

136 changes: 135 additions & 1 deletion Python/optimizer.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "Python.h"

#ifdef _Py_TIER2

#include "Python.h"
Fidget-Spinner marked this conversation as resolved.
Show resolved Hide resolved
#include "opcode.h"
#include "pycore_interp.h"
#include "pycore_backoff.h"
Expand Down Expand Up @@ -474,6 +475,9 @@ add_to_trace(
trace[trace_length].target = target;
trace[trace_length].oparg = oparg;
trace[trace_length].operand0 = operand;
#ifdef Py_STATS
trace[trace_length].execution_count = 0;
#endif
return trace_length + 1;
}

Expand Down Expand Up @@ -983,6 +987,9 @@ static void make_exit(_PyUOpInstruction *inst, int opcode, int target)
inst->operand0 = 0;
inst->format = UOP_FORMAT_TARGET;
inst->target = target;
#ifdef Py_STATS
inst->execution_count = 0;
#endif
}

/* Convert implicit exits, errors and deopts
Expand Down Expand Up @@ -1709,4 +1716,131 @@ _Py_Executors_InvalidateCold(PyInterpreterState *interp)
_Py_Executors_InvalidateAll(interp, 0);
}

static void
write_str(PyObject *str, FILE *out)
{
// Encode the Unicode object to the specified encoding
PyObject *encoded_obj = PyUnicode_AsEncodedString(str, "utf8", "strict");
if (encoded_obj == NULL) {
PyErr_Clear();
return;
}
const char *encoded_str = PyBytes_AsString(encoded_obj);
Py_ssize_t encoded_size = PyBytes_Size(encoded_obj);
fwrite(encoded_str, 1, encoded_size, out);
Py_DECREF(encoded_obj);
}

static int
find_line_number(PyCodeObject *code, _PyExecutorObject *executor)
{
int code_len = (int)Py_SIZE(code);
for (int i = 0; i < code_len; i++) {
_Py_CODEUNIT *instr = &_PyCode_CODE(code)[i];
int opcode = instr->op.code;
if (opcode == ENTER_EXECUTOR) {
_PyExecutorObject *exec = code->co_executors->executors[instr->op.arg];
if (exec == executor) {
return PyCode_Addr2Line(code, i*2);
}
}
i += _PyOpcode_Caches[_Py_GetBaseCodeUnit(code, i).op.code];
}
return -1;
ericsnowcurrently marked this conversation as resolved.
Show resolved Hide resolved
}

/* Writes the node and outgoing edges for a single tracelet in graphviz format.
* Each tracelet is presented as a table of the uops it contains.
* If Py_STATS is enabled, execution counts are included.
*
* https://graphviz.readthedocs.io/en/stable/manual.html
* https://graphviz.org/gallery/
*/
static void
executor_to_gv(_PyExecutorObject *executor, FILE *out)
{
PyCodeObject *code = executor->vm_data.code;
fprintf(out, "executor_%p [\n", executor);
ericsnowcurrently marked this conversation as resolved.
Show resolved Hide resolved
fprintf(out, " shape = none\n");

/* Write the HTML table for the uops */
fprintf(out, " label = <<table border=\"0\" cellspacing=\"0\">\n");
ericsnowcurrently marked this conversation as resolved.
Show resolved Hide resolved
fprintf(out, " <tr><td port=\"start\" border=\"1\" ><b>Executor</b></td></tr>\n");
if (code == NULL) {
fprintf(out, " <tr><td border=\"1\" >No code object</td></tr>\n");
}
else {
fprintf(out, " <tr><td border=\"1\" >");
write_str(code->co_qualname, out);
int line = find_line_number(code, executor);
fprintf(out, ": %d</td></tr>\n", line);
Comment on lines +1775 to +1776
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be worth handling the lno-not-found case?

For example:

Suggested change
int line = find_line_number(code, executor);
fprintf(out, ": %d</td></tr>\n", line);
int line = find_line_number(code, executor);
if (line > 0) { /* lno is 1-based. */
fprintf(out, ": %d", line);
}
fprintf(out, "</td></tr>\n", line);

That said, what you have is probably fine. I doubt it would be confusing.

}
for (uint32_t i = 0; i < executor->code_size; i++) {
ericsnowcurrently marked this conversation as resolved.
Show resolved Hide resolved
/* Write row for uop.
* The `port` is a marker so that outgoing edges can
* be placed correctly. If a row is marked `port=17`,
* then the outgoing edge is `{EXEC_NAME}:17 -> {TARGET}`
* https://graphviz.readthedocs.io/en/stable/manual.html#node-ports-compass
*/
_PyUOpInstruction const *inst = &executor->trace[i];
const char *opname = _PyOpcode_uop_name[inst->opcode];
#ifdef Py_STATS
fprintf(out, " <tr><td port=\"i%d\" border=\"1\" >%s -- %" PRIu64 "</td></tr>\n", i, opname, inst->execution_count);
#else
fprintf(out, " <tr><td port=\"i%d\" border=\"1\" >%s</td></tr>\n", i, opname);
Comment on lines +1788 to +1790
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add oparg and operand0 information as well? I've found it incredibly useful to debug polymorphism.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not in this PR.
Happy to do so in another PR, or review yours 🙂

#endif
if (inst->opcode == _EXIT_TRACE || inst->opcode == _JUMP_TO_TOP) {
break;
}
}
fprintf(out, " </table>>\n");
fprintf(out, "]\n\n");

/* Write all the outgoing edges */
for (uint32_t i = 0; i < executor->code_size; i++) {
ericsnowcurrently marked this conversation as resolved.
Show resolved Hide resolved
_PyUOpInstruction const *inst = &executor->trace[i];
uint16_t flags = _PyUop_Flags[inst->opcode];
_PyExitData *exit = NULL;
if (inst->opcode == _EXIT_TRACE) {
exit = (_PyExitData *)inst->operand0;
}
else if (flags & HAS_EXIT_FLAG) {
assert(inst->format == UOP_FORMAT_JUMP);
_PyUOpInstruction const *exit_inst = &executor->trace[inst->jump_target];
assert(exit_inst->opcode == _EXIT_TRACE);
exit = (_PyExitData *)exit_inst->operand0;
}
if (exit != NULL && exit->executor != NULL) {
fprintf(out, "executor_%p:i%d -> executor_%p:start\n", executor, i, exit->executor);
}
if (inst->opcode == _EXIT_TRACE || inst->opcode == _JUMP_TO_TOP) {
break;
}
}
}

/* Write the graph of all the live tracelets in graphviz format. */
int
_PyDumpExecutors(FILE *out)
{
fprintf(out, "digraph ideal {\n\n");
fprintf(out, " rankdir = \"LR\"\n\n");
PyInterpreterState *interp = PyInterpreterState_Get();
for (_PyExecutorObject *exec = interp->executor_list_head; exec != NULL;) {
executor_to_gv(exec, out);
exec = exec->vm_data.links.next;
}
fprintf(out, "}\n\n");
return 0;
}

#else

int
_PyDumpExecutors(FILE *out)
{
PyErr_SetString(PyExc_NotImplementedError, "No JIT available");
return -1;
}

#endif /* _Py_TIER2 */
25 changes: 25 additions & 0 deletions Python/sysmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2344,6 +2344,30 @@ sys_is_stack_trampoline_active_impl(PyObject *module)
Py_RETURN_FALSE;
}

/*[clinic input]
sys._dump_tracelets

outpath: object

Dump the graph of tracelets in graphviz format
[clinic start generated code]*/

static PyObject *
sys__dump_tracelets_impl(PyObject *module, PyObject *outpath)
/*[clinic end generated code: output=a7fe265e2bc3b674 input=5bff6880cd28ffd1]*/
{
FILE *out = _Py_fopen_obj(outpath, "wb");
if (out == NULL) {
return NULL;
}
int err = _PyDumpExecutors(out);
fclose(out);
if (err) {
return NULL;
}
Py_RETURN_NONE;
}


/*[clinic input]
sys._getframemodulename
Expand Down Expand Up @@ -2603,6 +2627,7 @@ static PyMethodDef sys_methods[] = {
#endif
SYS__GET_CPU_COUNT_CONFIG_METHODDEF
SYS__IS_GIL_ENABLED_METHODDEF
SYS__DUMP_TRACELETS_METHODDEF
{NULL, NULL} // sentinel
};

Expand Down
Loading