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
71 changes: 71 additions & 0 deletions Doc/c-api/init.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2074,6 +2074,77 @@ Python-level trace functions in previous versions.

.. versionadded:: 3.12


Trace/Profile callback notifications
------------------------------------

.. versionadded:: 3.15

.. c:type:: PyUnstable_EvalEvent

An enumeration of events that can trigger a callback registered via
:c:func:`PyUnstable_SetEvalCallback`. The possible values are:

.. c:macro:: PyUnstable_EVAL_TRACE_SET

A trace function was set via :func:`sys.settrace`, :c:func:`PyEval_SetTrace`,
or :c:func:`PyEval_SetTraceAllThreads`.

.. c:macro:: PyUnstable_EVAL_TRACE_CLEAR

The trace function was cleared.

.. c:macro:: PyUnstable_EVAL_PROFILE_SET

A profile function was set via :func:`sys.setprofile`, :c:func:`PyEval_SetProfile`,
or :c:func:`PyEval_SetProfileAllThreads`.

.. c:macro:: PyUnstable_EVAL_PROFILE_CLEAR

The profile function was cleared.


.. c:type:: int (*PyUnstable_EvalCallback)(PyUnstable_EvalEvent event, void *data)

The type of the callback function registered using
:c:func:`PyUnstable_SetEvalCallback`. The *event* parameter indicates
which tracing or profiling event occurred. The *data* parameter is the opaque
pointer that was provided when :c:func:`PyUnstable_SetEvalCallback` was called.

If the callback returns a negative value, the exception is logged using
:c:func:`PyErr_FormatUnraisable`.


.. c:function:: int PyUnstable_SetEvalCallback(PyUnstable_EvalCallback callback, void *data)

Register a callback to be notified when :func:`sys.settrace` or
:func:`sys.setprofile` (or their C equivalents) are called.

This allows JIT compilers and other tools using :pep:`523` frame evaluation
hooks to efficiently detect tracing or profiling changes without polling.

The *callback* will be invoked with an event indicating whether tracing or
profiling was set or cleared. The *data* pointer is passed through to the
callback.

Only one callback can be registered at a time per interpreter. Setting a new
callback replaces any previously registered callback. To clear the callback,
pass ``NULL`` for *callback*.

Return ``0`` on success.


.. c:function:: PyUnstable_EvalCallback PyUnstable_GetEvalCallback(void **data)

Retrieve the currently registered eval callback and its associated data.

If *data* is not ``NULL``, the opaque pointer that was passed to
:c:func:`PyUnstable_SetEvalCallback` is stored in ``*data``.

Return the currently registered callback, or ``NULL`` if no callback
is registered.


Reference tracing
=================

Expand Down
14 changes: 14 additions & 0 deletions Include/cpython/monitoring.h
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,20 @@ PyMonitoring_FireStopIterationEvent(PyMonitoringState *state, PyObject *codelike

#undef _PYMONITORING_IF_ACTIVE


/* 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;

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 = []
_testcapi.set_eval_callback_record(self.events)

def tearDown(self):
_testcapi.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):
_testcapi.clear_eval_callback()
events_after_clear = []
_testcapi.set_eval_callback_record(events_after_clear)
_testcapi.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: 40 additions & 2 deletions Modules/_testcapi/monitoring.c
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
#include "parts.h"
#include "util.h"

#define Py_BUILD_CORE
#include "internal/pycore_instruments.h"
#include "cpython/monitoring.h"

typedef struct {
PyObject_HEAD
Expand Down Expand Up @@ -488,6 +487,43 @@ exit_scope(PyObject *self, PyObject *args)
Py_RETURN_NONE;
}

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 TestMethods[] = {
{"fire_event_py_start", fire_event_py_start, METH_VARARGS},
{"fire_event_py_resume", fire_event_py_resume, METH_VARARGS},
Expand All @@ -508,6 +544,8 @@ static PyMethodDef TestMethods[] = {
{"fire_event_stop_iteration", fire_event_stop_iteration, METH_VARARGS},
{"monitoring_enter_scope", enter_scope, METH_VARARGS},
{"monitoring_exit_scope", exit_scope, METH_VARARGS},
{"set_eval_callback_record", set_eval_callback_record, METH_O},
{"clear_eval_callback", clear_eval_callback, METH_NOARGS},
{NULL},
};

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