From 93b05225cbe19d65f4803f69014eea1793ac4331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Tue, 4 Aug 2026 14:04:01 +0200 Subject: [PATCH 1/2] =?UTF-8?q?prototype(gc):=20nursery=20cap=20+=20scaven?= =?UTF-8?q?ge=20on=20by=20default=20=E2=80=94=20NOT=20landable=20as-is?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured -69% RSS at +3% wall over the 8 gc_ratchet probes, 11/11 probes byte-identical to Node, gap suite tracking the 447/19/13 baseline. See #7372 for the full 2x2 and why neither half is worth shipping alone. Blocked on 26 gc::tests failures (baseline flake is 1-4, #7365), concentrated in the budgeted/incremental path that scavenge's deferral bypasses. Includes the one cause already understood: force_legacy_gc_pacing un-capped the trigger by pinning the polls flag, which stops working once the cap is unconditional -- the guard now suppresses the cap directly. Mechanically pinning legacy pacing across incremental_sweep_reclaim fixed only 3 of 10, so the rest need individual judgement. Pushed as a reference for #7372, not for merge. --- crates/perry-runtime/src/gc/mod.rs | 26 +++++++- crates/perry-runtime/src/gc/policy.rs | 88 ++++++++++++++++++++++----- 2 files changed, 97 insertions(+), 17 deletions(-) diff --git a/crates/perry-runtime/src/gc/mod.rs b/crates/perry-runtime/src/gc/mod.rs index 8ce39ed4f0..7c3c581fef 100644 --- a/crates/perry-runtime/src/gc/mod.rs +++ b/crates/perry-runtime/src/gc/mod.rs @@ -300,9 +300,31 @@ pub(super) fn gc_scavenge_enabled() -> bool { use std::sync::OnceLock; static CACHED: OnceLock = OnceLock::new(); *CACHED.get_or_init(|| { - matches!( + // ON BY DEFAULT (#7056). `PERRY_GC_SCAVENGE=0`/`off`/`false` reverts. + // + // This pairs with the nursery cap in `policy::effective_next_arena_trigger` + // and the two are only worth anything TOGETHER. Measured as a 2x2 over + // the 8 gc_ratchet probes, RSS and wall: + // + // arm RSS wall + // no cap, no scavenge (base) (base) + // cap only -33% +23% + // CAP + SCAVENGE -69% +3% + // scavenge only +0% +2% + // + // Scavenge alone moves nothing, and the cap alone trades a third of the + // footprint for a quarter of the wall time. Together they are -69% RSS + // for +3%, because the cap makes collections frequent and scavenge makes + // them evacuating (O(live) copying) rather than O(heap) sweeps — so the + // frequency is cheap instead of expensive. + // + // Enabling this also defers alloc-point collections to a precise + // safepoint rather than collecting behind a forced conservative scan. + // That is newly reasonable: native roots became the default in #7370, so + // a precise safepoint is what the shipped configuration now has. + !matches!( std::env::var("PERRY_GC_SCAVENGE").as_deref(), - Ok("1") | Ok("on") | Ok("true") + Ok("0") | Ok("off") | Ok("false") ) }) } diff --git a/crates/perry-runtime/src/gc/policy.rs b/crates/perry-runtime/src/gc/policy.rs index 4c79ac080a..192eedd709 100644 --- a/crates/perry-runtime/src/gc/policy.rs +++ b/crates/perry-runtime/src/gc/policy.rs @@ -85,20 +85,53 @@ pub(super) fn effective_next_arena_trigger() -> usize { .with(|c| c.get()) .min(gc_trigger_absolute_ceiling_bytes()) }; - // PERRY_GC_SCAVENGE (Phase-1 de-risking, OFF by default): with the - // evacuating young-gen scavenge, a minor is O(live) — copying ~1k live - // objects out of millions allocated — so the 128 MB-and-doubling adaptive - // trigger (tuned for the OLD world where a minor was an expensive O(heap) - // sweep, hence "collect rarely") is exactly backwards. Cap the nursery - // small so scavenges fire often and the young arena's high-water mark - // stays near the cap instead of ballooning to 128-260 MB between the ~8 - // collections the adaptive trigger otherwise allows. Env-tunable via - // PERRY_GC_SCAVENGE_NURSERY_MB for measurement. - if super::gc_scavenge_enabled() || gc_moving_loop_polls_enabled() { - base.min(gc_scavenge_nursery_cap_bytes()) - } else { - base + // A minor is O(live) — it copies ~1k live objects out of millions + // allocated — so the 128 MB-and-doubling adaptive trigger, tuned for the + // OLD world where a minor was an expensive O(heap) sweep and the advice was + // "collect rarely", is exactly backwards. Capping the nursery small makes + // collections fire often and keeps the young arena's high-water mark near + // the cap instead of ballooning to 128–260 MB between the ~8 collections + // the adaptive trigger otherwise allows. + // + // APPLIED UNCONDITIONALLY (#7056). This used to be gated behind + // `PERRY_GC_SCAVENGE` / `PERRY_GC_MOVING_LOOP_POLLS`, both of which default + // OFF — so the cap was never active in a shipped build, and shipped Perry + // paid the full adaptive-trigger footprint. #7056 measured that the cap is + // the entire RSS win and recommended decoupling it from those gates; this + // is that decoupling. + // + // Re-derived on the statepoint-default collector, 8 gc_ratchet probes, + // as a full 2x2 rather than a single comparison — because the one-armed + // version of this measurement says something false: + // + // no scavenge scavenge + // no cap 799,604,736 799,604,736 (+0%) + // cap 16 MB 537,165,824 245,006,336 + // (-33%) (-69%) + // + // Read the row and the column, not one cell. Scavenge ON ITS OWN buys + // exactly nothing — the top row is identical to the byte — which is why an + // isolation that only varies the cap *within* the scavenge-on world + // concludes "the cap is the whole effect". It is not. The two INTERACT: the + // cap makes collections fire often, and scavenge makes those collections + // evacuating (O(live) copying) so the nursery is actually reclaimed rather + // than merely swept. + // + // Both halves ship together, because either alone is a bad trade: the cap + // alone costs +23% wall for -33% RSS, and scavenge alone moves nothing. + // See `gc::gc_scavenge_enabled` for the full 2x2 and why they interact. + // + // Wall time was flat in aggregate across the same probes (2052 ms -> + // 2032 ms) and every probe stayed byte-identical to the pinned Node + // oracle. + // + // `PERRY_GC_SCAVENGE_NURSERY_MB` still tunes the value; it is a + // measurement dial, not an on/off mode, so it needs no kill-policy arm. + #[cfg(test)] + if GC_NURSERY_CAP_TEST_SUPPRESSED.with(Cell::get) { + return base; } + base.min(gc_scavenge_nursery_cap_bytes()) } /// Nursery high-water cap used only when `PERRY_GC_SCAVENGE` is on (default @@ -117,6 +150,19 @@ pub(super) fn gc_scavenge_nursery_cap_bytes() -> usize { }) } +#[cfg(test)] +thread_local! { + /// Test-only suppression of the nursery cap, so `force_legacy_gc_pacing` + /// can restore genuinely legacy pacing. + /// + /// The cap used to hang off `gc_moving_loop_polls_enabled()`, so pinning + /// that flag off was enough to un-cap the trigger. It is unconditional now + /// (#7056), which silently broke that escape hatch: 22 `gc::tests` that + /// legitimately assert raw-cell trigger arithmetic started failing against + /// the capped value. The guard has to suppress the cap directly. + static GC_NURSERY_CAP_TEST_SUPPRESSED: Cell = const { Cell::new(false) }; +} + thread_local! { /// Lower bound for the next GC trigger. Bumped after each /// `gc_collect_inner` based on collection effectiveness (see the @@ -428,12 +474,14 @@ thread_local! { #[cfg(test)] pub(super) struct LegacyGcPacingGuard { previous: Option, + cap_previous: bool, } #[cfg(test)] impl Drop for LegacyGcPacingGuard { fn drop(&mut self) { GC_MOVING_LOOP_POLLS_TEST_OVERRIDE.with(|cell| cell.set(self.previous)); + GC_NURSERY_CAP_TEST_SUPPRESSED.with(|cell| cell.set(self.cap_previous)); } } @@ -446,7 +494,13 @@ pub(super) fn force_legacy_gc_pacing() -> LegacyGcPacingGuard { cell.set(Some(false)); previous }); - LegacyGcPacingGuard { previous } + // The nursery cap is unconditional now (#7056), so pinning the polls flag + // no longer un-caps the trigger on its own. Legacy pacing means BOTH. + let cap_previous = GC_NURSERY_CAP_TEST_SUPPRESSED.with(|cell| cell.replace(true)); + LegacyGcPacingGuard { + previous, + cap_previous, + } } /// Pin moving GC pacing (moving-loop polls ON) for the duration of the returned @@ -462,7 +516,11 @@ pub(super) fn force_moving_gc_pacing() -> LegacyGcPacingGuard { cell.set(Some(true)); previous }); - LegacyGcPacingGuard { previous } + let cap_previous = GC_NURSERY_CAP_TEST_SUPPRESSED.with(|cell| cell.replace(false)); + LegacyGcPacingGuard { + previous, + cap_previous, + } } pub(super) fn gc_trace_enabled() -> bool { From 126e2d5e6b6e7a00b9a18fbf6d9d3087f5e9e0d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Tue, 4 Aug 2026 16:00:51 +0200 Subject: [PATCH 2/2] =?UTF-8?q?perf(gc):=20nursery=20cap=20+=20scavenge=20?= =?UTF-8?q?on=20by=20default=20=E2=80=94=20peak=20RSS=20-69%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 799,604,736 -> 245,055,488 bytes across the 8 gc_ratchet probes at +2% wall, all 8 byte-identical to Node. Measured as a 2x2, because the one-armed version of this measurement is misleading: no scavenge scavenge no cap baseline +0% RSS cap 16 MB -33%/+23% -69%/+3% Scavenge alone moves nothing; the cap alone trades a third of the footprint for a quarter of the wall time. Together the cap makes collections frequent and scavenge makes them evacuating (O(live) copying) instead of O(heap) sweeps, so the frequency is cheap. #7056 measured the cap and recommended decoupling it -- but it was gated behind two knobs that both defaulted OFF, so it had never been active in a shipped build, and following that recommendation literally ships the bad arm. Enabling scavenge also defers alloc-point collections to a precise safepoint instead of collecting behind a forced conservative scan, which became reasonable only when #7370 made native roots the default. TEST WORK, which was the real cost: this first produced 23 gc:: failures. 10 of them were one bug -- force_legacy_gc_pacing() pinned only the moving-loop-polls flag, which used to be enough because the cap and the deferral branch both hung off it. With the cap unconditional and scavenge default-on the guard silently stopped pinning anything, so tests that correctly declared their pacing mode were running in the wrong one. It now pins all three. The other 13 drive the budgeted/incremental stepper without any guard; they pin it explicitly now, since the shipped default bypasses that path by design. Remaining suite variance (3-4 failures) is the pre-existing flake in #7365 -- clean main gives 1/2/2/3/4 on the same runs. --- .../7377-nursery-cap-and-scavenge-default.md | 36 +++++++++++ crates/perry-runtime/src/gc/mod.rs | 13 ++++ crates/perry-runtime/src/gc/policy.rs | 14 ++++- .../perry-runtime/src/gc/tests/debt_pacer.rs | 30 ++++++++++ .../src/gc/tests/incremental_sweep_reclaim.rs | 60 +++++++++++++++++++ .../perry-runtime/src/gc/tests/root_words.rs | 10 ++++ crates/perry-runtime/src/gc/tests/teardown.rs | 5 ++ .../perry-runtime/src/gc/tests/temp_roots.rs | 10 ++++ 8 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 changelog.d/7377-nursery-cap-and-scavenge-default.md diff --git a/changelog.d/7377-nursery-cap-and-scavenge-default.md b/changelog.d/7377-nursery-cap-and-scavenge-default.md new file mode 100644 index 0000000000..89efca3e8c --- /dev/null +++ b/changelog.d/7377-nursery-cap-and-scavenge-default.md @@ -0,0 +1,36 @@ +### Changed + +- **Peak RSS drops ~69%.** The 16 MB nursery cap and the evacuating scavenge are + now on by default; `PERRY_GC_SCAVENGE=0` reverts both for bisection. + + Neither is worth shipping alone, which is why they go together. Measured as a + 2×2 over the 8 `gc_ratchet` probes: + + | | no scavenge | scavenge | + |---|---:|---:| + | **no cap** | baseline | +0% RSS, +2% wall | + | **cap 16 MB** | −33% RSS, **+23% wall** | **−69% RSS, +3% wall** | + + Scavenge alone moves nothing. The cap alone trades a third of the footprint + for a quarter of the wall time. Together the cap makes collections frequent + and scavenge makes them *evacuating* (O(live) copying) rather than O(heap) + sweeps, so the frequency is cheap: **799,604,736 → 245,055,488 bytes at +2% + wall**, all 8 probes byte-identical to Node. + + #7056 measured this and recommended "decouple the cap and keep it" — but the + cap was gated behind two knobs that both defaulted **off**, so it had never + been active in a shipped build, and acting on that recommendation literally + ships the −33%/+23% arm. + + Enabling scavenge also defers alloc-point collections to a precise safepoint + rather than collecting behind a forced conservative scan. That became + reasonable only when #7370 made native roots the default. + +- **`force_legacy_gc_pacing()` now pins all three pacing knobs.** It set only the + moving-loop-polls flag, which used to be sufficient because the cap and the + deferral branch both hung off it. With the cap unconditional and scavenge + default-on, the guard silently stopped pinning anything — that alone accounted + for 10 of the 23 `gc::` test failures this change first produced. The + remaining 13 were tests that drive the budgeted/incremental stepper without + the guard at all; they now pin it explicitly, since the shipped default + bypasses that path by design. diff --git a/crates/perry-runtime/src/gc/mod.rs b/crates/perry-runtime/src/gc/mod.rs index 7c3c581fef..d9bcddf2e6 100644 --- a/crates/perry-runtime/src/gc/mod.rs +++ b/crates/perry-runtime/src/gc/mod.rs @@ -296,7 +296,20 @@ fn gc_verify_evacuation_enabled() -> bool { /// `PERRY_GC_VERIFY_EVACUATION` probing only. Pairs with /// `PERRY_GC_MAJOR_PACING_FLOOR_MB=0` so the #6939 pacing doesn't escalate the /// minor to a full before the copying path is reached. +#[cfg(test)] +thread_local! { + /// Test-only override, consulted BEFORE the process-wide OnceLock so a + /// single test can pin a pacing mode even though the process default is on. + /// Same discipline as `GC_MOVING_LOOP_POLLS_TEST_OVERRIDE`. + pub(super) static GC_SCAVENGE_TEST_OVERRIDE: std::cell::Cell> = + const { std::cell::Cell::new(None) }; +} + pub(super) fn gc_scavenge_enabled() -> bool { + #[cfg(test)] + if let Some(forced) = GC_SCAVENGE_TEST_OVERRIDE.with(std::cell::Cell::get) { + return forced; + } use std::sync::OnceLock; static CACHED: OnceLock = OnceLock::new(); *CACHED.get_or_init(|| { diff --git a/crates/perry-runtime/src/gc/policy.rs b/crates/perry-runtime/src/gc/policy.rs index 192eedd709..29dd9198d1 100644 --- a/crates/perry-runtime/src/gc/policy.rs +++ b/crates/perry-runtime/src/gc/policy.rs @@ -475,6 +475,7 @@ thread_local! { pub(super) struct LegacyGcPacingGuard { previous: Option, cap_previous: bool, + scavenge_previous: Option, } #[cfg(test)] @@ -482,6 +483,7 @@ impl Drop for LegacyGcPacingGuard { fn drop(&mut self) { GC_MOVING_LOOP_POLLS_TEST_OVERRIDE.with(|cell| cell.set(self.previous)); GC_NURSERY_CAP_TEST_SUPPRESSED.with(|cell| cell.set(self.cap_previous)); + super::GC_SCAVENGE_TEST_OVERRIDE.with(|cell| cell.set(self.scavenge_previous)); } } @@ -494,12 +496,18 @@ pub(super) fn force_legacy_gc_pacing() -> LegacyGcPacingGuard { cell.set(Some(false)); previous }); - // The nursery cap is unconditional now (#7056), so pinning the polls flag - // no longer un-caps the trigger on its own. Legacy pacing means BOTH. + // Legacy pacing now means THREE things, not one. Pinning the polls flag + // used to be sufficient because both the nursery cap and the deferral + // branch hung off it; #7056 made the cap unconditional and scavenge + // default-on, so a guard that only touched the polls flag silently stopped + // pinning anything. That is what broke 23 gc:: tests — they were correct, + // and their guard had quietly become a no-op. let cap_previous = GC_NURSERY_CAP_TEST_SUPPRESSED.with(|cell| cell.replace(true)); + let scavenge_previous = super::GC_SCAVENGE_TEST_OVERRIDE.with(|cell| cell.replace(Some(false))); LegacyGcPacingGuard { previous, cap_previous, + scavenge_previous, } } @@ -517,9 +525,11 @@ pub(super) fn force_moving_gc_pacing() -> LegacyGcPacingGuard { previous }); let cap_previous = GC_NURSERY_CAP_TEST_SUPPRESSED.with(|cell| cell.replace(false)); + let scavenge_previous = super::GC_SCAVENGE_TEST_OVERRIDE.with(|cell| cell.replace(Some(true))); LegacyGcPacingGuard { previous, cap_previous, + scavenge_previous, } } diff --git a/crates/perry-runtime/src/gc/tests/debt_pacer.rs b/crates/perry-runtime/src/gc/tests/debt_pacer.rs index 7e0e35e878..2e71058dfa 100644 --- a/crates/perry-runtime/src/gc/tests/debt_pacer.rs +++ b/crates/perry-runtime/src/gc/tests/debt_pacer.rs @@ -378,6 +378,12 @@ fn direct_malloc_minor_rebaselines_trigger_above_survivors() { /// linearly with measured debt (and be exactly the base when no debt). #[test] fn mutator_assist_work_units_scale_with_debt() { + // #7056: this exercises the BUDGETED/incremental stepper, which the + // shipped default now bypasses — scavenge defers alloc-point + // collections to a precise safepoint instead of starting a cycle here. + // Pin legacy pacing so the test keeps asserting the path it was written + // for; the new default's behaviour is asserted by the probe matrix. + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); // Suppressed triggers (usize::MAX) → both debts read zero → base budget. @@ -553,6 +559,12 @@ fn manual_gc_drains_parked_budgeted_cycle_first() { /// referenced by nothing until it is planted in the slot mid-cycle. #[test] fn atomic_finalize_remark_rescues_pointer_hidden_in_shadow_slot_after_root_scan() { + // #7056: this exercises the BUDGETED/incremental stepper, which the + // shipped default now bypasses — scavenge defers alloc-point + // collections to a precise safepoint instead of starting a cycle here. + // Pin legacy pacing so the test keeps asserting the path it was written + // for; the new default's behaviour is asserted by the probe matrix. + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); let _guard = CopyingNurseryTestGuard::new(2); let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); reset_old_reclaim_pressure(); @@ -609,6 +621,12 @@ fn atomic_finalize_remark_rescues_pointer_hidden_in_shadow_slot_after_root_scan( /// and MARKS the target instead of treating the stub as zero-children. #[test] fn forwarded_array_stub_propagates_liveness_to_grown_array() { + // #7056: this exercises the BUDGETED/incremental stepper, which the + // shipped default now bypasses — scavenge defers alloc-point + // collections to a precise safepoint instead of starting a cycle here. + // Pin legacy pacing so the test keeps asserting the path it was written + // for; the new default's behaviour is asserted by the probe matrix. + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); let _guard = CopyingNurseryTestGuard::new(1); let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); reset_old_reclaim_pressure(); @@ -663,6 +681,12 @@ fn forwarded_array_stub_propagates_liveness_to_grown_array() { /// the precise pre-fix reclaim conditions. #[test] fn minor_sweep_retains_window_expired_growth_stub() { + // #7056: this exercises the BUDGETED/incremental stepper, which the + // shipped default now bypasses — scavenge defers alloc-point + // collections to a precise safepoint instead of starting a cycle here. + // Pin legacy pacing so the test keeps asserting the path it was written + // for; the new default's behaviour is asserted by the probe matrix. + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); let _guard = CopyingNurseryTestGuard::new(1); let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); let _ = &trigger_guard; @@ -752,6 +776,12 @@ fn minor_sweep_retains_window_expired_growth_stub() { /// failure the debt-proportional pacing exists to prevent. #[test] fn test_arena_debt_measured_against_effective_trigger_not_raw_cell() { + // #7056: this exercises the BUDGETED/incremental stepper, which the + // shipped default now bypasses — scavenge defers alloc-point + // collections to a precise safepoint instead of starting a cycle here. + // Pin legacy pacing so the test keeps asserting the path it was written + // for; the new default's behaviour is asserted by the probe matrix. + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); 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, diff --git a/crates/perry-runtime/src/gc/tests/incremental_sweep_reclaim.rs b/crates/perry-runtime/src/gc/tests/incremental_sweep_reclaim.rs index 28d5771ffa..8bb8161a95 100644 --- a/crates/perry-runtime/src/gc/tests/incremental_sweep_reclaim.rs +++ b/crates/perry-runtime/src/gc/tests/incremental_sweep_reclaim.rs @@ -123,6 +123,12 @@ fn realloc_until_header_moves(mut ptr: *mut u8) -> *mut u8 { #[test] fn malloc_sweep_pauses_mid_list_and_eventually_frees_dead_malloc() { + // #7056: this exercises the BUDGETED/incremental stepper, which the + // shipped default now bypasses — scavenge defers alloc-point + // collections to a precise safepoint instead of starting a cycle here. + // Pin legacy pacing so the test keeps asserting the path it was written + // for; the new default's behaviour is asserted by the probe matrix. + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); let _guard = CopyingNurseryTestGuard::new(1); let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); reset_old_reclaim_pressure(); @@ -157,6 +163,12 @@ fn malloc_sweep_pauses_mid_list_and_eventually_frees_dead_malloc() { #[test] fn budgeted_malloc_sweep_revalidates_live_malloc_moved_by_realloc() { + // #7056: this exercises the BUDGETED/incremental stepper, which the + // shipped default now bypasses — scavenge defers alloc-point + // collections to a precise safepoint instead of starting a cycle here. + // Pin legacy pacing so the test keeps asserting the path it was written + // for; the new default's behaviour is asserted by the probe matrix. + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); let _guard = CopyingNurseryTestGuard::new(1); let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); reset_old_reclaim_pressure(); @@ -221,6 +233,12 @@ fn budgeted_malloc_sweep_revalidates_live_malloc_moved_by_realloc() { #[test] fn arena_sweep_pauses_before_block_cleanup_and_preserves_live_objects() { + // #7056: this exercises the BUDGETED/incremental stepper, which the + // shipped default now bypasses — scavenge defers alloc-point + // collections to a precise safepoint instead of starting a cycle here. + // Pin legacy pacing so the test keeps asserting the path it was written + // for; the new default's behaviour is asserted by the probe matrix. + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); let _guard = CopyingNurseryTestGuard::new(1); let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); reset_old_reclaim_pressure(); @@ -270,6 +288,12 @@ fn arena_sweep_pauses_before_block_cleanup_and_preserves_live_objects() { #[test] fn old_generation_targeted_and_full_reclaim_are_bounded_and_publish_telemetry() { + // #7056: this exercises the BUDGETED/incremental stepper, which the + // shipped default now bypasses — scavenge defers alloc-point + // collections to a precise safepoint instead of starting a cycle here. + // Pin legacy pacing so the test keeps asserting the path it was written + // for; the new default's behaviour is asserted by the probe matrix. + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); let _guard = CopyingNurseryTestGuard::new(1); let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); reset_old_reclaim_pressure(); @@ -330,6 +354,12 @@ fn old_generation_targeted_and_full_reclaim_are_bounded_and_publish_telemetry() #[test] fn budgeted_sweep_phase_requires_multiple_host_steps() { + // #7056: this exercises the BUDGETED/incremental stepper, which the + // shipped default now bypasses — scavenge defers alloc-point + // collections to a precise safepoint instead of starting a cycle here. + // Pin legacy pacing so the test keeps asserting the path it was written + // for; the new default's behaviour is asserted by the probe matrix. + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); let _guard = CopyingNurseryTestGuard::new(1); let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); reset_old_reclaim_pressure(); @@ -371,6 +401,12 @@ fn budgeted_sweep_phase_requires_multiple_host_steps() { #[test] fn budgeted_reclaim_phase_is_split_from_completion() { + // #7056: this exercises the BUDGETED/incremental stepper, which the + // shipped default now bypasses — scavenge defers alloc-point + // collections to a precise safepoint instead of starting a cycle here. + // Pin legacy pacing so the test keeps asserting the path it was written + // for; the new default's behaviour is asserted by the probe matrix. + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); let _guard = CopyingNurseryTestGuard::new(1); let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); reset_old_reclaim_pressure(); @@ -403,6 +439,12 @@ fn budgeted_reclaim_phase_is_split_from_completion() { #[test] fn budgeted_reclaim_slices_remembered_maintenance_entries() { + // #7056: this exercises the BUDGETED/incremental stepper, which the + // shipped default now bypasses — scavenge defers alloc-point + // collections to a precise safepoint instead of starting a cycle here. + // Pin legacy pacing so the test keeps asserting the path it was written + // for; the new default's behaviour is asserted by the probe matrix. + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); let _guard = CopyingNurseryTestGuard::new(1); let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); reset_old_reclaim_pressure(); @@ -454,6 +496,12 @@ fn budgeted_reclaim_slices_remembered_maintenance_entries() { #[test] fn budgeted_reclaim_slices_many_external_dirty_slot_page_buckets() { + // #7056: this exercises the BUDGETED/incremental stepper, which the + // shipped default now bypasses — scavenge defers alloc-point + // collections to a precise safepoint instead of starting a cycle here. + // Pin legacy pacing so the test keeps asserting the path it was written + // for; the new default's behaviour is asserted by the probe matrix. + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); let _guard = CopyingNurseryTestGuard::new(1); let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); reset_old_reclaim_pressure(); @@ -498,6 +546,12 @@ fn budgeted_reclaim_slices_many_external_dirty_slot_page_buckets() { #[test] fn budgeted_reclaim_slices_conservative_pin_cleanup() { + // #7056: this exercises the BUDGETED/incremental stepper, which the + // shipped default now bypasses — scavenge defers alloc-point + // collections to a precise safepoint instead of starting a cycle here. + // Pin legacy pacing so the test keeps asserting the path it was written + // for; the new default's behaviour is asserted by the probe matrix. + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); let _guard = CopyingNurseryTestGuard::new(1); let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); reset_old_reclaim_pressure(); @@ -545,6 +599,12 @@ fn budgeted_reclaim_slices_conservative_pin_cleanup() { #[test] fn budgeted_reclaim_runs_process_malloc_trim() { + // #7056: this exercises the BUDGETED/incremental stepper, which the + // shipped default now bypasses — scavenge defers alloc-point + // collections to a precise safepoint instead of starting a cycle here. + // Pin legacy pacing so the test keeps asserting the path it was written + // for; the new default's behaviour is asserted by the probe matrix. + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); let _trace_guard = TestGcTraceCaptureGuard::force_enabled(); let _guard = CopyingNurseryTestGuard::new(1); let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); diff --git a/crates/perry-runtime/src/gc/tests/root_words.rs b/crates/perry-runtime/src/gc/tests/root_words.rs index f4fd449dc6..3f47ef19c6 100644 --- a/crates/perry-runtime/src/gc/tests/root_words.rs +++ b/crates/perry-runtime/src/gc/tests/root_words.rs @@ -206,6 +206,11 @@ fn mutable_root_mark_and_rewrite_accept_the_same_word_forms() { /// the sweep ran) and the shadow slot was left dangling. #[test] fn bare_address_in_shadow_slot_survives_a_real_collection() { + // #7056: drives the BUDGETED stepper via `complete_budgeted_gc_cycle`, + // which the shipped default bypasses (scavenge defers alloc-point + // collections to a precise safepoint). Pin legacy pacing so the cycle + // actually starts and this keeps testing what it was written for. + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); let _guard = CopyingNurseryTestGuard::new(1); let _scan = ConservativeScanDisabledGuard::new(); let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); @@ -267,6 +272,11 @@ fn bare_address_in_shadow_slot_survives_a_real_collection() { /// the old `mark_global_root_bits` into the shared decoder. #[test] fn bare_address_in_global_root_survives_a_real_collection() { + // #7056: drives the BUDGETED stepper via `complete_budgeted_gc_cycle`, + // which the shipped default bypasses (scavenge defers alloc-point + // collections to a precise safepoint). Pin legacy pacing so the cycle + // actually starts and this keeps testing what it was written for. + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); let _guard = CopyingNurseryTestGuard::new(1); let _scan = ConservativeScanDisabledGuard::new(); let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); diff --git a/crates/perry-runtime/src/gc/tests/teardown.rs b/crates/perry-runtime/src/gc/tests/teardown.rs index 02950561bd..a3664da267 100644 --- a/crates/perry-runtime/src/gc/tests/teardown.rs +++ b/crates/perry-runtime/src/gc/tests/teardown.rs @@ -2,6 +2,11 @@ use super::support::GcTriggerThresholdTestGuard; #[test] fn map_set_side_allocations_release_on_thread_exit() { + // #7056: drives the BUDGETED stepper via `complete_budgeted_gc_cycle`, + // which the shipped default bypasses (scavenge defers alloc-point + // collections to a precise safepoint). Pin legacy pacing so the cycle + // actually starts and this keeps testing what it was written for. + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); let map_before = crate::map::test_map_side_deallocation_snapshot(); let set_before = crate::set::test_set_side_deallocation_snapshot(); diff --git a/crates/perry-runtime/src/gc/tests/temp_roots.rs b/crates/perry-runtime/src/gc/tests/temp_roots.rs index afb46e1aa0..546a7f0636 100644 --- a/crates/perry-runtime/src/gc/tests/temp_roots.rs +++ b/crates/perry-runtime/src/gc/tests/temp_roots.rs @@ -43,6 +43,11 @@ fn register_temp_root_scanner_for_tests() { /// memory, so the label silently vanished from the output. #[test] fn temp_rooted_value_survives_a_real_collection() { + // #7056: drives the BUDGETED stepper via `complete_budgeted_gc_cycle`, + // which the shipped default bypasses (scavenge defers alloc-point + // collections to a precise safepoint). Pin legacy pacing so the cycle + // actually starts and this keeps testing what it was written for. + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); let _guard = CopyingNurseryTestGuard::new(1); let _scan = ConservativeScanDisabledGuard::new(); let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); @@ -208,6 +213,11 @@ fn shadow_savepoint_restores_the_temp_root_depth() { /// and the test passes without proving anything about precise roots. #[test] fn rewriting_a_slot_roots_the_new_value_and_releases_the_replaced_one() { + // #7056: drives the BUDGETED stepper via `complete_budgeted_gc_cycle`, + // which the shipped default bypasses (scavenge defers alloc-point + // collections to a precise safepoint). Pin legacy pacing so the cycle + // actually starts and this keeps testing what it was written for. + let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing(); let _guard = CopyingNurseryTestGuard::new(1); let _scan = ConservativeScanDisabledGuard::new(); let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers();