Thanks to visit codestin.com
Credit goes to github.com

Skip to content

GH-93503: Add thread-specific APIs to set profiling and tracing functions in the C-API #93504

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

Merged
merged 7 commits into from
Aug 24, 2022
Merged
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
24 changes: 24 additions & 0 deletions Doc/c-api/init.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1774,6 +1774,18 @@ Python-level trace functions in previous versions.

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

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

Like :c:func:`PyEval_SetProfile` but sets the profile function in all running threads
belonging to the current interpreter instead of the setting it only on the current thread.

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

As :c:func:`PyEval_SetProfile`, this function ignores any exceptions raised while
setting the profile functions in all threads.

.. versionadded:: 3.12


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

Expand All @@ -1788,6 +1800,18 @@ Python-level trace functions in previous versions.

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

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

Like :c:func:`PyEval_SetTrace` but sets the tracing function in all running threads
belonging to the current interpreter instead of the setting it only on the current thread.

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

As :c:func:`PyEval_SetTrace`, this function ignores any exceptions raised while
setting the trace functions in all threads.

.. versionadded:: 3.12


.. _advanced-debugging:

Expand Down
8 changes: 8 additions & 0 deletions Doc/data/refcounts.dat
Original file line number Diff line number Diff line change
Expand Up @@ -796,10 +796,18 @@ PyEval_SetProfile:void:::
PyEval_SetProfile:Py_tracefunc:func::
PyEval_SetProfile:PyObject*:obj:+1:

PyEval_SetProfileAllThreads:void:::
PyEval_SetProfileAllThreads:Py_tracefunc:func::
PyEval_SetProfileAllThreads:PyObject*:obj:+1:

PyEval_SetTrace:void:::
PyEval_SetTrace:Py_tracefunc:func::
PyEval_SetTrace:PyObject*:obj:+1:

PyEval_SetTraceAllThreads:void:::
PyEval_SetTraceAllThreads:Py_tracefunc:func::
PyEval_SetTraceAllThreads:PyObject*:obj:+1:

PyEval_EvalCode:PyObject*::+1:
PyEval_EvalCode:PyObject*:co:0:
PyEval_EvalCode:PyObject*:globals:0:
Expand Down
18 changes: 18 additions & 0 deletions Doc/library/threading.rst
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,15 @@ This module defines the following functions:
The *func* will be passed to :func:`sys.settrace` for each thread, before its
:meth:`~Thread.run` method is called.

.. function:: settrace_all_threads(func)

Set a trace function for all threads started from the :mod:`threading` module
and all Python threads that are currently executing.

The *func* will be passed to :func:`sys.settrace` for each thread, before its
:meth:`~Thread.run` method is called.

.. versionadded:: 3.12

.. function:: gettrace()

Expand All @@ -178,6 +187,15 @@ This module defines the following functions:
The *func* will be passed to :func:`sys.setprofile` for each thread, before its
:meth:`~Thread.run` method is called.

.. function:: setprofile_all_threads(func)

Set a profile function for all threads started from the :mod:`threading` module
and all Python threads that are currently executing.

The *func* will be passed to :func:`sys.setprofile` for each thread, before its
:meth:`~Thread.run` method is called.

.. versionadded:: 3.12

.. function:: getprofile()

Expand Down
2 changes: 2 additions & 0 deletions Include/cpython/ceval.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
#endif

PyAPI_FUNC(void) PyEval_SetProfile(Py_tracefunc, PyObject *);
PyAPI_FUNC(void) PyEval_SetProfileAllThreads(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(void) PyEval_SetTraceAllThreads(Py_tracefunc, PyObject *);
PyAPI_FUNC(int) _PyEval_SetTrace(PyThreadState *tstate, Py_tracefunc func, PyObject *arg);

/* Helper to look up a builtin object */
Expand Down
59 changes: 59 additions & 0 deletions Lib/test/test_threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,7 @@ def callback():
callback()
finally:
sys.settrace(old_trace)
threading.settrace(old_trace)

def test_gettrace(self):
def noop_trace(frame, event, arg):
Expand All @@ -866,6 +867,35 @@ def noop_trace(frame, event, arg):
finally:
threading.settrace(old_trace)

def test_gettrace_all_threads(self):
def fn(*args): pass
old_trace = threading.gettrace()
first_check = threading.Event()
second_check = threading.Event()

trace_funcs = []
def checker():
trace_funcs.append(sys.gettrace())
first_check.set()
second_check.wait()
trace_funcs.append(sys.gettrace())

try:
t = threading.Thread(target=checker)
t.start()
first_check.wait()
threading.settrace_all_threads(fn)
second_check.set()
t.join()
self.assertEqual(trace_funcs, [None, fn])
self.assertEqual(threading.gettrace(), fn)
self.assertEqual(sys.gettrace(), fn)
finally:
threading.settrace_all_threads(old_trace)

self.assertEqual(threading.gettrace(), old_trace)
self.assertEqual(sys.gettrace(), old_trace)

def test_getprofile(self):
def fn(*args): pass
old_profile = threading.getprofile()
Expand All @@ -875,6 +905,35 @@ def fn(*args): pass
finally:
threading.setprofile(old_profile)

def test_getprofile_all_threads(self):
def fn(*args): pass
old_profile = threading.getprofile()
first_check = threading.Event()
second_check = threading.Event()

profile_funcs = []
def checker():
profile_funcs.append(sys.getprofile())
first_check.set()
second_check.wait()
profile_funcs.append(sys.getprofile())

try:
t = threading.Thread(target=checker)
t.start()
first_check.wait()
threading.setprofile_all_threads(fn)
second_check.set()
t.join()
self.assertEqual(profile_funcs, [None, fn])
self.assertEqual(threading.getprofile(), fn)
self.assertEqual(sys.getprofile(), fn)
finally:
threading.setprofile_all_threads(old_profile)

self.assertEqual(threading.getprofile(), old_profile)
self.assertEqual(sys.getprofile(), old_profile)

@cpython_only
def test_shutdown_locks(self):
for daemon in (False, True):
Expand Down
25 changes: 22 additions & 3 deletions Lib/threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
'Event', 'Lock', 'RLock', 'Semaphore', 'BoundedSemaphore', 'Thread',
'Barrier', 'BrokenBarrierError', 'Timer', 'ThreadError',
'setprofile', 'settrace', 'local', 'stack_size',
'excepthook', 'ExceptHookArgs', 'gettrace', 'getprofile']
'excepthook', 'ExceptHookArgs', 'gettrace', 'getprofile',
'setprofile_all_threads','settrace_all_threads']

# Rename some stuff so "from threading import *" is safe
_start_new_thread = _thread.start_new_thread
Expand Down Expand Up @@ -60,11 +61,20 @@ def setprofile(func):

The func will be passed to sys.setprofile() for each thread, before its
run() method is called.

"""
global _profile_hook
_profile_hook = func

def setprofile_all_threads(func):
"""Set a profile function for all threads started from the threading module
and all Python threads that are currently executing.

The func will be passed to sys.setprofile() for each thread, before its
run() method is called.
"""
setprofile(func)
_sys._setprofileallthreads(func)

def getprofile():
"""Get the profiler function as set by threading.setprofile()."""
return _profile_hook
Expand All @@ -74,11 +84,20 @@ def settrace(func):

The func will be passed to sys.settrace() for each thread, before its run()
method is called.

"""
global _trace_hook
_trace_hook = func

def settrace_all_threads(func):
"""Set a trace function for all threads started from the threading module
and all Python threads that are currently executing.

The func will be passed to sys.settrace() for each thread, before its run()
method is called.
"""
settrace(func)
_sys._settraceallthreads(func)

def gettrace():
"""Get the trace function as set by threading.settrace()."""
return _trace_hook
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Add two new public functions to the public C-API,
:c:func:`PyEval_SetProfileAllThreads` and
:c:func:`PyEval_SetTraceAllThreads`, that allow to set tracking and
profiling functions in all running threads in addition to the calling one.
Also, add a new *running_threads* parameter to :func:`threading.setprofile`
and :func:`threading.settrace` that allows to do the same from Python. Patch
by Pablo Galindo
45 changes: 45 additions & 0 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@
#define _Py_atomic_load_relaxed_int32(ATOMIC_VAL) _Py_atomic_load_relaxed(ATOMIC_VAL)
#endif

#define HEAD_LOCK(runtime) \
PyThread_acquire_lock((runtime)->interpreters.mutex, WAIT_LOCK)
#define HEAD_UNLOCK(runtime) \
PyThread_release_lock((runtime)->interpreters.mutex)

/* Forward declarations */
static PyObject *trace_call_function(
Expand Down Expand Up @@ -6455,6 +6459,27 @@ PyEval_SetProfile(Py_tracefunc func, PyObject *arg)
}
}

void
Copy link
Member

Choose a reason for hiding this comment

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

It's not great that the function cannot report errors. While not returning -1 on error and pass the exception to the caller, rather than handling it with _PyErr_WriteUnraisableMsg()?

Copy link
Member Author

@pablogsal pablogsal Aug 4, 2022

Choose a reason for hiding this comment

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

I am mimicking what we do already in PyEval_SetProfile and to avoid surprises I think we should keep these two as synchronized as possible.

Copy link
Member

Choose a reason for hiding this comment

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

We cannot fix the API of old functions, but we can avoid past mistakes in newly added functions.

Copy link
Member

Choose a reason for hiding this comment

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

If you want to ignore exceptions on purpose, please mention it in the function documentation.

Copy link
Member Author

Choose a reason for hiding this comment

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

If you want to ignore exceptions on purpose, please mention it in the function documentation.

Yeah, I want to do that because I don't want to stop setting the profile function on other threads if an exception in one fails. Unless you strongly disagree, I will document this.

PyEval_SetProfileAllThreads(Py_tracefunc func, PyObject *arg)
{
PyThreadState *this_tstate = _PyThreadState_GET();
PyInterpreterState* interp = this_tstate->interp;

_PyRuntimeState *runtime = &_PyRuntime;
HEAD_LOCK(runtime);
PyThreadState* ts = PyInterpreterState_ThreadHead(interp);
HEAD_UNLOCK(runtime);

while (ts) {
if (_PyEval_SetProfile(ts, func, arg) < 0) {
_PyErr_WriteUnraisableMsg("in PyEval_SetProfileAllThreads", NULL);
}
HEAD_LOCK(runtime);
ts = PyThreadState_Next(ts);
HEAD_UNLOCK(runtime);
}
}

int
_PyEval_SetTrace(PyThreadState *tstate, Py_tracefunc func, PyObject *arg)
{
Expand Down Expand Up @@ -6508,6 +6533,26 @@ PyEval_SetTrace(Py_tracefunc func, PyObject *arg)
}
}

void
PyEval_SetTraceAllThreads(Py_tracefunc func, PyObject *arg)
{
PyThreadState *this_tstate = _PyThreadState_GET();
PyInterpreterState* interp = this_tstate->interp;

_PyRuntimeState *runtime = &_PyRuntime;
HEAD_LOCK(runtime);
PyThreadState* ts = PyInterpreterState_ThreadHead(interp);
HEAD_UNLOCK(runtime);

while (ts) {
if (_PyEval_SetTrace(ts, func, arg) < 0) {
_PyErr_WriteUnraisableMsg("in PyEval_SetTraceAllThreads", NULL);
}
HEAD_LOCK(runtime);
ts = PyThreadState_Next(ts);
HEAD_UNLOCK(runtime);
}
}

int
_PyEval_SetCoroutineOriginTrackingDepth(int depth)
Expand Down
26 changes: 25 additions & 1 deletion Python/clinic/sysmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading