Skip to content

Commit 539595a

Browse files
committed
Merge branch 'gh-117783-immortalize' into nogil-integration
2 parents a825875 + 0cab82c commit 539595a

File tree

13 files changed

+144
-6
lines changed

13 files changed

+144
-6
lines changed

Include/internal/pycore_gc.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,18 @@ struct _gc_runtime_state {
312312
collections, and are awaiting to undergo a full collection for
313313
the first time. */
314314
Py_ssize_t long_lived_pending;
315+
316+
/* gh-117783: Deferred reference counting is not fully implemented yet, so
317+
as a temporary measure we treat objects using deferred referenence
318+
counting as immortal. */
319+
struct {
320+
/* Immortalize objects instead of marking them as using deferred
321+
reference counting. */
322+
int enabled;
323+
324+
/* Set enabled=1 when the first background thread is created. */
325+
int enable_on_thread;
326+
} immortalize;
315327
#endif
316328
};
317329

@@ -343,6 +355,11 @@ extern void _PyGC_ClearAllFreeLists(PyInterpreterState *interp);
343355
extern void _Py_ScheduleGC(PyThreadState *tstate);
344356
extern void _Py_RunGC(PyThreadState *tstate);
345357

358+
#ifdef Py_GIL_DISABLED
359+
// gh-117783: Immortalize objects that use deferred reference counting
360+
extern void _PyGC_ImmortalizeDeferredObjects(PyInterpreterState *interp);
361+
#endif
362+
346363
#ifdef __cplusplus
347364
}
348365
#endif

Lib/test/libregrtest/main.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
import time
88
import trace
99

10-
from test.support import os_helper, MS_WINDOWS, flush_std_streams
10+
from test.support import (os_helper, MS_WINDOWS, flush_std_streams,
11+
suppress_immortalization)
1112

1213
from .cmdline import _parse_args, Namespace
1314
from .findtests import findtests, split_test_packages, list_cases
@@ -526,7 +527,10 @@ def _run_tests(self, selected: TestTuple, tests: TestList | None) -> int:
526527
if self.num_workers:
527528
self._run_tests_mp(runtests, self.num_workers)
528529
else:
529-
self.run_tests_sequentially(runtests)
530+
# gh-117783: don't immortalize deferred objects when tracking
531+
# refleaks. Only releveant for the free-threaded build.
532+
with suppress_immortalization(runtests.hunt_refleak):
533+
self.run_tests_sequentially(runtests)
530534

531535
coverage = self.results.get_coverage_results()
532536
self.display_result(runtests)

Lib/test/libregrtest/single.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,10 @@ def run_single_test(test_name: TestName, runtests: RunTests) -> TestResult:
303303
result = TestResult(test_name)
304304
pgo = runtests.pgo
305305
try:
306-
_runtest(result, runtests)
306+
# gh-117783: don't immortalize deferred objects when tracking
307+
# refleaks. Only releveant for the free-threaded build.
308+
with support.suppress_immortalization(runtests.hunt_refleak):
309+
_runtest(result, runtests)
307310
except:
308311
if not pgo:
309312
msg = traceback.format_exc()

Lib/test/support/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,25 @@ def has_no_debug_ranges():
515515
def requires_debug_ranges(reason='requires co_positions / debug_ranges'):
516516
return unittest.skipIf(has_no_debug_ranges(), reason)
517517

518+
@contextlib.contextmanager
519+
def suppress_immortalization(suppress=True):
520+
"""Suppress immortalization of deferred objects."""
521+
try:
522+
import _testinternalcapi
523+
except ImportError:
524+
yield
525+
return
526+
527+
if not suppress:
528+
yield
529+
return
530+
531+
old_values = _testinternalcapi.set_immortalize_deferred(False)
532+
try:
533+
yield
534+
finally:
535+
_testinternalcapi.set_immortalize_deferred(*old_values)
536+
518537
MS_WINDOWS = (sys.platform == 'win32')
519538

520539
# Is not actually used in tests, but is kept for compatibility.

Lib/test/test_capi/test_watchers.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import unittest
22

33
from contextlib import contextmanager, ExitStack
4-
from test.support import catch_unraisable_exception, import_helper, gc_collect
4+
from test.support import (
5+
catch_unraisable_exception, import_helper,
6+
gc_collect, suppress_immortalization)
57

68

