gh-137291: Support perf profiler with an evaluation hook (#137292)
Support perf profiler with an evaluation hook
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
The perf profiler can now be used if a previous frame evaluation API has been provided.
|
||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user