Skip to content

bpo-39947: Add PyThreadState_SetTrace() function #29121

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
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
86 changes: 69 additions & 17 deletions Doc/c-api/init.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1187,8 +1187,68 @@ All of the following functions must be called after :c:func:`Py_Initialize`.
Resume tracing and profiling in the Python thread state *tstate* suspended
by the :c:func:`PyThreadState_EnterTracing` function.

See also :c:func:`PyEval_SetTrace` and :c:func:`PyEval_SetProfile`
functions.
See also :c:func:`PyThreadState_SetTrace` and
:c:func:`PyThreadState_SetProfile` functions.

.. versionadded:: 3.11


.. c:function:: int PyThreadState_SetProfile(PyThreadState *tstate, Py_tracefunc func, PyObject *arg)

Set the profile function of *tstate*.

The *arg* parameter is passed to the function as its first parameter, and
may be any Python object, or ``NULL``.

If the profile function needs to maintain state, using a different value for
*arg* for each thread provides a convenient and thread-safe place to store
it.

The profile function is called for all monitored events except
:const:`PyTrace_LINE` :const:`PyTrace_OPCODE` and
:const:`PyTrace_EXCEPTION`.

Calling ``PyThreadState_SetProfile(tstate, NULL, NULL)`` removes the profile
function.

Return 0 on success. Raise an exception and return -1 on error.

The caller must hold the :term:`GIL`. *func* and *arg* can be *NULL*.

See also functions:

* :func:`sys.setprofile`
* :c:func:`PyEval_SetProfile`
* :c:func:`PyThreadState_EnterTracing`
* :c:func:`PyThreadState_SetTrace`

.. versionadded:: 3.11


.. c:function:: void PyThreadState_SetTrace(PyThreadState *tstate, Py_tracefunc func, PyObject *arg)

Set the tracing function of *tstate*.

This is similar to :c:func:`PyThreadState_SetProfile`, except the tracing function
does receive line-number events and per-opcode events, but does not receive
any event related to C function objects being called. Any trace function
registered using :c:func:`PyThreadState_SetTrace` will not receive
:const:`PyTrace_C_CALL`, :const:`PyTrace_C_EXCEPTION` or
:const:`PyTrace_C_RETURN` as a value for the *what* parameter.

Calling ``PyThreadState_SetTrace(tstate, NULL, NULL)`` removes the trace
function.

Return 0 on success. Raise an exception and return -1 on error.

The caller must hold the :term:`GIL`. *func* and *arg* can be *NULL*.

See also functions:

* :func:`sys.settrace`
* :c:func:`PyEval_SetTrace`
* :c:func:`PyThreadState_EnterTracing`
* :c:func:`PyThreadState_SetProfile`

.. versionadded:: 3.11

Expand Down Expand Up @@ -1545,7 +1605,7 @@ Python-level trace functions in previous versions.

.. c:type:: int (*Py_tracefunc)(PyObject *obj, PyFrameObject *frame, int what, PyObject *arg)

The type of the trace function registered using :c:func:`PyEval_SetProfile` and
The type of the trace function registered using :c:func:`PyThreadState_SetProfile` and
:c:func:`PyEval_SetTrace`. The first parameter is the object passed to the
registration function as *obj*, *frame* is the frame object to which the event
pertains, *what* is one of the constants :const:`PyTrace_CALL`,
Expand Down Expand Up @@ -1636,28 +1696,20 @@ Python-level trace functions in previous versions.

.. c:function:: void PyEval_SetProfile(Py_tracefunc func, PyObject *obj)

Set the profiler function to *func*. The *obj* parameter is passed to the
function as its first parameter, and may be any Python object, or ``NULL``. If
the profile function needs to maintain state, using a different value for *obj*
for each thread provides a convenient and thread-safe place to store it. The
profile function is called for all monitored events except :const:`PyTrace_LINE`
:const:`PyTrace_OPCODE` and :const:`PyTrace_EXCEPTION`.
Set the trace function of the current thread using the
:c:func:`PyThreadState_SetProfile` function.

See also the :func:`sys.setprofile` function.
On error, log the unraisable exception with :data:`sys.unraisablehook`.

The caller must hold the :term:`GIL`.


.. c:function:: void PyEval_SetTrace(Py_tracefunc func, PyObject *obj)

