https://github.com/python/cpython/commit/375f484f976a1ed84c145a6ce4e467cd5b57db75
commit: 375f484f976a1ed84c145a6ce4e467cd5b57db75
branch: main
author: Dino Viehland <[email protected]>
committer: DinoV <[email protected]>
date: 2025-08-07T14:54:12-07:00
summary:

gh-137291: Support perf profiler with an evaluation hook (#137292)

Support perf profiler with an evaluation hook

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2025-07-31-23-02-02.gh-issue-137291.kIxVZd.rst
M Include/internal/pycore_interp_structs.h
M Lib/test/test_perf_profiler.py
M Python/perf_trampoline.c

diff --git a/Include/internal/pycore_interp_structs.h 
b/Include/internal/pycore_interp_structs.h
index 758bf5447ee32a..7cb5bce546ac74 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/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.
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) {

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to