Skip to content

Commit e34c82a

Browse files
authored
GH-93503: Add thread-specific APIs to set profiling and tracing functions in the C-API (#93504)
* gh-93503: Add APIs to set profiling and tracing functions in all threads in the C-API * Use a separate API * Fix NEWS entry * Add locks around the loop * Document ignoring exceptions * Use the new APIs in the sys module * Update docs
1 parent 657976a commit e34c82a

File tree

10 files changed

+271
-4
lines changed

10 files changed

+271
-4
lines changed

Doc/c-api/init.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1774,6 +1774,18 @@ Python-level trace functions in previous versions.
17741774
17751775
The caller must hold the :term:`GIL`.
17761776
1777+
.. c:function:: void PyEval_SetProfileAllThreads(Py_tracefunc func, PyObject *obj)
1778+
1779+
Like :c:func:`PyEval_SetProfile` but sets the profile function in all running threads
1780+
belonging to the current interpreter instead of the setting it only on the current thread.
1781+
1782+
The caller must hold the :term:`GIL`.
1783+
1784+
As :c:func:`PyEval_SetProfile`, this function ignores any exceptions raised while
1785+
setting the profile functions in all threads.
1786+
1787+
.. versionadded:: 3.12
1788+
17771789
17781790
.. c:function:: void PyEval_SetTrace(Py_tracefunc func, PyObject *obj)
17791791
@@ -1788,6 +1800,18 @@ Python-level trace functions in previous versions.
17881800
17891801
The caller must hold the :term:`GIL`.
17901802
1803+
.. c:function:: void PyEval_SetTraceAllThreads(Py_tracefunc func, PyObject *obj)
1804+
1805+
Like :c:func:`PyEval_SetTrace` but sets the tracing function in all running threads
1806+
belonging to the current interpreter instead of the setting it only on the current thread.
1807+
1808+
The caller must hold the :term:`GIL`.
1809+
1810+
As :c:func:`PyEval_SetTrace`, this function ignores any exceptions raised while
1811+
setting the trace functions in all threads.
1812+
1813+
.. versionadded:: 3.12
1814+
17911815
17921816
.. _advanced-debugging:
17931817

Doc/data/refcounts.dat

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,10 +796,18 @@ PyEval_SetProfile:void:::
796796
PyEval_SetProfile:Py_tracefunc:func::
797797
PyEval_SetProfile:PyObject*:obj:+1:
798798

799+
PyEval_SetProfileAllThreads:void:::
800+
PyEval_SetProfileAllThreads:Py_tracefunc:func::
801+
PyEval_SetProfileAllThreads:PyObject*:obj:+1:
802+
799803
PyEval_SetTrace:void:::
800804
PyEval_SetTrace:Py_tracefunc:func::
801805
PyEval_SetTrace:PyObject*:obj:+1:
802806

807+
PyEval_SetTraceAllThreads:void:::
808+
PyEval_SetTraceAllThreads:Py_tracefunc:func::
809+
PyEval_SetTraceAllThreads:PyObject*:obj:+1:
810+
803811
PyEval_EvalCode:PyObject*::+1:
804812
PyEval_EvalCode:PyObject*:co:0:
805813
PyEval_EvalCode:PyObject*:globals:0:

Doc/library/threading.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,15 @@ This module defines the following functions:
158158
The *func* will be passed to :func:`sys.settrace` for each thread, before its
159159
:meth:`~Thread.run` method is called.
160160

161+
.. function:: settrace_all_threads(func)
162+
163+
Set a trace function for all threads started from the :mod:`threading` module
164+
and all Python threads that are currently executing.
165+
166+
The *func* will be passed to :func:`sys.settrace` for each thread, before its
167+
:meth:`~Thread.run` method is called.
168+
169+
.. versionadded:: 3.12
161170

162171
.. function:: gettrace()
163172

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

190+
.. function:: setprofile_all_threads(func)
191+
192+
Set a profile function for all threads started from the :mod:`threading` module
193+
and all Python threads that are currently executing.
194+
195+
The *func* will be passed to :func:`sys.setprofile` for each thread, before its
196+
:meth:`~Thread.run` method is called.
197+
198+
.. versionadded:: 3.12
181199

182200
.. function:: getprofile()
183201

Include/cpython/ceval.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
#endif
44

55
PyAPI_FUNC(void) PyEval_SetProfile(Py_tracefunc, PyObject *);
6+
PyAPI_FUNC(void) PyEval_SetProfileAllThreads(Py_tracefunc, PyObject *);
67
PyAPI_DATA(int) _PyEval_SetProfile(PyThreadState *tstate, Py_tracefunc func, PyObject *arg);
78
PyAPI_FUNC(void) PyEval_SetTrace(Py_tracefunc, PyObject *);
9+
PyAPI_FUNC(void) PyEval_SetTraceAllThreads(Py_tracefunc, PyObject *);
810
PyAPI_FUNC(int) _PyEval_SetTrace(PyThreadState *tstate, Py_tracefunc func, PyObject *arg);
911

1012
/* Helper to look up a builtin object */

Lib/test/test_threading.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,7 @@ def callback():
853853
callback()
854854
finally:
855855
sys.settrace(old_trace)
856+
threading.settrace(old_trace)
856857

857858
def test_gettrace(self):
858859
def noop_trace(frame, event, arg):
@@ -866,6 +867,35 @@ def noop_trace(frame, event, arg):
866867
finally:
867868
threading.settrace(old_trace)
868869

870+
def test_gettrace_all_threads(self):
871+
def fn(*args): pass
872+
old_trace = threading.gettrace()
873+
first_check = threading.Event()
874+
second_check = threading.Event()
875+
876+
trace_funcs = []
877+
def checker():
878+
trace_funcs.append(sys.gettrace())
879+
first_check.set()
880+
second_check.wait()
881+
trace_funcs.append(sys.gettrace())
882+
883+
try:
884+
t = threading.Thread(target=checker)
885+
t.start()
886+
first_check.wait()
887+
threading.settrace_all_threads(fn)
888+
second_check.set()
889+
t.join()
890+
self.assertEqual(trace_funcs, [None, fn])
891+
self.assertEqual(threading.gettrace(), fn)
892+
self.assertEqual(sys.gettrace(), fn)
893+
finally:
894+
threading.settrace_all_threads(old_trace)
895+
896+
self.assertEqual(threading.gettrace(), old_trace)
897+
self.assertEqual(sys.gettrace(), old_trace)
898+
869899
def test_getprofile(self):
870900
def fn(*args): pass
871901
old_profile = threading.getprofile()
@@ -875,6 +905,35 @@ def fn(*args): pass
875905
finally:
876906
threading.setprofile(old_profile)
877907

908+
def test_getprofile_all_threads(self):
909+
def fn(*args): pass
910+
old_profile = threading.getprofile()
911+
first_check = threading.Event()
912+
second_check = threading.Event()
913+
914+
profile_funcs = []
915+
def checker():
916+
profile_funcs.append(sys.getprofile())
917+
first_check.set()
918+
second_check.wait()
919+
profile_funcs.append(sys.getprofile())
920+
921+
try:
922+
t = threading.Thread(target=checker)
923+
t.start()
924+
first_check.wait()
925+
threading.setprofile_all_threads(fn)
926+
second_check.set()
927+
t.join()
928+
self.assertEqual(profile_funcs, [None, fn])
929+
self.assertEqual(threading.getprofile(), fn)
930+
self.assertEqual(sys.getprofile(), fn)
931+
finally:
932+
threading.setprofile_all_threads(old_profile)
933+
934+
self.assertEqual(threading.getprofile(), old_profile)
935+
self.assertEqual(sys.getprofile(), old_profile)
936+
878937
@cpython_only
879938
def test_shutdown_locks(self):
880939
for daemon in (False, True):

Lib/threading.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
'Event', 'Lock', 'RLock', 'Semaphore', 'BoundedSemaphore', 'Thread',
2929
'Barrier', 'BrokenBarrierError', 'Timer', 'ThreadError',
3030
'setprofile', 'settrace', 'local', 'stack_size',
31-
'excepthook', 'ExceptHookArgs', 'gettrace', 'getprofile']
31+
'excepthook', 'ExceptHookArgs', 'gettrace', 'getprofile',
32+
'setprofile_all_threads','settrace_all_threads']
3233

3334
# Rename some stuff so "from threading import *" is safe
3435
_start_new_thread = _thread.start_new_thread
@@ -60,11 +61,20 @@ def setprofile(func):
6061
6162
The func will be passed to sys.setprofile() for each thread, before its
6263
run() method is called.
63-
6464
"""
6565
global _profile_hook
6666
_profile_hook = func
6767

68+
def setprofile_all_threads(func):
69+
"""Set a profile function for all threads started from the threading module
70+
and all Python threads that are currently executing.
71+
72+
The func will be passed to sys.setprofile() for each thread, before its
73+
run() method is called.
74+
"""
75+
setprofile(func)
76+
_sys._setprofileallthreads(func)
77+
6878
def getprofile():
6979
"""Get the profiler function as set by threading.setprofile()."""
7080
return _profile_hook
@@ -74,11 +84,20 @@ def settrace(func):
7484
7585
The func will be passed to sys.settrace() for each thread, before its run()
7686
method is called.
77-
7887
"""
7988
global _trace_hook
8089
_trace_hook = func
8190

91+
def settrace_all_threads(func):
92+
"""Set a trace function for all threads started from the threading module
93+
and all Python threads that are currently executing.
94+
95+
The func will be passed to sys.settrace() for each thread, before its run()
96+
method is called.
97+
"""
98+
settrace(func)
99+
_sys._settraceallthreads(func)
100+
82101
def gettrace():
83102
"""Get the trace function as set by threading.settrace()."""
84103
return _trace_hook
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Add two new public functions to the public C-API,
2+
:c:func:`PyEval_SetProfileAllThreads` and
3+
:c:func:`PyEval_SetTraceAllThreads`, that allow to set tracking and
4+
profiling functions in all running threads in addition to the calling one.
5+
Also, add a new *running_threads* parameter to :func:`threading.setprofile`
6+
and :func:`threading.settrace` that allows to do the same from Python. Patch
7+
by Pablo Galindo

