You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
test(runtime): prototype_addr_cache flake — the memoized prototype addresses are a process-global cache of a thread-local heap address #7955
gc::tests::runtime_roots::prototype_addr_cache::prototype_addr_reads_through_a_forwarding_stub fails roughly 1 run in 100 of cargo test --release -p perry-runtime at default parallelism. It is the one member of the #7946 flaky set that #7954 did not fix, and it is left named here rather than silently.
Message:
array_prototype_addr must resolve the GC forwarding chain: a stale from-space
address is a DIFFERENT address for the SAME object than every
clean_arr_ptr-resolved receiver ...
left: 4005900094920
right: 4007898578952
Cause
ARRAY_PROTO_ADDR / OBJECT_PROTO_ADDR (crates/perry-runtime/src/array/indexing.rs) are process-global AtomicUsize caches holding raw addresses into a thread-local arena. The test plants a synthetic forwarded stub in one of them and reads it back; concurrently, any other libtest thread can
call array_prototype_addr() / object_prototype_addr(), which heals the cache in place, or
run a collection, whose registered scan_prototype_addr_cache_roots_mut rewrites the slot,
and either overwrites what this test planted.
That the cache is process-wide at all is the deeper point: with perry/thread agents the address it holds is only meaningful on the thread that wrote it (this is the "runtime-side cache of a raw heap pointer" shape CLAUDE.md's known-weak-areas section describes).
The obvious fix — per_test_global! the two caches, as that PR does for the element-shape counters — deterministically broke two unrelated tests, 200 runs out of 200:
and introduced four intermittent promise::native_async::tests::* failures (~15 in 200 runs) that are not present on main.
The likely mechanism: PerThread materialisation is lazy, so the first read of the cache on a given thread misses and runs the full resolution path — js_get_global_this_builtin_value(b"Object"), i.e. a globalThis bootstrap, allocating objects. object_prototype_addr() is reachable from inside a GC root scan (scan_prototype_addr_cache_roots_mut) and from note_object_prototype_index_write on the array index-write path, so that bootstrap lands in places the shared cache never let it reach. This was reverted rather than shipped half-understood.
Suggested direction
Either
eagerly materialise the per-thread instance outside any collection (e.g. at thread/arena init) so no read can trigger a bootstrap, or
Whichever way, the acceptance criterion is the same as #7946's: ≥200 consecutive full-suite runs green at default parallelism, with the dead_owner_side_tables and promise::native_async families explicitly checked, not just the target test.
Activity
test(runtime): stop five process-globals leaking across libtest threa…
test(runtime): stop five process-globals leaking across libtest threa…
test(runtime): fix the last two suite flakes — shared prototype-addre…