| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
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
| 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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| Expand Up | @@ -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 ( | ||
| Expand Down Expand Up | @@ -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): | ||
| Expand All | @@ -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 | ||
|
|
||
|
|
||
| Expand All | @@ -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 [] | ||
| Expand All | @@ -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) | ||
| Expand Down Expand Up | @@ -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): | ||
| Expand Down Expand Up | @@ -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: | ||
| Expand Down Expand Up | @@ -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): | ||
|
Comment thread
Copy link
Copy Markdown
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. Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low QualityThe new test only asserts python_frames > 0 and jit_frames > 0, which a stub unwinder would pass. Two things that would help:
Sorry, something went wrong.
diegorusso reacted with thumbs up emoji
All reactions
Copy link
Copy Markdown
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. Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low QualityAddressed here: 01df239
Sorry, something went wrong.
All reactions
|
||
|
|
||
| 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() | ||
| 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 |
| Back | FazBrowse Home | New Git URL |
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.
Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality_PyJit_RegisterCode returns NULL for three different reasons (perf-only success, calloc failure, all backends failed) and the caller can't tell them apart. Could you rename registered -> any_registered and add a one-liner near the perf branch noting it's intentionally not counted? The deleted comment from jit_record_code about partial-failure being non-fatal would also be nice to restore.
Sorry, something went wrong.
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.
Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low QualityAddressed here: cf16da0
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.