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
78 changes: 65 additions & 13 deletions crates/perry-runtime/src/object/field_get_set/ic_miss.rs
Original file line number Diff line number Diff line change
Expand Up @@ -508,19 +508,35 @@ pub extern "C" fn js_object_get_field_ic_miss(
// run time — more than the entire polymorphic-dispatch fix above saved.
//
// `GC_TYPE_ARRAY` is a genuine dense array: buffers, typed arrays, lazy
// arrays, Sets and Maps all carry their own distinct `obj_type`, and an
// `class X extends Array` instance is an `ObjectHeader`
// (`GC_TYPE_OBJECT`). `js_array_length` still resolves growth-forwarding
// stubs, proxies and subclass receivers, so this only skips probes that
// cannot match — the expression returned is exactly the one
// `get_field_by_name_object_tail`'s array arm computes for this key,
// which is what makes it a pure short-circuit rather than a second
// implementation.
if unsafe { gc_type_of(obj) } == Some(crate::gc::GC_TYPE_ARRAY)
&& unsafe { key_bytes_are(key, b"length") }
{
let arr = obj as *const crate::array::ArrayHeader;
return crate::array::js_array_length(arr) as f64;
// arrays, Sets and Maps all carry their own distinct `obj_type`. A
// `class X extends Array` instance instead uses `GC_TYPE_OBJECT`, but
// the exact-ShapeId dense-layout proof can read its live own `length`
// slot without repeating generic object dispatch. Both arms retain
// their established helpers, making this a dispatch short-circuit
// rather than a second implementation of either representation.
if unsafe { key_bytes_are(key, b"length") } {
match unsafe { gc_type_of(obj) } {
Some(crate::gc::GC_TYPE_ARRAY) => {
let arr = obj as *const crate::array::ArrayHeader;
return crate::array::js_array_length(arr) as f64;
}
Some(crate::gc::GC_TYPE_OBJECT) => {
// Wolf ECS's Query and Archetype are `class ... extends
// Array` instances. They use ObjectHeader storage, so the
// Array arm above cannot recognize them and a megamorphic
// `.length` site otherwise repeats the full object lookup
// on every loop entry. Reuse the exact ShapeId-backed
// subclass layout proof already used by packed numeric
// reads. It declines accessor, prototype-override, sparse,
// and non-Array-subclass receivers, preserving the generic
// lookup below for every case it cannot prove.
let receiver = crate::value::js_nanbox_pointer(obj as i64);
if let Some(length) = crate::array::array_subclass_fast_length(receiver) {
return length;
}
}
_ => {}
}
}
unsafe {
if let Some(val) = closure_dynamic_prop_by_key(obj as usize, key) {
Expand Down Expand Up @@ -1955,4 +1971,40 @@ mod array_length_fast_path_tests {
);
}
}

/// Array subclasses are ObjectHeader-backed, so a polymorphic loop over
/// differently shaped instances cannot use the real-Array short circuit
/// above or reliably stay in one property PIC. The dense subclass proof
/// must return the live own `length`, while unrelated object-backed values
/// continue through ordinary property lookup.
#[test]
fn array_subclass_length_short_circuit_preserves_object_semantics() {
const CLASS_ID_ARRAY: u32 = 0xFFFF_0024;
const SUBCLASS_ID: u32 = 0x0077_8655;
let _lock = crate::gc::global_side_table_test_lock();
crate::object::js_register_class_parent(SUBCLASS_ID, CLASS_ID_ARRAY);

let obj = crate::object::js_object_alloc(SUBCLASS_ID, 2);
let receiver = crate::value::js_nanbox_pointer(obj as i64);
crate::node_stream::js_array_subclass_init(receiver, 0.0);
for (index, value) in [11.0, 22.0, 33.0].into_iter().enumerate() {
crate::object::js_object_set_index_polymorphic(obj as i64, index as f64, value);
}

let len_key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6);
let mut cache = [0i64; super::PIC_CACHE_WORDS];
let via_ic = super::js_object_get_field_ic_miss(obj, len_key, &mut cache);
let via_ladder = super::js_object_get_field_by_name_f64(obj, len_key);
assert_eq!(via_ic.to_bits(), via_ladder.to_bits());
assert_eq!(via_ic, 3.0, "the fast path must observe the live length");

let plain = crate::object::js_object_alloc(0, 1);
crate::object::js_object_set_field_by_name(plain, len_key, 123.0);
let mut plain_cache = [0i64; super::PIC_CACHE_WORDS];
assert_eq!(
super::js_object_get_field_ic_miss(plain, len_key, &mut plain_cache),
123.0,
"ordinary objects must retain their own `length` property semantics"
);
}
}
71 changes: 68 additions & 3 deletions crates/perry-runtime/src/typedarray/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,18 @@ pub const TA_CACHE_NEGATIVE: u64 = 0xFF;
pub static PERRY_TA_KIND_CACHE: [AtomicU64; TA_KIND_CACHE_SLOTS] =
[const { AtomicU64::new(0) }; TA_KIND_CACHE_SLOTS];

