Skip to content

gc: the native-roots forced-evacuation arm is vacuous — nothing moves, and 5 of 8 probes match zero stack-map records #7336

Description

@proggeramlug

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".

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions