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
32 changes: 32 additions & 0 deletions changelog.d/7913-old-defrag-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
### Fixed

- Restored safe old-generation page defragmentation and enabled it by default.
A minor trace does not prove unmarked old objects dead, so evacuation now
snapshots every indexed occupant of a selected source block, rejects the
block before forwarding if any occupant cannot move, and otherwise relocates
the complete block before reclaiming it. `PERRY_GC_OLD_DEFRAG=0` remains an
explicit diagnosis and rollback switch.

- Closed three runtime rewrite gaps: JSON's parse-key ring and the perf entry
keys cache now follow their structurally rooted owners, while diagnostics
symbol-keyed state is rekeyed after a move. Cached `@perry_class_keys_*`
copies are precise function-lifetime mutable roots rather than relying on the
old generation being immovable. The runtime-holder inventory now fails on
known or unevaluated movable-address gaps.

The historical corruption workload fails on rebuilt main when old-page
relocation is enabled, but passed six consecutive verified runs after this
change with output identical to the clean control. A mixed-size regression
also demonstrates that a fragmented source block is actually released.

### Performance

Old-page metadata selection now runs only after the copying-minor fast path has
declined the collection. This removed an O(old pages) charge from ordinary
copying minors: the worst affected retention benchmark moved from +8.29% in the
initial implementation to +0.14% best / -0.09% median in the final quiet-M1
best-of-15 comparison.

Precise class-key roots cost 3.70-4.45% in four tight shape/class allocation
kernels. That is the remaining correctness tradeoff; `interp` improves 2.85%,
and the other 20 programs remain within +/-1.5%.
45 changes: 45 additions & 0 deletions crates/perry-codegen/src/collectors/proven_this_routing_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,7 @@ fn tower_case_routes_to_proven_this_clone() {
/// guard block → `icmp eq i64` → the branch that enters the clone's block.
#[test]
fn tower_route_is_guarded_by_the_class_keys_token() {
let _shadow = crate::codegen::helpers::NativeRootsPin::shadow();
let ir = emit(&tower_site_module(), false);
let bs = blocks(&ir);
let clone = pshape_definitions(&ir)
Expand Down Expand Up @@ -812,6 +813,22 @@ fn tower_route_is_guarded_by_the_class_keys_token() {
.find(|l| l.contains(&format!("store i64 {}, ptr ", global_reg)))
.unwrap_or_else(|| panic!("the hoisted keys token is never stored:\n{ir}"));
let slot = store.rsplit(' ').next().expect("slot name");
let store_pos = ir
.find(store)
.expect("the hoisted class-keys store should be in the function");
let bind_pos = ir
.lines()
.find(|line| line.contains("call void @js_shadow_slot_bind") && line.contains(slot))
.and_then(|line| ir.find(line))
.unwrap_or_else(|| {
panic!(
"the cached class-keys pointer is not a mutable shadow root; old-page moves would leave this copy stale:\n{ir}"
)
});
assert!(
store_pos < bind_pos,
"the class-keys slot must be initialized before the root scanner can read it:\n{ir}"
);
// 3. … which the guard block reloads …
let expected = guard_body
.iter()
Expand Down Expand Up @@ -843,6 +860,34 @@ fn tower_route_is_guarded_by_the_class_keys_token() {
);
}

#[test]
fn tower_class_keys_cache_is_a_native_mutable_root() {
let _native = crate::codegen::helpers::NativeRootsPin::native();
let ir = emit(&tower_site_module(), false);
let global_load = ir
.lines()
.find(|line| line.contains("= load i64, ptr @perry_class_keys_"))
.unwrap_or_else(|| panic!("the class keys token is never read:\n{ir}"));
let global_reg = global_load.trim().split(' ').next().expect("ssa name");
let cast = ir
.lines()
.find(|line| line.contains(&format!("= inttoptr i64 {global_reg} to ptr addrspace(1)")))
.unwrap_or_else(|| {
panic!("the cached class-keys pointer never enters a native GC root slot:\n{ir}")
});
let cast_reg = cast.trim().split(' ').next().expect("cast ssa name");
let root_store = ir
.lines()
.find(|line| line.contains(&format!("store ptr addrspace(1) {cast_reg}, ptr ")))
.unwrap_or_else(|| panic!("the native class-keys root is never stored:\n{ir}"));
let slot = root_store.rsplit(' ').next().expect("root slot name");
assert!(
ir.lines()
.any(|line| line.contains(&format!("{slot} = alloca ptr addrspace(1)"))),
"the class-keys cache alloca must be in the collector address space:\n{ir}"
);
}

/// Profitability ratchet (#7142): a clone that deletes exactly ONE guarded
/// field site does not earn the tower's inline re-check, so the tower keeps
/// calling the public body.
Expand Down
3 changes: 2 additions & 1 deletion crates/perry-codegen/src/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,8 @@ pub(crate) use slot_rep::{

pub(crate) use dispatch::{lower_expr, lower_math_operand};
pub(crate) use scalar_slot_root::{
root_entry_alloca, root_scalar_replaced_slot, root_scalar_replaced_slot_unconditional,
entry_init_load_rooted_global, root_entry_alloca, root_scalar_replaced_slot,
root_scalar_replaced_slot_unconditional,
};
pub(crate) use shadow_slot::{
current_closure_ptr_value, emit_persistent_shadow_root_barrier,
Expand Down
27 changes: 27 additions & 0 deletions crates/perry-codegen/src/expr/scalar_slot_root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,33 @@ pub(crate) fn root_scalar_replaced_slot_unconditional(ctx: &mut FnCtx<'_>, slot:
root_entry_alloca(ctx, slot);
}

/// Cache a GC-pointer global in a function-entry slot and make that cached
/// copy a mutable root.
///
/// Registering the global itself keeps the object live and rewrites the
/// global, but an old-page move must also rewrite every function-local copy
/// loaded before the move. The bind is emitted in the same post-init setup
/// region, immediately after `entry_init_load_global` stores the initialized
/// value, so the collector never observes an uninitialized slot. There is no
/// per-store incremental barrier: the registered global already owns
/// liveness, and this immutable duplicate exists only to receive rewrites.
pub(crate) fn entry_init_load_rooted_global(
ctx: &mut FnCtx<'_>,
global_name: &str,
ty: crate::types::LlvmType,
) -> String {
let slot = ctx.func.entry_init_load_global(global_name, ty);
let Some(idx) = ctx.func.reserve_shadow_slot() else {
return slot;
};
Comment on lines +118 to +120

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 | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the root-reservation implementation and its callers.
ast-grep outline crates/perry-codegen/src --items all --match 'reserve_shadow_slot|entry_init_load_global|entry_setup_call_void'

# Inspect the no-slot configuration and related collector-mode gates.
rg -n -C 6 '\breserve_shadow_slot\s*\(|shadow[-_ ]slot|shadow[-_ ]stack|PERRY_GC_OLD_DEFRAG|old[-_ ]defrag|relocat' crates scripts

Repository: PerryTS/perry

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- scalar_slot_root.rs ---'
cat -n crates/perry-codegen/src/expr/scalar_slot_root.rs | sed -n '1,190p'

printf '%s\n' '--- reserve_shadow_slot implementation ---'
cat -n crates/perry-codegen/src/function.rs | sed -n '350,430p'

printf '%s\n' '--- native_stack_roots_enabled definitions and call sites ---'
rg -n -C 8 'fn native_stack_roots_enabled|native_stack_roots_enabled\(' crates/perry-codegen/src crates/perry-codegen/tests

printf '%s\n' '--- scalar helper callers ---'
rg -n -C 8 'entry_init_load_rooted_global|entry_init_load_global|scalar_slot_root|class.?keys|CLASS_KEYS' crates/perry-codegen/src/expr crates/perry-codegen/src

Repository: PerryTS/perry

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- shadow-frame enablement and configuration gates ---'
rg -n -C 12 'enable_(post_init_)?shadow_frame|shadow_frame_requested|shadow_stack_enabled\(|precise_root_analysis_enabled\(|PERRY_SHADOW_STACK|PERRY_CONSERVATIVE_STACK_SCAN|PERRY_RS4GC' crates/perry-codegen/src crates/perry-runtime/src

printf '%s\n' '--- collector mode and old-page relocation gates ---'
rg -n -C 10 'old.?page|defrag|relocat|conservative.*scan|CONSERVATIVE_STACK|precise.root|moving.*GC|minor.*mov|evacuat' crates/perry-runtime/src crates/perry-codegen/src | head -n 1200

printf '%s\n' '--- class-key cache lowering and uses ---'
rg -n -C 12 'entry_init_load_rooted_global|class_keys_slots|get_class_keys|class_keys_globals' crates/perry-codegen/src/expr crates/perry-codegen/src/codegen

Repository: PerryTS/perry

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- codegen helper mode definitions ---'
cat -n crates/perry-codegen/src/codegen/helpers.rs | sed -n '1,275p'

printf '%s\n' '--- exact class-key cache call sites ---'
rg -n -C 20 'entry_init_load_rooted_global|class_keys_slots' crates/perry-codegen/src/expr --glob '*.rs'

printf '%s\n' '--- runtime conservative-scan and defrag configuration ---'
rg -n -C 8 'PERRY_CONSERVATIVE_STACK_SCAN|CONSERVATIVE_STACK_SCAN|old_page|old-page|defrag|DEFRAG|evacuating|moving.*old|old.*moving' crates/perry-runtime/src --glob '*.rs' | head -n 1000

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- all configuration references ---'
rg -n -C 10 'PERRY_CONSERVATIVE_STACK_SCAN|conservative_stack_scan|conservative.*scan|stack_scan_enabled|old_page.*evac|evacuation_eligible|defrag_enabled|old.*defrag' . --glob '!target/**' --glob '!node_modules/**' | head -n 1600

printf '%s\n' '--- GC root scan entry points ---'
rg -n -C 12 'scan.*stack|stack.*scan|native.*root|stack_map|gcmap|GC_MAP|conservative' crates/perry-runtime/src/gc crates/perry-runtime/src --glob '*.rs' | head -n 1800

printf '%s\n' '--- relocation collection entry points ---'
rg -n -C 12 'old_pages|evacuate_old|evacuate.*old|defrag|evacuation|collect.*old|minor.*collect|copying' crates/perry-runtime/src/gc crates/perry-runtime/src/arena --glob '*.rs' | head -n 1800

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- conservative scan decision implementation ---'
rg -n 'enum ConservativeStackScanDecision|fn conservative_stack_scan_decision|PERRY_CONSERVATIVE_STACK_SCAN|mark_stack_roots_for_decision|pin_currently_marked_as_conservative' crates/perry-runtime/src/gc --glob '*.rs'
rg -n -C 18 'enum ConservativeStackScanDecision|fn conservative_stack_scan_decision|PERRY_CONSERVATIVE_STACK_SCAN|fn mark_stack_roots_for_decision|fn pin_currently_marked_as_conservative' crates/perry-runtime/src/gc --glob '*.rs'

printf '%s\n' '--- native stack scan and conservative pin semantics ---'
rg -n -C 18 'ConservativePin|conservative.*pin|pin.*conservative|stack_roots|scan_stack' crates/perry-runtime/src/gc --glob '*.rs' | head -n 1800

printf '%s\n' '--- exact old-page evacuation eligibility decisions ---'
rg -n -C 18 'evacuation_eligible|consider_evacuation|old_page.*evac|evacuate.*page|select.*page' crates/perry-runtime/src/gc crates/perry-runtime/src/arena --glob '*.rs' | head -n 1800

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- files defining conservative scan configuration ---'
rg -l 'ConservativeStackScanDecision|PERRY_CONSERVATIVE_STACK_SCAN|conservative_stack_scan_decision' crates/perry-runtime/src/gc
printf '%s\n' '--- files defining evacuation policy ---'
rg -l 'evacuation_eligible|conservative_pinned_bytes|consider_evacuation' crates/perry-runtime/src/gc crates/perry-runtime/src/arena

printf '%s\n' '--- concise configuration excerpts ---'
for f in $(rg -l 'ConservativeStackScanDecision|PERRY_CONSERVATIVE_STACK_SCAN|conservative_stack_scan_decision' crates/perry-runtime/src/gc); do
  echo "### $f"
  rg -n -C 20 'enum ConservativeStackScanDecision|conservative_stack_scan_decision|PERRY_CONSERVATIVE_STACK_SCAN|Scan|Skip' "$f" | head -n 220
done

printf '%s\n' '--- concise evacuation excerpts ---'
for f in $(rg -l 'evacuation_eligible|conservative_pinned_bytes|consider_evacuation' crates/perry-runtime/src/gc crates/perry-runtime/src/arena); do
  echo "### $f"
  rg -n -C 16 'evacuation_eligible|conservative_pinned_bytes|consider_evacuation|old_page_selected|pinned_bytes' "$f" | head -n 260
done

Repository: PerryTS/perry

Length of output: 19762


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- scan_mode.rs ---'
cat -n crates/perry-runtime/src/gc/roots/scan_mode.rs | sed -n '1,260p'

printf '%s\n' '--- copied-minor conservative fallback and old-page policy ---'
cat -n crates/perry-runtime/src/gc/copying.rs | sed -n '1080,1145p'
rg -n -C 25 'consider_evacuation|evacuation_policy|conservative_stack_scan_decision|conservative_pinned_bytes' crates/perry-runtime/src/gc/cycle.rs crates/perry-runtime/src/gc/oldgen.rs crates/perry-runtime/src/gc/policy.rs | head -n 1200

printf '%s\n' '--- configuration documentation and validation ---'
rg -n -C 12 'PERRY_SHADOW_STACK|PERRY_CONSERVATIVE_STACK_SCAN|PERRY_RS4GC|conservative stack scan' README.md docs crates scripts Cargo.toml .github 2>/dev/null | head -n 1600

Repository: PerryTS/perry

Length of output: 50370


Provide a precise root when reserve_shadow_slot() returns None.

With PERRY_SHADOW_STACK=0 and native roots inactive, reserve_shadow_slot() returns None. The default Auto scan mode skips the conservative stack scan, so moving collections can relocate the class-key object while the entry slot retains its old address.

Reject this configuration or use a supported precise-root mechanism for the cache slot.

🤖 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-codegen/src/expr/scalar_slot_root.rs` around lines 118 - 120,
Update the fallback in the scalar-slot root handling around reserve_shadow_slot
so a None result is not accepted when native roots are inactive and Auto scan
mode would omit conservative scanning. Reject this unsupported configuration or
route the cache slot through an existing precise-root mechanism, ensuring moving
collections cannot leave the entry slot pointing to a relocated class-key
object.

Source: Coding guidelines

ctx.scalar_slot_shadow_slots.insert(slot.clone(), idx);
ctx.func.entry_setup_call_void(
"js_shadow_slot_bind",
&[(I32, &idx.to_string()), (PTR, &slot)],
);
slot
}

/// Make an arbitrary entry-block alloca a **rewritten** GC root (#7202).
///
/// Scalar replacement is not the only producer of storage that holds a heap
Expand Down
5 changes: 3 additions & 2 deletions crates/perry-codegen/src/lower_call/method_override.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,8 @@ pub(super) fn emit_guarded_direct_method_call(
.unwrap_or_else(|| crate::codegen::generic_method_body_name(direct_fn));

let expected_class_id_str = expected_class_id.to_string();
let expected_keys_slot = ctx.func.entry_init_load_global(&keys_global_name, I64);
let expected_keys_slot =
crate::expr::entry_init_load_rooted_global(ctx, &keys_global_name, I64);
let expected_keys = ctx.block().load(I64, &expected_keys_slot);

let key_idx = ctx.strings.intern(property);
Expand All @@ -263,7 +264,7 @@ pub(super) fn emit_guarded_direct_method_call(
let subclass_keys: Vec<String> = subclass_arms
.iter()
.map(|arm| {
let slot = ctx.func.entry_init_load_global(&arm.keys_global, I64);
let slot = crate::expr::entry_init_load_rooted_global(ctx, &arm.keys_global, I64);
ctx.block().load(I64, &slot)
})
.collect();
Expand Down
4 changes: 2 additions & 2 deletions crates/perry-codegen/src/lower_call/new_alloc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ fn emit_instance_alloc_inner(
let keys_slot = if let Some(s) = ctx.class_keys_slots.get(class_name).cloned() {
s
} else {
let s = ctx.func.entry_init_load_global(&keys_global_name, I64);
let s = crate::expr::entry_init_load_rooted_global(ctx, &keys_global_name, I64);
ctx.class_keys_slots
.insert(class_name.to_string(), s.clone());
s
Expand Down Expand Up @@ -447,7 +447,7 @@ fn emit_instance_alloc_inner(
let keys_slot = if let Some(s) = ctx.class_keys_slots.get(class_name).cloned() {
s
} else {
let s = ctx.func.entry_init_load_global(&keys_global_name, I64);
let s = crate::expr::entry_init_load_rooted_global(ctx, &keys_global_name, I64);
ctx.class_keys_slots
.insert(class_name.to_string(), s.clone());
s
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ fn emit_tower_pshape_call(
) -> String {
// The global is read ONCE per function (entry-hoisted); the case block only
// reloads it from the stack slot, which mem2reg folds away.
let keys_slot = ctx.func.entry_init_load_global(&route.keys_global, I64);
let keys_slot = crate::expr::entry_init_load_rooted_global(ctx, &route.keys_global, I64);
let expected_keys = ctx.block().load(I64, &keys_slot);

let proven_idx = ctx.new_block(&format!("idispatch.case{}.pshape", case_no));
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/lower_call/scalar_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ fn materialize_scalar_receiver(
let keys_slot = if let Some(slot) = ctx.class_keys_slots.get(class_name).cloned() {
slot
} else {
let slot = ctx.func.entry_init_load_global(&keys_global_name, I64);
let slot = crate::expr::entry_init_load_rooted_global(ctx, &keys_global_name, I64);
ctx.class_keys_slots
.insert(class_name.to_string(), slot.clone());
slot
Expand Down
68 changes: 64 additions & 4 deletions crates/perry-codegen/src/testing/temp_slots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,10 +369,10 @@ pub fn derives_from_slot_load(fn_ir: &str, reg: &str, depth: usize) -> bool {
/// a null `addrspace(1)` store.
///
/// Required as well as the alloca type because an `alloca i64` is also how
/// codegen spells an unrelated scratch cell — the per-class inline-keys cache
/// in `@main` is one, and without this filter it read as a temp root and made
/// `a_class_that_runs_no_user_code_emits_no_instance_root` fail for a slot that
/// holds a static keys pointer.
/// codegen spells unrelated scratch cells. The per-class inline-keys cache is
/// now a precise function-lifetime root (#7876), so it has the same seed and
/// alloca type as a temp root; [`temp_root_slots`] excludes that one by the
/// provenance of the value stored into it.
pub fn zero_seeded_slots(fn_ir: &str) -> std::collections::BTreeSet<String> {
let mut touched: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
let mut seeded: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
Expand Down Expand Up @@ -432,6 +432,13 @@ pub fn temp_root_slots(fn_ir: &str) -> Vec<String> {
let defs = defs(fn_ir);
let undefined = undefined_literal();
let seeded = zero_seeded_slots(fn_ir);
let class_key_loads: Vec<&str> = defs
.iter()
.filter_map(|(&reg, def)| {
def.starts_with("load i64, ptr @perry_class_keys_")
.then_some(reg)
})
.collect();
slot_traffic(fn_ir)
.into_iter()
.filter(|(slot, _)| seeded.contains(slot))
Expand Down Expand Up @@ -460,6 +467,20 @@ pub fn temp_root_slots(fn_ir: &str) -> Vec<String> {
_ => false,
})
})
.filter(|(_, events)| {
// #7876: the registered class-keys global owns liveness, while a
// function-local immutable copy is a precise root solely so an
// old-page move can rewrite it. It lasts for the whole function;
// it is not one of #7487's scoped expression temporaries. Match
// the exact registered-global provenance so an ordinary temp that
// happens to lack its closing clear remains visible to this gate.
!events.iter().any(|event| match event {
SlotEvent::Store { value, .. } => class_key_loads
.iter()
.any(|load| derives_from(&defs, value, load, 4)),
_ => false,
})
})
.map(|(slot, _)| slot)
.collect()
}
Expand Down Expand Up @@ -633,4 +654,43 @@ entry.0:
"…in both lowerings"
);
}

#[test]
fn a_function_lifetime_class_keys_root_is_not_a_temp_root() {
let shadow = "\
define i32 @main() {
entry.0:
%keys = alloca i64
store i64 0, ptr %keys
%r1 = load i64, ptr @perry_class_keys_fixture
store i64 %r1, ptr %keys
%r2 = load i64, ptr %keys
call void @consume(i64 %r2)
ret i32 0
}
";
let native = "\
define i32 @main() gc \"statepoint-example\" {
entry.0:
%keys = alloca ptr addrspace(1)
store ptr addrspace(1) null, ptr %keys
%r1 = load i64, ptr @perry_class_keys_fixture
%r1.rs4p = inttoptr i64 %r1 to ptr addrspace(1)
store ptr addrspace(1) %r1.rs4p, ptr %keys
%r2.rs4p = load ptr addrspace(1), ptr %keys
%r2 = ptrtoint ptr addrspace(1) %r2.rs4p to i64
call void @consume(i64 %r2)
ret i32 0
}
";
assert_no_temp_rooting(shadow, "class-key cache shadow root");
assert_no_temp_rooting(native, "class-key cache native root");

let unrelated = shadow.replace("@perry_class_keys_fixture", "@some_other_global");
assert_eq!(
temp_root_slots(&unrelated),
vec!["%keys".to_string()],
"only the registered class-key provenance is exempt; a missing temp-root clear must stay visible"
);
}
}
14 changes: 9 additions & 5 deletions crates/perry-codegen/tests/temp_root_operand_temporaries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
//! `a_collection_free_construction_emits_no_this_slot_root` needed the pin even
//! though it was *passing*, because under the post-#7370 native-roots default
//! its `!contains("@js_shadow_slot_bind")` is true of every program (hazard 4:
//! the gate ran, its subject did not).
//! the gate ran, its subject did not). Since #7876 every class-key cache also
//! has one function-lifetime bind; that negative asserts there is exactly that
//! one bind rather than no bind anywhere in the function.
//!
//! The rest of this file is lowering-INDEPENDENT and deliberately unpinned.
//!
Expand Down Expand Up @@ -1312,9 +1314,11 @@ fn a_collection_free_construction_emits_no_this_slot_root() {
instance temp root — the `this`-slot bind is gated on the same \
predicate",
);
assert!(
!f.contains("@js_shadow_slot_bind"),
"an inert construction must not grow the shadow frame for a `this` \
slot that cannot go stale (#7202):\n{f}"
assert_eq!(
f.matches("@js_shadow_slot_bind").count(),
1,
"an inert construction needs only #7876's function-lifetime class-key \
root; it must not grow the shadow frame for a `this` slot that cannot \
go stale (#7202):\n{f}"
);
}
1 change: 1 addition & 0 deletions crates/perry-runtime/src/arena/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,5 +140,6 @@ pub(crate) use page_meta::{
pub(crate) use page_meta::{
deferred_old_page_registrations_len, generation_page_base,
old_arena_page_index_clear_for_tests, old_page_meta_for_tests,
old_page_meta_snapshot_calls_for_tests, reset_old_page_meta_snapshot_calls_for_tests,
DEFERRED_OLD_PAGE_REGISTRATION_CAP, GENERATION_CLASS_SHIFT, GENERATION_PAGE_SIZE,
};
17 changes: 17 additions & 0 deletions crates/perry-runtime/src/arena/page_meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,11 @@ thread_local! {
static OLD_GEN_PAGE_DIRTY_EPOCH: Cell<u64> = const { Cell::new(1) };
}

#[cfg(test)]
thread_local! {
static OLD_PAGE_META_SNAPSHOT_CALLS: Cell<usize> = const { Cell::new(0) };
}

// --- #7469 hot-TLS address providers. See `crate::tls_hot`. ---

/// Address of this thread's `PAGE_GENERATION_CACHE`.
Expand Down Expand Up @@ -1178,6 +1183,8 @@ pub(crate) fn old_page_summary() -> OldPageSummary {
}

pub(crate) fn old_page_meta_snapshot() -> Vec<OldPageMeta> {
#[cfg(test)]
OLD_PAGE_META_SNAPSHOT_CALLS.with(|calls| calls.set(calls.get().saturating_add(1)));
// #7624 READER (`OLD_GEN_PAGE_META`): this one drives real policy —
// `gc/oldgen_defrag.rs` selects evacuation pages from it.
flush_deferred_old_page_registrations();
Expand All @@ -1194,6 +1201,16 @@ pub(crate) fn old_page_meta_snapshot() -> Vec<OldPageMeta> {
})
}

#[cfg(test)]
pub(crate) fn old_page_meta_snapshot_calls_for_tests() -> usize {
OLD_PAGE_META_SNAPSHOT_CALLS.with(Cell::get)
}

#[cfg(test)]
pub(crate) fn reset_old_page_meta_snapshot_calls_for_tests() {
OLD_PAGE_META_SNAPSHOT_CALLS.with(|calls| calls.set(0));
}

/// Fold a stale `dirty_slots` stamp down to the effective value so a copied
/// `OldPageMeta` handed to a caller always reports this cycle's dirty-slot
/// count directly in `dirty_slots`, without the caller needing the epoch (#6181).
Expand Down
19 changes: 12 additions & 7 deletions crates/perry-runtime/src/gc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,13 +303,6 @@ fn gc_collect_minor_with_trigger_inner(
// recorded in the cycle trace and read back by the evacuation-policy tests.
let evacuation_policy_allowed = true;
let force_evacuation = gc_force_evacuate_enabled();
let old_page_selection = if old_to_young_tracking_complete() {
select_old_page_defrag_pages(force_evacuation)
} else {
OldPageDefragSelection::default()
};
let old_page_source_blocks =
crate::arena::old_arena_source_blocks_for_pages(&old_page_selection.pages);
// MARK_SEEDS persists across GC cycles. Clear before any try_mark
// call so trace sees only this cycle's freshly-marked headers.
clear_mark_seeds();
Expand All @@ -333,6 +326,18 @@ fn gc_collect_minor_with_trigger_inner(
};
}
clear_mark_seeds();
// Old-page defrag belongs to the non-copying fallback below. Snapshotting
// and sorting all old-page metadata before trying the copying fast path
// charged every ordinary minor an O(old pages) cost even though that path
// cannot consume the selection. Defer both selection and source-block
// expansion until the fast path has declined the collection.
let old_page_selection = if old_to_young_tracking_complete() {
select_old_page_defrag_pages(force_evacuation)
} else {
OldPageDefragSelection::default()
};
let old_page_source_blocks =
crate::arena::old_arena_source_blocks_for_pages(&old_page_selection.pages);
GcCycleState::new_minor_fallback(
trigger,
trace,
Expand Down
Loading
Loading