-
-
Notifications
You must be signed in to change notification settings - Fork 34.6k
GH-126910: Add GNU backtrace support for unwinding JIT frames #149104
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
Open
diegorusso
wants to merge
7
commits into
python:main
Choose a base branch
from
diegorusso:jit-backtrace-support
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
db967dc
GH-126910: Add GNU backtrace support for unwinding JIT frames
diegorusso 0979429
📜🤖 Added by blurb_it.
blurb-it[bot] e8de85d
Guard JIT backtrace unwind
pablogsal ef9ea52
Link probe HAVE_LIBGCC_EH_FRAME_REGISTRATION
diegorusso 01df239
Strengthen JIT unwind tests
diegorusso cf16da0
Clarify JIT publication registration semantics
diegorusso 5cd2e87
Add Pablo
diegorusso File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| #ifndef Py_INTERNAL_JIT_PUBLISH_H | ||
| #define Py_INTERNAL_JIT_PUBLISH_H | ||
|
|
||
| #ifndef Py_BUILD_CORE | ||
| # error "this header requires Py_BUILD_CORE define" | ||
| #endif | ||
|
|
||
| #include <stddef.h> | ||
|
|
||
| typedef struct _PyJitCodeRegistration _PyJitCodeRegistration; | ||
|
|
||
| #ifdef _Py_JIT | ||
|
|
||
| /* Publish JIT code to optional tooling backends. | ||
| * | ||
| * The return value is a backend-specific deregistration handle, not a | ||
| * success/failure indicator. NULL means there is nothing to unregister later: | ||
| * perf does not need a handle, and GDB/GNU backtrace registration failures | ||
| * are intentionally non-fatal because tooling support must not make JIT | ||
| * compilation fail. | ||
| */ | ||
| _PyJitCodeRegistration *_PyJit_RegisterCode(const void *code_addr, | ||
| size_t code_size, | ||
| const char *entry, | ||
| const char *filename); | ||
|
|
||
| void _PyJit_UnregisterCode(_PyJitCodeRegistration *registration); | ||
|
|
||
| #endif // _Py_JIT | ||
|
|
||
| #endif // Py_INTERNAL_JIT_PUBLISH_H | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,9 @@ | |
| raise unittest.SkipTest("test requires subprocess support") | ||
|
|
||
|
|
||
| STACK_DEPTH = 10 | ||
|
|
||
|
|
||
| def _frame_pointers_expected(machine): | ||
| cflags = " ".join( | ||
| value for value in ( | ||
|
|
@@ -70,7 +73,7 @@ def _frame_pointers_expected(machine): | |
| return None | ||
|
|
||
|
|
||
| def _build_stack_and_unwind(): | ||
| def _build_stack_and_unwind(unwinder): | ||
| import operator | ||
|
|
||
| def build_stack(n, unwinder, warming_up_caller=False): | ||
|
|
@@ -89,7 +92,7 @@ def build_stack(n, unwinder, warming_up_caller=False): | |
| result = operator.call(build_stack, n - 1, unwinder, warming_up) | ||
| return result | ||
|
|
||
| stack = build_stack(10, _testinternalcapi.manual_frame_pointer_unwind) | ||
| stack = build_stack(STACK_DEPTH, unwinder) | ||
| return stack | ||
|
|
||
|
|
||
|
|
@@ -112,8 +115,7 @@ def _classify_stack(stack, jit_enabled): | |
| return annotated, python_frames, jit_frames, other_frames | ||
|
|
||
|
|
||
| def _annotate_unwind(): | ||
| stack = _build_stack_and_unwind() | ||
| def _summarize_unwind(stack, unwinder_name): | ||
| jit_enabled = hasattr(sys, "_jit") and sys._jit.is_enabled() | ||
| jit_backend = _testinternalcapi.get_jit_backend() | ||
| ranges = _testinternalcapi.get_jit_code_ranges() if jit_enabled else [] | ||
|
|
@@ -126,19 +128,44 @@ def _annotate_unwind(): | |
| ) | ||
| for idx, addr, tag in annotated: | ||
| print(f"#{idx:02d} {addr:#x} -> {tag}") | ||
| return json.dumps({ | ||
| return { | ||
| "length": len(stack), | ||
| "python_frames": python_frames, | ||
| "jit_frames": jit_frames, | ||
| "other_frames": other_frames, | ||
| "jit_backend": jit_backend, | ||
| "unwinder": unwinder_name, | ||
| } | ||
|
|
||
|
|
||
| def _annotate_unwind(unwinder_name="manual_frame_pointer_unwind"): | ||
| unwinder = getattr(_testinternalcapi, unwinder_name) | ||
| stack = _build_stack_and_unwind(unwinder) | ||
| return json.dumps(_summarize_unwind(stack, unwinder_name)) | ||
|
|
||
|
|
||
| def _annotate_unwind_after_executor_free(unwinder_name="gnu_backtrace_unwind"): | ||
| # The first unwind runs at the bottom of _build_stack_and_unwind(), while | ||
| # the recursive helper may be executing in JIT code. After it returns, this | ||
| # helper is back in normal test code; clearing executor caches should remove | ||
| # the old JIT ranges, so the second unwind must not report stale JIT frames. | ||
| live = json.loads(_annotate_unwind(unwinder_name)) | ||
|
|
||
| sys._clear_internal_caches() | ||
| _testinternalcapi.clear_executor_deletion_list() | ||
|
|
||
| unwinder = getattr(_testinternalcapi, unwinder_name) | ||
| after_free = _summarize_unwind(unwinder(), unwinder_name) | ||
| return json.dumps({ | ||
| "live": live, | ||
| "after_free": after_free, | ||
| }) | ||
|
|
||
|
|
||
| def _manual_unwind_length(**env): | ||
| def _run_unwind_helper(helper_name, unwinder_name, **env): | ||
| code = ( | ||
| "from test.test_frame_pointer_unwind import _annotate_unwind; " | ||
| "print(_annotate_unwind());" | ||
| f"from test.test_frame_pointer_unwind import {helper_name}; " | ||
| f"print({helper_name}({unwinder_name!r}));" | ||
| ) | ||
| run_env = os.environ.copy() | ||
| run_env.update(env) | ||
|
|
@@ -166,6 +193,15 @@ def _manual_unwind_length(**env): | |
| ) from exc | ||
|
|
||
|
|
||
| def _unwind_result(unwinder_name, **env): | ||
| return _run_unwind_helper("_annotate_unwind", unwinder_name, **env) | ||
|
|
||
|
|
||
| def _unwind_after_executor_free_result(unwinder_name, **env): | ||
| return _run_unwind_helper( | ||
| "_annotate_unwind_after_executor_free", unwinder_name, **env) | ||
|
|
||
|
|
||
| @support.requires_gil_enabled("test requires the GIL enabled") | ||
| @unittest.skipIf(support.is_wasi, "test not supported on WASI") | ||
| class FramePointerUnwindTests(unittest.TestCase): | ||
|
|
@@ -197,14 +233,14 @@ def test_manual_unwind_respects_frame_pointers(self): | |
|
|
||
| for env, using_jit in envs: | ||
| with self.subTest(env=env): | ||
| result = _manual_unwind_length(**env) | ||
| result = _unwind_result("manual_frame_pointer_unwind", **env) | ||
| jit_frames = result["jit_frames"] | ||
| python_frames = result.get("python_frames", 0) | ||
| jit_backend = result.get("jit_backend") | ||
| if self.frame_pointers_expected: | ||
| self.assertGreater( | ||
| self.assertGreaterEqual( | ||
| python_frames, | ||
| 0, | ||
| STACK_DEPTH, | ||
| f"expected to find Python frames on {self.machine} with env {env}", | ||
| ) | ||
| if using_jit: | ||
|
|
@@ -240,5 +276,84 @@ def test_manual_unwind_respects_frame_pointers(self): | |
| ) | ||
|
|
||
|
|
||
| @support.requires_gil_enabled("test requires the GIL enabled") | ||
| @unittest.skipIf(support.is_wasi, "test not supported on WASI") | ||
| @unittest.skipUnless(sys.platform == "linux", "GNU backtrace unwinding test requires Linux") | ||
| class GnuBacktraceUnwindTests(unittest.TestCase): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The new test only asserts
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed here: 01df239 |
||
|
|
||
| def setUp(self): | ||
| super().setUp() | ||
| try: | ||
| _testinternalcapi.gnu_backtrace_unwind() | ||
| except RuntimeError as exc: | ||
| if "not supported" in str(exc): | ||
| self.skipTest("gnu backtrace unwinding not supported on this platform") | ||
| raise | ||
|
|
||
| def test_gnu_backtrace_unwinds_through_jit_frames(self): | ||
| jit_available = hasattr(sys, "_jit") and sys._jit.is_available() | ||
| envs = [({"PYTHON_JIT": "0"}, False)] | ||
| if jit_available: | ||
| envs.append(({"PYTHON_JIT": "1"}, True)) | ||
|
|
||
| for env, using_jit in envs: | ||
| with self.subTest(env=env): | ||
| result = _unwind_result("gnu_backtrace_unwind", **env) | ||
| python_frames = result.get("python_frames", 0) | ||
| jit_frames = result.get("jit_frames", 0) | ||
| jit_backend = result.get("jit_backend") | ||
|
|
||
| self.assertGreaterEqual( | ||
| python_frames, | ||
| STACK_DEPTH, | ||
| f"expected to find Python frames in GNU backtrace with env {env}", | ||
| ) | ||
| if using_jit and jit_backend == "jit": | ||
| self.assertGreater( | ||
| jit_frames, | ||
| 0, | ||
| f"expected GNU backtrace to include JIT frames with env {env}", | ||
| ) | ||
| else: | ||
| self.assertEqual( | ||
| jit_frames, | ||
| 0, | ||
| f"unexpected JIT frames counted in GNU backtrace with env {env}", | ||
| ) | ||
|
|
||
| def test_gnu_backtrace_jit_frames_disappear_after_executor_free(self): | ||
| if not (hasattr(sys, "_jit") and sys._jit.is_available()): | ||
| self.skipTest("JIT is not available") | ||
|
|
||
| result = _unwind_after_executor_free_result( | ||
| "gnu_backtrace_unwind", PYTHON_JIT="1") | ||
| live = result["live"] | ||
| if live.get("jit_backend") != "jit": | ||
| self.skipTest("JIT backend is not active") | ||
|
|
||
| self.assertGreaterEqual( | ||
| live.get("python_frames", 0), | ||
| STACK_DEPTH, | ||
| "expected live GNU backtrace to include recursive Python frames", | ||
| ) | ||
| self.assertGreater( | ||
| live.get("jit_frames", 0), | ||
| 0, | ||
| "expected live GNU backtrace to include JIT frames", | ||
| ) | ||
|
|
||
| after_free = result["after_free"] | ||
| self.assertGreater( | ||
| after_free.get("python_frames", 0), | ||
| 0, | ||
| "expected GNU backtrace after executor free to include Python frames", | ||
| ) | ||
| self.assertEqual( | ||
| after_free.get("jit_frames", 0), | ||
| 0, | ||
| "unexpected JIT frames in GNU backtrace after executor free", | ||
| ) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletions
1
Misc/NEWS.d/next/Core_and_Builtins/2026-05-02-18-02-41.gh-issue-126910.nqDVrp.rst
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Add support for unwinding JIT frames using GNU backtrace. Patch by Diego Russo and Pablo Galindo |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
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.
_PyJit_RegisterCodereturns NULL for three different reasons (perf-only success, calloc failure, all backends failed) and the caller can't tell them apart. Could you renameregistered->any_registeredand add a one-liner near the perf branch noting it's intentionally not counted? The deleted comment fromjit_record_codeabout partial-failure being non-fatal would also be nice to restore.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.
Addressed here: cf16da0