Skip to content
Closed
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
5 changes: 5 additions & 0 deletions changelog.d/9924-gc-promotion-rollback-young-logs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Fixed

- Preserve side-table young-root logs when the first copying collection
abandons speculative in-place promotion, so the evacuation retry does not
lose live objects or corrupt full applications during startup.
50 changes: 50 additions & 0 deletions crates/perry-runtime/src/gc/tests/promote_in_place.rs
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,56 @@ fn a_first_cycle_attempt_that_its_own_trace_refutes_rolls_back_and_evacuates() {
);
}

/// A speculative promotion temporarily gives every nursery block old-gen
/// semantics. Young-entry root logs must nevertheless survive an attempt that
/// is rolled back: the evacuation retry still depends on them to find values
/// reachable only through a side table.
#[test]
fn first_cycle_rollback_preserves_young_side_table_roots_for_the_retry() {
let _guard = CopyingNurseryTestGuard::new(0);
let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers();
let _promote = InPlacePromotionTestGuard::enabled(1000);
clear_young_survival_for_tests();
// Exhaust the blind-promotion budget so the speculative attempt performs
// the mutable-root mark/rewrite walks that consume young logs.
seed_untraced_promoted_bytes_for_tests(usize::MAX);
gc_register_mutable_root_scanner(crate::closure::scan_closure_dynamic_props_roots_mut);

let owner = crate::arena::arena_alloc_gc_old(
std::mem::size_of::<crate::closure::ClosureHeader>(),
std::mem::align_of::<crate::closure::ClosureHeader>(),
GC_TYPE_CLOSURE,
) as usize;
unsafe { init_test_closure(owner as *mut u8) };
// Arm the remembered-set reconstruction before publishing the value, then
// remove the ordinary old-object edge so the young log is the only root.
let _ = remembered_dirty_snapshot();
let value = young_leaf();
crate::closure::closure_set_dynamic_prop(owner, "memo", f64::from_bits(string_bits(value)));
remembered_set_clear();
for _ in 0..64 {
let _garbage = young_leaf();
}

let attempts_before = first_cycle_promotion_attempts();
let rollbacks_before = first_cycle_promotion_rollbacks();
let trace = collect_minor_trace(GcTriggerKind::Direct);

assert_copied_minor_trace(&trace, true, CopiedMinorFallbackReason::None, false);
assert_eq!(first_cycle_promotion_attempts() - attempts_before, 1);
assert_eq!(first_cycle_promotion_rollbacks() - rollbacks_before, 1);
let bits = crate::closure::closure_get_own_dynamic_prop(owner, "memo")
.expect("the old owner must retain its side-table value")
.to_bits();
let value_after = (bits & POINTER_MASK) as usize;
assert_eq!(bits & TAG_MASK, STRING_TAG);
assert_ne!(
value_after, value,
"the retry must evacuate a value reachable only through the young log"
);
assert!(crate::arena::pointer_in_nursery(value_after));
}

// ---------------------------------------------------------------------------
// #7913 interaction: old-page relocation vs a still-DESCRIBED promoted run
// ---------------------------------------------------------------------------
Expand Down
30 changes: 19 additions & 11 deletions crates/perry-runtime/src/gc/young_log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
use std::cell::RefCell;

use super::GC_HEADER_SIZE;
use crate::arena::HeapGeneration;
use crate::arena::HeapSpace;
use crate::value::{BIGINT_TAG, POINTER_MASK, POINTER_TAG, STRING_TAG, TAG_MASK};

/// Keys of side-table entries that may hold a pointer a minor can act on.
Expand Down Expand Up @@ -179,21 +179,29 @@ impl<K: Copy + Ord> YoungLog<K> {

/// Can a minor-scoped pass act on the object at `addr`?
///
/// `false` is authoritative for the old generation only: an old object is
/// neither moved nor swept by any minor, and it never becomes young again.
/// Everything a minor moves, marks-through or sweeps answers `true` —
/// nursery (eden + both survivor halves), longlived, and malloc-GC objects.
/// A non-heap word (handle id, foreign-thread address, integer) answers
/// `false` through the exact malloc-registry probe, never a header sniff.
/// `false` is authoritative for ordinary old-generation space only: an old
/// object is neither moved nor swept by any minor, and it never becomes young
/// again. `PromotedYoung` remains relevant while an in-place promotion is in
/// flight because a speculative first-cycle attempt can still roll its retag
/// back and evacuate those objects. Dropping their keys during the attempt
/// would make the retry skip live side-table roots. Everything else a minor
/// moves, marks-through or sweeps answers `true` — nursery (eden + both
/// survivor halves), longlived, and malloc-GC objects. A non-heap word (handle
/// id, foreign-thread address, integer) answers `false` through the exact
/// malloc-registry probe, never a header sniff.
#[inline]
pub(crate) fn addr_is_minor_relevant(addr: usize) -> bool {
if addr == 0 {
return false;
}
match crate::arena::classify_heap_generation(addr) {
HeapGeneration::Old => false,
HeapGeneration::Nursery | HeapGeneration::Longlived => true,
HeapGeneration::Unknown => {
match crate::arena::classify_heap_space(addr) {
HeapSpace::Old => false,
HeapSpace::NurseryEden
| HeapSpace::Survivor0
| HeapSpace::Survivor1
| HeapSpace::Longlived
| HeapSpace::PromotedYoung => true,
HeapSpace::Unknown => {
addr > GC_HEADER_SIZE
&& super::malloc::gc_malloc_header_is_tracked(
(addr - GC_HEADER_SIZE) as *const super::GcHeader,
Expand Down
Loading