Skip to content

Commit 426a915

Browse files
gh-144690: Add C API for trace/profile callback registration
1 parent b67a64d commit 426a915

6 files changed

Lines changed: 193 additions & 0 deletions

File tree

Include/internal/pycore_instruments.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,19 @@ typedef struct _PyCoMonitoringData {
123123
} _PyCoMonitoringData;
124124

125125

126+
/* Callback API for notifications when sys.settrace/sys.setprofile are called. */
127+
typedef enum {
128+
PyUnstable_EVAL_TRACE_SET = 0,
129+
PyUnstable_EVAL_TRACE_CLEAR = 1,
130+
PyUnstable_EVAL_PROFILE_SET = 2,
131+
PyUnstable_EVAL_PROFILE_CLEAR = 3,
132+
} PyUnstable_EvalEvent;
133+
134+
typedef int (*PyUnstable_EvalCallback)(PyUnstable_EvalEvent event, void *data);
135+
136+
PyAPI_FUNC(int) PyUnstable_SetEvalCallback(PyUnstable_EvalCallback callback, void *data);
137+
PyAPI_FUNC(PyUnstable_EvalCallback) PyUnstable_GetEvalCallback(void **data);
138+
126139
#ifdef __cplusplus
127140
}
128141
#endif

Include/internal/pycore_interp_structs.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,11 @@ struct _is {
10061006
#endif
10071007
#endif
10081008

1009+
struct {
1010+
PyUnstable_EvalCallback callback;
1011+
void *data;
1012+
} eval_callback;
1013+
10091014
/* the initial PyInterpreterState.threads.head */
10101015
_PyThreadStateImpl _initial_thread;
10111016
// _initial_thread should be the last field of PyInterpreterState.

Lib/test/test_capi/test_misc.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2858,6 +2858,84 @@ def func():
28582858
self.do_test(func, names)
28592859

28602860

2861+
class TestEvalCallback(unittest.TestCase):
2862+
"""Test PyUnstable_SetEvalCallback / PyUnstable_GetEvalCallback API"""
2863+
2864+
# Event constants matching PyUnstable_EvalEvent enum values
2865+
EVAL_TRACE_SET = 0
2866+
EVAL_TRACE_CLEAR = 1
2867+
EVAL_PROFILE_SET = 2
2868+
EVAL_PROFILE_CLEAR = 3
2869+
2870+
def setUp(self):
2871+
self.events = []
2872+
_testinternalcapi.set_eval_callback_record(self.events)
2873+
2874+
def tearDown(self):
2875+
_testinternalcapi.clear_eval_callback()
2876+
sys.settrace(None)
2877+
sys.setprofile(None)
2878+
2879+
def test_settrace_fires_callback(self):
2880+
def dummy_trace(frame, event, arg):
2881+
return dummy_trace
2882+
sys.settrace(dummy_trace)
2883+
self.assertIn(self.EVAL_TRACE_SET, self.events)
2884+
2885+
def test_settrace_none_fires_clear(self):
2886+
def dummy_trace(frame, event, arg):
2887+
return dummy_trace
2888+
sys.settrace(dummy_trace)
2889+
self.events.clear()
2890+
sys.settrace(None)
2891+
self.assertIn(self.EVAL_TRACE_CLEAR, self.events)
2892+
2893+
def test_setprofile_fires_callback(self):
2894+
def dummy_profile(frame, event, arg):
2895+
pass
2896+
sys.setprofile(dummy_profile)
2897+
self.assertIn(self.EVAL_PROFILE_SET, self.events)
2898+
2899+
def test_setprofile_none_fires_clear(self):
2900+
def dummy_profile(frame, event, arg):
2901+
pass
2902+
sys.setprofile(dummy_profile)
2903+
self.events.clear()
2904+
sys.setprofile(None)
2905+
self.assertIn(self.EVAL_PROFILE_CLEAR, self.events)
2906+
2907+
def test_multiple_set_clear_cycles(self):
2908+
def dummy_trace(frame, event, arg):
2909+
return dummy_trace
2910+
def dummy_profile(frame, event, arg):
2911+
pass
2912+
2913+
sys.settrace(dummy_trace)
2914+
sys.settrace(None)
2915+
sys.setprofile(dummy_profile)
2916+
sys.setprofile(None)
2917+
2918+
self.assertEqual(self.events, [
2919+
self.EVAL_TRACE_SET,
2920+
self.EVAL_TRACE_CLEAR,
2921+
self.EVAL_PROFILE_SET,
2922+
self.EVAL_PROFILE_CLEAR,
2923+
])
2924+
2925+
def test_clear_callback_stops_events(self):
2926+
_testinternalcapi.clear_eval_callback()
2927+
events_after_clear = []
2928+
_testinternalcapi.set_eval_callback_record(events_after_clear)
2929+
_testinternalcapi.clear_eval_callback()
2930+
2931+
def dummy_trace(frame, event, arg):
2932+
return dummy_trace
2933+
sys.settrace(dummy_trace)
2934+
sys.settrace(None)
2935+
2936+
self.assertEqual(events_after_clear, [])
2937+
2938+
28612939
@unittest.skipUnless(support.Py_GIL_DISABLED, 'need Py_GIL_DISABLED')
28622940
class TestPyThreadId(unittest.TestCase):
28632941
def test_py_thread_id(self):
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Added :c:func:`PyUnstable_SetEvalCallback` and
2+
:c:func:`PyUnstable_GetEvalCallback` to receive notifications when
3+
:func:`sys.settrace` or :func:`sys.setprofile` are called. This allows JIT
4+
compilers and other tools using :pep:`523` frame evaluation hooks to
5+
efficiently detect tracing/profiling changes without polling.

Modules/_testinternalcapi.c

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
#include "pycore_pystate.h" // _PyThreadState_GET()
3939
#include "pycore_runtime_structs.h" // _PY_NSMALLPOSINTS
4040
#include "pycore_unicodeobject.h" // _PyUnicode_TransformDecimalAndSpaceToASCII()
41+
#include "pycore_instruments.h" // PyUnstable_SetEvalCallback
4142

4243
#include "clinic/_testinternalcapi.c.h"
4344

@@ -2836,9 +2837,50 @@ test_threadstate_set_stack_protection(PyObject *self, PyObject *Py_UNUSED(args))
28362837
}
28372838

