Skip to content

Commit 6db952e

Browse files
authored
GH-143613: Add colours and some more edges to executor visualization graph (GH-143809)
1 parent ce8f5f9 commit 6db952e

File tree

1 file changed

+130
-19
lines changed

1 file changed

+130
-19
lines changed

Python/optimizer.c

Lines changed: 130 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1917,6 +1917,24 @@ _Py_Executors_InvalidateCold(PyInterpreterState *interp)
19171917
_Py_Executors_InvalidateAll(interp, 0);
19181918
}
19191919

1920+
static int
1921+
escape_angles(const char *input, Py_ssize_t size, char *buffer) {
1922+
int written = 0;
1923+
for (Py_ssize_t i = 0; i < size; i++) {
1924+
char c = input[i];
1925+
if (c == '<' || c == '>') {
1926+
buffer[written++] = '&';
1927+
buffer[written++] = c == '>' ? 'g' : 'l';
1928+
buffer[written++] = 't';
1929+
buffer[written++] = ';';
1930+
}
1931+
else {
1932+
buffer[written++] = c;
1933+
}
1934+
}
1935+
return written;
1936+
}
1937+
19201938
static void
19211939
write_str(PyObject *str, FILE *out)
19221940
{
@@ -1928,7 +1946,17 @@ write_str(PyObject *str, FILE *out)
19281946
}
19291947
const char *encoded_str = PyBytes_AsString(encoded_obj);
19301948
Py_ssize_t encoded_size = PyBytes_Size(encoded_obj);
1931-
fwrite(encoded_str, 1, encoded_size, out);
1949+
char buffer[120];
1950+
bool truncated = false;
1951+
if (encoded_size > 24) {
1952+
encoded_size = 24;
1953+
truncated = true;
1954+
}
1955+
int size = escape_angles(encoded_str, encoded_size, buffer);
1956+
fwrite(buffer, 1, size, out);
1957+
if (truncated) {
1958+
fwrite("...", 1, 3, out);
1959+
}
19321960
Py_DECREF(encoded_obj);
19331961
}
19341962

@@ -1950,6 +1978,85 @@ find_line_number(PyCodeObject *code, _PyExecutorObject *executor)
19501978
return -1;
19511979
}
19521980

1981+
#define RED "#ff0000"
1982+
#define WHITE "#ffffff"
1983+
#define BLUE "#0000ff"
1984+
#define BLACK "#000000"
1985+
#define LOOP "#00c000"
1986+
1987+
const char *COLORS[10] = {
1988+
"9",
1989+
"8",
1990+
"7",
1991+
"6",
1992+
"5",
1993+
"4",
1994+
"3",
1995+
"2",
1996+
"1",
1997+
WHITE,
1998+
};
1999+
2000+
#ifdef Py_STATS
2001+
const char *
2002+
get_background_color(_PyUOpInstruction const *inst, uint64_t max_hotness)
2003+
{
2004+
uint64_t hotness = inst->execution_count;
2005+
int index = (hotness * 10)/max_hotness;
2006+
if (index > 9) {
2007+
index = 9;
2008+
}
2009+
if (index < 0) {
2010+
index = 0;
2011+
}
2012+
return COLORS[index];
2013+
}
2014+
2015+
const char *
2016+
get_foreground_color(_PyUOpInstruction const *inst, uint64_t max_hotness)
2017+
{
2018+
if(_PyUop_Uncached[inst->opcode] == _DEOPT) {
2019+
return RED;
2020+
}
2021+
uint64_t hotness = inst->execution_count;
2022+
int index = (hotness * 10)/max_hotness;
2023+
if (index > 3) {
2024+
return BLACK;
2025+
}
2026+
return WHITE;
2027+
}
2028+
#endif
2029+
2030+
static void
2031+
write_row_for_uop(_PyExecutorObject *executor, uint32_t i, FILE *out)
2032+
{
2033+
/* Write row for uop.
2034+
* The `port` is a marker so that outgoing edges can
2035+
* be placed correctly. If a row is marked `port=17`,
2036+
* then the outgoing edge is `{EXEC_NAME}:17 -> {TARGET}`
2037+
* https://graphviz.readthedocs.io/en/stable/manual.html#node-ports-compass
2038+
*/
2039+
_PyUOpInstruction const *inst = &executor->trace[i];
2040+
const char *opname = _PyOpcode_uop_name[inst->opcode];
2041+
#ifdef Py_STATS
2042+
const char *bg_color = get_background_color(inst, executor->trace[0].execution_count);
2043+
const char *color = get_foreground_color(inst, executor->trace[0].execution_count);
2044+
fprintf(out, " <tr><td port=\"i%d\" border=\"1\" color=\"%s\" bgcolor=\"%s\" ><font color=\"%s\"> %s &nbsp;--&nbsp; %" PRIu64 "</font></td></tr>\n",
2045+
i, color, bg_color, color, opname, inst->execution_count);
2046+
#else
2047+
const char *color = (_PyUop_Uncached[inst->opcode] == _DEOPT) ? RED : BLACK;
2048+
fprintf(out, " <tr><td port=\"i%d\" border=\"1\" color=\"%s\" >%s op0=%" PRIu64 "</td></tr>\n", i, color, opname, inst->operand0);
2049+
#endif
2050+
}
2051+
2052+
static bool
2053+
is_stop(_PyUOpInstruction const *inst)
2054+
{
2055+
uint16_t base_opcode = _PyUop_Uncached[inst->opcode];
2056+
return (base_opcode == _EXIT_TRACE || base_opcode == _DEOPT || base_opcode == _JUMP_TO_TOP);
2057+
}
2058+
2059+
19532060
/* Writes the node and outgoing edges for a single tracelet in graphviz format.
19542061
* Each tracelet is presented as a table of the uops it contains.
19552062
* If Py_STATS is enabled, execution counts are included.
@@ -1977,21 +2084,8 @@ executor_to_gv(_PyExecutorObject *executor, FILE *out)
19772084
fprintf(out, ": %d</td></tr>\n", line);
19782085
}
19792086
for (uint32_t i = 0; i < executor->code_size; i++) {
1980-
/* Write row for uop.
1981-
* The `port` is a marker so that outgoing edges can
1982-
* be placed correctly. If a row is marked `port=17`,
1983-
* then the outgoing edge is `{EXEC_NAME}:17 -> {TARGET}`
1984-
* https://graphviz.readthedocs.io/en/stable/manual.html#node-ports-compass
1985-
*/
1986-
_PyUOpInstruction const *inst = &executor->trace[i];
1987-
uint16_t base_opcode = _PyUop_Uncached[inst->opcode];
1988-
const char *opname = _PyOpcode_uop_name[base_opcode];
1989-
#ifdef Py_STATS
1990-
fprintf(out, " <tr><td port=\"i%d\" border=\"1\" >%s -- %" PRIu64 "</td></tr>\n", i, opname, inst->execution_count);
1991-
#else
1992-
fprintf(out, " <tr><td port=\"i%d\" border=\"1\" >%s op0=%" PRIu64 "</td></tr>\n", i, opname, inst->operand0);
1993-
#endif
1994-
if (base_opcode == _EXIT_TRACE || base_opcode == _JUMP_TO_TOP) {
2087+
write_row_for_uop(executor, i, out);
2088+
if (is_stop(&executor->trace[i])) {
19952089
break;
19962090
}
19972091
}
@@ -2006,6 +2100,10 @@ executor_to_gv(_PyExecutorObject *executor, FILE *out)
20062100
uint16_t base_opcode = _PyUop_Uncached[inst->opcode];
20072101
uint16_t flags = _PyUop_Flags[base_opcode];
20082102
_PyExitData *exit = NULL;
2103+
if (base_opcode == _JUMP_TO_TOP) {
2104+
fprintf(out, "executor_%p:i%d -> executor_%p:i%d [color = \"" LOOP "\"]\n", executor, i, executor, inst->jump_target);
2105+
break;
2106+
}
20092107
if (base_opcode == _EXIT_TRACE) {
20102108
exit = (_PyExitData *)inst->operand0;
20112109
}
@@ -2016,10 +2114,22 @@ executor_to_gv(_PyExecutorObject *executor, FILE *out)
20162114
assert(base_exit_opcode == _EXIT_TRACE || base_exit_opcode == _DYNAMIC_EXIT);
20172115
exit = (_PyExitData *)exit_inst->operand0;
20182116
}
2019-
if (exit != NULL && exit->executor != cold && exit->executor != cold_dynamic) {
2020-
fprintf(out, "executor_%p:i%d -> executor_%p:start\n", executor, i, exit->executor);
2117+
if (exit != NULL) {
2118+
if (exit->executor == cold || exit->executor == cold_dynamic) {
2119+
#ifdef Py_STATS
2120+
/* Only mark as have cold exit if it has actually exited */
2121+
uint64_t diff = inst->execution_count - executor->trace[i+1].execution_count;
2122+
if (diff) {
2123+
fprintf(out, "cold_%p%d [ label = \"%" PRIu64 "\" shape = ellipse color=\"" BLUE "\" ]\n", executor, i, diff);
2124+
fprintf(out, "executor_%p:i%d -> cold_%p%d\n", executor, i, executor, i);
2125+
}
2126+
#endif
2127+
}
2128+
else {
2129+
fprintf(out, "executor_%p:i%d -> executor_%p:start\n", executor, i, exit->executor);
2130+
}
20212131
}
2022-
if (base_opcode == _EXIT_TRACE || base_opcode == _JUMP_TO_TOP) {
2132+
if (is_stop(inst)) {
20232133
break;
20242134
}
20252135
}
@@ -2031,6 +2141,7 @@ _PyDumpExecutors(FILE *out)
20312141
{
20322142
fprintf(out, "digraph ideal {\n\n");
20332143
fprintf(out, " rankdir = \"LR\"\n\n");
2144+
fprintf(out, " node [colorscheme=greys9]\n");
20342145
PyInterpreterState *interp = PyInterpreterState_Get();
20352146
for (_PyExecutorObject *exec = interp->executor_list_head; exec != NULL;) {
20362147
executor_to_gv(exec, out);

0 commit comments

Comments
 (0)