79
# Skip this test if the _testcapi module isn't available.
@@ -382,6 +384,7 @@ def assert_event_counts(self, exp_created_0, exp_destroyed_0,
382384
self.assertEqual(
383385
exp_destroyed_1, _testcapi.get_code_watcher_num_destroyed_events(1))
384386

387+
@suppress_immortalization()
385388
def test_code_object_events_dispatched(self):
386389
# verify that all counts are zero before any watchers are registered
387390
self.assert_event_counts(0, 0, 0, 0)
@@ -428,6 +431,7 @@ def test_error(self):
428431
self.assertIsNone(cm.unraisable.object)
429432
self.assertEqual(str(cm.unraisable.exc_value), "boom!")
430433

434+
@suppress_immortalization()
431435
def test_dealloc_error(self):
432436
co = _testcapi.code_newempty("test_watchers", "dummy0", 0)
433437
with self.code_watcher(2):

Lib/test/test_code.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@
141141
ctypes = None
142142
from test.support import (cpython_only,
143143
check_impl_detail, requires_debug_ranges,
144-
gc_collect, Py_GIL_DISABLED)
144+
gc_collect, Py_GIL_DISABLED,
145+
suppress_immortalization)
145146
from test.support.script_helper import assert_python_ok
146147
from test.support import threading_helper, import_helper
147148
from test.support.bytecode_helper import instructions_with_positions
@@ -577,6 +578,7 @@ def test_interned_string_with_null(self):
577578

578579
class CodeWeakRefTest(unittest.TestCase):
579580

581+
@suppress_immortalization()
580582
def test_basic(self):
581583
# Create a code object in a clean environment so that we know we have
582584
# the only reference to it left.
@@ -827,6 +829,7 @@ def test_bad_index(self):
827829
self.assertEqual(GetExtra(f.__code__, FREE_INDEX+100,
828830
ctypes.c_voidp(100)), 0)
829831

832+
@suppress_immortalization()
830833
def test_free_called(self):
831834
# Verify that the provided free function gets invoked
832835
# when the code object is cleaned up.
@@ -854,6 +857,7 @@ def test_get_set(self):
854857
del f
855858

856859
@threading_helper.requires_working_threading()
860+
@suppress_immortalization()
857861
def test_free_different_thread(self):
858862
# Freeing a code object on a different thread then
859863
# where the co_extra was set should be safe.

Lib/test/test_functools.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1833,6 +1833,7 @@ def f():
18331833
return 1
18341834
self.assertEqual(f.cache_parameters(), {'maxsize': 1000, "typed": True})
18351835

1836+
@support.suppress_immortalization()
18361837
def test_lru_cache_weakrefable(self):
18371838
@self.module.lru_cache
18381839
def test_function(x):

Lib/test/test_weakref.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import random
1313

1414
from test import support
15-
from test.support import script_helper, ALWAYS_EQ
15+
from test.support import script_helper, ALWAYS_EQ, suppress_immortalization
1616
from test.support import gc_collect
1717
from test.support import import_helper
1818
from test.support import threading_helper
@@ -650,6 +650,7 @@ class C(object):
650650
# deallocation of c2.
651651
del c2
652652

653+
@suppress_immortalization()
653654
def test_callback_in_cycle(self):
654655
import gc
655656

@@ -742,6 +743,7 @@ class D:
742743
del c1, c2, C, D
743744
gc.collect()
744745

746+
@suppress_immortalization()
745747
def test_callback_in_cycle_resurrection(self):
746748
import gc
747749

@@ -877,6 +879,7 @@ def test_init(self):
877879
# No exception should be raised here
878880
gc.collect()
879881

882+
@suppress_immortalization()
880883
def test_classes(self):
881884
# Check that classes are weakrefable.
882885
class A(object):

Modules/_testinternalcapi.c

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1936,6 +1936,27 @@ get_py_thread_id(PyObject *self, PyObject *Py_UNUSED(ignored))
19361936
}
19371937
#endif
19381938

