Skip to content

Commit 332356b

Browse files
authored
gh-125900: Clean-up logic around immortalization in free-threading (#125901)
* Remove `@suppress_immortalization` decorator * Make suppression flag per-thread instead of per-interpreter * Suppress immortalization in `eval()` to avoid refleaks in three tests (test_datetime.test_roundtrip, test_logging.test_config8_ok, and test_random.test_after_fork). * frozenset() is constant, but not a singleton. When run multiple times, the test could fail due to constant interning.
1 parent 1306f33 commit 332356b

File tree

23 files changed

+36
-137
lines changed

23 files changed

+36
-137
lines changed

Include/internal/pycore_gc.h

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -342,14 +342,6 @@ struct _gc_runtime_state {
342342
collections, and are awaiting to undergo a full collection for
343343
the first time. */
344344
Py_ssize_t long_lived_pending;
345-
346-
/* gh-117783: Deferred reference counting is not fully implemented yet, so
347-
as a temporary measure we treat objects using deferred reference
348-
counting as immortal. The value may be zero, one, or a negative number:
349-
0: immortalize deferred RC objects once the first thread is created
350-
1: immortalize all deferred RC objects immediately
351-
<0: suppressed; don't immortalize objects */
352-
int immortalize;
353345
#endif
354346
};
355347

Include/internal/pycore_tstate.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ typedef struct _PyThreadStateImpl {
4141
// If set, don't use per-thread refcounts
4242
int is_finalized;
4343
} refcounts;
44+
45+
// When >1, code objects do not immortalize their non-string constants.
46+
int suppress_co_const_immortalization;
4447
#endif
4548

4649
#if defined(Py_REF_DEBUG) && defined(Py_GIL_DISABLED)

Lib/test/libregrtest/main.py

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

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

1312
from .cmdline import _parse_args, Namespace
1413
from .findtests import findtests, split_test_packages, list_cases
@@ -535,10 +534,7 @@ def _run_tests(self, selected: TestTuple, tests: TestList | None) -> int:
535534
if self.num_workers:
536535
self._run_tests_mp(runtests, self.num_workers)
537536
else:
538-
# gh-117783: don't immortalize deferred objects when tracking
539-
# refleaks. Only relevant for the free-threaded build.
540-
with suppress_immortalization(runtests.hunt_refleak):
541-
self.run_tests_sequentially(runtests)
537+
self.run_tests_sequentially(runtests)
542538

543539
coverage = self.results.get_coverage_results()
544540
self.display_result(runtests)

Lib/test/libregrtest/single.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -304,10 +304,7 @@ def run_single_test(test_name: TestName, runtests: RunTests) -> TestResult:
304304
result = TestResult(test_name)
305305
pgo = runtests.pgo
306306
try:
307-
# gh-117783: don't immortalize deferred objects when tracking
308-
# refleaks. Only relevant for the free-threaded build.
309-
with support.suppress_immortalization(runtests.hunt_refleak):
310-
_runtest(result, runtests)
307+
_runtest(result, runtests)
311308
except:
312309
if not pgo:
313310
msg = traceback.format_exc()

Lib/test/seq_tests.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,6 @@ def test_pickle(self):
426426
self.assertEqual(lst2, lst)
427427
self.assertNotEqual(id(lst2), id(lst))
428428

429-
@support.suppress_immortalization()
430429
def test_free_after_iterating(self):
431430
support.check_free_after_iterating(self, iter, self.type2test)
432431
support.check_free_after_iterating(self, reversed, self.type2test)

Lib/test/support/__init__.py

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -512,33 +512,6 @@ def has_no_debug_ranges():
512512
def requires_debug_ranges(reason='requires co_positions / debug_ranges'):
513513
return unittest.skipIf(has_no_debug_ranges(), reason)
514514

515-
@contextlib.contextmanager
516-
def suppress_immortalization(suppress=True):
517-
"""Suppress immortalization of deferred objects."""
518-
try:
519-
import _testinternalcapi
520-
except ImportError:
521-
yield
522-
return
523-
524-
if not suppress:
525-
yield
526-
return
527-
528-
_testinternalcapi.suppress_immortalization(True)
529-
try:
530-
yield
531-
finally:
532-
_testinternalcapi.suppress_immortalization(False)
533-
534-
def skip_if_suppress_immortalization():
535-
try:
536-
import _testinternalcapi
537-
except ImportError:
538-
return
539-
return unittest.skipUnless(_testinternalcapi.get_immortalize_deferred(),
540-
"requires immortalization of deferred objects")
541-
542515

543516
MS_WINDOWS = (sys.platform == 'win32')
544517

Lib/test/test_ast/test_ast.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2259,7 +2259,7 @@ def test_validation(self):
22592259
"got an invalid type in Constant: list")
22602260

