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

Skip to content

Commit d8ea6bb

Browse files
committed
tracing thread safety
1 parent d180b50 commit d8ea6bb

File tree

8 files changed

+472
-86
lines changed

8 files changed

+472
-86
lines changed

Include/internal/pycore_ceval_state.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ struct _ceval_runtime_state {
6363
} perf;
6464
/* Pending calls to be made only on the main thread. */
6565
struct _pending_calls pending_mainthread;
66+
PyMutex sys_trace_profile_mutex;
6667
};
6768

6869
#ifdef PY_HAVE_PERF_TRAMPOLINE

Include/internal/pycore_pyatomic_ft_wrappers.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,25 @@ extern "C" {
2323
#define FT_ATOMIC_LOAD_SSIZE(value) _Py_atomic_load_ssize(&value)
2424
#define FT_ATOMIC_LOAD_SSIZE_RELAXED(value) \
2525
_Py_atomic_load_ssize_relaxed(&value)
26+
#define FT_ATOMIC_LOAD_PTR_ACQUIRE(value) \
27+
_Py_atomic_load_ptr_acquire(&value)
2628
#define FT_ATOMIC_STORE_PTR_RELAXED(value, new_value) \
2729
_Py_atomic_store_ptr_relaxed(&value, new_value)
2830
#define FT_ATOMIC_STORE_PTR_RELEASE(value, new_value) \
2931
_Py_atomic_store_ptr_release(&value, new_value)
3032
#define FT_ATOMIC_STORE_SSIZE_RELAXED(value, new_value) \
3133
_Py_atomic_store_ssize_relaxed(&value, new_value)
34+
#define FT_ATOMIC_STORE_UINT8_RELAXED(value, new_value) \
35+
_Py_atomic_store_uint8_relaxed(&value, new_value)
3236
#else
3337
#define FT_ATOMIC_LOAD_SSIZE(value) value
3438
#define FT_ATOMIC_LOAD_SSIZE_RELAXED(value) value
39+
#define FT_ATOMIC_LOAD_PTR_ACQUIRE(value) value
3540
#define FT_ATOMIC_STORE_PTR_RELAXED(value, new_value) value = new_value
3641
#define FT_ATOMIC_STORE_PTR_RELEASE(value, new_value) value = new_value
3742
#define FT_ATOMIC_STORE_SSIZE_RELAXED(value, new_value) value = new_value
43+
#define FT_ATOMIC_STORE_UINT8_RELAXED(value, new_value) value = new_value
44+
3845
#endif
3946

4047
#ifdef __cplusplus
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import os
2+
3+
from test import support
4+
5+
6+
def load_tests(*args):
7+
return support.load_package_tests(os.path.dirname(__file__), *args)
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
"""Tests monitoring, sys.settrace, and sys.setprofile in a multi-threaded
2+
environmenet to verify things are thread-safe in a free-threaded build"""
3+
4+
import sys
5+
import time
6+
import unittest
7+
import weakref
8+
9+
from sys import monitoring
10+
from test.support import is_wasi
11+
from threading import Thread
12+
from unittest import TestCase
13+
14+
15+
class InstrumentationMultiThreadedMixin:
16+
if not hasattr(sys, "gettotalrefcount"):
17+
thread_count = 50
18+
func_count = 1000
19+
fib = 15
20+
else:
21+
# Run a little faster in debug builds...
22+
thread_count = 25
23+
func_count = 500
24+
fib = 15
25+
26+
def after_threads(self):
27+
"""Runs once after all the threads have started"""
28+
pass
29+
30+
def during_threads(self):
31+
"""Runs repeatedly while the threads are still running"""
32+
pass
33+
34+
def work(self, n, funcs):
35+
"""Fibonacci function which also calls a bunch of random functions"""
36+
for func in funcs:
37+
func()
38+
if n < 2:
39+
return n
40+
return self.work(n - 1, funcs) + self.work(n - 2, funcs)
41+
42+
def start_work(self, n, funcs):
43+
# With the GIL builds we need to make sure that the hooks have
44+
# a chance to run as it's possible to run w/o releasing the GIL.
45+
time.sleep(1)
46+
self.work(n, funcs)
47+
48+
def after_test(self):
49+
"""Runs once after the test is done"""
50+
pass
51+
52+
def test_instrumention(self):
53+
# Setup a bunch of functions which will need instrumentation...
54+
funcs = []
55+
for i in range(self.func_count):
56+
x = {}
57+
exec("def f(): pass", x)
58+
funcs.append(x["f"])
59+
60+
threads = []
61+
for i in range(self.thread_count):
62+
# Each thread gets a copy of the func list to avoid contention
63+
t = Thread(target=self.start_work, args=(self.fib, list(funcs)))
64+
t.start()
65+
threads.append(t)
66+
67+
self.after_threads()
68+
69+
while True:
70+
any_alive = False
71+
for t in threads:
72+
if t.is_alive():
73+
any_alive = True
74+
break
75+
76+
if not any_alive:
77+
break
78+
79+
self.during_threads()
80+
81+
self.after_test()
82+
83+
84+
class MonitoringTestMixin:
85+
def setUp(self):
86+
for i in range(6):
87+
if monitoring.get_tool(i) is None:
88+
self.tool_id = i
89+
monitoring.use_tool_id(i, self.__class__.__name__)
90+
break
91+
92+
def tearDown(self):
93+
monitoring.free_tool_id(self.tool_id)
94+
95+
96+
@unittest.skipIf(is_wasi, "WASI has no threads.")
97+
class SetPreTraceMultiThreaded(InstrumentationMultiThreadedMixin, TestCase):
98+
"""Sets tracing one time after the threads have started"""
99+
100+
def setUp(self):
101+
super().setUp()
102+
self.called = False
103+
104+
def after_test(self):
105+
self.assertTrue(self.called)
106+
107+
def trace_func(self, frame, event, arg):
108+
self.called = True
109+
return self.trace_func
110+
111+
def after_threads(self):
112+
sys.settrace(self.trace_func)
113+
114+
115+
@unittest.skipIf(is_wasi, "WASI has no threads.")
116+
class MonitoringMultiThreaded(
117+
MonitoringTestMixin, InstrumentationMultiThreadedMixin, TestCase
118+
):
119+
"""Uses sys.monitoring and repeatedly toggles instrumentation on and off"""
120+
121+
def setUp(self):
122+
super().setUp()
123+
self.set = False
124+
self.called = False
125+
monitoring.register_callback(
126+
self.tool_id, monitoring.events.LINE, self.callback
127+
)
128+
129+
def tearDown(self):
130+
monitoring.set_events(self.tool_id, 0)
131+
super().tearDown()
132+
133+
def callback(self, *args):
134+
self.called = True
135+
136+
def after_test(self):
137+
self.assertTrue(self.called)
138+
139+
def during_threads(self):
140+
if self.set:
141+
monitoring.set_events(
142+
self.tool_id, monitoring.events.CALL | monitoring.events.LINE
143+
)
144+
else:
145+
monitoring.set_events(self.tool_id, 0)
146+
self.set = not self.set
147+
148+
149+
@unittest.skipIf(is_wasi, "WASI has no threads.")
150+
class SetTraceMultiThreaded(InstrumentationMultiThreadedMixin, TestCase):
151+
"""Uses sys.settrace and repeatedly toggles instrumentation on and off"""
152+
153+
def setUp(self):
154+
self.set = False
155+
self.called = False
156+
157+
def after_test(self):
158+
self.assertTrue(self.called)
159+
160+
def tearDown(self):
161+
sys.settrace(None)
162+
163+
def trace_func(self, frame, event, arg):
164+
self.called = True
165+
return self.trace_func
166+
167+
def during_threads(self):
168+
if self.set:
169+
sys.settrace(self.trace_func)
170+
else:
171+
sys.settrace(None)
172+
self.set = not self.set
173+
174+
175+
@unittest.skipIf(is_wasi, "WASI has no threads.")
176+
class SetProfileMultiThreaded(InstrumentationMultiThreadedMixin, TestCase):
177+
"""Uses sys.setprofile and repeatedly toggles instrumentation on and off"""
178+
thread_count = 25
179+
func_count = 200
180+
fib = 15
181+
182+
def setUp(self):
183+
self.set = False
184+
self.called = False
185+
186+
def after_test(self):
187+
self.assertTrue(self.called)
188+
189+
def tearDown(self):
190+
sys.setprofile(None)
191+
192+
def trace_func(self, frame, event, arg):
193+
self.called = True
194+
return self.trace_func
195+
196+
def during_threads(self):
197+
if self.set:
198+
sys.setprofile(self.trace_func)
199+
else:
200+
sys.setprofile(None)
201+
self.set = not self.set
202+
203+
204+
@unittest.skipIf(is_wasi, "WASI has no threads.")
205+
class MonitoringMisc(MonitoringTestMixin, TestCase):
206+
def register_callback(self):
207+
def callback(*args):
208+
pass
209+
210+
for i in range(200):
211+
monitoring.register_callback(self.tool_id, monitoring.events.LINE, callback)
212+
213+
self.refs.append(weakref.ref(callback))
214+
215+
def test_register_callback(self):
216+
self.refs = []
217+
threads = []
218+
for i in range(50):
219+
t = Thread(target=self.register_callback)
220+
t.start()
221+
threads.append(t)
222+
223+
for thread in threads:
224+
thread.join()
225+
226+
monitoring.register_callback(self.tool_id, monitoring.events.LINE, None)
227+
for ref in self.refs:
228+
self.assertEqual(ref(), None)
229+
230+
231+
if __name__ == "__main__":
232+
unittest.main()

Makefile.pre.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2374,6 +2374,7 @@ TESTSUBDIRS= idlelib/idle_test \
23742374
test/test_doctest \
23752375
test/test_email \
23762376
test/test_email/data \
2377+
test/test_free_threading \
23772378
test/test_future_stmt \
23782379
test/test_gdb \
23792380
test/test_import \

0 commit comments

Comments
 (0)