Skip to content

Commit 375f484

Browse files
authored
gh-137291: Support perf profiler with an evaluation hook (#137292)
Support perf profiler with an evaluation hook
1 parent e3ad900 commit 375f484

File tree

4 files changed

+54
-47
lines changed

4 files changed

+54
-47
lines changed

Include/internal/pycore_interp_structs.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ struct _ceval_runtime_state {
8888
struct trampoline_api_st trampoline_api;
8989
FILE *map_file;
9090
Py_ssize_t persist_after_fork;
91+
_PyFrameEvalFunction prev_eval_frame;
9192
#else
9293
int _not_used;
9394
#endif

Lib/test/test_perf_profiler.py

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -162,48 +162,55 @@ def baz():
162162

163163
@unittest.skipIf(support.check_bolt_optimized(), "fails on BOLT instrumented binaries")
164164
def test_sys_api(self):
165-
code = """if 1:
166-
import sys
167-
def foo():
168-
pass
169-
170-
def spam():
171-
pass
165+
for define_eval_hook in (False, True):
166+
code = """if 1:
167+
import sys
168+
def foo():
169+
pass
172170
173-
def bar():
174-
sys.deactivate_stack_trampoline()
175-
foo()
176-
sys.activate_stack_trampoline("perf")
177-
spam()
171+
def spam():
172+
pass
178173
179-
def baz():
180-
bar()
174+
def bar():
175+
sys.deactivate_stack_trampoline()
176+
foo()
177+
sys.activate_stack_trampoline("perf")
178+
spam()
181179
182-
sys.activate_stack_trampoline("perf")
183-
baz()
184-
"""
185-
with temp_dir() as script_dir:
186-
script = make_script(script_dir, "perftest", code)
187-
env = {**os.environ, "PYTHON_JIT": "0"}
188-
with subprocess.Popen(
189-
[sys.executable, script],
190-
text=True,
191-
stderr=subprocess.PIPE,
192-
stdout=subprocess.PIPE,
193-
env=env,
194-
) as process:
195-
stdout, stderr = process.communicate()
180+
def baz():
181+
bar()
196182
197-
self.assertEqual(stderr, "")
198-
self.assertEqual(stdout, "")
183+
sys.activate_stack_trampoline("perf")
184+
baz()
185+
"""
186+
if define_eval_hook:
187+
set_eval_hook = """if 1:
188+
import _testinternalcapi
189+
_testinternalcapi.set_eval_frame_record([])
190+
"""
191+
code = set_eval_hook + code
192+
with temp_dir() as script_dir:
193+
script = make_script(script_dir, "perftest", code)
194+
env = {**os.environ, "PYTHON_JIT": "0"}
195+
with subprocess.Popen(
196+
[sys.executable, script],
197+
text=True,
198+
stderr=subprocess.PIPE,
199+
stdout=subprocess.PIPE,
200+
env=env,
201+
) as process:
202+
stdout, stderr = process.communicate()
199203

200-
perf_file = pathlib.Path(f"/tmp/perf-{process.pid}.map")
201-
self.assertTrue(perf_file.exists())
202-
perf_file_contents = perf_file.read_text()
203-
self.assertNotIn(f"py::foo:{script}", perf_file_contents)
204-
self.assertIn(f"py::spam:{script}", perf_file_contents)
205-
self.assertIn(f"py::bar:{script}", perf_file_contents)
206-
self.assertIn(f"py::baz:{script}", perf_file_contents)
204+
self.assertEqual(stderr, "")
205+
self.assertEqual(stdout, "")
206+
207+
perf_file = pathlib.Path(f"/tmp/perf-{process.pid}.map")
208+
self.assertTrue(perf_file.exists())
209+
perf_file_contents = perf_file.read_text()
210+
self.assertNotIn(f"py::foo:{script}", perf_file_contents)
211+
self.assertIn(f"py::spam:{script}", perf_file_contents)
212+
self.assertIn(f"py::bar:{script}", perf_file_contents)
213+
self.assertIn(f"py::baz:{script}", perf_file_contents)
207214

208215
def test_sys_api_with_existing_trampoline(self):
209216
code = """if 1:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The perf profiler can now be used if a previous frame evaluation API has been provided.

Python/perf_trampoline.c

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ enum perf_trampoline_type {
202202
#define perf_map_file _PyRuntime.ceval.perf.map_file
203203
#define persist_after_fork _PyRuntime.ceval.perf.persist_after_fork
204204
#define perf_trampoline_type _PyRuntime.ceval.perf.perf_trampoline_type
205+
#define prev_eval_frame _PyRuntime.ceval.perf.prev_eval_frame
205206

206207
static void
207208
perf_map_write_entry(void *state, const void *code_addr,
@@ -407,9 +408,12 @@ py_trampoline_evaluator(PyThreadState *ts, _PyInterpreterFrame *frame,
407408
f = new_trampoline;
408409
}
409410
assert(f != NULL);
410-
return f(ts, frame, throw, _PyEval_EvalFrameDefault);
411+
return f(ts, frame, throw, prev_eval_frame != NULL ? prev_eval_frame : _PyEval_EvalFrameDefault);
411412
default_eval:
412413
// Something failed, fall back to the default evaluator.
414+
if (prev_eval_frame) {
415+
return prev_eval_frame(ts, frame, throw);
416+
}
413417
return _PyEval_EvalFrameDefault(ts, frame, throw);
414418
}
415419
#endif // PY_HAVE_PERF_TRAMPOLINE
@@ -481,18 +485,12 @@ _PyPerfTrampoline_Init(int activate)
481485
{
482486
#ifdef PY_HAVE_PERF_TRAMPOLINE
483487
PyThreadState *tstate = _PyThreadState_GET();
484-
if (tstate->interp->eval_frame &&
485-
tstate->interp->eval_frame != py_trampoline_evaluator) {
486-
PyErr_SetString(PyExc_RuntimeError,
487-
"Trampoline cannot be initialized as a custom eval "
488-
"frame is already present");
489-
return -1;
490-
}
491488
if (!activate) {
492-
_PyInterpreterState_SetEvalFrameFunc(tstate->interp, NULL);
489+
_PyInterpreterState_SetEvalFrameFunc(tstate->interp, prev_eval_frame);
493490
perf_status = PERF_STATUS_NO_INIT;
494491
}
495-
else {
492+
else if (tstate->interp->eval_frame != py_trampoline_evaluator) {
493+
prev_eval_frame = _PyInterpreterState_GetEvalFrameFunc(tstate->interp);
496494
_PyInterpreterState_SetEvalFrameFunc(tstate->interp, py_trampoline_evaluator);
497495
extra_code_index = _PyEval_RequestCodeExtraIndex(NULL);
498496
if (extra_code_index == -1) {

0 commit comments

Comments
 (0)