22612261
def test_singletons(self):
2262-
for const in (None, False, True, Ellipsis, b'', frozenset()):
2262+
for const in (None, False, True, Ellipsis, b''):
22632263
with self.subTest(const=const):
22642264
value = self.compile_constant(const)
22652265
self.assertIs(value, const)

Lib/test/test_capi/test_misc.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
from test.support import threading_helper
2626
from test.support import warnings_helper
2727
from test.support import requires_limited_api
28-
from test.support import suppress_immortalization
2928
from test.support import expected_failure_if_gil_disabled
3029
from test.support import Py_GIL_DISABLED
3130
from test.support.script_helper import assert_python_failure, assert_python_ok, run_python_until_end
@@ -481,7 +480,6 @@ def test_heap_ctype_doc_and_text_signature(self):
481480
def test_null_type_doc(self):
482481
self.assertEqual(_testcapi.NullTpDocType.__doc__, None)
483482

484-
@suppress_immortalization()
485483
def test_subclass_of_heap_gc_ctype_with_tpdealloc_decrefs_once(self):
486484
class HeapGcCTypeSubclass(_testcapi.HeapGcCType):
487485
def __init__(self):
@@ -499,7 +497,6 @@ def __init__(self):
499497
del subclass_instance
500498
self.assertEqual(type_refcnt - 1, sys.getrefcount(HeapGcCTypeSubclass))
501499

502-
@suppress_immortalization()
503500
def test_subclass_of_heap_gc_ctype_with_del_modifying_dunder_class_only_decrefs_once(self):
504501
class A(_testcapi.HeapGcCType):
505502
def __init__(self):

Lib/test/test_capi/test_watchers.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from contextlib import contextmanager, ExitStack
55
from test.support import (
66
catch_unraisable_exception, import_helper,
7-
gc_collect, suppress_immortalization)
7+
gc_collect)
88

99

1010
# Skip this test if the _testcapi module isn't available.
@@ -404,7 +404,6 @@ def assert_event_counts(self, exp_created_0, exp_destroyed_0,
404404
self.assertEqual(
405405
exp_destroyed_1, _testcapi.get_code_watcher_num_destroyed_events(1))
406406

407-
@suppress_immortalization()
408407
def test_code_object_events_dispatched(self):
409408
# verify that all counts are zero before any watchers are registered
410409
self.assert_event_counts(0, 0, 0, 0)
@@ -451,7 +450,6 @@ def test_error(self):
451450
self.assertIsNone(cm.unraisable.object)
452451
self.assertEqual(str(cm.unraisable.exc_value), "boom!")
453452

454-
@suppress_immortalization()
455453
def test_dealloc_error(self):
456454
co = _testcapi.code_newempty("test_watchers", "dummy0", 0)
457455
with self.code_watcher(2):

Lib/test/test_code.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,7 @@
141141
ctypes = None
142142
from test.support import (cpython_only,
143143
check_impl_detail, requires_debug_ranges,
144-
gc_collect, Py_GIL_DISABLED,
145-
suppress_immortalization,
146-
skip_if_suppress_immortalization)
144+
gc_collect, Py_GIL_DISABLED)
147145
from test.support.script_helper import assert_python_ok
148146
from test.support import threading_helper, import_helper
149147
from test.support.bytecode_helper import instructions_with_positions
@@ -579,7 +577,6 @@ def test_interned_string_with_null(self):
579577

580578
@cpython_only
581579
@unittest.skipUnless(Py_GIL_DISABLED, "does not intern all constants")
582-
@skip_if_suppress_immortalization()
583580
def test_interned_constants(self):
584581
# compile separately to avoid compile time de-duping
585582

@@ -599,7 +596,6 @@ def func2():
599596

600597
class CodeWeakRefTest(unittest.TestCase):
601598

602-
@suppress_immortalization()
603599
def test_basic(self):
604600
# Create a code object in a clean environment so that we know we have
605601
# the only reference to it left.
@@ -850,7 +846,6 @@ def test_bad_index(self):
850846
self.assertEqual(GetExtra(f.__code__, FREE_INDEX+100,
851847
ctypes.c_voidp(100)), 0)
852848

853-
@suppress_immortalization()
854849
def test_free_called(self):
855850
# Verify that the provided free function gets invoked
856851
# when the code object is cleaned up.
@@ -878,7 +873,6 @@ def test_get_set(self):
878873
del f
879874

880875
@threading_helper.requires_working_threading()
881-
@suppress_immortalization()
882876
def test_free_different_thread(self):
883877
# Freeing a code object on a different thread then
884878
# where the co_extra was set should be safe.

Lib/test/test_descr.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5076,7 +5076,6 @@ def __new__(cls):
50765076
cls.lst = [2**i for i in range(10000)]
50775077
X.descr
50785078

5079-
@support.suppress_immortalization()
50805079
def test_remove_subclass(self):
50815080
# bpo-46417: when the last subclass of a type is deleted,
50825081
# remove_subclass() clears the internal dictionary of subclasses:

Lib/test/test_functools.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1992,7 +1992,6 @@ def f():
19921992
return 1
19931993
self.assertEqual(f.cache_parameters(), {'maxsize': 1000, "typed": True})
19941994

1995-
@support.suppress_immortalization()
19961995
def test_lru_cache_weakrefable(self):
19971996
@self.module.lru_cache
19981997
def test_function(x):

Lib/test/test_gc.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from test import support
44
from test.support import (verbose, refcount_test,
55
cpython_only, requires_subprocess,
6-
requires_gil_enabled, suppress_immortalization,
6+
requires_gil_enabled,
77
Py_GIL_DISABLED)
88
from test.support.import_helper import import_module
99
from test.support.os_helper import temp_dir, TESTFN, unlink
@@ -110,7 +110,6 @@ def test_tuple(self):
110110
del l
111111
self.assertEqual(gc.collect(), 2)
112112

113-
@suppress_immortalization()
114113
def test_class(self):
115114
class A:
116115
pass
@@ -119,7 +118,6 @@ class A:
119118
del A
120119
self.assertNotEqual(gc.collect(), 0)
121120

122-
@suppress_immortalization()
123121
def test_newstyleclass(self):
124122
class A(object):
125123
pass
@@ -136,7 +134,6 @@ class A:
136134
del a
137135
self.assertNotEqual(gc.collect(), 0)
138136

139-
@suppress_immortalization()
140137
def test_newinstance(self):
141138
class A(object):
142139
pass
@@ -223,7 +220,6 @@ class B(object):
223220
self.fail("didn't find obj in garbage (finalizer)")
224221
gc.garbage.remove(obj)
225222

226-
@suppress_immortalization()
227223
def test_function(self):
228224
# Tricky: f -> d -> f, code should call d.clear() after the exec to
229225
# break the cycle.
@@ -566,7 +562,6 @@ def test_get_referents(self):
566562

567563
self.assertEqual(gc.get_referents(1, 'a', 4j), [])
568564

569-
@suppress_immortalization()
570565
def test_is_tracked(self):
571566
# Atomic built-in types are not tracked, user-defined objects and
572567
# mutable containers are.
@@ -604,9 +599,7 @@ class UserFloatSlots(float):
604599
class UserIntSlots(int):
605600
__slots__ = ()
606601

607-
if not Py_GIL_DISABLED:
608-
# gh-117783: modules may be immortalized in free-threaded build
609-
self.assertTrue(gc.is_tracked(gc))
602+
self.assertTrue(gc.is_tracked(gc))
610603
self.assertTrue(gc.is_tracked(UserClass))
611604
self.assertTrue(gc.is_tracked(UserClass()))
612605
self.assertTrue(gc.is_tracked(UserInt()))

Lib/test/test_inspect/test_inspect.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
except ImportError:
3636
ThreadPoolExecutor = None
3737