Set the tracing function to *func*. This is similar to
:c:func:`PyEval_SetProfile`, except the tracing function does receive line-number
events and per-opcode events, but does not receive any event related to C function
objects being called. Any trace function registered using :c:func:`PyEval_SetTrace`
will not receive :const:`PyTrace_C_CALL`, :const:`PyTrace_C_EXCEPTION` or
:const:`PyTrace_C_RETURN` as a value for the *what* parameter.
Set the trace function of the current thread using the
:c:func:`PyThreadState_SetTrace` function.

See also the :func:`sys.settrace` function.
On error, log the unraisable exception with :data:`sys.unraisablehook`.

The caller must hold the :term:`GIL`.

Expand Down
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.11.rst
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,11 @@ New Features
suspend and resume tracing and profiling.
(Contributed by Victor Stinner in :issue:`43760`.)

* Add new :c:func:`PyThreadState_SetProfile` and
:c:func:`PyThreadState_SetTrace` functions to set the profile and the trace
function of a Python thread state.
(Contributed by Victor Stinner in :issue:`39947`.)

Porting to Python 3.11
----------------------

Expand Down
2 changes: 0 additions & 2 deletions Include/cpython/ceval.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
PyAPI_FUNC(PyObject *) _PyEval_CallTracing(PyObject *func, PyObject *args);

PyAPI_FUNC(void) PyEval_SetProfile(Py_tracefunc, PyObject *);
PyAPI_DATA(int) _PyEval_SetProfile(PyThreadState *tstate, Py_tracefunc func, PyObject *arg);
PyAPI_FUNC(void) PyEval_SetTrace(Py_tracefunc, PyObject *);
PyAPI_FUNC(int) _PyEval_SetTrace(PyThreadState *tstate, Py_tracefunc func, PyObject *arg);
PyAPI_FUNC(int) _PyEval_GetCoroutineOriginTrackingDepth(void);
PyAPI_FUNC(int) _PyEval_SetAsyncGenFirstiter(PyObject *);
PyAPI_FUNC(PyObject *) _PyEval_GetAsyncGenFirstiter(void);
Expand Down
8 changes: 8 additions & 0 deletions Include/cpython/pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,14 @@ PyAPI_FUNC(void) PyThreadState_EnterTracing(PyThreadState *tstate);
// function is set, otherwise disable them.
PyAPI_FUNC(void) PyThreadState_LeaveTracing(PyThreadState *tstate);

PyAPI_FUNC(int) PyThreadState_SetProfile(PyThreadState *tstate,
Py_tracefunc func,
PyObject *arg);

PyAPI_FUNC(int) PyThreadState_SetTrace(PyThreadState *tstate,
Py_tracefunc func,
PyObject *arg);

/* PyGILState */

/* Helper/diagnostic function - return 1 if the current thread
Expand Down
10 changes: 10 additions & 0 deletions Include/internal/pycore_pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,16 @@ _PyThreadState_ResumeTracing(PyThreadState *tstate)
tstate->cframe->use_tracing = (use_tracing ? 255 : 0);
}

#ifndef NDEBUG
// Ensure that tstate is valid: sanity check for PyEval_AcquireThread() and
// PyEval_RestoreThread(). Detect if tstate memory was freed. It can happen
// when a thread continues to run after Python finalization, especially
// daemon threads.
//
// Usage: assert(_PyThreadState_CheckConsistency(tstate));
extern int _PyThreadState_CheckConsistency(PyThreadState *tstate);
#endif


/* Other */

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add new :c:func:`PyThreadState_SetProfile` and
:c:func:`PyThreadState_SetTrace` functions to set the profile and the trace

Choose a reason for hiding this comment

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

I would add "(replacing _PyEval_SetProfile)" and "(replacing _PyEval_SetTrace)" since those are likely to be the names people have if they are searching for changes, instead of just reading in advance.

Choose a reason for hiding this comment

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

Also mention renaming and move is_tstate_valid to _PyThreadState_CheckConsistency

Copy link
Member Author

Choose a reason for hiding this comment

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

is_tstate_valid() is a static function, it cannot be used outside ceval.c. We don't document changes which don't affected the public C API.

Copy link
Member Author

Choose a reason for hiding this comment

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

I would add "(replacing _PyEval_SetProfile)" and "(replacing _PyEval_SetTrace)" since those are likely to be the names people have if they are searching for changes, instead of just reading in advance.

Usually, we never mention private functions in the changelog.

function of a Python thread state. Patch by Victor Stinner.
6 changes: 3 additions & 3 deletions Modules/_lsprof.c
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,7 @@ profiler_enable(ProfilerObject *self, PyObject *args, PyObject *kwds)
}

PyThreadState *tstate = _PyThreadState_GET();
if (_PyEval_SetProfile(tstate, profiler_callback, (PyObject*)self) < 0) {
if (PyThreadState_SetProfile(tstate, profiler_callback, (PyObject*)self) < 0) {
return NULL;
}

Expand Down Expand Up @@ -708,7 +708,7 @@ static PyObject*
profiler_disable(ProfilerObject *self, PyObject* noarg)
{
PyThreadState *tstate = _PyThreadState_GET();
if (_PyEval_SetProfile(tstate, NULL, NULL) < 0) {
if (PyThreadState_SetProfile(tstate, NULL, NULL) < 0) {
return NULL;
}
self->flags &= ~POF_ENABLED;
Expand Down Expand Up @@ -745,7 +745,7 @@ profiler_dealloc(ProfilerObject *op)
{
if (op->flags & POF_ENABLED) {
PyThreadState *tstate = _PyThreadState_GET();
if (_PyEval_SetProfile(tstate, NULL, NULL) < 0) {
if (PyThreadState_SetProfile(tstate, NULL, NULL) < 0) {
PyErr_WriteUnraisable((PyObject *)op);
}
}
Expand Down
89 changes: 7 additions & 82 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -123,20 +123,6 @@ static long dxp[256];
#endif
#endif

#ifndef NDEBUG
/* Ensure that tstate is valid: sanity check for PyEval_AcquireThread() and
PyEval_RestoreThread(). Detect if tstate memory was freed. It can happen
when a thread continues to run after Python finalization, especially
daemon threads. */
static int
is_tstate_valid(PyThreadState *tstate)
{
assert(!_PyMem_IsPtrFreed(tstate));
assert(!_PyMem_IsPtrFreed(tstate->interp));
return 1;
}
#endif


/* This can set eval_breaker to 0 even though gil_drop_request became
1. We believe this is all right because the eval loop will release
Expand Down Expand Up @@ -403,7 +389,7 @@ PyEval_AcquireThread(PyThreadState *tstate)
void
PyEval_ReleaseThread(PyThreadState *tstate)
{
assert(is_tstate_valid(tstate));
assert(_PyThreadState_CheckConsistency(tstate));

_PyRuntimeState *runtime = tstate->interp->runtime;
PyThreadState *new_tstate = _PyThreadState_Swap(&runtime->gilstate, NULL);
Expand Down Expand Up @@ -623,7 +609,7 @@ Py_AddPendingCall(int (*func)(void *), void *arg)
static int
handle_signals(PyThreadState *tstate)
{
assert(is_tstate_valid(tstate));
assert(_PyThreadState_CheckConsistency(tstate));
if (!_Py_ThreadCanHandleSignals(tstate->interp)) {
return 0;
}
Expand Down Expand Up @@ -691,7 +677,7 @@ void
_Py_FinishPendingCalls(PyThreadState *tstate)
{
assert(PyGILState_Check());
assert(is_tstate_valid(tstate));
assert(_PyThreadState_CheckConsistency(tstate));

struct _pending_calls *pending = &tstate->interp->ceval.pending;

Expand All @@ -716,7 +702,7 @@ Py_MakePendingCalls(void)
assert(PyGILState_Check());

PyThreadState *tstate = _PyThreadState_GET();
assert(is_tstate_valid(tstate));
assert(_PyThreadState_CheckConsistency(tstate));

/* Python signal handler doesn't really queue a callback: it only signals
that a signal was received, see _PyEval_SignalReceived(). */
Expand Down Expand Up @@ -5841,7 +5827,7 @@ make_coro_frame(PyThreadState *tstate,
PyObject *const *args, Py_ssize_t argcount,
PyObject *kwnames)
{
assert(is_tstate_valid(tstate));
assert(_PyThreadState_CheckConsistency(tstate));
assert(con->fc_defaults == NULL || PyTuple_CheckExact(con->fc_defaults));
PyCodeObject *code = (PyCodeObject *)con->fc_code;
int size = code->co_nlocalsplus+code->co_stacksize + FRAME_SPECIALS_SIZE;
Expand Down Expand Up @@ -6381,84 +6367,23 @@ maybe_call_line_trace(Py_tracefunc func, PyObject *obj,
return result;
}

int
_PyEval_SetProfile(PyThreadState *tstate, Py_tracefunc func, PyObject *arg)
{
assert(is_tstate_valid(tstate));
/* The caller must hold the GIL */
assert(PyGILState_Check());

/* Call _PySys_Audit() in the context of the current thread state,
even if tstate is not the current thread state. */
PyThreadState *current_tstate = _PyThreadState_GET();
if (_PySys_Audit(current_tstate, "sys.setprofile", NULL) < 0) {
return -1;
}

PyObject *profileobj = tstate->c_profileobj;

tstate->c_profilefunc = NULL;
tstate->c_profileobj = NULL;
/* Must make sure that tracing is not ignored if 'profileobj' is freed */
_PyThreadState_ResumeTracing(tstate);
Py_XDECREF(profileobj);

Py_XINCREF(arg);
tstate->c_profileobj = arg;
tstate->c_profilefunc = func;

/* Flag that tracing or profiling is turned on */
_PyThreadState_ResumeTracing(tstate);
return 0;
}

void
PyEval_SetProfile(Py_tracefunc func, PyObject *arg)
{
PyThreadState *tstate = _PyThreadState_GET();
if (_PyEval_SetProfile(tstate, func, arg) < 0) {
if (PyThreadState_SetProfile(tstate, func, arg) < 0) {
/* Log _PySys_Audit() error */
_PyErr_WriteUnraisableMsg("in PyEval_SetProfile", NULL);
}
}

int
_PyEval_SetTrace(PyThreadState *tstate, Py_tracefunc func, PyObject *arg)
{
assert(is_tstate_valid(tstate));
/* The caller must hold the GIL */
assert(PyGILState_Check());

/* Call _PySys_Audit() in the context of the current thread state,
even if tstate is not the current thread state. */
PyThreadState *current_tstate = _PyThreadState_GET();
if (_PySys_Audit(current_tstate, "sys.settrace", NULL) < 0) {
return -1;
}

PyObject *traceobj = tstate->c_traceobj;

tstate->c_tracefunc = NULL;
tstate->c_traceobj = NULL;
/* Must make sure that profiling is not ignored if 'traceobj' is freed */
_PyThreadState_ResumeTracing(tstate);
Py_XDECREF(traceobj);

Py_XINCREF(arg);
tstate->c_traceobj = arg;
tstate->c_tracefunc = func;

/* Flag that tracing or profiling is turned on */
_PyThreadState_ResumeTracing(tstate);

return 0;
}

void
PyEval_SetTrace(Py_tracefunc func, PyObject *arg)
{
PyThreadState *tstate = _PyThreadState_GET();
if (_PyEval_SetTrace(tstate, func, arg) < 0) {
if (PyThreadState_SetTrace(tstate, func, arg) < 0) {
/* Log _PySys_Audit() error */
_PyErr_WriteUnraisableMsg("in PyEval_SetTrace", NULL);
}
Expand Down
8 changes: 4 additions & 4 deletions Python/ceval_gil.h
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ drop_gil(struct _ceval_runtime_state *ceval, struct _ceval_state *ceval2,
/* Not switched yet => wait */
if (((PyThreadState*)_Py_atomic_load_relaxed(&gil->last_holder)) == tstate)
{
assert(is_tstate_valid(tstate));
assert(_PyThreadState_CheckConsistency(tstate));
RESET_GIL_DROP_REQUEST(tstate->interp);
/* NOTE: if COND_WAIT does not atomically start waiting when
releasing the mutex, another thread can run through, take
Expand Down Expand Up @@ -228,7 +228,7 @@ take_gil(PyThreadState *tstate)
PyThread_exit_thread();
}

assert(is_tstate_valid(tstate));
assert(_PyThreadState_CheckConsistency(tstate));
PyInterpreterState *interp = tstate->interp;
struct _ceval_runtime_state *ceval = &interp->runtime->ceval;
struct _ceval_state *ceval2 = &interp->ceval;
Expand Down Expand Up @@ -264,7 +264,7 @@ take_gil(PyThreadState *tstate)
MUTEX_UNLOCK(gil->mutex);
PyThread_exit_thread();
}
assert(is_tstate_valid(tstate));
assert(_PyThreadState_CheckConsistency(tstate));

SET_GIL_DROP_REQUEST(interp);
}
Expand Down Expand Up @@ -302,7 +302,7 @@ take_gil(PyThreadState *tstate)
drop_gil(ceval, ceval2, tstate);
PyThread_exit_thread();
}
assert(is_tstate_valid(tstate));
assert(_PyThreadState_CheckConsistency(tstate));

if (_Py_atomic_load_relaxed(&ceval2->gil_drop_request)) {
RESET_GIL_DROP_REQUEST(interp);
Expand Down
Loading