// The generic kind cache deliberately uses the exact slot formula duplicated
// by codegen. Large, equal-sized ECS columns can therefore share the same low
// address bits and continually evict one another. Whole-loop admission needs
// the stronger, persistent fact "this exact address is an owning Uint32Array",
// so keep a separate direct cache whose index folds higher address bits too.
// A hit is safe until unregister: a TypedArray header's kind and owning/view
// storage class never change during its lifetime, and unregister clears both
// caches before an address can be reused.
const INLINE_OWNING_U32_CACHE_SLOTS: usize = 64;
static INLINE_OWNING_U32_CACHE: [AtomicU64; INLINE_OWNING_U32_CACHE_SLOTS] =
[const { AtomicU64::new(0) }; INLINE_OWNING_U32_CACHE_SLOTS];

/// #5525 follow-up: process-global "any exotic typed-array views exist" guard,
/// exported under a stable link name for the codegen inline element path. A
/// non-owning typed array (an `ArrayBuffer`-aliasing view, or a native-arena
Expand Down Expand Up @@ -287,6 +299,33 @@ fn ta_kind_cache_invalidate(addr: usize) {
}
}

#[inline]
fn inline_owning_u32_cache_slot(addr: usize) -> usize {
let word = addr >> 3;
let mixed = word ^ (word >> 6) ^ (word >> 12);
mixed & (INLINE_OWNING_U32_CACHE_SLOTS - 1)
}

#[inline]
fn inline_owning_u32_cache_get(addr: usize) -> bool {
INLINE_OWNING_U32_CACHE[inline_owning_u32_cache_slot(addr)].load(Ordering::Relaxed)
== addr as u64
}

#[inline]
fn inline_owning_u32_cache_store(addr: usize) {
INLINE_OWNING_U32_CACHE[inline_owning_u32_cache_slot(addr)]
.store(addr as u64, Ordering::Relaxed);
}

#[inline]
fn inline_owning_u32_cache_invalidate(addr: usize) {
let slot = inline_owning_u32_cache_slot(addr);
if INLINE_OWNING_U32_CACHE[slot].load(Ordering::Relaxed) == addr as u64 {
INLINE_OWNING_U32_CACHE[slot].store(0, Ordering::Relaxed);
}
}

/// Cache probe: `None` = miss (consult the registry), `Some(None)` = cached
/// negative ("not a typed array"), `Some(Some(kind))` = cached typed array.
#[inline]
Expand Down Expand Up @@ -339,6 +378,7 @@ pub(crate) fn typed_array_registry_ever_used() -> bool {
pub fn unregister_typed_array(ptr: *const TypedArrayHeader) {
let owner = ptr as usize;
ta_kind_cache_invalidate(owner);
inline_owning_u32_cache_invalidate(owner);
TYPED_ARRAY_REGISTRY.with(|r| {
r.borrow_mut().remove(&owner);
});
Expand Down Expand Up @@ -587,22 +627,28 @@ pub extern "C" fn js_typed_array_masked_window_data_ptr(receiver: f64) -> i64 {

/// One-time loop admission primitive for erased ECS component columns. Return
/// the stable owning-header address only for an exact inline `Uint32Array`.
/// Consult the authoritative registry instead of the tiny direct-mapped kind
/// cache: sibling columns can collide there, which is harmless for individual
/// accesses but must not make a whole-loop proof spuriously fail forever.
/// Use the admission-specific address cache first, then consult the
/// authoritative registry on a miss. The generic direct-mapped kind cache is
/// intentionally not authority here: sibling columns can collide there,
/// which is harmless for individual accesses but must not make a whole-loop
/// proof spuriously fail forever.
#[inline]
pub(crate) fn inline_u32_addr(receiver: f64) -> usize {
let value = crate::value::JSValue::from_bits(receiver.to_bits());
if !value.is_pointer() {
return 0;
}
let addr = value.as_pointer::<TypedArrayHeader>() as usize;
if inline_owning_u32_cache_get(addr) {
return addr;
}
if lookup_typed_array_kind(addr) != Some(KIND_UINT32)
|| crate::native_arena::is_native_typed_view(addr as *const TypedArrayHeader)
|| crate::typedarray_view::view_meta_of(addr).is_some()
{
return 0;
}
inline_owning_u32_cache_store(addr);
addr
}

Expand Down Expand Up @@ -1253,6 +1299,25 @@ pub extern "C" fn js_native_memory_copy(dst_raw: u64, src_raw: u64) {
mod tests {
use super::*;

#[test]
fn owning_u32_admission_cache_skips_registry_and_invalidates() {
let ta = typed_array_alloc(KIND_UINT32, 16);
let boxed = crate::value::js_nanbox_pointer(ta as i64);

let before = test_typed_array_registry_probe_count();
assert_eq!(inline_u32_addr(boxed), ta as usize);
let primed = test_typed_array_registry_probe_count();
assert_eq!(primed, before + 1);
assert_eq!(inline_u32_addr(boxed), ta as usize);
assert_eq!(test_typed_array_registry_probe_count(), primed);

unregister_typed_array(ta);
assert_eq!(inline_u32_addr(boxed), 0);
assert_eq!(test_typed_array_registry_probe_count(), primed + 1);
// Leave the live allocation registered for its eventual finalizer.
register_typed_array(ta, KIND_UINT32);
}

#[test]
fn large_object_typed_array_alloc_uses_old_gc_header_and_stays_usable() {
let ta = typed_array_alloc(KIND_UINT8, crate::gc::LARGE_OBJECT_THRESHOLD_BYTES as u32);
Expand Down
Loading