28382839

2840+
// Helper for testing PyUnstable_SetEvalCallback / PyUnstable_GetEvalCallback
2841+
static int
2842+
test_eval_callback(PyUnstable_EvalEvent event, void *data)
2843+
{
2844+
if (data == NULL) {
2845+
return 0;
2846+
}
2847+
PyObject *event_int = PyLong_FromLong((long)event);
2848+
if (event_int == NULL) {
2849+
return -1;
2850+
}
2851+
int res = PyList_Append((PyObject *)data, event_int);
2852+
Py_DECREF(event_int);
2853+
return res;
2854+
}
2855+
2856+
static PyObject *
2857+
set_eval_callback_record(PyObject *self, PyObject *list)
2858+
{
2859+
if (!PyList_Check(list)) {
2860+
PyErr_SetString(PyExc_TypeError, "argument must be a list");
2861+
return NULL;
2862+
}
2863+
if (PyUnstable_SetEvalCallback(test_eval_callback, list) < 0) {
2864+
return NULL;
2865+
}
2866+
Py_RETURN_NONE;
2867+
}
2868+
2869+
static PyObject *
2870+
clear_eval_callback(PyObject *self, PyObject *Py_UNUSED(args))
2871+
{
2872+
if (PyUnstable_SetEvalCallback(NULL, NULL) < 0) {
2873+
return NULL;
2874+
}
2875+
Py_RETURN_NONE;
2876+
}
2877+
2878+
28392879
static PyMethodDef module_functions[] = {
28402880
{"get_configs", get_configs, METH_NOARGS},
28412881
{"get_eval_frame_stats", get_eval_frame_stats, METH_NOARGS, NULL},
2882+
{"set_eval_callback_record", set_eval_callback_record, METH_O, NULL},
2883+
{"clear_eval_callback", clear_eval_callback, METH_NOARGS, NULL},
28422884
{"get_recursion_depth", get_recursion_depth, METH_NOARGS},
28432885
{"get_c_recursion_remaining", get_c_recursion_remaining, METH_NOARGS},
28442886
{"get_stack_pointer", get_stack_pointer, METH_NOARGS},

Python/legacy_tracing.c

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "pycore_ceval.h" // export _PyEval_SetProfile()
88
#include "pycore_frame.h" // PyFrameObject members
99
#include "pycore_interpframe.h" // _PyFrame_GetCode()
10+
#include "pycore_instruments.h" // PyUnstable_SetEvalCallback
1011

1112
#include "opcode.h"
1213
#include <stddef.h>
@@ -521,6 +522,39 @@ set_monitoring_profile_events(PyInterpreterState *interp)
521522
return _PyMonitoring_SetEvents(PY_MONITORING_SYS_PROFILE_ID, events);
522523
}
523524

