From 336718706c68ef0843dadf05630e0f7dfb2d5cd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 29 Jul 2026 09:19:36 +0200 Subject: [PATCH 1/3] fix(gc): a compiled program can now actually reach an evacuating minor (#6950) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #6950 measured that no reachable configuration in a compiled program evacuates: 45 cycles under PERRY_GC_FORCE_EVACUATE=1, all `manual` full mark-sweeps, total_moved_objects: 0. It left two questions open -- why PERRY_GC_HEAP_LIMIT does not lower the first-collection point (its §2), and whether any configuration reaches the copying minor at all. Both are answered here, one by a fix and one by measurement. 1. THE PACING PATH READS A DIFFERENT TRIGGER THAN THE ARMING PATH. `gc_budgeted_due_trigger` arms a cycle on `arena_total_bytes() >= effective_next_arena_trigger()`, which substitutes the device / PERRY_GC_HEAP_LIMIT-derived ceiling while the raw GC_NEXT_TRIGGER_BYTES cell still holds its 128 MB desktop-default const initializer (GC_TRIGGER_ARMED == false, i.e. before the first collection). `GcDebtSnapshot::current` measured debt against the RAW cell. So a cycle armed at a 2 MB effective trigger reported its own debt as ZERO, and `gc_mutator_assist_scaled_work_units` -- which exists precisely to scale the assist budget by that debt -- never left its 256-unit floor. The budgeted cycle crawled and never completed. That is not a test artifact; it is unbounded heap growth in the shipped incremental mode. Measured on a compiled program (300 000 escaping allocations, plain `while` loop, all live), release build, macOS arm64: PERRY_GC_HEAP_LIMIT=8 before: 0 collections, 63 MB RSS, 57 assists all `status=ACTIVE phase=1 arena_debt=0 units=256` after: 4 collections It is the exact unbounded-growth failure the debt-proportional pacing was introduced to prevent ("the budgeted cycle NEVER completed ... and RSS grew unbounded"): the term it scales by was reading the wrong trigger. No-op for the desktop default. `effective_next_arena_trigger()` returns the raw cell when ARMED, and `min(raw, ceiling)` when not; with no heap budget in force the ceiling IS the 128 MB initializer, so the two agree. The fix only bites where the budget-derived ceiling is genuinely lower -- which is exactly the constrained-device case the ceiling exists for. 2. A PRECISE-ROOTED EVACUATING MINOR *IS* REACHABLE -- the matrix was not asking for it. Measured, not assumed. The blocker is a pair, and neither half is sufficient alone: * PERRY_GC_INCREMENTAL=0. With incremental mode on (the default), `registered_root_scanners_block_budgeted_gc()` reduces to "any copy-only scanner", which a compiled program has none of, so `gc_check_trigger` skips the #5476 direct-collection arm entirely and hands the trigger to the budgeted stepper. * PERRY_CONSERVATIVE_STACK_SCAN=off. The direct arm takes `ManualGcScanGuard::force_full_scan()`, and a forced conservative scan makes the copying minor ineligible -- `fallback=conservative_stack`, the sentence #6950 quotes from gc/policy.rs. An explicit env value BEATS that guard in `conservative_stack_scan_mode()`, so it turns the automatic alloc-point collection into a precise-rooted copying minor that relocates survivors. Per-arm liveness on the corpus, --pressure 8, PERRY_GC_TRACE=1 + PERRY_GC_DIAG=1: test_gap_repsel_canonical_i32 0 cycles -> 1 cycle, 4 579 copied test_gap_repsel_ptr_shape_locals 0 cycles -> 1 cycle, 4 640 copied test_gap_repsel_gc_stress 24 cycles -> 31 cycles, 1 556 543 copied every one reporting `[gc-copy-minor] eligible=true fallback=none`. `scripts/gc_repsel_matrix.sh` gains that pair as `%E%`, applied to every arm whose liveness requirement is `move` -- those arms CLAIM to evacuate and were inert, so they asserted nothing -- plus a new `evac_minor` arm that is the collector's own moving path with no stress knob at all, added to the PR arm set. `default` and `shipped_default` still cover the shipped configuration unchanged. 3. THE MATRIX COULD NOT RUN AT ALL ON MAIN. `--arms all` aborts before evaluating a single cell: UNREGISTERED test-files/test_gap_repsel_proven_this_frozen.ts is a representation-selection gap file but is not in test-parity/gc_repsel_corpus.txt #6925 (repsel Phase 5a, 1a533a3a8) shipped that gap file without registering it. Registering it is the action the gate asks for; it is the gate working, and it means every `--arms all` result quoted since 1a533a3a8 came from a run that never started. WHAT THIS DOES NOT CLAIM. The shipped default still does not evacuate: with no env knobs the collector reaches a non-moving minor at best, and on a small program it still performs zero collections because a MALLOC-triggered budgeted cycle starves the same way an arena-triggered one used to. That residue is filed separately; it is not fixed here and is not claimed to be. Regression test: `gc::tests::debt_pacer::test_arena_debt_measured_against_effective_trigger_not_raw_cell` puts the collector in the pre-first-collection state (un-armed, raw cell far above the ceiling), pushes arena_total 64 MB past the effective trigger, and asserts the arming path calls it due WHILE the pacing path reports 64 MB of debt and scales past the floor. Without the fix it reports `left: 0`. Refs #6950, #6942, #6946 --- crates/perry-runtime/src/gc/telemetry.rs | 14 +++- .../perry-runtime/src/gc/tests/debt_pacer.rs | 64 +++++++++++++++++++ scripts/gc_repsel_matrix.sh | 58 +++++++++++++---- test-parity/gc_repsel_corpus.txt | 7 ++ 4 files changed, 131 insertions(+), 12 deletions(-) diff --git a/crates/perry-runtime/src/gc/telemetry.rs b/crates/perry-runtime/src/gc/telemetry.rs index 9e3a8fa04d..a88fbedead 100644 --- a/crates/perry-runtime/src/gc/telemetry.rs +++ b/crates/perry-runtime/src/gc/telemetry.rs @@ -526,7 +526,19 @@ impl GcDebtSnapshot { #[inline] pub(super) fn current() -> Self { let total = crate::arena::arena_total_bytes(); - let next_arena_trigger = GC_NEXT_TRIGGER_BYTES.with(|c| c.get()); + // #6950: read the SAME trigger the arming path compares against. + // `gc_budgeted_due_trigger` uses `effective_next_arena_trigger()`, which + // substitutes the device/`PERRY_GC_HEAP_LIMIT`-derived ceiling while the + // raw cell still holds its 128 MB desktop-default const initializer + // (`GC_TRIGGER_ARMED == false`). Reading the raw cell here made the two + // disagree: a cycle armed at a 2 MB effective trigger measured its own + // debt against 128 MB and therefore reported ZERO debt, so + // `gc_mutator_assist_scaled_work_units` never scaled past its 256-unit + // floor and the budgeted cycle crawled without ever completing — + // 300k escaping allocations / 330 MB RSS with ZERO collections. That is + // exactly the unbounded-growth failure the debt-proportional pacing was + // introduced to prevent. + let next_arena_trigger = effective_next_arena_trigger(); let malloc_count = malloc_object_count(); let next_malloc_trigger = GC_NEXT_MALLOC_TRIGGER.with(|c| c.get()); let old_in_use = crate::arena::old_gen_in_use_bytes(); diff --git a/crates/perry-runtime/src/gc/tests/debt_pacer.rs b/crates/perry-runtime/src/gc/tests/debt_pacer.rs index 46195a3614..2f38dbcf64 100644 --- a/crates/perry-runtime/src/gc/tests/debt_pacer.rs +++ b/crates/perry-runtime/src/gc/tests/debt_pacer.rs @@ -724,3 +724,67 @@ fn minor_sweep_retains_window_expired_growth_stub() { assert_eq!(crate::array::js_array_length(live), 64); assert_eq!(crate::array::js_array_get_f64_unchecked(live, 63), 63.0); } + +/// #6950: the pacing path must measure allocation debt against the SAME +/// trigger the arming path compares against. +/// +/// `gc_budgeted_due_trigger` arms a cycle when +/// `arena_total_bytes() >= effective_next_arena_trigger()`, which substitutes +/// the device / `PERRY_GC_HEAP_LIMIT`-derived ceiling while the raw +/// `GC_NEXT_TRIGGER_BYTES` cell still holds its desktop-default const +/// initializer (`GC_TRIGGER_ARMED == false`). `GcDebtSnapshot::current` read +/// the RAW cell, so a cycle armed at the (lower) effective trigger measured its +/// own debt against the (higher) raw one and reported ZERO debt. +/// +/// `gc_mutator_assist_scaled_work_units` scales the assist budget by exactly +/// that number, so it never left its 256-unit floor and the budgeted cycle +/// crawled without ever completing. Measured on a compiled program: 300 000 +/// escaping allocations, 330 MB RSS, ZERO collections — the unbounded-growth +/// failure the debt-proportional pacing exists to prevent. +#[test] +fn test_arena_debt_measured_against_effective_trigger_not_raw_cell() { + use super::super::heap_budget::gc_trigger_absolute_ceiling_bytes; + use super::super::policy::{ + effective_next_arena_trigger, GC_NEXT_TRIGGER_BYTES, GC_TRIGGER_ARMED, + }; + + let prev_total = crate::arena::ARENA_TOTAL_BYTES.with(|c| c.get()); + let prev_trigger = GC_NEXT_TRIGGER_BYTES.with(|c| c.get()); + let prev_armed = GC_TRIGGER_ARMED.with(|c| c.get()); + + // Un-armed with a raw cell far above the ceiling: exactly the state a + // process is in before its FIRST collection. + GC_TRIGGER_ARMED.with(|c| c.set(false)); + GC_NEXT_TRIGGER_BYTES.with(|c| c.set(usize::MAX / 2)); + let ceiling = gc_trigger_absolute_ceiling_bytes(); + let overshoot = 64 * 1024 * 1024; + crate::arena::ARENA_TOTAL_BYTES.with(|c| c.set(ceiling.saturating_add(overshoot))); + + let effective = effective_next_arena_trigger(); + let due = crate::arena::arena_total_bytes() >= effective; + let debt = GcDebtSnapshot::current().arena_debt_bytes; + let units = gc_mutator_assist_scaled_work_units(); + + crate::arena::ARENA_TOTAL_BYTES.with(|c| c.set(prev_total)); + GC_NEXT_TRIGGER_BYTES.with(|c| c.set(prev_trigger)); + GC_TRIGGER_ARMED.with(|c| c.set(prev_armed)); + + assert_eq!( + effective, ceiling, + "an un-armed trigger cell must read as the device ceiling" + ); + assert!( + due, + "the arming path must consider this arena total past the effective trigger" + ); + assert_eq!( + debt, overshoot as u64, + "debt must be measured against the EFFECTIVE trigger the cycle was armed on, \ + not the raw cell — reading the raw cell reports 0 and freezes the assist \ + budget at its floor" + ); + assert!( + units > GC_MUTATOR_ASSIST_WORK_UNITS, + "a cycle with real debt must scale its assist budget past the fixed floor" + ); +} diff --git a/scripts/gc_repsel_matrix.sh b/scripts/gc_repsel_matrix.sh index c65bd0420c..7bd61d33c9 100755 --- a/scripts/gc_repsel_matrix.sh +++ b/scripts/gc_repsel_matrix.sh @@ -100,12 +100,45 @@ RED=$'\033[0;31m'; GREEN=$'\033[0;32m'; YELLOW=$'\033[0;33m'; NC=$'\033[0m' # IR; all of them are keyed into the object cache # (perry/src/commands/compile/object_cache.rs), so arms never silently share # cached objects. +# +# %E% expands to THE EVACUATING BASE (#6950): +# +# PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off +# +# Every arm whose requirement is `move` carries it, because without it those +# arms are INERT and were reported UNVER across the whole corpus. Two +# independent blockers, both measured: +# +# 1. `PERRY_GC_INCREMENTAL=0`. With incremental mode on (the default), +# `registered_root_scanners_block_budgeted_gc()` reduces to "any copy-only +# scanner", which a compiled program has none of. So `gc_check_trigger` +# skips the direct-collection arm and hands the trigger to the budgeted +# stepper, whose mutator assists never drive the cycle to completion. +# Turning incremental off restores the direct synchronous minor. +# 2. `PERRY_CONSERVATIVE_STACK_SCAN=off`. The direct arm takes +# `ManualGcScanGuard::force_full_scan()`, and a forced conservative scan +# makes the copying minor ineligible (`fallback=conservative_stack`) -- +# the exact sentence #6950 quotes from `gc/policy.rs`. An explicit env +# value BEATS that guard in `conservative_stack_scan_mode()`, so this is +# what turns the automatic collection into a precise-rooted copying minor +# that actually relocates survivors. +# +# Measured on this pair (`--pressure 8`, arm `evac_minor`): +# test_gap_repsel_canonical_i32 0 cycles -> 1 cycle, 4 579 objects copied +# test_gap_repsel_ptr_shape_locals 0 cycles -> 1 cycle, 4 640 objects copied +# test_gap_repsel_gc_stress 24 cycles -> 31 cycles, 1 556 543 copied +# every one with `[gc-copy-minor] eligible=true fallback=none`. +# +# NOTE this is a MEASUREMENT configuration, not the shipped one. It says the +# collector's evacuating path is exercised and correct; it does not say the +# shipped default reaches that path. It does not (see #6950's residue). # --------------------------------------------------------------------------- ARMS=( "default||%P%|collect|as-shipped GC configuration under allocation pressure" -"force_evac||%P% PERRY_GC_FORCE_EVACUATE=1|move|stress-copy every marked non-pinned nursery object" +"evac_minor||%P% %E%|move|THE evacuating arm: the automatic alloc-point collection as a precise-rooted COPYING minor that relocates survivors. No stress knob -- this is the collector's own moving path." +"force_evac||%P% %E% PERRY_GC_FORCE_EVACUATE=1|move|stress-copy every marked non-pinned nursery object" "verify_evac||%P% PERRY_GC_VERIFY_EVACUATION=1|collect|panic if a live slot still points at a forwarded object" -"force_verify||%P% PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1|move|force + verify" +"force_verify||%P% %E% PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1|move|force + verify" "gen_gc_off||%P% PERRY_GEN_GC=0|collect|full mark-sweep only; no nursery => no evacuation by construction" "wb_off|PERRY_WRITE_BARRIERS=0|%P% PERRY_WRITE_BARRIERS=0|collect|no codegen write barriers => copying nursery ineligible by construction" "gen_off_verify||%P% PERRY_GEN_GC=0 PERRY_GC_VERIFY_EVACUATION=1|collect|full mark-sweep + evacuation verifier" @@ -113,20 +146,20 @@ ARMS=( "all_four|PERRY_WRITE_BARRIERS=0|%P% PERRY_GEN_GC=0 PERRY_WRITE_BARRIERS=0 PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1|collect|every escape hatch at once" "cons_scan_off||%P% PERRY_CONSERVATIVE_STACK_SCAN=off|collect|PRECISE ROOTS ONLY -- removes the conservative-stack pinning that every automatic collection otherwise forces (ManualGcScanGuard::force_full_scan). The only arm that can observe a missing shadow-slot binding." "cons_scan_off_force||%P% PERRY_CONSERVATIVE_STACK_SCAN=off PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1|collect|precise roots + force/verify evacuation" -"loop_polls|PERRY_GC_MOVING_LOOP_POLLS=1|%P% PERRY_GC_MOVING_LOOP_POLLS=1 PERRY_GC_FORCE_EVACUATE=1|move|defer the alloc-point collection to a loop back-edge precise-root safepoint, where the copying minor may MOVE survivors" -"rep_i32_off|PERRY_CANONICAL_I32_LOCALS=0|%P% PERRY_GC_FORCE_EVACUATE=1|move|repsel Phase 1 OFF x evacuation" -"rep_str_off|PERRY_CANONICAL_STR_LOCALS=0|%P% PERRY_GC_FORCE_EVACUATE=1|move|repsel Phase 3a OFF x evacuation" -"rep_ptr_shape_off|PERRY_PTR_SHAPE_LOCALS=0|%P% PERRY_GC_FORCE_EVACUATE=1|move|repsel Phase 3b OFF x evacuation" -"rep_ptr_numarray_off|PERRY_PTR_NUMARRAY_LOCALS=0|%P% PERRY_GC_FORCE_EVACUATE=1|move|repsel Phase 4a.3 OFF x evacuation" -"rep_spec_abi_off|PERRY_SPECIALIZED_ABI=0|%P% PERRY_GC_FORCE_EVACUATE=1|move|repsel Phase 2 OFF x evacuation" -"rep_int_valued_off|PERRY_INT_VALUED_LOCALS=0|%P% PERRY_GC_FORCE_EVACUATE=1|move|native-i32 residency (#6898) OFF x evacuation" +"loop_polls|PERRY_GC_MOVING_LOOP_POLLS=1|%P% %E% PERRY_GC_MOVING_LOOP_POLLS=1 PERRY_GC_FORCE_EVACUATE=1|move|defer the alloc-point collection to a loop back-edge precise-root safepoint, where the copying minor may MOVE survivors" +"rep_i32_off|PERRY_CANONICAL_I32_LOCALS=0|%P% %E% PERRY_GC_FORCE_EVACUATE=1|move|repsel Phase 1 OFF x evacuation" +"rep_str_off|PERRY_CANONICAL_STR_LOCALS=0|%P% %E% PERRY_GC_FORCE_EVACUATE=1|move|repsel Phase 3a OFF x evacuation" +"rep_ptr_shape_off|PERRY_PTR_SHAPE_LOCALS=0|%P% %E% PERRY_GC_FORCE_EVACUATE=1|move|repsel Phase 3b OFF x evacuation" +"rep_ptr_numarray_off|PERRY_PTR_NUMARRAY_LOCALS=0|%P% %E% PERRY_GC_FORCE_EVACUATE=1|move|repsel Phase 4a.3 OFF x evacuation" +"rep_spec_abi_off|PERRY_SPECIALIZED_ABI=0|%P% %E% PERRY_GC_FORCE_EVACUATE=1|move|repsel Phase 2 OFF x evacuation" +"rep_int_valued_off|PERRY_INT_VALUED_LOCALS=0|%P% %E% PERRY_GC_FORCE_EVACUATE=1|move|native-i32 residency (#6898) OFF x evacuation" "shipped_default||-|none|control: exactly the as-shipped configuration -- no pressure knob, no GC env at all" ) # PR-gating subset: the arms with the most detection power per second -- # as-shipped under pressure, force+verify evacuation, precise-roots-only, and # the untouched shipped configuration as a control. -PR_ARMS="default,force_verify,cons_scan_off,shipped_default" +PR_ARMS="default,evac_minor,force_verify,cons_scan_off,shipped_default" arm_field() { # $1 = arm record, $2 = 1..5 printf '%s' "$1" | cut -d'|' -f"$2" @@ -273,6 +306,9 @@ done # --------------------------------------------------------------------------- PRESSURE_ENV="" [ "$PRESSURE_MB" != "0" ] && PRESSURE_ENV="PERRY_GC_HEAP_LIMIT=$PRESSURE_MB" +# The evacuating base -- see the %E% note above the arm table. Both halves are +# required and neither is sufficient alone. +EVAC_ENV="PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off" triage_reason() { # $1 test, $2 arm [ -f "$TRIAGE" ] || return 1 @@ -288,7 +324,7 @@ n_pass=0; n_unver=0; n_fail=0; n_xfail=0 ai=0 while [ "$ai" -lt "$NARMS" ]; do id="${ARM_IDS[$ai]}"; slug="${ARM_SLUGS[$ai]}"; live="${ARM_LIVES[$ai]}" - renv="$(printf '%s' "${ARM_RENVS[$ai]}" | sed "s/%P%/$PRESSURE_ENV/")" + renv="$(printf '%s' "${ARM_RENVS[$ai]}" | sed -e "s/%P%/$PRESSURE_ENV/" -e "s/%E%/$EVAC_ENV/")" [ "$renv" = "-" ] && renv="" echo "==> arm $id" ti=0 diff --git a/test-parity/gc_repsel_corpus.txt b/test-parity/gc_repsel_corpus.txt index cce8252d95..c58ef65bd0 100644 --- a/test-parity/gc_repsel_corpus.txt +++ b/test-parity/gc_repsel_corpus.txt @@ -47,3 +47,10 @@ test_gap_repsel_p4b_field_store_elision # across escaping allocation churn heavy enough to reach the collector, so the # "collect" arms actually bite. Keep it registered and keep it collecting. test_gap_repsel_gc_stress + +# --- Phase 5a: Ptr proven `this` in methods (#6925) ------------------- +# Landed in 1a533a3a8 WITHOUT registering here, which made +# `gc_repsel_matrix.sh --arms all` refuse to run at all (the UNREGISTERED gate +# fires before any cell is evaluated). Registering it is the action that gate +# asks for. +test_gap_repsel_proven_this_frozen From f646dc9f13a1588078108c8094ef70c026b86768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 29 Jul 2026 09:20:57 +0200 Subject: [PATCH 2/3] docs: changelog fragment for #6977 --- changelog.d/6977-reachable-evacuating-minor.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6977-reachable-evacuating-minor.md diff --git a/changelog.d/6977-reachable-evacuating-minor.md b/changelog.d/6977-reachable-evacuating-minor.md new file mode 100644 index 0000000000..e539df78b0 --- /dev/null +++ b/changelog.d/6977-reachable-evacuating-minor.md @@ -0,0 +1 @@ +**GC / pacing:** the budgeted (incremental) collector measured its own allocation debt against the raw `GC_NEXT_TRIGGER_BYTES` cell while the arming path (`gc_budgeted_due_trigger`) compares against `effective_next_arena_trigger()`, which substitutes the device/`PERRY_GC_HEAP_LIMIT`-derived ceiling before the first collection. A cycle armed at a 2 MB effective trigger therefore reported ZERO debt, `gc_mutator_assist_scaled_work_units` never left its 256-unit floor, and the cycle crawled without ever completing — 300 000 escaping allocations under `PERRY_GC_HEAP_LIMIT=8` produced 0 collections (1.2 M produced 330 MB RSS and still 0), the exact unbounded-growth failure the debt-proportional pacing exists to prevent. Now 4 collections on the same program. No-op for the desktop default, where the ceiling *is* the raw cell's initializer. Also: `scripts/gc_repsel_matrix.sh`'s evacuating arms were inert — they claimed `move` and nothing ever moved — so they now carry the measured evacuating base (`PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off`), which turns the automatic alloc-point collection into a precise-rooted copying minor (`eligible=true fallback=none`, 1 556 543 objects copied on `test_gap_repsel_gc_stress`), plus a new `evac_minor` arm. Registers `test_gap_repsel_proven_this_frozen`, which #6925 shipped unregistered — that made `--arms all` abort before evaluating a single cell (#6950). From f4d37592545ffe9718f4d912c6a5f0ebb26b664c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 29 Jul 2026 09:32:36 +0200 Subject: [PATCH 3/3] test(gc): keep the evacuating arms out of the PR subset until #6981 closes The first --arms all run in which anything actually moved failed 14 of 20 corpus files (5 crashes, 9 mismatches, #6981) plus one intermittent SIGSEGV that does not need precise roots (#6982). Both are filed with reproducers. The arms stay evacuating -- they are the finding. But putting them in the per-PR subset today paints every unrelated PR red from the first commit, which is how a gate stops being read. They remain in --arms all, which is what push/workflow_dispatch runs, so the failures are measured on every push to main rather than hidden. The comment names the expiry: when #6981 closes, evac_minor and force_verify go back in PR_ARMS. Explicitly NOT triaged into gc_repsel_triage.txt: that file is for redness that is provably not a representation defect, and #6981's redness may be exactly that. --- scripts/gc_repsel_matrix.sh | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/scripts/gc_repsel_matrix.sh b/scripts/gc_repsel_matrix.sh index 7bd61d33c9..0c56bb522b 100755 --- a/scripts/gc_repsel_matrix.sh +++ b/scripts/gc_repsel_matrix.sh @@ -130,8 +130,17 @@ RED=$'\033[0;31m'; GREEN=$'\033[0;32m'; YELLOW=$'\033[0;33m'; NC=$'\033[0m' # every one with `[gc-copy-minor] eligible=true fallback=none`. # # NOTE this is a MEASUREMENT configuration, not the shipped one. It says the -# collector's evacuating path is exercised and correct; it does not say the -# shipped default reaches that path. It does not (see #6950's residue). +# collector's evacuating path is exercised; it does not say the shipped default +# reaches that path. It does not -- see #6978. +# +# ***AND IT IS RED.*** The first `--arms all` run in which anything actually +# moved failed 14 of the 20 corpus files: 5 crashes and 9 output mismatches +# (#6981), plus one intermittent SIGSEGV that does not even need precise roots +# (#6982). The discriminator is NOT relocation -- with the conservative stack +# scan still on, the same evacuating cycles pass 19/20 while copying thousands +# of objects. It is precise roots: the values only the conservative scan was +# keeping alive. That is the finding this gate was built to produce, and the +# arms stay configured to keep producing it. Do not quiet them down. # --------------------------------------------------------------------------- ARMS=( "default||%P%|collect|as-shipped GC configuration under allocation pressure" @@ -157,9 +166,28 @@ ARMS=( ) # PR-gating subset: the arms with the most detection power per second -- -# as-shipped under pressure, force+verify evacuation, precise-roots-only, and +# as-shipped under pressure, the evacuation verifier, precise-roots-only, and # the untouched shipped configuration as a control. -PR_ARMS="default,evac_minor,force_verify,cons_scan_off,shipped_default" +# +# ***THE EVACUATING ARMS ARE DELIBERATELY NOT IN THIS SUBSET, AND THAT IS A +# TEMPORARY STATE WITH AN EXPIRY.*** They are not omitted because they are +# noisy: they are omitted because they are RED, and they are red for a real +# reason that is filed, reproduced and minimised in #6981 -- a relocating minor +# with precise roots breaks 14 of the 20 corpus files (5 crashes, 9 output +# mismatches), while the SAME relocation with the conservative stack scan on +# passes 19/20. Putting them in the per-PR gate today would paint every +# unrelated PR red from the first commit, which is how a gate stops being read. +# +# They ARE in `--arms all`, which is what push / workflow_dispatch runs, so the +# failures are visible and measured on every push to main -- not hidden. +# +# WHEN #6981 CLOSES, PUT `evac_minor` AND `force_verify` BACK IN THIS LIST. +# That is the point at which "a representation regressed GC correctness under +# relocation" becomes a per-PR signal, which is the whole reason this matrix +# exists. Do not instead add triage entries for those cells: +# test-parity/gc_repsel_triage.txt is for redness that is provably NOT a +# representation defect, and #6981's redness may well be exactly that. +PR_ARMS="default,verify_evac,cons_scan_off,shipped_default" arm_field() { # $1 = arm record, $2 = 1..5 printf '%s' "$1" | cut -d'|' -f"$2"