Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Include/internal/pycore_instruments.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,19 @@ typedef struct _PyCoMonitoringData {
} _PyCoMonitoringData;


/* Callback API for notifications when sys.settrace/sys.setprofile are called. */
typedef enum {
PyUnstable_EVAL_TRACE_SET = 0,
PyUnstable_EVAL_TRACE_CLEAR = 1,
PyUnstable_EVAL_PROFILE_SET = 2,
PyUnstable_EVAL_PROFILE_CLEAR = 3,
} PyUnstable_EvalEvent;
Comment on lines +127 to +132
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Public C APIs should go in Include/cpython/..., not Include/internal.


typedef int (*PyUnstable_EvalCallback)(PyUnstable_EvalEvent event, void *data);

PyAPI_FUNC(int) PyUnstable_SetEvalCallback(PyUnstable_EvalCallback callback, void *data);
PyAPI_FUNC(PyUnstable_EvalCallback) PyUnstable_GetEvalCallback(void **data);

#ifdef __cplusplus
}
#endif
Expand Down
5 changes: 5 additions & 0 deletions Include/internal/pycore_interp_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,11 @@ struct _is {
#endif
#endif

struct {
PyUnstable_EvalCallback callback;
void *data;
} eval_callback;

/* the initial PyInterpreterState.threads.head */
_PyThreadStateImpl _initial_thread;
// _initial_thread should be the last field of PyInterpreterState.
Expand Down
78 changes: 78 additions & 0 deletions Lib/test/test_capi/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2858,6 +2858,84 @@ def func():
self.do_test(func, names)


class TestEvalCallback(unittest.TestCase):
"""Test PyUnstable_SetEvalCallback / PyUnstable_GetEvalCallback API"""

# Event constants matching PyUnstable_EvalEvent enum values
EVAL_TRACE_SET = 0
EVAL_TRACE_CLEAR = 1
EVAL_PROFILE_SET = 2
EVAL_PROFILE_CLEAR = 3

def setUp(self):
self.events = []
_testinternalcapi.set_eval_callback_record(self.events)

def tearDown(self):
_testinternalcapi.clear_eval_callback()
sys.settrace(None)
sys.setprofile(None)

def test_settrace_fires_callback(self):
def dummy_trace(frame, event, arg):
return dummy_trace
sys.settrace(dummy_trace)
self.assertIn(self.EVAL_TRACE_SET, self.events)

def test_settrace_none_fires_clear(self):
def dummy_trace(frame, event, arg):
return dummy_trace
sys.settrace(dummy_trace)
self.events.clear()
sys.settrace(None)
self.assertIn(self.EVAL_TRACE_CLEAR, self.events)

def test_setprofile_fires_callback(self):
def dummy_profile(frame, event, arg):
pass
sys.setprofile(dummy_profile)
self.assertIn(self.EVAL_PROFILE_SET, self.events)

def test_setprofile_none_fires_clear(self):
def dummy_profile(frame, event, arg):
pass
sys.setprofile(dummy_profile)
self.events.clear()
sys.setprofile(None)
self.assertIn(self.EVAL_PROFILE_CLEAR, self.events)

def test_multiple_set_clear_cycles(self):
def dummy_trace(frame, event, arg):
return dummy_trace
def dummy_profile(frame, event, arg):
pass

sys.settrace(dummy_trace)
sys.settrace(None)
sys.setprofile(dummy_profile)
sys.setprofile(None)

self.assertEqual(self.events, [
self.EVAL_TRACE_SET,
self.EVAL_TRACE_CLEAR,
self.EVAL_PROFILE_SET,
self.EVAL_PROFILE_CLEAR,
])

def test_clear_callback_stops_events(self):
_testinternalcapi.clear_eval_callback()
events_after_clear = []
_testinternalcapi.set_eval_callback_record(events_after_clear)
_testinternalcapi.clear_eval_callback()

def dummy_trace(frame, event, arg):
return dummy_trace
sys.settrace(dummy_trace)
sys.settrace(None)

self.assertEqual(events_after_clear, [])


@unittest.skipUnless(support.Py_GIL_DISABLED, 'need Py_GIL_DISABLED')
class TestPyThreadId(unittest.TestCase):
def test_py_thread_id(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Added :c:func:`PyUnstable_SetEvalCallback` and
:c:func:`PyUnstable_GetEvalCallback` to receive notifications when
:func:`sys.settrace` or :func:`sys.setprofile` are called. This allows JIT
compilers and other tools using :pep:`523` frame evaluation hooks to
efficiently detect tracing/profiling changes without polling.
42 changes: 42 additions & 0 deletions Modules/_testinternalcapi.c
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PyUnstable APIs are not internal; please move this to _testcapi.

Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
#include "pycore_pystate.h" // _PyThreadState_GET()
#include "pycore_runtime_structs.h" // _PY_NSMALLPOSINTS
#include "pycore_unicodeobject.h" // _PyUnicode_TransformDecimalAndSpaceToASCII()
#include "pycore_instruments.h" // PyUnstable_SetEvalCallback

#include "clinic/_testinternalcapi.c.h"

Expand Down Expand Up @@ -2836,9 +2837,50 @@ test_threadstate_set_stack_protection(PyObject *self, PyObject *Py_UNUSED(args))
}


// Helper for testing PyUnstable_SetEvalCallback / PyUnstable_GetEvalCallback
static int
test_eval_callback(PyUnstable_EvalEvent event, void *data)
{
if (data == NULL) {
return 0;
}
PyObject *event_int = PyLong_FromLong((long)event);
if (event_int == NULL) {
return -1;
}
int res = PyList_Append((PyObject *)data, event_int);
Py_DECREF(event_int);
return res;
}

static PyObject *
set_eval_callback_record(PyObject *self, PyObject *list)
{
if (!PyList_Check(list)) {
PyErr_SetString(PyExc_TypeError, "argument must be a list");
return NULL;
}
if (PyUnstable_SetEvalCallback(test_eval_callback, list) < 0) {
return NULL;
}
Py_RETURN_NONE;
}

static PyObject *
clear_eval_callback(PyObject *self, PyObject *Py_UNUSED(args))
{
if (PyUnstable_SetEvalCallback(NULL, NULL) < 0) {
return NULL;
}
Py_RETURN_NONE;
}


static PyMethodDef module_functions[] = {
{"get_configs", get_configs, METH_NOARGS},
{"get_eval_frame_stats", get_eval_frame_stats, METH_NOARGS, NULL},
{"set_eval_callback_record", set_eval_callback_record, METH_O, NULL},
{"clear_eval_callback", clear_eval_callback, METH_NOARGS, NULL},
{"get_recursion_depth", get_recursion_depth, METH_NOARGS},
{"get_c_recursion_remaining", get_c_recursion_remaining, METH_NOARGS},
{"get_stack_pointer", get_stack_pointer, METH_NOARGS},
Expand Down
50 changes: 50 additions & 0 deletions Python/legacy_tracing.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "pycore_ceval.h" // export _PyEval_SetProfile()
#include "pycore_frame.h" // PyFrameObject members
#include "pycore_interpframe.h" // _PyFrame_GetCode()
#include "pycore_instruments.h" // PyUnstable_SetEvalCallback

#include "opcode.h"
#include <stddef.h>
Expand Down Expand Up @@ -521,6 +522,39 @@ set_monitoring_profile_events(PyInterpreterState *interp)
return _PyMonitoring_SetEvents(PY_MONITORING_SYS_PROFILE_ID, events);
}

int
PyUnstable_SetEvalCallback(PyUnstable_EvalCallback callback, void *data)
{
PyInterpreterState *interp = _PyInterpreterState_GET();
interp->eval_callback.callback = callback;
interp->eval_callback.data = data;
return 0;
}

PyUnstable_EvalCallback
PyUnstable_GetEvalCallback(void **data)
{
PyInterpreterState *interp = _PyInterpreterState_GET();
if (data != NULL) {
*data = interp->eval_callback.data;
}
return interp->eval_callback.callback;
}

static inline void
notify_eval_callback(PyInterpreterState *interp, PyUnstable_EvalEvent event)
{
if (interp->eval_callback.callback != NULL) {
void *data = interp->eval_callback.data;
if (interp->eval_callback.callback(event, data) < 0) {
PyErr_FormatUnraisable(
"Exception ignored in %s eval callback",
(event == PyUnstable_EVAL_TRACE_SET || event == PyUnstable_EVAL_TRACE_CLEAR)
? "trace" : "profile");
}
}
}

int
_PyEval_SetProfile(PyThreadState *tstate, Py_tracefunc func, PyObject *arg)
{
Expand All @@ -546,6 +580,10 @@ _PyEval_SetProfile(PyThreadState *tstate, Py_tracefunc func, PyObject *arg)
int ret = set_monitoring_profile_events(interp);
_PyEval_StartTheWorld(interp);
Py_XDECREF(old_profileobj); // needs to be decref'd outside of stop-the-world

PyUnstable_EvalEvent event = (func != NULL) ? PyUnstable_EVAL_PROFILE_SET : PyUnstable_EVAL_PROFILE_CLEAR;
notify_eval_callback(interp, event);

return ret;
}

Expand Down Expand Up @@ -586,6 +624,10 @@ _PyEval_SetProfileAllThreads(PyInterpreterState *interp, Py_tracefunc func, PyOb
int ret = set_monitoring_profile_events(interp);
_PyEval_StartTheWorld(interp);
Py_XDECREF(old_profileobjs); // needs to be decref'd outside of stop-the-world

PyUnstable_EvalEvent event = (func != NULL) ? PyUnstable_EVAL_PROFILE_SET : PyUnstable_EVAL_PROFILE_CLEAR;
notify_eval_callback(interp, event);

return ret;
}

Expand Down Expand Up @@ -719,6 +761,10 @@ _PyEval_SetTrace(PyThreadState *tstate, Py_tracefunc func, PyObject *arg)
done:
_PyEval_StartTheWorld(interp);
Py_XDECREF(old_traceobj); // needs to be decref'd outside stop-the-world

PyUnstable_EvalEvent event = (func != NULL) ? PyUnstable_EVAL_TRACE_SET : PyUnstable_EVAL_TRACE_CLEAR;
notify_eval_callback(interp, event);

return err;
}

Expand Down Expand Up @@ -770,5 +816,9 @@ _PyEval_SetTraceAllThreads(PyInterpreterState *interp, Py_tracefunc func, PyObje
int err = set_monitoring_trace_events(interp);
_PyEval_StartTheWorld(interp);
Py_XDECREF(old_trace_objs); // needs to be decref'd outside of stop-the-world

PyUnstable_EvalEvent event = (func != NULL) ? PyUnstable_EVAL_TRACE_SET : PyUnstable_EVAL_TRACE_CLEAR;
notify_eval_callback(interp, event);

return err;
}
Loading