From 00179a57ebba111a7c83969e52374a49b8de4e50 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Wed, 21 Oct 2026 00:37:23 -0400 Subject: [PATCH] test: flush GraalPy's inline caches before judging collectability (#1963) test_cache_is_per_class_and_collectable failed on GraalPy 25.4.4 in sharded runs. The class is not freed late: graalpython's LookupAttributeInMRONode.SlowPath.lookupConstantMRO caches the receiver class strongly, up to AttributeAccessInlineCacheMaxDepth classes, until lookupCachedLen replaces it (graal-25.4.4). So a class measured while a node in the nested-type walk still has room stays reachable however long gc.collect is given. Unsharded runs passed because earlier tests had already overflowed those caches. On GraalPy only, when the first collection leaves the class alive, send throwaway classes of the same shape through the walk and collect again. The smallest flush that frees the class every time equals the option's value, measured at the default and a raised one; the constant is headroom. A reinstated module-level WeakKeyDictionary pin still fails on both interpreters. CPython runs the check unchanged. --- .../corekit/test_from_dict_roundtrip_unit.py | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/tests/corekit/test_from_dict_roundtrip_unit.py b/tests/corekit/test_from_dict_roundtrip_unit.py index 4f74e81fa0..7aff521d61 100644 --- a/tests/corekit/test_from_dict_roundtrip_unit.py +++ b/tests/corekit/test_from_dict_roundtrip_unit.py @@ -24,13 +24,55 @@ """ import gc +import sys import unittest from tests._support import reimport_once_per_class -#: Test tier (#1958): hand-picked round-trip cases, not a sweep. +#: Test tier (#1958): hand-picked round-trip cases, not a sweep, and a +#: collectability check that branches on the interpreter. __tier__ = 'ALL' +#: Throwaway classes :func:`release_inline_caches` sends through +#: :func:`~pcapkit.corekit.infoclass._nested_info_types` before a class's +#: collectability is judged (#1963). GraalPy's ``lookupConstantMRO`` +#: specialisation of ``LookupAttributeInMRONode.SlowPath`` caches the receiver +#: class as ``@Cached("klass") Object cachedKlass``, a strong reference where its +#: sibling ``lookupConstantMROCached`` passes ``weak = true``, for up to +#: ``AttributeAccessInlineCacheMaxDepth`` classes (default 5, ``PythonOptions``), +#: until ``lookupCachedLen`` replaces it (oracle/graalpython, ``graalpython/ +#: com.oracle.graal.python/src/com/oracle/graal/python/nodes/attributes/ +#: LookupAttributeInMRONode.java`` at tag ``graal-25.4.4``). ``cls.mro()`` on a +#: plain class reaches it. Measured on GraalPy 25.4.4: such a class stays reachable +#: through repeated :func:`gc.collect` calls until other classes pass the same call +#: sites; the number left reachable tracks that option when it is set; and the +#: smallest flush that frees the class every time equals the option's value. The +#: constant is headroom over the default for a runner that raises the option. +#: CPython keeps no such cache, so it gets none. +INLINE_CACHE_FLUSH = 64 if sys.implementation.name == 'graalpy' else 0 + + +def release_inline_caches() -> None: + """Overflow the interpreter's inline caches along the nested-type walk. + + Each throwaway class is shaped like the one under test, so it takes the same + path through :func:`~pcapkit.corekit.infoclass.info_final` and + :func:`~pcapkit.corekit.infoclass._nested_info_types`. A strong reference + :mod:`pcapkit` itself keeps does not let go of the class however many pass. + + """ + from pcapkit.corekit.infoclass import Info, _nested_info_types, info_final + + for _ in range(INLINE_CACHE_FLUSH): + @info_final + class Throwaway(Info): + nxt: int + + Throwaway.__annotations__['nxt'] = Throwaway + _nested_info_types(Throwaway) + del Throwaway + + #: PCAP-NG blocks, little-endian, one per block schema that claims a type code. #: Each nests its body in :attr:`PCAPNG.block #: `, which is the @@ -256,6 +298,9 @@ class Node(Info): ref = weakref.ref(Node) del Node gc.collect() + if INLINE_CACHE_FLUSH and ref() is not None: + release_inline_caches() + gc.collect() self.assertIsNone(ref()) def test_a_subclass_does_not_inherit_its_parents_map(self) -> None: