Repository navigation
gc: PERRY_GC_OLD_DEFRAG=0 (the rollback path for default-on old-page relocation) is exercised by no test and no CI arm #7917
Description
Summary
#7913 flipped old-page relocation on by default (PERRY_GC_OLD_DEFRAG=0 is the documented rollback). The change itself audits well (see below), but its OFF state — the rollback path — is exercised by nothing, and the PR deleted the one automated assertion that previously covered it.
This is a direct violation of the binding policy in CLAUDE.md:
Every GC env knob either has a required CI arm exercising its OFF state, or it is deleted after one release of soak.
Evidence
- No test sets the knob off.
OLD_DEFRAG_TEST_OVERRIDE(gc/oldgen_defrag.rs:86) is only ever set toSome(true)(line 97) or reset toNone(line 105). It is neverSome(false). - The codebase's own convention is the opposite. Sibling knobs test their OFF state:
gc/barrier_arming.rs:178setsSome(false),gc/policy.rs:614setsSome(false). PERRY_GC_OLD_DEFRAGappears in exactly one file onmain—crates/perry-runtime/src/gc/oldgen_defrag.rs. It is referenced by no workflow, no script, and no test.- The OFF path's only prior automated assertion was deleted by this same PR.
scripts/gc_root_dominance_check.py::_probe_old_defrag_off_by_defaultasserted thatselect_old_page_defrag_pagesshort-circuits to an empty selection when disabled. Deleting it was correct — its assertion ("off by default") is now false by design — but nothing replaced it with the inverse assertion.
Why this matters concretely
The OFF state is the escape hatch for a change that relocates old-generation objects in production. If the rollback path is broken, there is no way to fall back and nothing that would reveal it. This repo has the precedent on file: a kill switch whose broken OFF state was itself what exposed a default-path bug.
Compounding: no GC gate ran
At the time of writing, every GC workflow is queued/pending and none has executed — gc-ratchet, gc-moving-witnesses, gc-root-dominance, gc-native-roots, gc-ptr-shape-off-witness, oldest queued 05:27. So a change that turns on old-generation object relocation by default merged with zero GC gates having run. That is CLAUDE.md's failure mode (3)/(4) — a gate that cannot fail because it never executes.
Requested
- A test that runs the defrag path with the override set to
falseand assertsselect_old_page_defrag_pagesreturns an empty selection and no old-gen object moves. - A CI arm exercising
PERRY_GC_OLD_DEFRAG=0end-to-end on the corpus, or an explicit decision to delete the knob per the kill-policy's second half. - Sabotage-verify it: break the short-circuit and confirm the new test goes red.
What audits WELL in #7913 (recorded so this is not mistaken for a rejection)
scripts/gc_root_dominance_allowlist.jsonis still empty — the ~40 suppressed@perry_class_keys_*hits were genuinely fixed, not re-suppressed.- The holder policy was tightened, not loosened:
open_gapandunverifiedverdicts now FAIL while relocation ships enabled. - The two holders whose safety explicitly rested on "only old-gen defrag can move them" (
PARSE_KEY_RINGGC: enumerate the unrooted runtime-side caches — the population no static checker can see #7231,DIAG_CHANNEL_BY_KEY) were fixed, not exempted. - It honored the tripwire the deleted exemption left behind — that exemption's own
becomes_real_whennamed exactly this trigger ("PERRY_GC_OLD_DEFRAG ships on by default"). - The ON path asserts its subject was live:
assert_eq!(moved.old_page_moved_objects, 2)and== live_headers.len(), plusassert!(!source_blocks.block_indices.is_empty()). - It tests the actual hazard — that an unmarked old-gen neighbour is still moved, i.e. a minor trace cannot treat it as dead.
Activity
gc: idle-time old-gen compaction at the park site (#9644)
gc: drop excluded holes once per evacuation, not once per object (#9644)