Skip to content

Commit d28e6a5

Browse files
author
Matt Page
committed
Add tests for draining QSBR page lists during GC
Add _testinternalcapi.get_qsbr_page_list_length(), which counts the pages on all threads' QSBR page lists, and use it to check that gc.collect() frees empty pages parked by the calling thread and by an idle thread. Also check that the GC skips thread states that have not initialized their mimalloc state yet.
1 parent a751341 commit d28e6a5

2 files changed

Lines changed: 108 additions & 0 deletions

File tree

‎Lib/test/test_free_threading/test_gc.py‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
from test import support
1111
from test.support import threading_helper
1212

13+
try:
14+
import _testinternalcapi
15+
except ImportError:
16+
_testinternalcapi = None
17+
1318

1419
class MyObj:
1520
pass
@@ -210,5 +215,85 @@ def reader():
210215
pass
211216

212217

218+
def allocate_and_free():
219+
# Free enough small objects to leave many mimalloc pages empty. Empty
220+
# pages in the object heaps wait on QSBR before they are freed.
221+
junk = [[] for _ in range(100_000)]
222+
del junk
223+
224+
225+
@threading_helper.requires_working_threading()
226+
@unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi")
227+
class TestQSBRPageList(TestCase):
228+
# gh-158972: gc.collect() should free empty pages waiting on QSBR, even
229+
# if the thread that parked them is idle.
230+
231+
def parked_pages(self):
232+
return _testinternalcapi.get_qsbr_page_list_length()
233+
234+
def test_gc_collect_frees_parked_pages(self):
235+
gc.collect()
236+
allocate_and_free()
237+
parked = self.parked_pages()
238+
if parked < 10:
239+
self.skipTest(f"only {parked} pages were parked")
240+
gc.collect()
241+
self.assertLess(self.parked_pages(), parked // 4)
242+
243+
def test_gc_collect_frees_pages_parked_by_idle_thread(self):
244+
done = threading.Lock()
245+
done.acquire()
246+
resume = threading.Lock()
247+
resume.acquire()
248+
249+
def worker():
250+
allocate_and_free()
251+
done.release()
252+
# Block without allocating so the worker doesn't free its parked
253+
# pages itself.
254+
resume.acquire()
255+
256+
t = Thread(target=worker)
257+
t.start()
258+
try:
259+
done.acquire()
260+
parked = self.parked_pages()
261+
if parked < 10:
262+
self.skipTest(f"only {parked} pages were parked")
263+
gc.collect()
264+
self.assertLess(self.parked_pages(), parked // 4)
265+
finally:
266+
resume.release()
267+
t.join()
268+
269+
def test_gc_collect_while_starting_threads(self):
270+
# A new thread is added to the interpreter before it initializes its
271+
# mimalloc state, so the GC can see thread states without a usable
272+
# page list.
273+
stop = threading.Event()
274+
275+
def collector():
276+
while not stop.is_set():
277+
gc.collect()
278+
279+
deadline = time.monotonic() + 2.0
280+
281+
def starter():
282+
while time.monotonic() < deadline:
283+
threads = [Thread(target=lambda: None) for _ in range(16)]
284+
with threading_helper.start_threads(threads):
285+
pass
286+
287+
c = Thread(target=collector)
288+
c.start()
289+
try:
290+
starters = [Thread(target=starter) for _ in range(4)]
291+
with threading_helper.start_threads(starters):
292+
pass
293+
finally:
294+
stop.set()
295+
c.join()
296+
297+
213298
if __name__ == "__main__":
214299
unittest.main()

‎Modules/_testinternalcapi.c‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2885,6 +2885,28 @@ get_long_lived_total(PyObject *self, PyObject *Py_UNUSED(ignored))
28852885
return PyLong_FromInt64(PyInterpreterState_Get()->gc.long_lived_total);
28862886
}
28872887

2888+
// Return the total number of mimalloc pages on all threads' QSBR page lists.
2889+
static PyObject *
2890+
get_qsbr_page_list_length(PyObject *self, PyObject *Py_UNUSED(ignored))
2891+
{
2892+
PyInterpreterState *interp = _PyInterpreterState_GET();
2893+
Py_ssize_t count = 0;
2894+
_PyEval_StopTheWorld(interp);
2895+
_Py_FOR_EACH_TSTATE_BEGIN(interp, p) {
2896+
struct _mimalloc_thread_state *m = &((_PyThreadStateImpl *)p)->mimalloc;
2897+
if (!_Py_atomic_load_int(&m->initialized)) {
2898+
continue;
2899+
}
2900+
struct llist_node *node;
2901+
llist_for_each(node, &m->page_list) {
2902+
count++;
2903+
}
2904+
}
2905+
_Py_FOR_EACH_TSTATE_END(interp);
2906+
_PyEval_StartTheWorld(interp);
2907+
return PyLong_FromSsize_t(count);
2908+
}
2909+
28882910
#endif
28892911

28902912
static PyObject *
@@ -3442,6 +3464,7 @@ static PyMethodDef module_functions[] = {
34423464
{"get_tlbc", get_tlbc, METH_O, NULL},
34433465
{"get_tlbc_id", get_tlbc_id, METH_O, NULL},
34443466
{"get_long_lived_total", get_long_lived_total, METH_NOARGS},
3467+
{"get_qsbr_page_list_length", get_qsbr_page_list_length, METH_NOARGS},
34453468
#endif
34463469
#ifdef _Py_TIER2
34473470
{"uop_symbols_test", _Py_uop_symbols_test, METH_NOARGS},

0 commit comments

Comments
 (0)