Skip to content

Commit 1c9174f

Browse files
author
Ralph Küpper
committed
test(gc): pin that old-page relocation expands a described promoted run first
1 parent c109b08 commit 1c9174f

2 files changed

Lines changed: 135 additions & 1 deletion

File tree

crates/perry-runtime/src/arena/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ pub(crate) use page_meta::{
144144
pub(crate) use page_meta::{
145145
deferred_old_page_registrations_len, generation_page_base,
146146
old_arena_page_index_clear_for_tests, old_page_meta_for_tests,
147-
old_page_meta_snapshot_calls_for_tests, pending_promoted_page_runs,
147+
old_page_meta_snapshot_calls_for_tests, pending_promoted_page_runs, register_promoted_page_run,
148148
reset_old_page_meta_snapshot_calls_for_tests, DEFERRED_OLD_PAGE_REGISTRATION_CAP,
149149
GENERATION_CLASS_SHIFT, GENERATION_PAGE_SIZE,
150150
};

crates/perry-runtime/src/gc/tests/promote_in_place.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,3 +413,137 @@ fn a_low_survival_cycle_still_evacuates_and_moves_the_object() {
413413
);
414414
assert!(crate::arena::pointer_in_nursery(after));
415415
}
416+
417+
// ---------------------------------------------------------------------------
418+
// #7913 interaction: old-page relocation vs a still-DESCRIBED promoted run
419+
// ---------------------------------------------------------------------------
420+
421+
/// #7914 describes a promoted page's object list by its `first_header` address
422+
/// and expands it lazily; #7913 relocates old pages BY DEFAULT. If a page
423+
/// carrying an unexpanded run could be relocated, the deferred expansion would
424+
/// later parse stale addresses — the silent-corruption shape.
425+
///
426+
/// It cannot, and this pins the mechanism that makes it so:
427+
/// `evacuate_selected_old_pages_collecting` snapshots its source-block
428+
/// occupants through `old_arena_walk_objects_on_pages`, which expands every
429+
/// pending run on every page of the source blocks BEFORE a single object is
430+
/// forwarded. #7913 widened that enumeration from the selected pages to the
431+
/// whole containing source blocks, so it covers strictly more than before.
432+
///
433+
/// The test is written so it cannot pass by accident: the objects are removed
434+
/// from the eager index first, so a run that is NOT expanded leaves
435+
/// `source_headers` empty, the all-or-nothing guard returns early, and
436+
/// `old_page_moved_objects` reads 0 instead of 3. Deleting the
437+
/// `materialize_promoted_page_runs` call in `old_arena_walk_objects_on_pages`
438+
/// turns this red.
439+
#[test]
440+
fn old_page_relocation_expands_a_described_run_before_it_moves_anything() {
441+
let _isolation = copying_nursery_isolation_lock();
442+
reset_remembered_set();
443+
clear_marks();
444+
clear_mark_seeds();
445+
CONS_PINNED.with(|s| s.borrow_mut().clear());
446+
447+
// Three real old-gen objects, contiguous and linearly parseable — the
448+
// shape a promoted block hands to OLD_ARENA.
449+
let users: Vec<usize> = (0..3)
450+
.map(|_| crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT) as usize)
451+
.collect();
452+
let headers: Vec<(*mut GcHeader, usize)> =
453+
users.iter().map(|&u| old_test_header_and_size(u)).collect();
454+
let first = headers[0].0 as usize;
455+
let last = headers[2].0 as usize;
456+
let total: usize = headers.iter().map(|&(_, t)| t).sum();
457+
458+
let mut selected_pages = crate::fast_hash::new_ptr_hash_set();
459+
for &(header, size) in &headers {
460+
for (page, _) in crate::arena::old_object_page_overlaps(header as usize, size) {
461+
selected_pages.insert(page);
462+
}
463+
}
464+
// All three must share one page for the run to describe them as one span;
465+
// 64-byte objects out of a fresh block do, but assert rather than assume.
466+
assert_eq!(
467+
selected_pages.len(),
468+
1,
469+
"test setup expects one page; a multi-page split would need one run per page"
470+
);
471+
let page = *selected_pages.iter().next().unwrap();
472+
473+
// Drop the eager index built at birth, then DESCRIBE the same objects.
474+
// From here the run is the only record that this page has occupants.
475+
crate::arena::old_arena_page_index_clear_for_tests();
476+
crate::arena::register_promoted_page_run(page, first, last, headers.len(), total);
477+
assert_eq!(
478+
crate::arena::pending_promoted_page_runs(),
479+
1,
480+
"the page must be DESCRIBED going in, or this test proves nothing"
481+
);
482+
483+
for &(header, _) in &headers {
484+
unsafe {
485+
(*header).gc_flags |= GC_FLAG_MARKED;
486+
}
487+
}
488+
489+
let mut new_headers = Vec::new();
490+
let mut original_headers = Vec::new();
491+
let moved = evacuate_selected_old_pages_collecting(
492+
&selected_pages,
493+
&mut new_headers,
494+
&mut original_headers,
495+
);
496+
497+
assert_eq!(
498+
moved.old_page_moved_objects,
499+
headers.len(),
500+
"relocation must see every occupant of a DESCRIBED page. Seeing fewer \
501+
means it relocated a block while some occupants were still only \
502+
described — their memory would be released with live objects in it, \
503+
and the deferred expansion would parse recycled addresses"
504+
);
505+
assert_eq!(
506+
crate::arena::pending_promoted_page_runs(),
507+
0,
508+
"the run must have been consumed by the relocation's own enumeration"
509+
);
510+
for &(header, _) in &headers {
511+
unsafe {
512+
assert_ne!(
513+
(*header).gc_flags & GC_FLAG_FORWARDED,
514+
0,
515+
"every described occupant must be forwarded, not just indexed"
516+
);
517+
}
518+
}
519+
520+
release_evacuated_original_forwarding_stubs(&original_headers);
521+
clear_marks();
522+
CONS_PINNED.with(|s| s.borrow_mut().clear());
523+
}
524+
525+
/// The second, independent line of defence, stated as a test rather than a
526+
/// comment: a page that has only just been promoted is not defrag-eligible at
527+
/// all, because `register_promoted_page_run` records live bytes and never dead
528+
/// ones, and `old_page_defrag_eligible` requires `dead_bytes > 0`. Dead bytes
529+
/// come only from the old-gen sweep, and the sweep's cycle constructor
530+
/// (`GcCycleState::new_full`) expands every pending run before it starts.
531+
#[test]
532+
fn a_freshly_described_page_is_not_defrag_eligible() {
533+
let _isolation = copying_nursery_isolation_lock();
534+
let user = crate::arena::arena_alloc_gc_old(64, 8, GC_TYPE_OBJECT) as usize;
535+
let (header, size) = old_test_header_and_size(user);
536+
let page = crate::arena::old_object_page_overlaps(header as usize, size)[0].0;
537+
crate::arena::old_arena_page_index_clear_for_tests();
538+
crate::arena::register_promoted_page_run(page, header as usize, header as usize, 1, size);
539+
540+
let meta = crate::arena::old_page_meta_for_tests(page).expect("promotion records page meta");
541+
assert_eq!(
542+
meta.dead_bytes, 0,
543+
"a promotion records live bytes only; dead bytes are the sweep's to record"
544+
);
545+
assert!(
546+
!super::super::oldgen_defrag::old_page_defrag_eligible(meta),
547+
"a page whose run is still described must not be a defrag candidate"
548+
);
549+
}

0 commit comments

Comments
 (0)