]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-137291: Support perf profiler with an evaluation hook (#137292)
authorDino Viehland <dinoviehland@meta.com>
Thu, 7 Aug 2025 21:54:12 +0000 (14:54 -0700)
committerGitHub <noreply@github.com>
Thu, 7 Aug 2025 21:54:12 +0000 (14:54 -0700)
Support perf profiler with an evaluation hook

Include/internal/pycore_interp_structs.h
Lib/test/test_perf_profiler.py
Misc/NEWS.d/next/Core_and_Builtins/2025-07-31-23-02-02.gh-issue-137291.kIxVZd.rst [new file with mode: 0644]
Python/perf_trampoline.c

index 758bf5447ee32a5c414bd2d35b811711b5c7ca7c..7cb5bce546ac745e33e56e63757a996c4e098047 100644 (file)
@@ -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
index 0207843cc0e8f7dd6ac29549b99fe71267d7388a..13424991639215054ca71381115c62e8373704df 100644 (file)
@@ -162,48 +162,55 @@ class TestPerfTrampoline(unittest.TestCase):
 
     @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/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 (file)
index 0000000..0995e3b
--- /dev/null
@@ -0,0 +1 @@
+The perf profiler can now be used if a previous frame evaluation API has been provided.
index a2da3c7d56df500703522c3a9326a94d2e19139b..987e8d2a11a659e8c45e452dae8086ee49734aba 100644 (file)
@@ -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) {