gc-native-roots's headline arm — the probe matrix under
PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1 — is green for reasons
that have almost nothing to do with statepoint roots.
Measured at e98dfdb25 on macOS/aarch64, one row per probe, summed over every
gc_cycle the run emits (PERRY_GC_TRACE=1, same env the gate uses):
| probe |
sm walks |
records matched |
locations visited |
conservative roots |
copied |
moved |
| 01_nursery_churn |
1 |
0 |
0 |
14 |
0 |
0 |
| 02_survivor_promotion |
1 |
1 |
1 |
8 |
0 |
0 |
| 03_cross_gen_writes |
3 |
2 |
2 |
18 |
0 |
0 |
| 04_dead_after_deep_stack |
1 |
0 |
0 |
100 |
0 |
0 |
| 05_closure_capture |
1 |
0 |
0 |
6 |
0 |
0 |
| 06_string_retention |
1 |
0 |
0 |
10 |
0 |
0 |
| 07_array_grow_evacuate |
1 |
0 |
0 |
6 |
0 |
0 |
| 08_map_set_sidetables |
1 |
1 |
2 |
9 |
0 |
0 |
(09_try_catch_roots cannot compile under the explicit bridge since #7330, so
it is not measurable on current main. An older binary of it matched 71 records
/ 71 locations — by far the most of any probe, which is its own argument about
what the suite is missing.)
Three separate problems, each independently sufficient to make the arm assert
much less than it appears to:
1. Nothing ever moves — copied = 0, moved = 0, on every probe
evacuation.moved_objects and copying_nursery.copied_objects are zero
everywhere, and evacuation_policy reports force: false, considered: false,
reason: "not_evaluated" — with PERRY_GC_FORCE_EVACUATE=1 set.
This is #6942/#6946 exactly, and CLAUDE.md already names it: "PERRY_GC_FORCE_EVACUATE
was inert for every gc()-driven test — it is read only on the minor path,
while gc() runs a full mark-sweep with a forced conservative scan." The probes
drive collection with explicit gc(), so the knob is inert here.
A relocating collector that relocates nothing cannot exercise the one thing
statepoints exist for: rewriting a root after its object moves. The
PERRY_GC_VERIFY_EVACUATION=1 half is then trivially satisfied — there is no
forwarded object for any slot to still point at.
2. The conservative stack scan is doing the rooting
root_sources.native_stack_fallback reports decision: "scan", scanned: true,
and 6–100 roots_found per probe — while native_stack_maps.records_matched is
0 on five of the eight. On those five the precise mechanism contributes
nothing and the run is correct because the conservative scan found the roots.
That is a safety net doing its job, but it means "the probe matched the oracle"
is not evidence the stack maps are right. It would look identical if the compact
map were empty.
3. The existing liveness assert cannot catch either
scripts/gc_walker_trace_assert.py --require-fp-walks asserts fp_walks > 0 —
that a walk happened. Every row above has walks: 1 and would pass it, including
the five that matched zero records and visited zero locations. CLAUDE.md's
fourth failure mode, in the gate written to avoid it: "A gate must assert its
subject was live, not merely that nothing threw."
What would make the arm mean something
Why this matters now
The stated plan is to make statepoints the default root mechanism and delete the
shadow stack. The evidence for that is this gate. As measured, the gate does not
distinguish "the statepoint roots are correct" from "the conservative scan
covered for them and nothing moved anyway".
gc-native-roots's headline arm — the probe matrix underPERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1— is green for reasonsthat have almost nothing to do with statepoint roots.
Measured at
e98dfdb25on macOS/aarch64, one row per probe, summed over everygc_cyclethe run emits (PERRY_GC_TRACE=1, same env the gate uses):(
09_try_catch_rootscannot compile under the explicit bridge since #7330, soit is not measurable on current
main. An older binary of it matched 71 records/ 71 locations — by far the most of any probe, which is its own argument about
what the suite is missing.)
Three separate problems, each independently sufficient to make the arm assert
much less than it appears to:
1. Nothing ever moves —
copied = 0,moved = 0, on every probeevacuation.moved_objectsandcopying_nursery.copied_objectsare zeroeverywhere, and
evacuation_policyreportsforce: false,considered: false,reason: "not_evaluated"— withPERRY_GC_FORCE_EVACUATE=1set.This is #6942/#6946 exactly, and CLAUDE.md already names it: "
PERRY_GC_FORCE_EVACUATEwas inert for every
gc()-driven test — it is read only on the minor path,while
gc()runs a full mark-sweep with a forced conservative scan." The probesdrive collection with explicit
gc(), so the knob is inert here.A relocating collector that relocates nothing cannot exercise the one thing
statepoints exist for: rewriting a root after its object moves. The
PERRY_GC_VERIFY_EVACUATION=1half is then trivially satisfied — there is noforwarded object for any slot to still point at.
2. The conservative stack scan is doing the rooting
root_sources.native_stack_fallbackreportsdecision: "scan",scanned: true,and 6–100
roots_foundper probe — whilenative_stack_maps.records_matchedis0 on five of the eight. On those five the precise mechanism contributes
nothing and the run is correct because the conservative scan found the roots.
That is a safety net doing its job, but it means "the probe matched the oracle"
is not evidence the stack maps are right. It would look identical if the compact
map were empty.
3. The existing liveness assert cannot catch either
scripts/gc_walker_trace_assert.py --require-fp-walksassertsfp_walks > 0—that a walk happened. Every row above has
walks: 1and would pass it, includingthe five that matched zero records and visited zero locations. CLAUDE.md's
fourth failure mode, in the gate written to avoid it: "A gate must assert its
subject was live, not merely that nothing threw."
What would make the arm mean something
records_matched > 0andlocations_visited > 0, per probe, notjust
fp_walks > 0.copied_objects > 0ormoved_objects > 0— the gc-matrix: four of the six PR-gating arms run ZERO copying minors corpus-wide (default/verify_evac/cons_scan_off at copy-minor 0/49; header still claims 12/22) #7255 rule — so an armnamed "forced evacuation" fails when nothing was evacuated.
PERRY_GC_FORCE_EVACUATEis live at all, i.e.not an explicit
gc(). Compile-timePERRY_GC_MOVING_LOOP_POLLS=1plusPERRY_GC_ZEAL=1is the documented way to get moving minors.compiled frame. The one probe that matched records in bulk is the
try/catch one, and it is currently uncompilable (ci: native-roots-aarch64 is red on main — #7330's invoke refusal makes 09_try_catch_roots uncompilable in five of its steps #7335).
Why this matters now
The stated plan is to make statepoints the default root mechanism and delete the
shadow stack. The evidence for that is this gate. As measured, the gate does not
distinguish "the statepoint roots are correct" from "the conservative scan
covered for them and nothing moved anyway".