Python/ceval.c

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@
9696
#define _Py_atomic_load_relaxed_int32(ATOMIC_VAL) _Py_atomic_load_relaxed(ATOMIC_VAL)
9797
#endif
9898

99+
#define HEAD_LOCK(runtime) \
100+
PyThread_acquire_lock((runtime)->interpreters.mutex, WAIT_LOCK)
101+
#define HEAD_UNLOCK(runtime) \
102+
PyThread_release_lock((runtime)->interpreters.mutex)
99103

100104
/* Forward declarations */
101105
static PyObject *trace_call_function(
@@ -6455,6 +6459,27 @@ PyEval_SetProfile(Py_tracefunc func, PyObject *arg)
64556459
}
64566460
}
64576461

6462+
void
6463+
PyEval_SetProfileAllThreads(Py_tracefunc func, PyObject *arg)
6464+
{
6465+
PyThreadState *this_tstate = _PyThreadState_GET();
6466+
PyInterpreterState* interp = this_tstate->interp;
6467+
6468+
_PyRuntimeState *runtime = &_PyRuntime;
6469+
HEAD_LOCK(runtime);
6470+
PyThreadState* ts = PyInterpreterState_ThreadHead(interp);
6471+
HEAD_UNLOCK(runtime);
6472+
6473+
while (ts) {
6474+
if (_PyEval_SetProfile(ts, func, arg) < 0) {
6475+
_PyErr_WriteUnraisableMsg("in PyEval_SetProfileAllThreads", NULL);
6476+
}
6477+
HEAD_LOCK(runtime);
6478+
ts = PyThreadState_Next(ts);
6479+
HEAD_UNLOCK(runtime);
6480+
}
6481+
}
6482+
64586483
int
64596484
_PyEval_SetTrace(PyThreadState *tstate, Py_tracefunc func, PyObject *arg)
64606485
{
@@ -6508,6 +6533,26 @@ PyEval_SetTrace(Py_tracefunc func, PyObject *arg)
65086533
}
65096534
}
65106535

6536+
void
6537+
PyEval_SetTraceAllThreads(Py_tracefunc func, PyObject *arg)
6538+
{
6539+
PyThreadState *this_tstate = _PyThreadState_GET();
6540+
PyInterpreterState* interp = this_tstate->interp;
6541+
6542+
_PyRuntimeState *runtime = &_PyRuntime;
6543+
HEAD_LOCK(runtime);
6544+
PyThreadState* ts = PyInterpreterState_ThreadHead(interp);
6545+
HEAD_UNLOCK(runtime);
6546+
6547+
while (ts) {
6548+
if (_PyEval_SetTrace(ts, func, arg) < 0) {
6549+
_PyErr_WriteUnraisableMsg("in PyEval_SetTraceAllThreads", NULL);
6550+
}
6551+
HEAD_LOCK(runtime);
6552+
ts = PyThreadState_Next(ts);
6553+
HEAD_UNLOCK(runtime);
6554+
}
6555+
}
65116556

65126557
int
65136558
_PyEval_SetCoroutineOriginTrackingDepth(int depth)

Python/clinic/sysmodule.c.h

Lines changed: 25 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)