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 8ce39ed4f0..d9bcddf2e6 100644 --- a/crates/perry-runtime/src/gc/mod.rs +++ b/crates/perry-runtime/src/gc/mod.rs @@ -296,13 +296,48 @@ 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(|| { - 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..29dd9198d1 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,16 @@ thread_local! { #[cfg(test)] pub(super) struct LegacyGcPacingGuard { previous: Option, + cap_previous: bool, + scavenge_previous: Option, } #[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)); + super::GC_SCAVENGE_TEST_OVERRIDE.with(|cell| cell.set(self.scavenge_previous)); } } @@ -446,7 +496,19 @@ pub(super) fn force_legacy_gc_pacing() -> LegacyGcPacingGuard { cell.set(Some(false)); previous }); - LegacyGcPacingGuard { previous } + // 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, + } } /// Pin moving GC pacing (moving-loop polls ON) for the duration of the returned @@ -462,7 +524,13 @@ 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)); + let scavenge_previous = super::GC_SCAVENGE_TEST_OVERRIDE.with(|cell| cell.replace(Some(true))); + LegacyGcPacingGuard { + previous, + cap_previous, + scavenge_previous, + } } pub(super) fn gc_trace_enabled() -> bool { 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();