Skip to content

Commit d37f6a3

Browse files
committed
pythongh-93503: Add APIs to set profiling and tracing functions in all threads in the C-API
1 parent 0342c93 commit d37f6a3

File tree

10 files changed

+244
-5
lines changed

10 files changed

+244
-5
lines changed

Doc/c-api/init.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1774,6 +1774,15 @@ 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+
instead of the setting it only on the current thread.
1781+
1782+
The caller must hold the :term:`GIL`.
1783+
1784+
.. versionadded:: 3.12
1785+
17771786
17781787
.. c:function:: void PyEval_SetTrace(Py_tracefunc func, PyObject *obj)
17791788
@@ -1788,6 +1797,15 @@ Python-level trace functions in previous versions.
17881797
17891798
The caller must hold the :term:`GIL`.
17901799
1800+
.. c:function:: void PyEval_SetTraceAllThreads(Py_tracefunc func, PyObject *obj)
1801+
1802+
Like :c:func:`PyEval_SetTrace` but sets the tracing function in all running threads
1803+
instead of the setting it only on the current thread.
1804+
1805+
The caller must hold the :term:`GIL`.
1806+
1807+
.. versionadded:: 3.12
1808+
17911809
17921810
.. _advanced-debugging:
17931811

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: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,14 +138,19 @@ This module defines the following functions:
138138
.. versionadded:: 3.4
139139

140140

141-
.. function:: settrace(func)
141+
.. function:: settrace(func, *, running_threads=False)
142142

143143
.. index:: single: trace function
144144

145145
Set a trace function for all threads started from the :mod:`threading` module.
146146
The *func* will be passed to :func:`sys.settrace` for each thread, before its
147147
:meth:`~Thread.run` method is called.
148148

149+
If *running_threads* is set, the tracing function will be installed in all running
150+
threads.
151+
152+
.. versionchanged:: 3.12
153+
The *running_threads* parameter was added.
149154

150155
.. function:: gettrace()
151156

@@ -158,14 +163,19 @@ This module defines the following functions:
158163
.. versionadded:: 3.10
159164

160165

161-
.. function:: setprofile(func)
166+
.. function:: setprofile(func, *, running_threads=False)
162167

163168
.. index:: single: profile function
164169

165170
Set a profile function for all threads started from the :mod:`threading` module.
166171
The *func* will be passed to :func:`sys.setprofile` for each thread, before its
167172
:meth:`~Thread.run` method is called.
168173

174+
If *running_threads* is set, the profile function will be installed in all running
175+
threads.
176+
177+
.. versionchanged:: 3.12
178+
The *running_threads* parameter was added.
169179

170180
.. function:: getprofile()
171181

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: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,34 @@ def noop_trace(frame, event, arg):
866866
finally:
867867
threading.settrace(old_trace)
868868

869+
def test_gettrace_all_threads(self):
870+
def fn(*args): pass
871+
old_trace = threading.gettrace()
872+
first_check = threading.Event()
873+
second_check = threading.Event()
874+
875+
trace_funcs = []
876+
def checker():
877+
trace_funcs.append(sys.gettrace())
878+
first_check.set()
879+
second_check.wait()
880+
trace_funcs.append(sys.gettrace())
881+
882+
try:
883+
t = threading.Thread(target=checker)
884+
t.start()
885+
first_check.wait()
886+
threading.settrace(fn, running_threads=True)
887+
second_check.set()
888+
t.join()
889+
self.assertEqual(trace_funcs, [None, fn])
890+
self.assertEqual(threading.gettrace(), fn)
891+
self.assertEqual(sys.gettrace(), fn)
892+
finally:
893+
threading.settrace(old_trace, running_threads=True)
894+
self.assertEqual(threading.gettrace(), old_trace)
895+
self.assertEqual(sys.gettrace(), old_trace)
896+
869897
def test_getprofile(self):
870898
def fn(*args): pass
871899
old_profile = threading.getprofile()
@@ -875,6 +903,35 @@ def fn(*args): pass
875903
finally:
876904
threading.setprofile(old_profile)
877905

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

Lib/threading.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,30 +55,40 @@
5555
_profile_hook = None
5656
_trace_hook = None
5757

58-
def setprofile(func):
58+
def setprofile(func, *, running_threads=False):
5959
"""Set a profile function for all threads started from the threading module.
6060
6161
The func will be passed to sys.setprofile() for each thread, before its
6262
run() method is called.
6363
64+
If running_threads is set, the profile function will be installed in all running
65+
threads.
6466
"""
6567
global _profile_hook
6668
_profile_hook = func
6769

70+
if running_threads:
71+
_sys._setprofileallthreads(func)
72+
6873
def getprofile():
6974
"""Get the profiler function as set by threading.setprofile()."""
7075
return _profile_hook
7176

72-
def settrace(func):
77+
def settrace(func, *, running_threads=False):
7378
"""Set a trace function for all threads started from the threading module.
7479
7580
The func will be passed to sys.settrace() for each thread, before its run()
7681
method is called.
7782
83+
If running_threads is set, the trace function will be installed in all running
84+
threads.
7885
"""
7986
global _trace_hook
8087
_trace_hook = func
8188

89+
if running_threads:
90+
_sys._settraceallthreads(func)
91+
8292
def gettrace():
8393
"""Get the trace function as set by threading.settrace()."""
8494
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: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7033,6 +7033,20 @@ PyEval_SetProfile(Py_tracefunc func, PyObject *arg)
70337033
}
70347034
}
70357035

7036+
void
7037+
PyEval_SetProfileAllThreads(Py_tracefunc func, PyObject *arg)
7038+
{
7039+
PyThreadState *this_tstate = _PyThreadState_GET();
7040+
PyInterpreterState* interp = this_tstate->interp;
7041+
PyThreadState* ts = PyInterpreterState_ThreadHead(interp);
7042+
while (ts) {
7043+
if (_PyEval_SetProfile(ts, func, arg) < 0) {
7044+
_PyErr_WriteUnraisableMsg("in PyEval_SetProfileAllThreads", NULL);
7045+
}
7046+
ts = PyThreadState_Next(ts);
7047+
}
7048+
}
7049+
70367050
int
70377051
_PyEval_SetTrace(PyThreadState *tstate, Py_tracefunc func, PyObject *arg)
70387052
{
@@ -7086,6 +7100,19 @@ PyEval_SetTrace(Py_tracefunc func, PyObject *arg)
70867100
}
70877101
}
70887102

7103+
void
7104+
PyEval_SetTraceAllThreads(Py_tracefunc func, PyObject *arg)
7105+
{
7106+
PyThreadState *this_tstate = _PyThreadState_GET();
7107+
PyInterpreterState* interp = this_tstate->interp;
7108+
PyThreadState* ts = PyInterpreterState_ThreadHead(interp);
7109+
while (ts) {
7110+
if (_PyEval_SetTrace(ts, func, arg) < 0) {
7111+
_PyErr_WriteUnraisableMsg("in PyEval_SetTraceAllThreads", NULL);
7112+
}
7113+
ts = PyThreadState_Next(ts);
7114+
}
7115+
}
70897116

70907117
int
70917118
_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)