Repository navigation
gc: PERRY_GEN_GC_EVACUATE=0 moves zero cells on all 12 gc-ratchet probes — the knob's off-state is unexercised #7611
Description
Found while trying to sabotage-verify the repaired GC ratchet (#7609, for #7554).
The intent was an end-to-end proof that the restored gate can go red: disable an
evacuation knob, re-measure, watch the counters collapse. It did not collapse
anything.
The measurement
Pinned quiet host perry-macos, main 26b9c9d59 (0.5.1346), identical
binaries and protocol in both arms (--repeats 7, warmup 1,
PERRY_NO_AUTO_OPTIMIZE=1, prebuilt static archives), the only difference being
the knob:
arm A: PERRY_RUNTIME_DIR=... PERRY_NO_AUTO_OPTIMIZE=1
arm B: PERRY_RUNTIME_DIR=... PERRY_NO_AUTO_OPTIMIZE=1 PERRY_GEN_GC_EVACUATE=0
Cell-by-cell diff of the two measurements over all 12 probes and
minor_cycles, step_cycles, copied_objects, copied_bytes,
promoted_objects, promoted_bytes, freed_bytes, heap_used_bytes:
cells that moved: 0
Zero. Not "small deltas inside the band" — bit-identical medians on every one
of the 96 cells. gc_ratchet.py check against the pinned baseline returns exit
0 with the knob set.
For contrast, the same procedure with PERRY_GEN_GC=0 moved 79 cells and
returned exit 1 with 90 findings, so the method is sound and the harness is
sensitive — this knob specifically changes nothing the ratchet can see.
Why this matters
CLAUDE.md's GC knob kill-policy is explicit: "Every GC env knob either has a
required CI arm exercising its OFF state, or it is deleted after one release of
soak… A mode that still exists is a decision that hasn't been made." It also
records two precedents of exactly this shape:
PERRY_GC_FORCE_EVACUATEwas inert for everygc()-driven test
(gc: no reachable configuration exercises an evacuating minor with unpinned runtime locals — the #6655/#6935 bug class is untestable #6942/GC testing: PERRY_GC_FORCE_EVACUATE is inert for gc()-driven tests (full mark-sweep + forced conservative scan) — stress claims may be unsupported #6946) — months of "passes under evacuation" meant nothing.- The matrix's
--pressureknob disabled the very path it was measuring
(gc-matrix: --pressure disables the very path #7019 added — the 'default' arm runs ZERO copying minors on all 22 corpus rows #7024).
This is the same failure mode a third time. The practical consequence: any past
or future claim of the form "the suite still passes with
PERRY_GEN_GC_EVACUATE=0" carries no information about the GC ratchet's twelve
probes, because the knob does not perturb them.
What I did not establish
I have not confirmed the mechanism from the code. The plausible reading is
that the knob gates the C4b policy evacuation of tenured objects into old-gen,
while copied_objects/copied_bytes count the copying minor's scavenge, which
is a different path — so the probes may simply never reach the gated branch. That
is a hypothesis from CLAUDE.md's description, not something these runs prove.
Worth confirming before acting.
Suggested acceptance, in kill-policy terms
Either:
- The knob's OFF state gets an arm that demonstrably exercises it — a workload
where policy evacuation actually fires, with a liveness assertion (moved > 0
or equivalent) so the arm cannot pass having run zero policy evacuations; or - The OFF state and the branch behind it are deleted, so the losing mode stops
compiling rather than lingering as an untested configuration a future bisect
will trust.
Either way, if a GC probe suite is meant to be evidence about evacuation, at
least one probe should be able to tell whether evacuation happened.
Activity
fix(gc-ratchet): fail open per cell, fail closed on the verdict; re-p…
gc: the evacuation knobs stop lying about what they gate (#7611, #6946,
proggeramlug commentedon Aug 9, 2026
Your measurement holds, the hypothesis is confirmed from the code, and the knob is deleted. PR #7684.
Mechanism (the "not established" part).
PERRY_GEN_GC_EVACUATEgatedevacuation_policy_allowed: the C4b tenured→old-gen policy evacuation in the non-copying fallback minor (GcCycleState::new_minor_fallback) plus the old-page defrag selection. Every counter the ratchet reads —copied_objects,copied_bytes,promoted_objects,promoted_bytes— comes fromgc_collect_minor_copying_fast_path, which is reached before the fallback and which the knob never gated. Exactly your reading.Chose option 2 (delete), not option 1, and for a reason beyond "nobody exercises it". Its one unique live effect was a footgun: it vetoed
gc_force_evacuate_enabled(), so an ambientPERRY_GEN_GC_EVACUATE=0silently disarmedPERRY_GC_ZEAL— the #7154 instrument. CLAUDE.md documented that as a caveat in the zeal row. Worse,zeal_implies_forced_evacuationhad a precedence arm that took the ambient-off branch andreturned without exercising zeal at all: the test for the interaction was itself vacuous under the configuration it was testing. It now has one arm and that arm always runs.The branch is not deleted with the knob, because it has another controller that IS exercised.
evacuation_policy_allowedis still false on every budgeted low-pause cycle (low_pause_non_moving), andbudgeted_low_pause_minor_does_not_evacuateasserts that arm behaviourally: nothing moved, no forwarding stub, old-page selection skipped,trace.evacuation_policy.reason == "low_pause_non_moving". So the losing mode still compiles and still has a test; what stopped existing is the untested configuration.crates/perry-runtime/src/gc/mod.rscarries the full writeup at the site where the function used to be, including your 0-of-96 numbers, so the next person who reaches for an escape hatch here reads why there isn't one.gc: the evacuation knobs stop lying about what they gate (#7611, #6946,
docs(gc): stop documenting the deleted PERRY_GEN_GC_EVACUATE knob
gc: the evacuation knobs stop lying about what they gate (#7611, #6946,
feat(gc): seeded GC-schedule fuzzing (PERRY_GC_SCHEDULE_SEED)
test(gc): drop the PERRY_GEN_GC_EVACUATE veto branches — the knob was…
Make a rare garbage-collector bug reproducible on demand (PERRY_GC_SC…