From b5c4e6d919a8dc793c4e7b129294d03513f00de5 Mon Sep 17 00:00:00 2001 From: Dino Viehland Date: Thu, 31 Jul 2025 15:57:38 -0700 Subject: [PATCH 1/2] Support perf profiler with an evaluation hook --- Include/internal/pycore_interp_structs.h | 1 + Lib/test/test_perf_profiler.py | 81 +++++++++++++----------- Python/perf_trampoline.c | 18 +++--- 3 files changed, 53 insertions(+), 47 deletions(-) diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index 542a75617b4d3c..3d240acd012687 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -88,6 +88,7 @@ struct _ceval_runtime_state { struct trampoline_api_st trampoline_api; FILE *map_file; Py_ssize_t persist_after_fork; + _PyFrameEvalFunction prev_eval_frame; #else int _not_used; #endif diff --git a/Lib/test/test_perf_profiler.py b/Lib/test/test_perf_profiler.py index 0207843cc0e8f7..13424991639215 100644 --- a/Lib/test/test_perf_profiler.py +++ b/Lib/test/test_perf_profiler.py @@ -162,48 +162,55 @@ def baz(): @unittest.skipIf(support.check_bolt_optimized(), "fails on BOLT instrumented binaries") def test_sys_api(self): - code = """if 1: - import sys - def foo(): - pass - - def spam(): - pass + for define_eval_hook in (False, True): + code = """if 1: + import sys + def foo(): + pass - def bar(): - sys.deactivate_stack_trampoline() - foo() - sys.activate_stack_trampoline("perf") - spam() + def spam(): + pass - def baz(): - bar() + def bar(): + sys.deactivate_stack_trampoline() + foo() + sys.activate_stack_trampoline("perf") + spam() - sys.activate_stack_trampoline("perf") - baz() - """ - with temp_dir() as script_dir: - script = make_script(script_dir, "perftest", code) - env = {**os.environ, "PYTHON_JIT": "0"} - with subprocess.Popen( - [sys.executable, script], - text=True, - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - env=env, - ) as process: - stdout, stderr = process.communicate() + def baz(): + bar() - self.assertEqual(stderr, "") - self.assertEqual(stdout, "") + sys.activate_stack_trampoline("perf") + baz() + """ + if define_eval_hook: + set_eval_hook = """if 1: + import _testinternalcapi + _testinternalcapi.set_eval_frame_record([]) +""" + code = set_eval_hook + code + with temp_dir() as script_dir: + script = make_script(script_dir, "perftest", code) + env = {**os.environ, "PYTHON_JIT": "0"} + with subprocess.Popen( + [sys.executable, script], + text=True, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + env=env, + ) as process: + stdout, stderr = process.communicate() - perf_file = pathlib.Path(f"/tmp/perf-{process.pid}.map") - self.assertTrue(perf_file.exists()) - perf_file_contents = perf_file.read_text() - self.assertNotIn(f"py::foo:{script}", perf_file_contents) - self.assertIn(f"py::spam:{script}", perf_file_contents) - self.assertIn(f"py::bar:{script}", perf_file_contents) - self.assertIn(f"py::baz:{script}", perf_file_contents) + self.assertEqual(stderr, "") + self.assertEqual(stdout, "") + + perf_file = pathlib.Path(f"/tmp/perf-{process.pid}.map") + self.assertTrue(perf_file.exists()) + perf_file_contents = perf_file.read_text() + self.assertNotIn(f"py::foo:{script}", perf_file_contents) + self.assertIn(f"py::spam:{script}", perf_file_contents) + self.assertIn(f"py::bar:{script}", perf_file_contents) + self.assertIn(f"py::baz:{script}", perf_file_contents) def test_sys_api_with_existing_trampoline(self): code = """if 1: diff --git a/Python/perf_trampoline.c b/Python/perf_trampoline.c index a2da3c7d56df50..987e8d2a11a659 100644 --- a/Python/perf_trampoline.c +++ b/Python/perf_trampoline.c @@ -202,6 +202,7 @@ enum perf_trampoline_type { #define perf_map_file _PyRuntime.ceval.perf.map_file #define persist_after_fork _PyRuntime.ceval.perf.persist_after_fork #define perf_trampoline_type _PyRuntime.ceval.perf.perf_trampoline_type +#define prev_eval_frame _PyRuntime.ceval.perf.prev_eval_frame static void perf_map_write_entry(void *state, const void *code_addr, @@ -407,9 +408,12 @@ py_trampoline_evaluator(PyThreadState *ts, _PyInterpreterFrame *frame, f = new_trampoline; } assert(f != NULL); - return f(ts, frame, throw, _PyEval_EvalFrameDefault); + return f(ts, frame, throw, prev_eval_frame != NULL ? prev_eval_frame : _PyEval_EvalFrameDefault); default_eval: // Something failed, fall back to the default evaluator. + if (prev_eval_frame) { + return prev_eval_frame(ts, frame, throw); + } return _PyEval_EvalFrameDefault(ts, frame, throw); } #endif // PY_HAVE_PERF_TRAMPOLINE @@ -481,18 +485,12 @@ _PyPerfTrampoline_Init(int activate) { #ifdef PY_HAVE_PERF_TRAMPOLINE PyThreadState *tstate = _PyThreadState_GET(); - if (tstate->interp->eval_frame && - tstate->interp->eval_frame != py_trampoline_evaluator) { - PyErr_SetString(PyExc_RuntimeError, - "Trampoline cannot be initialized as a custom eval " - "frame is already present"); - return -1; - } if (!activate) { - _PyInterpreterState_SetEvalFrameFunc(tstate->interp, NULL); + _PyInterpreterState_SetEvalFrameFunc(tstate->interp, prev_eval_frame); perf_status = PERF_STATUS_NO_INIT; } - else { + else if (tstate->interp->eval_frame != py_trampoline_evaluator) { + prev_eval_frame = _PyInterpreterState_GetEvalFrameFunc(tstate->interp); _PyInterpreterState_SetEvalFrameFunc(tstate->interp, py_trampoline_evaluator); extra_code_index = _PyEval_RequestCodeExtraIndex(NULL); if (extra_code_index == -1) { From a376dfb3a027237248b56f51ed5a2950942655b7 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 23:02:07 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2025-07-31-23-02-02.gh-issue-137291.kIxVZd.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-07-31-23-02-02.gh-issue-137291.kIxVZd.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-07-31-23-02-02.gh-issue-137291.kIxVZd.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-07-31-23-02-02.gh-issue-137291.kIxVZd.rst new file mode 100644 index 00000000000000..0995e3b4644e7c --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-07-31-23-02-02.gh-issue-137291.kIxVZd.rst @@ -0,0 +1 @@ +The perf profiler can now be used if a previous frame evaluation API has been provided.