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
6 changes: 6 additions & 0 deletions changelog.d/10223-large-array-tracing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Fix garbage collection of large arrays whose backing capacity exceeds their
live length. The collector now bounds the element range by the allocation size
instead of an unrelated capacity cutoff, preserving heap references after array
growth. Regression coverage checks the live range, shared slot descriptors and
child marking at 9, 10 and 16 million elements, plus invalid-capacity and sparse
array bounds.
4 changes: 3 additions & 1 deletion crates/perry-runtime/src/array/header_gc_slots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ pub(crate) unsafe fn gc_element_slot_range(
}
let length = (*arr).length as usize;
let capacity = (*arr).capacity as usize;
if capacity > 16_000_000 || capacity > super::array_physical_capacity(arr) {
// Capacity includes growth slack. Only the allocation bounds it: a valid
// live prefix must remain visible to every GC walk after a grow.
if capacity > super::array_physical_capacity(arr) {
return None;
}
if length > capacity {
Expand Down
1 change: 1 addition & 0 deletions crates/perry-runtime/src/gc/tests/layout_trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use super::support::*;
mod array_layout;
mod declared_at_allocation;
mod element_shape;
mod large_array_slots;
mod object_closure_slots;
mod object_layout_invalidation;
mod per_object_tables;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//! Array tracing is bounded by allocated storage, including unused growth slack.
//! These checks inspect ranges and marking without sweeping or moving objects.

use super::*;

#[test]
fn large_array_live_prefix_is_enumerated_and_marked() {
let _isolation = copying_nursery_isolation_lock();
let _trigger = GcTriggerThresholdTestGuard::suppress_automatic_triggers();
clear_marks();
clear_mark_seeds();

// The capacity produced by geometric growth is larger than every tested
// live prefix. Allocate it once; the unused slots remain initialized holes.
let arr = crate::array::js_array_alloc(1 << 24);
let header = unsafe { header_from_user_ptr(arr.cast()) };
for length in [9_000_000, 10_000_000, 16_000_000] {
unsafe {
(*arr).length = length;
let range = crate::array::gc_element_slot_range(arr)
.expect("a valid allocation must expose its live prefix");
assert_eq!(range.slot_count(), length as usize);
}

let text = b"large-array-live-child";
let child = crate::string::js_string_from_bytes(text.as_ptr(), text.len() as u32);
let child_header = unsafe { header_from_user_ptr(child.cast()) };
let index = length - 1;
crate::array::js_array_set_f64(arr, 0, 42.0);
crate::array::js_array_set_f64(arr, index, crate::value::js_nanbox_string(child as i64));

// Visit the collector's shared descriptors, not just the range helper.
// Mark, copy, rewrite and dirty-slot scans all consume this slot set.
let expected_slot = unsafe { crate::array::array_elements_ptr(arr).add(index as usize) };
let mut found = false;
unsafe {
visit_gc_rewrite_slot_descriptors(header, |descriptor| {
descriptor.visit_slots(&mut |slot| {
found |= slot.slot == expected_slot;
});
});
}
assert!(found, "the last live element must be enumerated");

let valid_ptrs = build_valid_pointer_set();
let mut worklist = Vec::new();
unsafe {
trace_array(arr.cast(), &valid_ptrs, &mut worklist);
assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0);
}
clear_marks();
clear_mark_seeds();
}
}

#[test]
fn array_slot_range_preserves_allocation_and_sparse_bounds() {
let _trigger = GcTriggerThresholdTestGuard::suppress_automatic_triggers();
let arr = crate::array::js_array_alloc(16);
unsafe {
let capacity = (*arr).capacity;
(*arr).length = 3;
(*arr).capacity = capacity + 1;
assert!(crate::array::gc_element_slot_range(arr).is_none());
(*arr).capacity = capacity;

// Sparse logical length does not authorize scanning outside storage.
(*arr).length = u32::MAX;
assert_eq!(
crate::array::gc_element_slot_range(arr)
.unwrap()
.slot_count(),
capacity as usize
);
// A consumed queue prefix reduces the remaining dense capacity.
(*arr).capacity -= 4;
assert_eq!(
crate::array::gc_element_slot_range(arr)
.unwrap()
.slot_count(),
(capacity - 4) as usize
);
(*arr).length = 0;
(*arr).capacity = capacity;
}
}
Loading