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

Support perf profiler with an evaluation hook
This commit is contained in:
Dino Viehland
2025-08-07 14:54:12 -07:00
committed by GitHub
parent e3ad9003c5
commit 375f484f97
4 changed files with 54 additions and 47 deletions

View File

@@ -88,6 +88,7 @@ struct _ceval_runtime_state {
struct trampoline_api_st trampoline_api; struct trampoline_api_st trampoline_api;
FILE *map_file; FILE *map_file;
Py_ssize_t persist_after_fork; Py_ssize_t persist_after_fork;
_PyFrameEvalFunction prev_eval_frame;
#else #else
int _not_used; int _not_used;
#endif #endif

View File

@@ -162,48 +162,55 @@ class TestPerfTrampoline(unittest.TestCase):
@unittest.skipIf(support.check_bolt_optimized(), "fails on BOLT instrumented binaries") @unittest.skipIf(support.check_bolt_optimized(), "fails on BOLT instrumented binaries")
def test_sys_api(self): def test_sys_api(self):
code = """if 1: for define_eval_hook in (False, True):
import sys code = """if 1:
def foo(): import sys
pass def foo():
pass
def spam(): def spam():
pass pass
def bar():
sys.deactivate_stack_trampoline()
foo()
sys.activate_stack_trampoline("perf")
spam()
def baz():
bar()
def bar():
sys.deactivate_stack_trampoline()
foo()
sys.activate_stack_trampoline("perf") sys.activate_stack_trampoline("perf")
spam() 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()
def baz(): self.assertEqual(stderr, "")
bar() self.assertEqual(stdout, "")
sys.activate_stack_trampoline("perf") perf_file = pathlib.Path(f"/tmp/perf-{process.pid}.map")
baz() self.assertTrue(perf_file.exists())
""" perf_file_contents = perf_file.read_text()
with temp_dir() as script_dir: self.assertNotIn(f"py::foo:{script}", perf_file_contents)
script = make_script(script_dir, "perftest", code) self.assertIn(f"py::spam:{script}", perf_file_contents)
env = {**os.environ, "PYTHON_JIT": "0"} self.assertIn(f"py::bar:{script}", perf_file_contents)
with subprocess.Popen( self.assertIn(f"py::baz:{script}", perf_file_contents)
[sys.executable, script],
text=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
env=env,
) as process:
stdout, stderr = process.communicate()
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): def test_sys_api_with_existing_trampoline(self):
code = """if 1: code = """if 1:

View File

@@ -0,0 +1 @@
The perf profiler can now be used if a previous frame evaluation API has been provided.

View File

@@ -202,6 +202,7 @@ enum perf_trampoline_type {
#define perf_map_file _PyRuntime.ceval.perf.map_file #define perf_map_file _PyRuntime.ceval.perf.map_file
#define persist_after_fork _PyRuntime.ceval.perf.persist_after_fork #define persist_after_fork _PyRuntime.ceval.perf.persist_after_fork
#define perf_trampoline_type _PyRuntime.ceval.perf.perf_trampoline_type #define perf_trampoline_type _PyRuntime.ceval.perf.perf_trampoline_type
#define prev_eval_frame _PyRuntime.ceval.perf.prev_eval_frame
static void static void
perf_map_write_entry(void *state, const void *code_addr, perf_map_write_entry(void *state, const void *code_addr,
@@ -407,9 +408,12 @@ py_trampoline_evaluator(PyThreadState *ts, _PyInterpreterFrame *frame,
f = new_trampoline; f = new_trampoline;
} }
assert(f != NULL); 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: default_eval:
// Something failed, fall back to the default evaluator. // 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); return _PyEval_EvalFrameDefault(ts, frame, throw);
} }
#endif // PY_HAVE_PERF_TRAMPOLINE #endif // PY_HAVE_PERF_TRAMPOLINE
@@ -481,18 +485,12 @@ _PyPerfTrampoline_Init(int activate)
{ {
#ifdef PY_HAVE_PERF_TRAMPOLINE #ifdef PY_HAVE_PERF_TRAMPOLINE
PyThreadState *tstate = _PyThreadState_GET(); 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) { if (!activate) {
_PyInterpreterState_SetEvalFrameFunc(tstate->interp, NULL); _PyInterpreterState_SetEvalFrameFunc(tstate->interp, prev_eval_frame);
perf_status = PERF_STATUS_NO_INIT; 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); _PyInterpreterState_SetEvalFrameFunc(tstate->interp, py_trampoline_evaluator);
extra_code_index = _PyEval_RequestCodeExtraIndex(NULL); extra_code_index = _PyEval_RequestCodeExtraIndex(NULL);
if (extra_code_index == -1) { if (extra_code_index == -1) {