1939+
static PyObject *
1940+
set_immortalize_deferred(PyObject *self, PyObject *value)
1941+
{
1942+
#ifdef Py_GIL_DISABLED
1943+
PyInterpreterState *interp = PyInterpreterState_Get();
1944+
int old_enabled = interp->gc.immortalize.enabled;
1945+
int old_enabled_on_thread = interp->gc.immortalize.enable_on_thread;
1946+
int enabled_on_thread = 0;
1947+
if (!PyArg_ParseTuple(value, "i|i",
1948+
&interp->gc.immortalize.enabled,
1949+
&enabled_on_thread))
1950+
{
1951+
return NULL;
1952+
}
1953+
interp->gc.immortalize.enable_on_thread = enabled_on_thread;
1954+
return Py_BuildValue("ii", old_enabled, old_enabled_on_thread);
1955+
#else
1956+
return Py_BuildValue("OO", Py_False, Py_False);
1957+
#endif
1958+
}
1959+
19391960
static PyObject *
19401961
has_inline_values(PyObject *self, PyObject *obj)
19411962
{
@@ -2029,6 +2050,7 @@ static PyMethodDef module_functions[] = {
20292050
#ifdef Py_GIL_DISABLED
20302051
{"py_thread_id", get_py_thread_id, METH_NOARGS},
20312052
#endif
2053+
{"set_immortalize_deferred", set_immortalize_deferred, METH_VARARGS},
20322054
{"uop_symbols_test", _Py_uop_symbols_test, METH_NOARGS},
20332055
{NULL, NULL} /* sentinel */
20342056
};

Objects/object.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2430,6 +2430,13 @@ _PyObject_SetDeferredRefcount(PyObject *op)
24302430
assert(PyType_IS_GC(Py_TYPE(op)));
24312431
assert(_Py_IsOwnedByCurrentThread(op));
24322432
assert(op->ob_ref_shared == 0);
2433+
PyInterpreterState *interp = _PyInterpreterState_GET();
2434+
if (interp->gc.immortalize.enabled) {
2435+
// gh-117696: immortalize objects instead of using deferred reference
2436+
// counting for now.
2437+
_Py_SetImmortal(op);
2438+
return;
2439+
}
24332440
op->ob_gc_bits |= _PyGC_BITS_DEFERRED;
24342441
op->ob_ref_local += 1;
24352442
op->ob_ref_shared = _Py_REF_QUEUED;

Python/gc_free_threading.c

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,10 @@ _PyGC_Init(PyInterpreterState *interp)
704704
{
705705
GCState *gcstate = &interp->gc;
706706

707+
if (_Py_IsMainInterpreter(interp)) {
708+
gcstate->immortalize.enable_on_thread = 1;
709+
}
710+
707711
gcstate->garbage = PyList_New(0);
708712
if (gcstate->garbage == NULL) {
709713
return _PyStatus_NO_MEMORY();
@@ -1781,6 +1785,30 @@ custom_visitor_wrapper(const mi_heap_t *heap, const mi_heap_area_t *area,
17811785
return true;
17821786
}
17831787

1788+
// gh-117783: Immortalize objects that use deferred reference counting to
1789+
// temporarily work around scaling bottlenecks.
1790+
static bool
1791+
immortalize_visitor(const mi_heap_t *heap, const mi_heap_area_t *area,
1792+
void *block, size_t block_size, void *args)
1793+
{
1794+
PyObject *op = op_from_block(block, args, false);
1795+
if (op != NULL && _PyObject_HasDeferredRefcount(op)) {
1796+
_Py_SetImmortal(op);
1797+
op->ob_gc_bits &= ~_PyGC_BITS_DEFERRED;
1798+
}
1799+
return true;
1800+
}
1801+
1802+
void
1803+
_PyGC_ImmortalizeDeferredObjects(PyInterpreterState *interp)
1804+
{
1805+
struct visitor_args args;
1806+
_PyEval_StopTheWorld(interp);
1807+
gc_visit_heaps(interp, &immortalize_visitor, &args);
1808+
interp->gc.immortalize.enabled = 1;
1809+
_PyEval_StartTheWorld(interp);
1810+
}
1811+
17841812
void
17851813
PyUnstable_GC_VisitObjects(gcvisitobjects_t callback, void *arg)
17861814
{

Python/pylifecycle.c

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1491,6 +1491,22 @@ finalize_modules_delete_special(PyThreadState *tstate, int verbose)
14911491
}
14921492
}
14931493

1494+
static void
1495+
swap_module_dict(PyModuleObject *mod)
1496+
{
1497+
if (_Py_IsImmortal(mod->md_dict)) {
1498+
// gh-117783: Immortalizing module dicts can cause some finalizers to
1499+
// run much later than typical leading to attribute errors due to
1500+
// partially cleared modules. To avoid this, we copy the module dict
1501+
// if it was immortalized.
1502+
PyObject *copy = PyDict_Copy(mod->md_dict);
1503+
if (copy == NULL) {
1504+
PyErr_FormatUnraisable("Exception ignored on removing modules");
1505+
return;
1506+
}
1507+
Py_SETREF(mod->md_dict, copy);
1508+
}
1509+
}
14941510

14951511
static PyObject*
14961512
finalize_remove_modules(PyObject *modules, int verbose)
@@ -1521,6 +1537,7 @@ finalize_remove_modules(PyObject *modules, int verbose)
15211537
if (verbose && PyUnicode_Check(name)) { \
15221538
PySys_FormatStderr("# cleanup[2] removing %U\n", name); \
15231539
} \
1540+
swap_module_dict((PyModuleObject *)mod); \
15241541
STORE_MODULE_WEAKREF(name, mod); \
15251542
if (PyObject_SetItem(modules, name, Py_None) < 0) { \
15261543
PyErr_FormatUnraisable("Exception ignored on removing modules"); \

Python/pystate.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1568,6 +1568,15 @@ new_threadstate(PyInterpreterState *interp, int whence)
15681568
// Must be called with lock unlocked to avoid re-entrancy deadlock.
15691569
PyMem_RawFree(new_tstate);
15701570
}
1571+
else {
1572+
#ifdef Py_GIL_DISABLED
1573+
if (interp->gc.immortalize.enable_on_thread && !interp->gc.immortalize.enabled) {
1574+
// Immortalize objects marked as using deferred reference counting
1575+
// the first time a non-main thread is created.
1576+
_PyGC_ImmortalizeDeferredObjects(interp);
1577+
}
1578+
#endif
1579+
}
15711580

15721581
#ifdef Py_GIL_DISABLED
15731582
// Must be called with lock unlocked to avoid lock ordering deadlocks.

0 commit comments

Comments
 (0)