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
3 changes: 3 additions & 0 deletions changelog.d/9376-large-presized-array-fill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Large pre-sized arrays now materialize their dense backing storage incrementally
during sequential indexed writes instead of falling back to quadratic sparse
property insertion. Growth also preserves expandos and GC-traced element slots.
9 changes: 6 additions & 3 deletions crates/perry-codegen/src/expr/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,9 +300,13 @@ pub(crate) fn lower_index_set_fast(
let hdr_capacity = blk.load(I32, &cap_ptr);
let index_nonnegative = blk.icmp_slt(I32, &idx_i32, "0");
let index_nonnegative = blk.icmp_eq(I1, &index_nonnegative, "false");
let length_sane = blk.icmp_ule(I32, &hdr_length, "16000000");
let capacity_sane = blk.icmp_ule(I32, &hdr_capacity, "16000000");
let length_within_capacity = blk.icmp_ule(I32, &hdr_length, &hdr_capacity);
let index_within_capacity = blk.icmp_ult(I32, &idx_i32, &hdr_capacity);
// #9371: `new Array(largeLength)` intentionally has length above
// capacity. A store into its allocated prefix is still safe; only
// the boundary write needs the runtime to grow that prefix.
let storage_safe = blk.or(I1, &length_within_capacity, &index_within_capacity);

let mut guard_ok = blk.and(I1, &is_array, &not_forwarded);
guard_ok = blk.and(I1, &guard_ok, &integrity_clean);
Expand All @@ -318,9 +322,8 @@ pub(crate) fn lower_index_set_fast(
}
guard_ok = blk.and(I1, &guard_ok, &default_prototype_chain);
guard_ok = blk.and(I1, &guard_ok, &index_nonnegative);
guard_ok = blk.and(I1, &guard_ok, &length_sane);
guard_ok = blk.and(I1, &guard_ok, &capacity_sane);
guard_ok = blk.and(I1, &guard_ok, &length_within_capacity);
guard_ok = blk.and(I1, &guard_ok, &storage_safe);
// #9237: kept exactly where it is load-bearing. The comment below
// is about the RAW store — a `number[]` slot can genuinely receive a
// non-number at runtime, and writing its NaN-boxed tag verbatim as a
Expand Down
8 changes: 4 additions & 4 deletions crates/perry-codegen/src/expr/index_set_guarded.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,18 +179,18 @@ pub(super) fn emit_guarded_inbounds_array_store(
// STRICTLY in bounds: `index == length` is an extend, which changes
// `length` and may need a realloc, so it belongs on the slow arm.
let index_in_bounds = blk.icmp_ult(I32, idx_i32, &length);
let length_sane = blk.icmp_ule(I32, &length, "16000000");
let capacity_sane = blk.icmp_ule(I32, &capacity, "16000000");
let length_within_capacity = blk.icmp_ule(I32, &length, &capacity);
let index_within_capacity = blk.icmp_ult(I32, idx_i32, &capacity);

let mut guard_ok = blk.and(I1, &is_array, &not_forwarded);
guard_ok = blk.and(I1, &guard_ok, &integrity_clean);
guard_ok = blk.and(I1, &guard_ok, &default_prototype_chain);
guard_ok = blk.and(I1, &guard_ok, &index_nonnegative);
guard_ok = blk.and(I1, &guard_ok, &index_in_bounds);
guard_ok = blk.and(I1, &guard_ok, &length_sane);
guard_ok = blk.and(I1, &guard_ok, &capacity_sane);
guard_ok = blk.and(I1, &guard_ok, &length_within_capacity);
// #9371: a large pre-sized holey Array has `length > capacity`, but
// an in-bounds index below capacity still names allocated storage.
guard_ok = blk.and(I1, &guard_ok, &index_within_capacity);
blk.cond_br(&guard_ok, &fast_label, &slow_label);
// The live head's `_reserved` word dominates the fast arm; the numeric
// write note below is gated on it.
Expand Down
58 changes: 57 additions & 1 deletion crates/perry-runtime/src/array/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,26 @@ fn merge_array_named_props(
barrier_array_named_props(owner, entry);
}

/// Rekey an array's named-property side table when `js_array_grow` replaces
/// its inline allocation. GC moves do this through the mutable-root visitor;
/// array growth is outside the collector and must perform the same ownership
/// transfer explicitly before publishing the forwarding stub.
pub(crate) fn transfer_array_named_property_owner(old_owner: usize, new_owner: usize) {
if old_owner == 0
|| new_owner == 0
|| old_owner == new_owner
|| !ARRAY_NAMED_PROPS_EVER.load(std::sync::atomic::Ordering::Acquire)
{
return;
}
ARRAY_NAMED_PROPS.with(|m| {
let mut props = m.borrow_mut();
if let Some(old_props) = props.remove(&old_owner) {
merge_array_named_props(&mut props, new_owner, old_props);
}
});
}

pub(crate) fn scan_array_named_property_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) {
ARRAY_NAMED_PROPS.with(|m| {
let mut props = m.borrow_mut();
Expand Down Expand Up @@ -508,6 +528,24 @@ pub(crate) unsafe fn array_has_named_properties_resolved(arr: *const ArrayHeader
})
}

/// Whether an already-resolved array owns numeric indices in the named-property
/// side table. Those indices live beyond the dense allocation. Growing the
/// allocation across one without migrating it would hide the property because
/// indexed reads consult the side table only at `index >= capacity`.
#[inline]
pub(crate) unsafe fn array_has_sparse_index_properties_resolved(arr: *const ArrayHeader) -> bool {
if !ARRAY_NAMED_PROPS_EVER.load(std::sync::atomic::Ordering::Acquire) {
return false;
}
ARRAY_NAMED_PROPS.with(|m| {
m.borrow().get(&(arr as usize)).is_some_and(|props| {
props
.iter()
.any(|prop| crate::object::canonical_array_index(&prop.name).is_some())
})
})
}

pub(crate) unsafe fn array_named_property_get(
arr: *const ArrayHeader,
key: *const crate::StringHeader,
Expand Down Expand Up @@ -798,9 +836,19 @@ pub(crate) fn clean_arr_ptr(arr: *const ArrayHeader) -> *const ArrayHeader {
// wave them through; everything else at this size is
// almost certainly corrupted.
let addr = cleaned as usize;
// #9371: a large pre-sized holey array grows its dense prefix on
// demand, so a legitimate sparse header can have capacity above
// the old one-million cutoff while it is still below `length`.
// Prove the capacity against the tracked allocation's exact byte
// size instead of imposing a second semantic threshold. Corrupt
// length/capacity words still fail closed unless they describe
// precisely the allocation the GC owns at this address.
let sparse_array_shape = tracked_obj_type == Some(crate::gc::GC_TYPE_ARRAY)
&& hdr.length > hdr.capacity
&& hdr.capacity <= 1_000_000;
&& tracked_header.is_some_and(|gc_header| {
checked_array_allocation_size(hdr.capacity as usize)
== Some((*gc_header.as_ptr()).size as usize)
});
if sparse_array_shape {
return cleaned;
}
Expand Down Expand Up @@ -1910,6 +1958,14 @@ pub(crate) fn array_byte_size(capacity: usize) -> usize {
std::mem::size_of::<ArrayHeader>() + capacity * std::mem::size_of::<f64>()
}

#[inline]
pub(super) fn checked_array_allocation_size(capacity: usize) -> Option<usize> {
capacity
.checked_mul(std::mem::size_of::<f64>())
.and_then(|elements| std::mem::size_of::<ArrayHeader>().checked_add(elements))
.and_then(|payload| crate::gc::GC_HEADER_SIZE.checked_add(payload))
}

#[inline]
pub(super) unsafe fn array_elements_ptr(arr: *mut ArrayHeader) -> *mut u64 {
(arr as *mut u8).add(std::mem::size_of::<ArrayHeader>()) as *mut u64
Expand Down
34 changes: 28 additions & 6 deletions crates/perry-runtime/src/array/header_gc_slots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,30 @@ pub(crate) unsafe fn gc_element_slot_range(
}
let length = (*arr).length as usize;
let capacity = (*arr).capacity as usize;
if length > capacity || length > 16_000_000 {
if capacity > 16_000_000 {
return None;
}
if length > capacity {
// Preserve the old corruption fail-closed behavior while admitting
// legitimate sparse headers: the claimed capacity must exactly match
// the GC allocation that owns this payload.
let Some(gc_header) = crate::value::addr_class::try_read_tracked_gc_header(arr as usize)
else {
return None;
};
if checked_array_allocation_size(capacity) != Some((*gc_header.as_ptr()).size as usize) {
return None;
}
}
// A large `new Array(length)` intentionally starts with `length >
// capacity`. Only the allocated dense prefix can contain inline child
// edges; indices beyond it live in the separately-rooted named-property
// table. Returning no range here would lose pointer values written into
// that prefix while it grows (#9371).
let dense_prefix = length.min(capacity);
Some(crate::gc::HeapSlotRange::new(
array_elements_ptr(arr),
length,
dense_prefix,
))
}

Expand Down Expand Up @@ -248,13 +266,17 @@ pub(crate) unsafe fn replay_array_growth_write_barriers(arr: *mut ArrayHeader) {
return;
}

let length = (*arr).length as usize;
if length == 0 || length > 16_000_000 {
// `length` may exceed the allocation for a large fresh holey array. Growth
// copies and barriers only the allocated dense prefix; scanning logical
// length here walked beyond intermediate growth allocations and corrupted
// earlier values once the array became dense (#9371).
let dense_prefix = ((*arr).length as usize).min((*arr).capacity as usize);
if dense_prefix == 0 || dense_prefix > 16_000_000 {
return;
}

let slots = array_elements_ptr(arr);
if crate::gc::layout_visit_pointer_slots_for_user(arr as usize, length, |index| {
if crate::gc::layout_visit_pointer_slots_for_user(arr as usize, dense_prefix, |index| {
let slot = slots.add(index);
crate::gc::runtime_write_barrier_slot(arr as usize, slot as usize, *slot);
}) {
Expand All @@ -265,7 +287,7 @@ pub(crate) unsafe fn replay_array_growth_write_barriers(arr: *mut ArrayHeader) {
// per-store entry point would re-derive the parent classification `length`
// times and re-assert a page-granular fact ~512 times per page. See
// `gc::barrier::replay_old_parent_slot_range`.
crate::gc::replay_old_parent_slot_range_barriers(arr as usize, slots, length);
crate::gc::replay_old_parent_slot_range_barriers(arr as usize, slots, dense_prefix);
}

#[inline]
Expand Down
52 changes: 22 additions & 30 deletions crates/perry-runtime/src/array/indexing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,6 @@ use proto_chain::{array_object_proto_index_owner, ArrayCustomProto};

const MAX_DENSE_ARRAY_GROW_LENGTH: u32 = 1_000_000;

/// Largest hole (`index - length`) an extending write may create while still
/// growing the dense backing store, once the array is past
/// `MAX_DENSE_ARRAY_GROW_LENGTH`. Sparse storage is for *jumps* far beyond the
/// current length (`a[2**32-2] = v` on a 3-element array must not allocate
/// 34 GB); sequential growth (`for (i...) arr[i] = v`, gap 0) must stay dense
/// no matter how large the array gets — routing it through string-keyed
/// property sets is quadratic and hung the 10M-element `03_array_write`
/// benchmark for 6 hours (Regression Check, v0.5.1129–v0.5.1150).
const DENSE_ARRAY_GAP_LIMIT: u32 = 1024;

#[inline]
pub(crate) fn invalidate_array_index_fast_path() {
PERRY_ARRAY_INDEX_FAST_PATH_INVALIDATED.store(1, Ordering::Relaxed);
Expand Down Expand Up @@ -85,26 +75,6 @@ pub(crate) fn note_array_index_write(arr: usize) {
}
}

#[inline]
unsafe fn array_sparse_index_property_get(arr: *const ArrayHeader, index: u32) -> Option<f64> {
let arr = clean_arr_ptr(arr);
if arr.is_null() || index < (*arr).capacity {
return None;
}
let key = index.to_string();
array_named_property_get_by_name(arr, &key)
}

unsafe fn array_sparse_index_property_set(arr: *mut ArrayHeader, index: u32, value: f64) {
let key = index.to_string();
let key_ptr = crate::string::js_string_from_bytes(key.as_ptr(), key.len() as u32);
array_named_property_set(arr, key_ptr, value);
let new_length = index + 1;
if (*arr).length < new_length {
(*arr).length = new_length;
}
}

/// Whether iterating `arr` with the raw dense-store loop would diverge from the
/// spec `[[HasProperty]]`/`[[Get]]` protocol. True ("exotic") when the array has
/// index accessors / custom-attr descriptors, lives in (partly) sparse storage,
Expand Down Expand Up @@ -1559,6 +1529,28 @@ unsafe fn js_array_set_f64_extend_resolved(
return arr;
}
if index >= (*arr).capacity {
if blocks_extension {
return arr;
}
let capacity = (*arr).capacity;
// A large pre-sized holey array has logical length but only a
// small dense prefix. Writes at (or near) that prefix are an
// ordinary dense fill, not sparse property creation. Grow
// geometrically just as the extending path does. Once a true
// far index has entered sparse storage, do not grow across it:
// indexed reads below capacity intentionally skip the named-
// property table, so covering it would hide the property.
if index - capacity <= DENSE_ARRAY_GAP_LIMIT
&& !array_has_sparse_index_properties_resolved(arr)
{
let arr = js_array_grow(arr, index + 1);
let value = value_handle.get_nanbox_f64();
let store_flags = array_object_flags_resolved(arr);
// GC_STORE_AUDIT(BARRIERED): the resolved store performs
// the layout note and write barrier on the grown head.
store_array_slot_resolved(arr, index as usize, value, store_flags);
return arr;
}
let value = value_handle.get_nanbox_f64();
array_sparse_index_property_set(arr, index, value);
return arr;
Expand Down
39 changes: 36 additions & 3 deletions crates/perry-runtime/src/array/indexing_support.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,44 @@
//! Indexing support split out of `indexing.rs` to keep it under the repo's
//! 2000-line cap: the strict-store TypeError throwers, the prototype
//! indexed-property / iterator invalidation latches, and the dense keys-array
//! slot helpers. Pure move except for the `use` lines and `pub(super)`
//! visibility on items `indexing.rs` still calls.
//! indexed-property / iterator invalidation latches, sparse index helpers, and
//! the dense keys-array slot helpers. Pure move except for the `use` lines and
//! `pub(super)` visibility on items `indexing.rs` still calls.
use super::*;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};

/// Largest hole (`index - length`) an extending write may create while still
/// growing the dense backing store once the array is large. Sparse storage is
/// for jumps far beyond the current length; sequential growth must stay dense
/// because routing it through string-keyed property sets is quadratic.
pub(super) const DENSE_ARRAY_GAP_LIMIT: u32 = 1024;

#[inline]
pub(super) unsafe fn array_sparse_index_property_get(
arr: *const ArrayHeader,
index: u32,
) -> Option<f64> {
let arr = clean_arr_ptr(arr);
if arr.is_null() || index < (*arr).capacity {
return None;
}
let key = index.to_string();
array_named_property_get_by_name(arr, &key)
}

pub(super) unsafe fn array_sparse_index_property_set(
arr: *mut ArrayHeader,
index: u32,
value: f64,
) {
let key = index.to_string();
let key_ptr = crate::string::js_string_from_bytes(key.as_ptr(), key.len() as u32);
array_named_property_set(arr, key_ptr, value);
let new_length = index + 1;
if (*arr).length < new_length {
(*arr).length = new_length;
}
}

/// Resolve a raw array head a generated loop re-read from its root after a
/// callback returned: the callback may have grown the array, leaving the root
/// on a forwarding stub. Pure `clean_arr_ptr`; null for anything that is not
Expand Down
62 changes: 62 additions & 0 deletions crates/perry-runtime/src/array/large_presized_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//! Regression coverage for #9371: a large `new Array(length)` keeps a small
//! initial backing store, but writes at the dense frontier must grow that
//! store instead of accumulating numeric keys in the named-property table.

use super::*;

#[test]
fn large_presized_array_grows_its_dense_frontier() {
unsafe {
const LENGTH: u32 = 1_200_000;
let mut arr = js_array_constructor_single(LENGTH as f64);
assert_eq!((*arr).length, LENGTH);
assert_eq!((*arr).capacity, MIN_ARRAY_CAPACITY);

for index in 0..LENGTH {
let old_capacity = (*arr).capacity;
arr = js_array_set_f64_extend(arr, index, index as f64 + 0.25);
if (*arr).capacity != old_capacity {
assert_eq!(
js_array_get_f64(arr, 0),
0.25,
"growth at index {index} lost the first value (capacity {old_capacity} -> {})",
(*arr).capacity
);
assert_eq!(
js_array_get_f64(arr, index),
index as f64 + 0.25,
"growth at index {index} lost the current value"
);
}
}

assert_eq!((*arr).length, LENGTH);
assert!(
(*arr).capacity >= LENGTH,
"sequential in-bounds writes must grow dense storage"
);
for index in 0..LENGTH {
assert_eq!(js_array_get_f64(arr, index), index as f64 + 0.25);
}
}
}

#[test]
fn existing_sparse_indices_prevent_dense_growth_from_hiding_them() {
unsafe {
let mut arr = js_array_constructor_single(1_000_001.0);
arr = js_array_set_f64_extend(arr, 500_000, 7.0);
let capacity = (*arr).capacity;
assert!(500_000 >= capacity, "fixture must take sparse storage");

arr = js_array_set_f64_extend(arr, capacity, 9.0);

assert_eq!(
(*arr).capacity,
capacity,
"growth must not cover an existing sparse numeric property"
);
assert_eq!(js_array_get_f64(arr, capacity), 9.0);
assert_eq!(js_array_get_f64(arr, 500_000), 7.0);
}
}
Loading
Loading