38-
from test.support import cpython_only, import_helper, suppress_immortalization
38+
from test.support import cpython_only, import_helper
3939
from test.support import MISSING_C_DOCSTRINGS, ALWAYS_EQ
4040
from test.support.import_helper import DirsOnSysPath, ready_to_import
4141
from test.support.os_helper import TESTFN, temp_cwd
@@ -771,7 +771,6 @@ def test_getfile_builtin_function_or_method(self):
771771
inspect.getfile(list.append)
772772
self.assertIn('expected, got', str(e_append.exception))
773773

774-
@suppress_immortalization()
775774
def test_getfile_class_without_module(self):
776775
class CM(type):
777776
@property
@@ -2576,7 +2575,6 @@ def __getattribute__(self, attr):
25762575

25772576
self.assertFalse(test.called)
25782577

2579-
@suppress_immortalization()
25802578
def test_cache_does_not_cause_classes_to_persist(self):
25812579
# regression test for gh-118013:
25822580
# check that the internal _shadowed_dict cache does not cause

Lib/test/test_module/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import weakref
55
from test.support import gc_collect
66
from test.support import import_helper
7-
from test.support import suppress_immortalization
87
from test.support.script_helper import assert_python_ok
98

109
import sys
@@ -104,7 +103,6 @@ def f():
104103
gc_collect()
105104
self.assertEqual(f().__dict__["bar"], 4)
106105

107-
@suppress_immortalization()
108106
def test_clear_dict_in_ref_cycle(self):
109107
destroyed = []
110108
m = ModuleType("foo")
@@ -120,7 +118,6 @@ def __del__(self):
120118
gc_collect()
121119
self.assertEqual(destroyed, [1])
122120

123-
@suppress_immortalization()
124121
def test_weakref(self):
125122
m = ModuleType("foo")
126123
wr = weakref.ref(m)

Lib/test/test_ordered_dict.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import weakref
1313
from collections.abc import MutableMapping
1414
from test import mapping_tests, support
15-
from test.support import import_helper, suppress_immortalization
15+
from test.support import import_helper
1616

1717

1818
py_coll = import_helper.import_fresh_module('collections',
@@ -669,7 +669,6 @@ def test_dict_update(self):
669669
dict.update(od, [('spam', 1)])
670670
self.assertNotIn('NULL', repr(od))
671671

672-
@suppress_immortalization()
673672
def test_reference_loop(self):
674673
# Issue 25935
675674
OrderedDict = self.OrderedDict

Lib/test/test_struct.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import weakref
1111

1212
from test import support
13-
from test.support import import_helper, suppress_immortalization
13+
from test.support import import_helper
1414
from test.support.script_helper import assert_python_ok
1515
from test.support.testcase import ComplexesAreIdenticalMixin
1616

@@ -697,7 +697,6 @@ def __del__(self):
697697
self.assertIn(b"Exception ignored in:", stderr)
698698
self.assertIn(b"C.__del__", stderr)
699699

700-
@suppress_immortalization()
701700
def test__struct_reference_cycle_cleaned_up(self):
702701
# Regression test for python/cpython#94207.
703702

Lib/test/test_trace.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import os
22
from pickle import dump
33
import sys
4-
from test.support import captured_stdout, requires_resource, requires_gil_enabled
4+
from test.support import captured_stdout, requires_resource
55
from test.support.os_helper import (TESTFN, rmtree, unlink)
66
from test.support.script_helper import assert_python_ok, assert_python_failure
77
import textwrap
@@ -301,7 +301,6 @@ def test_loop_caller_importing(self):
301301

302302
@unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
303303
'pre-existing trace function throws off measurements')
304-
@requires_gil_enabled("gh-117783: immortalization of types affects traced method names")
305304
def test_inst_method_calling(self):
306305
obj = TracedClass(20)
307306
self.tracer.runfunc(obj.inst_method_calling, 1)
@@ -335,7 +334,6 @@ def setUp(self):
335334

336335
@unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
337336
'pre-existing trace function throws off measurements')
338-
@requires_gil_enabled("gh-117783: immortalization of types affects traced method names")
339337
def test_loop_caller_importing(self):
340338
self.tracer.runfunc(traced_func_importing_caller, 1)
341339

0 commit comments

Comments
 (0)