525+
int
526+
PyUnstable_SetEvalCallback(PyUnstable_EvalCallback callback, void *data)
527+
{
528+
PyInterpreterState *interp = _PyInterpreterState_GET();
529+
interp->eval_callback.callback = callback;
530+
interp->eval_callback.data = data;
531+
return 0;
532+
}
533+
534+
PyUnstable_EvalCallback
535+
PyUnstable_GetEvalCallback(void **data)
536+
{
537+
PyInterpreterState *interp = _PyInterpreterState_GET();
538+
if (data != NULL) {
539+
*data = interp->eval_callback.data;
540+
}
541+
return interp->eval_callback.callback;
542+
}
543+
544+
static inline void
545+
notify_eval_callback(PyInterpreterState *interp, PyUnstable_EvalEvent event)
546+
{
547+
if (interp->eval_callback.callback != NULL) {
548+
void *data = interp->eval_callback.data;
549+
if (interp->eval_callback.callback(event, data) < 0) {
550+
PyErr_FormatUnraisable(
551+
"Exception ignored in %s eval callback",
552+
(event == PyUnstable_EVAL_TRACE_SET || event == PyUnstable_EVAL_TRACE_CLEAR)
553+
? "trace" : "profile");
554+
}
555+
}
556+
}
557+
524558
int
525559
_PyEval_SetProfile(PyThreadState *tstate, Py_tracefunc func, PyObject *arg)
526560
{
@@ -546,6 +580,10 @@ _PyEval_SetProfile(PyThreadState *tstate, Py_tracefunc func, PyObject *arg)
546580
int ret = set_monitoring_profile_events(interp);
547581
_PyEval_StartTheWorld(interp);
548582
Py_XDECREF(old_profileobj); // needs to be decref'd outside of stop-the-world
583+
584+
PyUnstable_EvalEvent event = (func != NULL) ? PyUnstable_EVAL_PROFILE_SET : PyUnstable_EVAL_PROFILE_CLEAR;
585+
notify_eval_callback(interp, event);
586+
549587
return ret;
550588
}
551589

@@ -586,6 +624,10 @@ _PyEval_SetProfileAllThreads(PyInterpreterState *interp, Py_tracefunc func, PyOb
586624
int ret = set_monitoring_profile_events(interp);
587625
_PyEval_StartTheWorld(interp);
588626
Py_XDECREF(old_profileobjs); // needs to be decref'd outside of stop-the-world
627+
628+
PyUnstable_EvalEvent event = (func != NULL) ? PyUnstable_EVAL_PROFILE_SET : PyUnstable_EVAL_PROFILE_CLEAR;
629+
notify_eval_callback(interp, event);
630+
589631
return ret;
590632
}
591633

@@ -719,6 +761,10 @@ _PyEval_SetTrace(PyThreadState *tstate, Py_tracefunc func, PyObject *arg)
719761
done:
720762
_PyEval_StartTheWorld(interp);
721763
Py_XDECREF(old_traceobj); // needs to be decref'd outside stop-the-world
764+
765+
PyUnstable_EvalEvent event = (func != NULL) ? PyUnstable_EVAL_TRACE_SET : PyUnstable_EVAL_TRACE_CLEAR;
766+
notify_eval_callback(interp, event);
767+
722768
return err;
723769
}
724770

@@ -770,5 +816,9 @@ _PyEval_SetTraceAllThreads(PyInterpreterState *interp, Py_tracefunc func, PyObje
770816
int err = set_monitoring_trace_events(interp);
771817
_PyEval_StartTheWorld(interp);
772818
Py_XDECREF(old_trace_objs); // needs to be decref'd outside of stop-the-world
819+
820+
PyUnstable_EvalEvent event = (func != NULL) ? PyUnstable_EVAL_TRACE_SET : PyUnstable_EVAL_TRACE_CLEAR;
821+
notify_eval_callback(interp, event);
822+
773823
return err;
774824
}

0 commit comments

Comments
 (0)