Skip to content

gc: PERRY_GEN_GC_EVACUATE=0 moves zero cells on all 12 gc-ratchet probes — the knob's off-state is unexercised #7611

Closed

Description

@proggeramlug

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:

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:

  1. 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
  2. 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

  1. proggeramlug commented on Aug 9, 2026

    @proggeramlug
    ContributorAuthor

    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_EVACUATE gated evacuation_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 from gc_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 ambient PERRY_GEN_GC_EVACUATE=0 silently disarmed PERRY_GC_ZEAL — the #7154 instrument. CLAUDE.md documented that as a caveat in the zeal row. Worse, zeal_implies_forced_evacuation had a precedence arm that took the ambient-off branch and returned 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_allowed is still false on every budgeted low-pause cycle (low_pause_non_moving), and budgeted_low_pause_minor_does_not_evacuate asserts 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.rs carries 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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