Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions crates/perry-runtime/src/gc/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,15 @@ pub(crate) fn gc_incremental_enabled() -> bool {
/// deferral and the polls that drain it stay coherent — a runtime default-on with
/// a codegen default-off (or vice versa) would defer collections that never drain.
pub(crate) fn gc_moving_loop_polls_enabled() -> bool {
// Test-only mode override (see `force_legacy_gc_pacing`). Consulted BEFORE
// the process-wide OnceLock so a single test can pin legacy (non-moving,
// budgeted/direct, 128 MiB-ceiling) pacing for its duration even though the
// process default is moving-on. Compiled out entirely in release builds.
#[cfg(test)]
if let Some(forced) = GC_MOVING_LOOP_POLLS_TEST_OVERRIDE.with(Cell::get) {
return forced;
}

static CACHED: OnceLock<bool> = OnceLock::new();
*CACHED.get_or_init(|| {
!matches!(
Expand All @@ -376,6 +385,50 @@ pub(crate) fn gc_moving_loop_polls_enabled() -> bool {
})
}

#[cfg(test)]
thread_local! {
/// Test-only override for [`gc_moving_loop_polls_enabled`]. When `Some(v)`,
/// the getter returns `v` before consulting the process-wide OnceLock. This
/// is the ONLY way a unit test can select GC pacing mode per-test: the
/// OnceLock caches the env-derived default once for the whole process, so
/// the entire test binary otherwise runs in a single mode. Because the
/// nursery-cap in `effective_next_arena_trigger`, the alloc-point routing in
/// `gc_check_trigger`, and the eager malloc-registry build in
/// `CopyingPointerSet::new` all consult `gc_moving_loop_polls_enabled()`
/// (and `gc_scavenge_enabled()` is env-gated OFF by default in tests), this
/// single override flips all of the moving-mode behavior coherently.
static GC_MOVING_LOOP_POLLS_TEST_OVERRIDE: Cell<Option<bool>> = const { Cell::new(None) };
}

/// RAII guard that pins LEGACY (non-moving, budgeted/direct, 128 MiB-ceiling) GC
/// pacing for the tests that assert the budgeted/direct pacer + trigger
/// arithmetic — the mechanism that, since the moving-nursery default-on flip,
/// lives behind the `PERRY_GC_MOVING_LOOP_POLLS=0` kill switch rather than the
/// default path. Restores the previous override state on drop. Test-only.
#[cfg(test)]
pub(super) struct LegacyGcPacingGuard {
previous: Option<bool>,
}

#[cfg(test)]
impl Drop for LegacyGcPacingGuard {
fn drop(&mut self) {
GC_MOVING_LOOP_POLLS_TEST_OVERRIDE.with(|cell| cell.set(self.previous));
}
}

/// Pin legacy GC pacing (moving-loop polls OFF) for the duration of the returned
/// guard. See [`LegacyGcPacingGuard`] and [`gc_moving_loop_polls_enabled`].
#[cfg(test)]
pub(super) fn force_legacy_gc_pacing() -> LegacyGcPacingGuard {
let previous = GC_MOVING_LOOP_POLLS_TEST_OVERRIDE.with(|cell| {
let previous = cell.get();
cell.set(Some(false));
previous
});
LegacyGcPacingGuard { previous }
}
Comment on lines +388 to +430

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
fd -a 'policy.rs' crates/perry-runtime/src/gc || true

echo "== relevant code =="
sed -n '350,445p' crates/perry-runtime/src/gc/policy.rs

echo "== search for force_legacy_gc_pacing / LegacyGcPacingGuard usages =="
rg -n "force_legacy_gc_pacing|LegacyGcPacingGuard|GC_MOVING_LOOP_POLLS_TEST_OVERRIDE" crates/perry-runtime/src || true

echo "== check module export visibility =="
rg -n "policy" crates/perry-runtime/src/gc -g '*.rs' || true

echo "== standalone Send check for current struct shape =="
python3 - <<'PY'
import pathlib, re
p = pathlib.Path('crates/perry-runtime/src/gc/policy.rs')
s = p.read_text()
m = re.search(r'pub \(super\) struct LegacyGcPacingGuard \{(?P<body>.*?)\n\}', s, re.S)
if m:
    body = m.group('body')
    fields = re.findall(r'^\s*(?:pub\s+\()?\s*([A-Za-z_][A-Za-z_0-9]*)\s*:', body, re.M)
    t = 'struct LegacyGcPacingGuard {\n' + body + '\n}'
    print("fields:", fields)
    print("contains PhantomData:", 'PhantomData' in body)
    print("contains Rc:", 'Rc<' in body)
else:
    print("LegacyGcPacingGuard not found")

# Behavioral probe: model TLS Cell<Option<bool>> and guard drop restoring thread TLS state.
class Tls:
    def __init__(self, threads):
        self.threads = threads
    def with_(self, thread, f):
        return f(self.threads[thread])
    def get(self, thread):
        return self.threads[thread].get()
    def set(self, thread, v):
        self.threads[thread].get().set(v)

tls = Tls([{None: None, 'cell': {'value': {'value': None}}}, {None: None, 'cell': {'value': {'value': None}}}])
previous = tls.with_('thread1', lambda c: (c.get(), c.set(False)))[0]
# model drop without PhantomData: previous=Previous(None), thread2 Drop restores thread2 TLS = None
tls2 = {'thread2': {'cell': {'value': {'value': 'legacy', 'mode': 'moving'}}}}
def restore(thread, previous_value):
    tls[thread].set(previous_value)
restore('thread2', previous)
print("after originating thread sets legacy:")
print("thread1:", tls['thread1']['cell']['value']['value'])
print("after legacy guard drop on another thread restores that other thread:")
print("thread2:", tls2['thread2']['cell']['value']['value'])
PY

Repository: PerryTS/perry

Length of output: 36350


🌐 Web query:

Rust std::marker::PhantomData<T> Send Sync non-variant Phantom marker Send Sync false

💡 Result:

In Rust, std::marker::PhantomData<T> acts as if your type contains an actual field of type T [1][2]. Consequently, its implementation of auto traits like Send and Sync is derived from the requirements placed on T [1][3]. Specifically, PhantomData<T> implements Send if and only if T: Send, and it implements Sync if and only if T: Sync [1][3]. Because PhantomData simply mirrors the Send/Sync status of its contained type T, you can control these properties by selecting an appropriate T [2][4]: 1. To make a struct Send and Sync, you can use PhantomData<fn() -> T> [4][5]. Unlike *const T (which is !Send and !Sync), function pointers are always Send and Sync, regardless of the types involved [6][4]. 2. To make a struct !Send or !Sync, you can use PhantomData<T> where T is a type that is !Send or !Sync [7][8]. Common idiomatic examples include using *const () (for !Send + !Sync) [4][7], or importing types from the standard library like std::cell::Cell<()> (for !Sync) or std::sync::MutexGuard<'static, ()> (for !Send) to explicitly opt out of those traits [7][8]. Because PhantomData does not own the data it marks, it is often used to manage variance and drop check requirements without forcing specific Send or Sync behavior unless explicitly intended [2][4]. For example, while PhantomData<*const T> is often used to signal that a struct conceptually owns or uses a *const T (making it !Send + !Sync by default), using PhantomData<fn() -> T> achieves the same variance without imposing the !Send/!Sync constraints [2][4][5].

Citations:


Make LegacyGcPacingGuard thread-affine.

LegacyGcPacingGuard only stores Option<bool>, so it is Send; moving it out of creating thread keeps the originating thread pinning legacy GC pacing while Drop restores the wrong thread-local override. Add a marker such as PhantomData<Rc<()>> to make the guard !Send.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/gc/policy.rs` around lines 388 - 430, Add a non-Send
marker such as PhantomData<Rc<()>> to LegacyGcPacingGuard and initialize it in
force_legacy_gc_pacing, ensuring the RAII guard cannot move between threads
while preserving its existing thread-local restoration behavior.


pub(super) fn gc_trace_enabled() -> bool {
#[cfg(test)]
if GC_TRACE_TEST_FORCE.with(Cell::get) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ fn test_copying_minor_sweeps_malloc_when_due_on_arena_trigger() {

#[test]
fn test_gc_check_trigger_copied_minor_malloc_sweep_rebaselines_trigger() {
let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing();
let _guard = CopyingNurseryTestGuard::new(1);
let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers();
let live_malloc = gc_malloc(
Expand Down Expand Up @@ -340,6 +341,7 @@ fn test_gc_check_trigger_copied_minor_malloc_sweep_rebaselines_trigger() {

#[test]
fn test_gc_check_trigger_copied_minor_without_malloc_sweep_preserves_malloc_trigger() {
let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing();
let _guard = CopyingNurseryTestGuard::new(1);
let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers();
deactivate_malloc_registry_for_tests();
Expand Down Expand Up @@ -407,6 +409,7 @@ fn test_gc_check_trigger_copied_minor_without_malloc_sweep_preserves_malloc_trig

#[test]
fn test_copied_minor_malloc_scaling_no_roots_skips_registry_walk() {
let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing();
let _guard = CopyingNurseryTestGuard::new(1);
let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers();
deactivate_malloc_registry_for_tests();
Expand Down Expand Up @@ -488,6 +491,7 @@ fn test_copied_minor_malloc_scaling_live_root_with_active_registry() {

#[test]
fn test_copied_minor_malloc_scaling_falls_back_when_registry_unavailable() {
let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing();
let _guard = CopyingNurseryTestGuard::new(0);
let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers();
let live_malloc = gc_malloc(
Expand Down
16 changes: 16 additions & 0 deletions crates/perry-runtime/src/gc/tests/debt_pacer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ fn budgeted_step_until_phase(target: GcCyclePhase) -> JsGcStepResult {

#[test]
fn arena_threshold_debt_starts_bounded_assist_without_monolithic_collection() {
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();
Expand Down Expand Up @@ -74,6 +75,7 @@ fn arena_threshold_debt_starts_bounded_assist_without_monolithic_collection() {

#[test]
fn malloc_threshold_debt_reclaims_dead_churn_after_host_drain() {
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();
Expand Down Expand Up @@ -131,6 +133,7 @@ fn malloc_threshold_debt_reclaims_dead_churn_after_host_drain() {

#[test]
fn active_cycle_gc_check_trigger_calls_pay_bounded_assist_work() {
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();
Expand Down Expand Up @@ -181,6 +184,7 @@ fn active_cycle_gc_check_trigger_calls_pay_bounded_assist_work() {
/// covered separately in `incremental_sweep_reclaim.rs`.
#[test]
fn allocation_assists_complete_finalize_sweep_and_reclaim() {
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();
Expand Down Expand Up @@ -271,6 +275,7 @@ fn noop_copy_only_root_scanner(_visit: &mut dyn FnMut(f64)) {}
/// bounded live set that never made progress.
#[test]
fn direct_arena_minor_rebaselines_trigger_above_live_set() {
let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing();
let _nursery = CopyingNurseryTestGuard::new(1);
// A registered copy-only scanner makes the budgeted stepper ineligible, so
// gc_check_trigger takes the direct synchronous-minor arm.
Expand Down Expand Up @@ -318,6 +323,7 @@ fn direct_arena_minor_rebaselines_trigger_above_live_set() {
/// re-arms a full synchronous minor.
#[test]
fn direct_malloc_minor_rebaselines_trigger_above_survivors() {
let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing();
let _nursery = CopyingNurseryTestGuard::new(1);
let _scanners = ScopedRootScannerRegistryGuard::new();
gc_register_root_scanner(noop_copy_only_root_scanner);
Expand Down Expand Up @@ -407,6 +413,7 @@ fn mutator_assist_work_units_scale_with_debt() {
/// supply but 300 debt-scaled assists comfortably can.
#[test]
fn debt_scaled_assists_cannot_be_outrun_by_allocation() {
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();
Expand Down Expand Up @@ -457,6 +464,7 @@ fn debt_scaled_assists_cannot_be_outrun_by_allocation() {
/// the barrier window), and birth flags reset once the cycle completes.
#[test]
fn budgeted_cycle_allocations_are_born_marked_for_the_whole_cycle() {
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();
Expand Down Expand Up @@ -500,6 +508,7 @@ fn budgeted_cycle_allocations_are_born_marked_for_the_whole_cycle() {
/// objects (measured as the #6224 stress SIGSEGV).
#[test]
fn manual_gc_drains_parked_budgeted_cycle_first() {
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();
Expand Down Expand Up @@ -748,6 +757,13 @@ fn test_arena_debt_measured_against_effective_trigger_not_raw_cell() {
effective_next_arena_trigger, GC_NEXT_TRIGGER_BYTES, GC_TRIGGER_ARMED,
};

// Asserts the legacy pre-first-collection trigger arithmetic (un-armed cell
// reads as the 128 MiB device ceiling). Under default-on moving the effective
// trigger is nursery-capped (16 MiB); pin legacy to keep testing the raw-vs-
// effective debt mechanism this test was written for. (The nursery-cap value
// is asserted under the default by triggers::test_effective_arena_trigger_respects_armed_values.)
let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing();

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());
Expand Down
1 change: 1 addition & 0 deletions crates/perry-runtime/src/gc/tests/roots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ impl Drop for GcUnsafeZoneResetGuard {

#[test]
fn lock_safe_runtime_scanners_tui_state_defers_gc_check_trigger() {
let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing();
let _test_lock = lock_safe_runtime_scanner_test_guard();
let _reset = ShadowAndGlobalRootResetGuard;
ensure_lock_safe_runtime_scanners_registered();
Expand Down
11 changes: 11 additions & 0 deletions crates/perry-runtime/src/gc/tests/runtime_roots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,7 @@ fn test_set_gc_field_rewrite_reindexes_elements() {

#[test]
fn test_transient_runtime_handle_string_concat_gc() {
let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing();
let _guard = CopyingNurseryTestGuard::new(0);
let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers();
register_runtime_handle_root_scanner_for_tests();
Expand Down Expand Up @@ -1016,6 +1017,7 @@ fn test_transient_runtime_handle_string_concat_gc() {

#[test]
fn test_dynamic_string_add_roots_left_string_across_rhs_coercion_gc() {
let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing();
let _guard = CopyingNurseryTestGuard::new(0);
let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers();
register_runtime_handle_root_scanner_for_tests();
Expand Down Expand Up @@ -1053,6 +1055,7 @@ fn test_dynamic_string_add_roots_left_string_across_rhs_coercion_gc() {

#[test]
fn test_dynamic_bigint_add_roots_both_bigint_across_gc() {
let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing();
// #2908: a BigInt operator now requires BOTH operands to be BigInt
// (`1n + 1` throws TypeError instead of coercing). This test exercises
// the same GC-rooting guarantee — the left BigInt must survive a minor
Expand Down Expand Up @@ -1102,6 +1105,7 @@ fn test_dynamic_bigint_add_roots_both_bigint_across_gc() {

#[test]
fn test_bigint_method_add_roots_receiver_across_rhs_number_coercion_gc() {
let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing();
let _guard = CopyingNurseryTestGuard::new(0);
let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers();
register_runtime_handle_root_scanner_for_tests();
Expand Down Expand Up @@ -1145,6 +1149,7 @@ fn test_bigint_method_add_roots_receiver_across_rhs_number_coercion_gc() {

#[test]
fn test_string_method_split_roots_receiver_across_separator_materialization_gc() {
let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing();
let _guard = CopyingNurseryTestGuard::new(0);
let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers();
activate_malloc_registry_for_tests();
Expand Down Expand Up @@ -1189,6 +1194,7 @@ fn test_string_method_split_roots_receiver_across_separator_materialization_gc()

#[test]
fn test_string_method_replace_roots_receiver_across_pattern_materialization_gc() {
let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing();
let _guard = CopyingNurseryTestGuard::new(0);
let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers();
activate_malloc_registry_for_tests();
Expand Down Expand Up @@ -1232,6 +1238,7 @@ fn test_string_method_replace_roots_receiver_across_pattern_materialization_gc()

#[test]
fn test_transient_runtime_handle_array_push_gc() {
let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing();
let _guard = CopyingNurseryTestGuard::new(0);
let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers();
register_runtime_handle_root_scanner_for_tests();
Expand Down Expand Up @@ -1261,6 +1268,7 @@ fn test_transient_runtime_handle_array_push_gc() {

#[test]
fn test_transient_runtime_handle_object_set_gc() {
let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing();
let _guard = CopyingNurseryTestGuard::new(1);
let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers();
register_runtime_handle_root_scanner_for_tests();
Expand Down Expand Up @@ -1297,6 +1305,7 @@ fn test_transient_runtime_handle_object_set_gc() {

#[test]
fn test_transient_runtime_handle_closure_captures_gc() {
let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing();
extern "C" fn captured_func(_closure: *const crate::closure::ClosureHeader) -> f64 {
0.0
}
Expand Down Expand Up @@ -1571,6 +1580,7 @@ fn drain_promise_microtasks_for_test() {

#[test]
fn test_async_hook_option_lookup_roots_callbacks_across_copied_minor_gc() {
let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing();
let _async_hook_guard = AsyncHookRuntimeTestGuard::new();
let _guard = CopyingNurseryTestGuard::new(0);
let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers();
Expand All @@ -1592,6 +1602,7 @@ fn test_async_hook_option_lookup_roots_callbacks_across_copied_minor_gc() {

#[test]
fn test_closure_rest_dispatch_roots_args_during_rest_array_alloc_gc() {
let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing();
let _async_hook_guard = AsyncHookRuntimeTestGuard::new();
let _guard = CopyingNurseryTestGuard::new(0);
let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use super::*;
/// allocation. This mirrors the proven concat / dynamic-add tests in the parent.
#[test]
fn test_transient_runtime_handle_string_slice_gc() {
let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing();
let _guard = CopyingNurseryTestGuard::new(0);
let trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers();
register_runtime_handle_root_scanner_for_tests();
Expand Down
44 changes: 38 additions & 6 deletions crates/perry-runtime/src/gc/tests/triggers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,15 @@ fn test_gc_bump_medium_parse_allows_one_arena_bump_per_gc_cycle() {

#[test]
fn test_gc_bump_never_lowers_existing_arena_trigger() {
// The "never lower" invariant is asserted against the RAW trigger cell, whose
// relationship to the bump target only holds under legacy pacing: with moving
// mode on, `effective_next_arena_trigger` is clamped to the small nursery cap,
// so a bump target above that cap legitimately re-arms the cell (without ever
// lowering the EFFECTIVE, capped trigger). Pin legacy to keep asserting the
// raw-cell arithmetic this test was written for. (The new nursery-cap value
// itself is asserted under the default by
// `test_effective_arena_trigger_respects_armed_values`.)
let _legacy_pacing = crate::gc::policy::force_legacy_gc_pacing();
let existing_trigger = GC_TRIGGER_ABSOLUTE_CEILING + (32 * 1024 * 1024);
let _guard = GcBumpTriggerTestGuard::new(existing_trigger, GC_THRESHOLD_INITIAL_BYTES);
let bytes_now = GC_TRIGGER_ABSOLUTE_CEILING + (16 * 1024 * 1024);
Expand Down Expand Up @@ -248,26 +257,49 @@ fn test_budget_scaled_clamps_only_under_budget() {
fn test_effective_arena_trigger_respects_armed_values() {
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,
effective_next_arena_trigger, gc_moving_loop_polls_enabled, gc_scavenge_nursery_cap_bytes,
GC_NEXT_TRIGGER_BYTES, GC_TRIGGER_ARMED,
};
// `effective_next_arena_trigger` additionally clamps to the small nursery cap
// whenever moving mode is active (the default-on evacuating scavenge) or the
// PERRY_GC_SCAVENGE de-risking flag is set; otherwise it clamps only the
// UN-armed cell to the device ceiling and lets an armed trigger exceed it.
// Assert the value correct for the mode this process runs in, so the NEW
// nursery-cap behavior is exercised under the default and the legacy ceiling
// behavior under the PERRY_GC_MOVING_LOOP_POLLS=0 kill switch. This mirrors
// the gate in `effective_next_arena_trigger` exactly.
let nursery_capped = super::super::gc_scavenge_enabled() || gc_moving_loop_polls_enabled();
let ceiling = gc_trigger_absolute_ceiling_bytes();
let nursery_cap = gc_scavenge_nursery_cap_bytes();

let prev_trigger = GC_NEXT_TRIGGER_BYTES.with(|c| c.get());
let prev_armed = GC_TRIGGER_ARMED.with(|c| c.get());

GC_TRIGGER_ARMED.with(|c| c.set(false));
GC_NEXT_TRIGGER_BYTES.with(|c| c.set(usize::MAX / 2));
let expected_unarmed = if nursery_capped {
ceiling.min(nursery_cap)
} else {
ceiling
};
assert_eq!(
effective_next_arena_trigger(),
gc_trigger_absolute_ceiling_bytes(),
"un-armed trigger must clamp to the device ceiling"
expected_unarmed,
"un-armed trigger must clamp to the device ceiling (further to the nursery cap when moving)"
);

GC_TRIGGER_ARMED.with(|c| c.set(true));
let above_ceiling = gc_trigger_absolute_ceiling_bytes() * 3;
let above_ceiling = ceiling * 3;
GC_NEXT_TRIGGER_BYTES.with(|c| c.set(above_ceiling));
let expected_armed = if nursery_capped {
above_ceiling.min(nursery_cap)
} else {
above_ceiling
};
assert_eq!(
effective_next_arena_trigger(),
above_ceiling,
"armed triggers above the ceiling are legitimate and must survive"
expected_armed,
"armed triggers above the ceiling survive under legacy pacing and clamp to the nursery cap when moving"
);

GC_NEXT_TRIGGER_BYTES.with(|c| c.set(prev_trigger));
Expand Down
Loading