-
-
Notifications
You must be signed in to change notification settings - Fork 34.4k
GH-126910: Add gdb support for unwinding JIT frames #146071
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
base: main
Are you sure you want to change the base?
Changes from 6 commits
5cd7ade
669dfb9
255c0b3
ac018d6
b0bab8c
a0dff1f
2b52588
e44170e
2e40f1d
d890add
965a543
17be0a2
f47d763
67ae6cb
a18cb96
bdc8d12
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| #ifndef Py_CORE_JIT_UNWIND_H | ||
| #define Py_CORE_JIT_UNWIND_H | ||
|
|
||
| #ifdef PY_HAVE_PERF_TRAMPOLINE | ||
|
|
||
| #include <stddef.h> | ||
|
|
||
| /* Return the size of the generated .eh_frame data for the given encoding. */ | ||
| size_t _PyJitUnwind_EhFrameSize(int absolute_addr); | ||
|
|
||
| /* | ||
| * Build DWARF .eh_frame data for JIT code; returns size written or 0 on error. | ||
| * absolute_addr selects the FDE address encoding: | ||
| * - 0: PC-relative offsets (perf jitdump synthesized DSO). | ||
| * - nonzero: absolute addresses (GDB JIT in-memory ELF). | ||
| */ | ||
| size_t _PyJitUnwind_BuildEhFrame(uint8_t *buffer, size_t buffer_size, | ||
| const void *code_addr, size_t code_size, | ||
| int absolute_addr); | ||
|
|
||
| void _PyJitUnwind_GdbRegisterCode(const void *code_addr, | ||
| unsigned int code_size, | ||
| const char *entry, | ||
| const char *filename); | ||
|
|
||
| #endif // PY_HAVE_PERF_TRAMPOLINE | ||
|
|
||
| #endif // Py_CORE_JIT_UNWIND_H |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| # Sample script for use by test_gdb.test_jit | ||
|
|
||
| import operator | ||
| import sys | ||
|
|
||
|
|
||
| def jit_bt_hot(depth, warming_up_caller=False): | ||
| if warming_up_caller: | ||
| return | ||
| if depth == 0: | ||
| id(42) | ||
| return | ||
|
|
||
| warming_up = True | ||
| while warming_up: | ||
| warming_up = sys._jit.is_enabled() & (not sys._jit.is_active()) | ||
| operator.call(jit_bt_hot, depth - 1, warming_up) | ||
|
|
||
|
|
||
| jit_bt_hot(10) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import os | ||
| import re | ||
| import sys | ||
| import unittest | ||
|
|
||
| from .util import setup_module, DebuggerTests | ||
|
|
||
|
|
||
| JIT_SAMPLE_SCRIPT = os.path.join(os.path.dirname(__file__), "gdb_jit_sample.py") | ||
|
|
||
|
|
||
| def setUpModule(): | ||
| setup_module() | ||
|
|
||
|
|
||
| @unittest.skipUnless( | ||
| hasattr(sys, "_jit") and sys._jit.is_available(), | ||
| "requires a JIT-enabled build", | ||
| ) | ||
| class JitBacktraceTests(DebuggerTests): | ||
| def test_bt_unwinds_through_jit_frames(self): | ||
| gdb_output = self.get_stack_trace( | ||
| script=JIT_SAMPLE_SCRIPT, | ||
| cmds_after_breakpoint=["bt"], | ||
| PYTHON_JIT="1", | ||
| ) | ||
| self.assertIn("py::jit_executor:<jit>", gdb_output) | ||
| self.assertIn("py::jit_shim:<jit>", gdb_output) | ||
| self.assertRegex( | ||
| gdb_output, | ||
| re.compile( | ||
| r"py::jit_executor:<jit>.*(_PyEval_EvalFrameDefault|_PyEval_Vector)", | ||
| re.DOTALL, | ||
| ), | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Add support for unwinding JIT frames using GDB. Patch by Diego Russo |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ | |
| #include "pycore_interpframe.h" | ||
| #include "pycore_interpolation.h" | ||
| #include "pycore_intrinsics.h" | ||
| #include "pycore_jit_unwind.h" | ||
| #include "pycore_lazyimportobject.h" | ||
| #include "pycore_list.h" | ||
| #include "pycore_long.h" | ||
|
|
@@ -60,6 +61,28 @@ jit_error(const char *message) | |
| PyErr_Format(PyExc_RuntimeWarning, "JIT %s (%d)", message, hint); | ||
| } | ||
|
|
||
| static void | ||
| jit_record_code(const void *code_addr, size_t code_size, | ||
| const char *entry, const char *filename) | ||
| { | ||
| #ifdef PY_HAVE_PERF_TRAMPOLINE | ||
| _PyPerf_Callbacks callbacks; | ||
| _PyPerfTrampoline_GetCallbacks(&callbacks); | ||
| if (callbacks.write_state == _Py_perfmap_jit_callbacks.write_state) { | ||
| _PyPerfJit_WriteNamedCode( | ||
| code_addr, (unsigned int)code_size, entry, filename); | ||
| return; | ||
| } | ||
| _PyJitUnwind_GdbRegisterCode( | ||
| code_addr, (unsigned int)code_size, entry, filename); | ||
|
||
| #else | ||
| (void)code_addr; | ||
| (void)code_size; | ||
| (void)entry; | ||
| (void)filename; | ||
| #endif | ||
| } | ||
|
|
||
| static size_t _Py_jit_shim_size = 0; | ||
|
|
||
| static int | ||
|
|
@@ -731,6 +754,10 @@ _PyJIT_Compile(_PyExecutorObject *executor, const _PyUOpInstruction trace[], siz | |
| } | ||
| executor->jit_code = memory; | ||
| executor->jit_size = total_size; | ||
| jit_record_code(memory, | ||
| code_size + state.trampolines.size, | ||
| "jit_executor", | ||
| "<jit>"); | ||
| return 0; | ||
| } | ||
|
|
||
|
|
@@ -781,6 +808,10 @@ compile_shim(void) | |
| return NULL; | ||
| } | ||
| _Py_jit_shim_size = total_size; | ||
| jit_record_code(memory, | ||
| code_size + state.trampolines.size, | ||
| "jit_shim", | ||
| "<jit>"); | ||
| return (_PyJitEntryFuncPtr)memory; | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this loop hang? When
warming_up=True, the call passeswarming_up_caller=Truewhich returns immediately at line 8, so the recursive body never actually executes. If the JIT does not activate via some other path, would this not spin forever until the timeout kills it? Should there be a max iteration count as a safety net?Also, line 16 uses bitwise
&instead ofand. Was that intentional? It meansis_active()is always evaluated even whenis_enabled()isFalse.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've simplified the test, the loop is not more controlled and deterministic.