From 439250b6f42d432c1886b3a1133352f3e914cb28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 10 Aug 2026 14:07:57 +0200 Subject: [PATCH 1/8] perf(array): gate the Map/Set registry probes on the receiver's own GC tag (#7768) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `js_array_get_f64` and `js_array_length` asked both collection registries whether an ordinary array was secretly a Set or a Map on every element read. On `gc-handoff/apps/asyncpipe.ts` those two probes were 13.5% of the run: the program uses Map and Set, so the #7474 latch is armed and each probe really resolved a thread-local and hashed. Every registered Map/Set IS its `arena_alloc_gc(_, _, GC_TYPE_MAP|GC_TYPE_SET)` header, so the object's own `obj_type` answers in one byte. ABA-proof by construction — the tag lives inside the candidate bytes, so recycling the address rewrites it — which is what an address-keyed negative memo could not offer (#7755). The registry stays authoritative for the positive answer. The same header read now also supplies the descriptor flags that `array_object_flags` re-derived through a second `clean_arr_ptr`. --- ...ay-receiver-tag-gates-collection-probes.md | 63 +++++ .../src/array/collection_tag_tests.rs | 260 ++++++++++++++++++ crates/perry-runtime/src/array/header.rs | 50 ++++ crates/perry-runtime/src/array/indexing.rs | 41 ++- crates/perry-runtime/src/array/mod.rs | 5 +- crates/perry-runtime/src/map.rs | 16 ++ .../src/object/field_get_set/field_ops.rs | 21 +- .../field_get_set/get_field_by_name_tail.rs | 19 +- crates/perry-runtime/src/set.rs | 13 + 9 files changed, 468 insertions(+), 20 deletions(-) create mode 100644 changelog.d/7768-array-receiver-tag-gates-collection-probes.md create mode 100644 crates/perry-runtime/src/array/collection_tag_tests.rs diff --git a/changelog.d/7768-array-receiver-tag-gates-collection-probes.md b/changelog.d/7768-array-receiver-tag-gates-collection-probes.md new file mode 100644 index 0000000000..f0ebfb0d8b --- /dev/null +++ b/changelog.d/7768-array-receiver-tag-gates-collection-probes.md @@ -0,0 +1,63 @@ +### Array element reads stop asking whether an array is a Map (#7768) + +`gc-handoff/apps/asyncpipe.ts` — an async service pipeline, and the worst gap in +the corpus at 13x node — spent **13.5% of its run in `set::is_registered_set` + +`map::is_registered_map`**. Not on Set or Map work: on `js_array_get_f64` and +`js_array_length` asking both collection registries whether an ordinary array +was secretly a collection, on every element read. + +#7755 made *unused*-feature registry probes free with monotone latches and named +Map/Set as the deliberate residual: asyncpipe uses both, so the #7474 latch is +correctly armed and each probe is real work — a Darwin `_tlv_get_addr`, a +`RefCell` borrow and a hash, per read. It also named why the trick that works +for typed arrays does not transfer: an address-keyed negative memo is an ABA +hazard for Map/Set, whose headers are recyclable arena objects. + +**The object already knows.** `js_map_alloc` and `js_set_alloc` allocate their +headers through `arena_alloc_gc(_, _, GC_TYPE_MAP | GC_TYPE_SET)`, and each is +the single registration site for its registry — so a registered collection's +address *is* its GC header, and `obj_type` answers "is this a Map?" from one +byte. Both hot call sites now gate their probes on it. + +This is ABA-proof by construction rather than by bookkeeping: the tag lives +INSIDE the candidate bytes, so whatever allocation owns those bytes next stamps +its own `obj_type` before the pointer is handed out. A recycled address answers +for its new owner with no invalidation step to get wrong — which is exactly what +an address-keyed memo could not offer. The registry remains authoritative for +the positive answer, so nothing about *when* an entry is added or swept moves. + +Also correct for a header-*less* receiver. Buffers and typed arrays are +`std::alloc`-backed, so the eight bytes below them are allocator bookkeeping +that can read as any value — but both are already routed by the (latched, +free) probes above, and either way the bookkeeping byte reads the outcome is +unchanged: a byte that happens to read as `GC_TYPE_SET`/`GC_TYPE_MAP` still +falls through to the authoritative registry, and any other value skips a probe +that would have answered `false` anyway. Neither call site gains a dereference: +`js_array_get_f64` reads this header through `clean_arr_ptr`, and +`js_array_length` reads it eight lines further down for its +`GC_TYPE_LAZY_ARRAY` / `GC_TYPE_OBJECT` arms, under the same magnitude guard. + +The same one header read also feeds the descriptor-flag check further down +`js_array_get_f64`, which `array_object_flags` used to re-derive through a +second `clean_arr_ptr` and a second header read (3.1% of the profile on its +own). + +`crates/perry-runtime/src/array/collection_tag_tests.rs` asserts THE SUBJECT, +not just the answer — the registry is a correct fallback, so a test that only +compared values would still pass with the gates deleted (CLAUDE.md, "four ways a +gate can be unable to fail", case 4). `is_registered_map` / `is_registered_set` +carry a test-only entry counter, and `plain_array_element_reads_never_probe_the_collection_registries` +asserts 64 passes over a 4-element array move it by zero while both registries +are non-empty. Delete either gate and that is what fails. +`a_stale_registry_entry_over_recycled_bytes_does_not_read_as_a_map` plants the +ABA state directly — a live registry entry over bytes re-stamped `GC_TYPE_ARRAY` +— and pins that the answer comes from the bytes; it fails if the header +confirmation at the end of `is_registered_map` is removed. +`every_registered_collection_address_carries_its_own_type_tag` pins the +invariant the gates rest on, across capacity growth, so a future registration +path that forgot the tag goes red here rather than silently. + +Two comments claiming Map/Set headers are `alloc()`-backed with no `GcHeader` +— the stated reason the registries are consulted before any header read — are +corrected in place; they predate the move into the managed arena and are how the +answer stayed hidden. diff --git a/crates/perry-runtime/src/array/collection_tag_tests.rs b/crates/perry-runtime/src/array/collection_tag_tests.rs new file mode 100644 index 0000000000..65bf9ff26c --- /dev/null +++ b/crates/perry-runtime/src/array/collection_tag_tests.rs @@ -0,0 +1,260 @@ +//! Receiver-tag gating of the `Map`/`Set` registry probes (#7768). +//! +//! `js_array_get_f64` and `js_array_length` used to ask both collection +//! registries "is this receiver a Set? a Map?" on every element read of an +//! ordinary array. Once a program creates one `Map` the #7474 monotone latch is +//! armed and both probes are real work — a thread-local resolution plus a hash, +//! per read, to prove an array is not a Map. They are now gated on the +//! receiver's own `GcHeader.obj_type`. +//! +//! These tests assert THE SUBJECT, not just the answer. The registry is a +//! correct fallback, so a test that only compared values would still pass with +//! the gate deleted — case 4 of CLAUDE.md's "four ways a gate can be unable to +//! fail". `TEST_{MAP,SET}_REGISTRY_PROBES` count every entry into +//! `is_registered_map` / `is_registered_set`, and the plain-array case asserts +//! those counters do not move. Delete either gate and that assertion fails. +//! +//! The ABA case has its own test. The tag is ABA-proof because it lives INSIDE +//! the candidate bytes: whatever allocation owns those bytes next stamps its own +//! `obj_type` through `arena_alloc_gc` before the pointer is handed out, so a +//! recycled address answers for its new owner. That is the property an +//! address-keyed negative memo could not have (#7755), and +//! `a_stale_registry_entry_over_recycled_bytes_does_not_read_as_a_map` plants +//! exactly that state. + +use super::*; +use crate::map::{js_map_alloc, js_map_set, js_map_size, MapHeader}; +use crate::set::{js_set_add, js_set_alloc, js_set_size, SetHeader}; + +fn probes() -> (u64, u64) { + ( + crate::map::test_map_registry_probe_count(), + crate::set::test_set_registry_probe_count(), + ) +} + +fn dense(values: &[f64]) -> *mut ArrayHeader { + let arr = js_array_alloc(values.len().max(1) as u32); + let mut cur = arr; + for v in values { + cur = js_array_push_f64(cur, *v); + } + cur +} + +/// Arm both #7474 latches, so nothing in this file is measuring the +/// "no collection has ever existed" fast-out instead of the tag gate. +fn arm_both_registries() -> (*mut MapHeader, *mut SetHeader) { + let map = js_map_alloc(4); + js_map_set(map, 1.0, 10.0); + let set = js_set_alloc(4); + js_set_add(set, 5.0); + assert!( + crate::map::is_registered_map(map as usize), + "the map registry must be armed for these tests to mean anything" + ); + assert!( + crate::set::is_registered_set(set as usize), + "the set registry must be armed for these tests to mean anything" + ); + (map, set) +} + +unsafe fn gc_obj_type(addr: usize) -> u8 { + let header = (addr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + (*header).obj_type +} + +unsafe fn set_gc_obj_type(addr: usize, obj_type: u8) { + let header = (addr as *mut u8).sub(crate::gc::GC_HEADER_SIZE) as *mut crate::gc::GcHeader; + (*header).obj_type = obj_type; +} + +#[test] +fn plain_array_element_reads_never_probe_the_collection_registries() { + let (map, set) = arm_both_registries(); + assert_eq!(js_map_size(map), 1); + assert_eq!(js_set_size(set), 1); + + let arr = dense(&[1.0, 2.0, 3.0, 4.0]); + // Prime anything lazily built on first touch, then measure. + let _ = js_array_length(arr); + let _ = js_array_get_f64(arr, 0); + + let before = probes(); + let mut sum = 0.0; + for _ in 0..64 { + let len = js_array_length(arr); + assert_eq!(len, 4); + for i in 0..len { + sum += js_array_get_f64(arr, i); + } + } + let after = probes(); + + assert_eq!(sum, 64.0 * 10.0, "the reads must still return the elements"); + assert_eq!( + after, before, + "a GC_TYPE_ARRAY receiver must never reach is_registered_map / \ + is_registered_set — remove either receiver-tag gate in \ + js_array_get_f64 / js_array_length and this is what fails" + ); +} + +#[test] +fn a_live_set_receiver_still_reads_its_elements_through_the_registry() { + let (_map, set) = arm_both_registries(); + js_set_add(set, 6.0); + js_set_add(set, 7.0); + assert_eq!(js_set_size(set), 3); + assert_eq!( + unsafe { gc_obj_type(set as usize) }, + crate::gc::GC_TYPE_SET, + "js_set_alloc must stamp GC_TYPE_SET — the gate reads exactly this byte" + ); + + let as_array = set as *const ArrayHeader; + let before = probes(); + assert_eq!(js_array_length(as_array), 3); + assert_eq!(js_array_get_f64(as_array, 0), 5.0); + assert_eq!(js_array_get_f64(as_array, 1), 6.0); + assert_eq!(js_array_get_f64(as_array, 2), 7.0); + let after = probes(); + + assert!( + after.1 > before.1, + "a GC_TYPE_SET receiver must still be confirmed against the \ + authoritative registry, not served on the tag alone" + ); +} + +#[test] +fn a_live_map_receiver_still_reports_its_size_through_the_registry() { + let (map, _set) = arm_both_registries(); + js_map_set(map, 2.0, 20.0); + assert_eq!(js_map_size(map), 2); + assert_eq!( + unsafe { gc_obj_type(map as usize) }, + crate::gc::GC_TYPE_MAP, + "js_map_alloc must stamp GC_TYPE_MAP — the gate reads exactly this byte" + ); + + let as_array = map as *const ArrayHeader; + let before = probes(); + assert_eq!(js_array_length(as_array), 2); + assert_eq!(js_array_get_f64(as_array, 0), 1.0, "entry 0's key"); + assert_eq!(js_array_get_f64(as_array, 1), 2.0, "entry 1's key"); + let after = probes(); + + assert!( + after.0 > before.0, + "a GC_TYPE_MAP receiver must still be confirmed against the \ + authoritative registry, not served on the tag alone" + ); +} + +/// The ABA case #7755 named: an address that WAS a `Map` and is now something +/// else, while the registry has not caught up. +/// +/// The bytes are re-stamped exactly as `arena_alloc_gc` would when it hands the +/// address to a plain array — `obj_type` first, then the new object's own words +/// — and the registry deliberately still holds the old entry, which is the +/// worst case a sweep-ordering bug could produce. The answer must come from the +/// bytes. +/// +/// Sabotage: delete the `try_read_gc_header` confirmation at the end of +/// `map::is_registered_map` and this fails — the stale registry entry alone +/// then reports `true`, and the element read serves `MapHeader::entries` as if +/// the recycled array were a Map. +#[test] +fn a_stale_registry_entry_over_recycled_bytes_does_not_read_as_a_map() { + let (map, _set) = arm_both_registries(); + js_map_set(map, 2.0, 20.0); + js_map_set(map, 3.0, 30.0); + assert_eq!(js_map_size(map), 3); + + let addr = map as usize; + assert!( + crate::map::is_registered_map(addr), + "precondition: the address is a registered Map" + ); + + // Recycle the bytes into a two-element dense array, registry untouched. + unsafe { + set_gc_obj_type(addr, crate::gc::GC_TYPE_ARRAY); + let recycled = addr as *mut ArrayHeader; + (*recycled).length = 2; + (*recycled).capacity = 2; + let elements = (addr as *mut u8).add(std::mem::size_of::()) as *mut f64; + std::ptr::write(elements, 111.0); + std::ptr::write(elements.add(1), 222.0); + } + + assert!( + !crate::map::is_registered_map(addr), + "the tag lives in the recycled bytes, so the address must stop \ + answering as a Map the instant another allocation owns them — a \ + stale registry entry must not override that" + ); + let as_array = addr as *const ArrayHeader; + assert_eq!( + js_array_length(as_array), + 2, + "js_array_length must report the recycled array's length, not the \ + dead Map's size" + ); + assert_eq!(js_array_get_f64(as_array, 0), 111.0); + assert_eq!(js_array_get_f64(as_array, 1), 222.0); + + // Restore the Map shape so teardown's side-allocation release stays sound. + unsafe { + set_gc_obj_type(addr, crate::gc::GC_TYPE_MAP); + (*map).size = 3; + (*map).capacity = 4; + } + assert!(crate::map::is_registered_map(addr)); + assert_eq!(js_map_size(map), 3); +} + +/// The invariant the whole gate rests on: a registered collection's address IS +/// its `arena_alloc_gc` header, so its `obj_type` is a complete answer. +/// +/// `js_map_alloc` / `js_set_alloc` are the single registration site for each, +/// and both grow their side buffer by `realloc` without moving the header — so +/// this must survive growth too. A future registration path that forgot the tag +/// would make the fast negative wrong, and this is what would catch it. +#[test] +fn every_registered_collection_address_carries_its_own_type_tag() { + let mut maps = Vec::new(); + let mut sets = Vec::new(); + for n in 0..8u32 { + let map = js_map_alloc(if n % 3 == 0 { 0 } else { n }); + for k in 0..(n * 4 + 1) { + js_map_set(map, k as f64, (k * 2) as f64); + } + maps.push(map); + + let set = js_set_alloc(if n % 2 == 0 { 0 } else { n }); + for k in 0..(n * 4 + 1) { + js_set_add(set, (k + 1000) as f64); + } + sets.push(set); + } + + for map in maps { + assert_eq!( + unsafe { gc_obj_type(map as usize) }, + crate::gc::GC_TYPE_MAP, + "registered Map at {map:p} must carry GC_TYPE_MAP" + ); + assert!(crate::map::is_registered_map(map as usize)); + } + for set in sets { + assert_eq!( + unsafe { gc_obj_type(set as usize) }, + crate::gc::GC_TYPE_SET, + "registered Set at {set:p} must carry GC_TYPE_SET" + ); + assert!(crate::set::is_registered_set(set as usize)); + } +} diff --git a/crates/perry-runtime/src/array/header.rs b/crates/perry-runtime/src/array/header.rs index 1144b9303d..925ebab811 100644 --- a/crates/perry-runtime/src/array/header.rs +++ b/crates/perry-runtime/src/array/header.rs @@ -68,6 +68,56 @@ pub(crate) fn array_object_flags(arr: *const ArrayHeader) -> u16 { } } +/// The `obj_type` and flag word of the `GcHeader` that precedes `arr`, read +/// once, for a receiver [`clean_arr_ptr`] has already resolved. `(0, 0)` when +/// `arr` is too low to carry a header — `0` is not a legal `obj_type`, so it +/// reads as "unknown" at every call site. +/// +/// A non-zero tag is NOT proof that a real header exists. `Buffer` and +/// `TypedArray` payloads are `std::alloc`-backed, so the eight bytes below them +/// are allocator bookkeeping and can read as any value. Use the answer only +/// where a wrong tag is harmless: +/// +/// * to *skip* a registry probe whose answer for that receiver would have been +/// `false` anyway — the caller must already have routed real buffers and +/// typed arrays elsewhere; or +/// * as the `GC_TYPE_ARRAY` test [`array_object_flags`] already performs on +/// this very byte, where those bookkeeping bytes are allowed to be wrong +/// today. +/// +/// What makes the tag *authoritative* for the collection receivers is that +/// every GC allocation carries a header, `Map` and `Set` included: +/// `js_map_alloc` / `js_set_alloc` stamp `GC_TYPE_MAP` / `GC_TYPE_SET` through +/// `arena_alloc_gc`, and that is the single registration site for each. Several +/// comments in the tree still say Map/Set headers come from a bare `alloc()` +/// with no `GcHeader` and that only the registry can classify them; that +/// stopped being true when they moved into the managed arena. +#[inline] +pub(crate) fn array_receiver_gc_tag(arr: *const ArrayHeader) -> (u8, u16) { + if (arr as usize) < crate::gc::GC_HEADER_SIZE + 0x1000 { + return (0, 0); + } + // SAFETY: the same `arr - GC_HEADER_SIZE` read `clean_arr_ptr` performs on + // this pointer (forwarding chain, lazy/object rejection), under the same + // magnitude guard. + unsafe { + let gc_header = + (arr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; + ((*gc_header).obj_type, (*gc_header)._reserved) + } +} + +/// [`array_object_flags`] answered from a tag [`array_receiver_gc_tag`] +/// already read, for a receiver `clean_arr_ptr` already resolved. +#[inline] +pub(crate) fn array_object_flags_from_tag(tag: (u8, u16)) -> u16 { + if tag.0 == crate::gc::GC_TYPE_ARRAY { + tag.1 + } else { + 0 + } +} + #[inline] pub(crate) fn array_is_frozen(arr: *const ArrayHeader) -> bool { array_object_flags(arr) & crate::gc::OBJ_FLAG_FROZEN != 0 diff --git a/crates/perry-runtime/src/array/indexing.rs b/crates/perry-runtime/src/array/indexing.rs index e4efb93dca..6da5937b26 100644 --- a/crates/perry-runtime/src/array/indexing.rs +++ b/crates/perry-runtime/src/array/indexing.rs @@ -590,10 +590,17 @@ pub extern "C" fn js_array_length(arr: *const ArrayHeader) -> u32 { }; if !arr.is_null() { let addr = arr as usize; - if crate::set::is_registered_set(addr) { + // #7768: gate both probes on the receiver's own type tag — see + // `js_array_get_f64` for why the tag answers, why it is ABA-proof, and + // why a header-less buffer receiver still lands on the same result. + // This reads the byte the `GC_TYPE_LAZY_ARRAY` / `GC_TYPE_OBJECT` block + // a few lines below already reads, under the same magnitude guard, so + // it adds no dereference this function did not already perform. + let receiver_type = array_receiver_gc_tag(arr).0; + if receiver_type == crate::gc::GC_TYPE_SET && crate::set::is_registered_set(addr) { return crate::set::js_set_size(arr as *const crate::set::SetHeader); } - if crate::map::is_registered_map(addr) { + if receiver_type == crate::gc::GC_TYPE_MAP && crate::map::is_registered_map(addr) { return crate::map::js_map_size(arr as *const crate::map::MapHeader); } } @@ -814,8 +821,32 @@ pub extern "C" fn js_array_get_f64(arr: *const ArrayHeader, index: u32) -> f64 { crate::buffer::js_buffer_get(arr as *const crate::buffer::BufferHeader, index as i32); return byte_val as f64; } + // #7768: ONE `GcHeader` read now gates both collection probes below and + // supplies the descriptor flags further down, which `array_object_flags` + // used to re-derive through a second `clean_arr_ptr` and a second header + // read. On `gc-handoff/apps/asyncpipe_big.ts` this call site was 76% of all + // `is_registered_set` samples and 82% of all `is_registered_map` ones — + // both registries are non-empty there, so the #7474 latch is correctly + // armed and each probe really was resolving a thread-local and hashing, on + // every element read of an ordinary array, to prove an array is not a Map. + // + // The tag answers because every registered `Map`/`Set` IS its + // `arena_alloc_gc(_, _, GC_TYPE_MAP|GC_TYPE_SET)` header (one registration + // site each), and it is ABA-proof by construction: it lives INSIDE the + // candidate bytes, so recycling the address into anything else rewrites it + // before the new pointer is handed out. That is exactly what an + // address-keyed negative memo could not offer (#7755). + // + // Correct for a header-LESS receiver too. Buffers and typed arrays are + // `std::alloc`-backed, so their preceding bytes are allocator bookkeeping — + // but both are already routed above, and whichever way those bytes read the + // outcome is unchanged: a bookkeeping byte that happens to read as + // `GC_TYPE_SET`/`GC_TYPE_MAP` still falls through to the authoritative + // registry (which answers `false`), and any other value skips a probe that + // would have answered `false` anyway. + let receiver_tag = array_receiver_gc_tag(arr); // Check if this is a Set — read from elements pointer (not inline) - if crate::set::is_registered_set(arr as usize) { + if receiver_tag.0 == crate::gc::GC_TYPE_SET && crate::set::is_registered_set(arr as usize) { let set = arr as *const crate::set::SetHeader; unsafe { let size = (*set).size; @@ -827,7 +858,7 @@ pub extern "C" fn js_array_get_f64(arr: *const ArrayHeader, index: u32) -> f64 { } } // Check if this is a Map — return entries as [key, value] pairs - if crate::map::is_registered_map(arr as usize) { + if receiver_tag.0 == crate::gc::GC_TYPE_MAP && crate::map::is_registered_map(arr as usize) { let map = arr as *const crate::map::MapHeader; unsafe { let size = (*map).size; @@ -843,7 +874,7 @@ pub extern "C" fn js_array_get_f64(arr: *const ArrayHeader, index: u32) -> f64 { // `array_has_own_index`) — this probe allocated two Strings on EVERY // checked element read once any descriptor existed process-wide, which // taxed every internal keys_array walk (`in`, defineProperty, Object.keys). - if array_object_flags(arr) & crate::gc::OBJ_FLAG_ARRAY_DESCRIPTORS != 0 { + if array_object_flags_from_tag(receiver_tag) & crate::gc::OBJ_FLAG_ARRAY_DESCRIPTORS != 0 { let key = index.to_string(); if let Some(acc) = crate::object::get_accessor_descriptor(arr as usize, &key) { if acc.get != 0 { diff --git a/crates/perry-runtime/src/array/mod.rs b/crates/perry-runtime/src/array/mod.rs index 541721a51a..15476bb72d 100644 --- a/crates/perry-runtime/src/array/mod.rs +++ b/crates/perry-runtime/src/array/mod.rs @@ -24,6 +24,8 @@ mod species; mod splice_slice; mod subclass; +#[cfg(test)] +mod collection_tag_tests; #[cfg(test)] mod spread_dense_tests; #[cfg(test)] @@ -189,7 +191,8 @@ pub(crate) use self::header::{ array_named_property_get, array_named_property_get_by_name, array_named_property_has, array_named_property_names, array_named_property_set, array_numeric_raw_f64_get, array_numeric_raw_f64_push_inbounds, array_numeric_raw_f64_set_inbounds, array_object_flags, - array_ptr_as_proxy, canonicalize_array_numeric_store_value, clean_arr_ptr, clean_arr_ptr_mut, + array_object_flags_from_tag, array_ptr_as_proxy, array_receiver_gc_tag, + canonicalize_array_numeric_store_value, clean_arr_ptr, clean_arr_ptr_mut, clear_array_numeric_layout, clear_array_numeric_layout_ptr, gc_element_slot_range, mark_array_layout_unknown, mark_array_raw_f64_holes_fresh, normalize_array_receiver, note_array_slot, note_array_slot_layout_only, rebuild_array_layout, rebuild_array_layout_exact, diff --git a/crates/perry-runtime/src/map.rs b/crates/perry-runtime/src/map.rs index 3913c190e5..d6fef958ce 100644 --- a/crates/perry-runtime/src/map.rs +++ b/crates/perry-runtime/src/map.rs @@ -191,7 +191,23 @@ fn register_map(ptr: *mut MapHeader, entries: *mut f64, capacity: usize) { }); } +/// Every entry into [`is_registered_map`], i.e. every caller that could not +/// rule a `Map` out more cheaply. The `js_array_get_f64` / `js_array_length` +/// receiver-tag gates (#7768) are asserted against this: a plain-array element +/// read must not move it. Remove those gates and the assertion fails, which is +/// the point — a fast path nobody can prove ran is not a fast path. +#[cfg(test)] +pub(crate) static TEST_MAP_REGISTRY_PROBES: std::sync::atomic::AtomicU64 = + std::sync::atomic::AtomicU64::new(0); + +#[cfg(test)] +pub(crate) fn test_map_registry_probe_count() -> u64 { + TEST_MAP_REGISTRY_PROBES.load(std::sync::atomic::Ordering::Relaxed) +} + pub fn is_registered_map(addr: usize) -> bool { + #[cfg(test)] + TEST_MAP_REGISTRY_PROBES.fetch_add(1, std::sync::atomic::Ordering::Relaxed); // #7469: nothing has ever been registered ⟹ nothing can be found. Checked // first because it is the only arm that costs neither a thread-local // resolution nor a hash. diff --git a/crates/perry-runtime/src/object/field_get_set/field_ops.rs b/crates/perry-runtime/src/object/field_get_set/field_ops.rs index ad18e9ea3c..b96f03684e 100644 --- a/crates/perry-runtime/src/object/field_get_set/field_ops.rs +++ b/crates/perry-runtime/src/object/field_get_set/field_ops.rs @@ -183,11 +183,10 @@ pub extern "C" fn js_object_set_field(obj: *mut ObjectHeader, field_index: u32, /// this function to compare the receiver's class id against every user /// class implementing the same method name. Without the GC-type guard we /// blindly read 4 bytes at offset 4 of the receiver — which for a -/// `SetHeader` (allocated via std::alloc, no GcHeader, layout -/// `{ size: u32, capacity: u32, elements: *mut f64 }`) is its `capacity` -/// field. `js_set_alloc(0)` defaults capacity to 4, which collides with -/// whichever user class lands at id 4, routing the call into the wrong -/// method body and crashing on the bogus `this` pointer. +/// `SetHeader` (layout `{ size: u32, capacity: u32, elements: *mut f64 }`) is +/// its `capacity` field. `js_set_alloc(0)` defaults capacity to 4, which +/// collides with whichever user class lands at id 4, routing the call into the +/// wrong method body and crashing on the bogus `this` pointer. #[no_mangle] pub extern "C" fn js_object_get_class_id(obj: *const ObjectHeader) -> u32 { if crate::value::addr_class::is_handle_band(obj as usize) { @@ -195,9 +194,15 @@ pub extern "C" fn js_object_get_class_id(obj: *const ObjectHeader) -> u32 { } let addr = obj as usize; // Built-in headers (Set / Map / Regex) live in their own per-type - // registries — they're never user class instances. Reject them first - // so we never try to read a GcHeader at obj-8, which doesn't exist - // for these std::alloc'd headers. + // registries — they're never user class instances. Reject them first. + // + // The reason given here used to be that Set/Map headers are `std::alloc`'d + // with no `GcHeader` at `obj - 8`. That stopped being true when + // `js_set_alloc` / `js_map_alloc` moved to + // `arena_alloc_gc(_, _, GC_TYPE_SET|GC_TYPE_MAP)` — both DO carry a header, + // and the `GC_TYPE_OBJECT` test below already rejects them on it. Regex + // pointers are the remaining header-less case, which is why the registry + // order is kept. if crate::set::is_registered_set(addr) || crate::map::is_registered_map(addr) || crate::regex::is_regex_pointer(obj as *const u8) diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs index f93e2111a8..ba2efc5cea 100644 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs @@ -451,12 +451,19 @@ pub(crate) fn get_field_by_name_object_tail( } return JSValue::undefined(); } - // Sets: SetHeader is allocated via raw `alloc()` (no GcHeader), - // so we can't safely read the byte preceding the pointer to - // determine its type. Detect via the SET_REGISTRY first. Route - // `.size` to `js_set_size` and synthesize method values for - // prototype functions such as `.has`, which Node exposes through - // ordinary property reads. + // Sets: detect via the SET_REGISTRY, which is authoritative and + // dereference-free. Route `.size` to `js_set_size` and synthesize + // method values for prototype functions such as `.has`, which Node + // exposes through ordinary property reads. + // + // (This used to say a `SetHeader` comes from a raw `alloc()` with no + // `GcHeader`, so the preceding byte could not be read. That has not + // been true since `js_set_alloc` moved to + // `arena_alloc_gc(_, _, GC_TYPE_SET)`: a registered Set IS a GC + // allocation and its `obj_type` classifies it. `js_array_get_f64` and + // `js_array_length` gate their probes on exactly that byte (#7768); + // this receiver is not proven to carry a header at this point, so it + // still asks the registry.) if crate::set::is_registered_set(obj as usize) { if !key.is_null() { let key_ptr = (key as *const u8).add(std::mem::size_of::()); diff --git a/crates/perry-runtime/src/set.rs b/crates/perry-runtime/src/set.rs index 316ac05c78..850455b09b 100644 --- a/crates/perry-runtime/src/set.rs +++ b/crates/perry-runtime/src/set.rs @@ -221,7 +221,20 @@ fn register_set(ptr: *mut SetHeader, elements: *mut f64, capacity: usize) { }); } +/// Every entry into [`is_registered_set`]. Twin of +/// `map::TEST_MAP_REGISTRY_PROBES` — see that counter for what it pins down. +#[cfg(test)] +pub(crate) static TEST_SET_REGISTRY_PROBES: std::sync::atomic::AtomicU64 = + std::sync::atomic::AtomicU64::new(0); + +#[cfg(test)] +pub(crate) fn test_set_registry_probe_count() -> u64 { + TEST_SET_REGISTRY_PROBES.load(std::sync::atomic::Ordering::Relaxed) +} + pub fn is_registered_set(addr: usize) -> bool { + #[cfg(test)] + TEST_SET_REGISTRY_PROBES.fetch_add(1, std::sync::atomic::Ordering::Relaxed); // #7469: nothing registered ⟹ nothing to find, without a thread-local // resolution or a hash. See `map::is_registered_map` for the pairing. if set_registry_never_used() { From 0c0d6729c2073256326f22f022c72c223f79c393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 10 Aug 2026 14:28:40 +0200 Subject: [PATCH 2/8] perf(array): serve the object keys walk from the array's own words (#7768) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The object field-get funnel already proves `keys` is a live `GC_TYPE_ARRAY` and caps the index below its capacity, then called `js_array_get` per key — which re-establishes both facts from scratch: a `clean_arr_ptr` forwarding walk, a lazy-header probe, the exotic-receiver classifications and a descriptor-flag read. That funnel was 78% of all `js_array_get_f64` samples on `gc-handoff/apps/asyncpipe_big.ts`. `keys_array_len_capped_to_capacity` paid the same toll through `js_array_length` once per property read. `keys_array_slot` serves the dense, descriptor-free, non-forwarded case from the array's own words and delegates everything else — a hole, an out-of-range index, a forwarded or descriptor-carrying array, a null pointer — so no general semantics move. A test-only per-thread fallback counter pins both directions, so a fast path that stopped applying and one that started swallowing a shape it should have delegated are equally red. Also switches the #7768 receiver-tag read to `addr_class::try_read_gc_header`: this file's usual `>= GC_HEADER_SIZE + 0x1000` floor sits BELOW the handle band, and `js_array_length` reaches it before proxy/handle receivers are routed. Keeps the addr-class ratchet green too. --- .../src/array/collection_tag_tests.rs | 59 +++++++++++++++ crates/perry-runtime/src/array/header.rs | 19 +++-- crates/perry-runtime/src/array/indexing.rs | 74 +++++++++++++++++++ crates/perry-runtime/src/array/mod.rs | 6 +- crates/perry-runtime/src/map.rs | 13 +++- .../object/field_get_set/get_field_by_name.rs | 2 +- .../field_get_set/get_field_by_name_tail.rs | 4 +- crates/perry-runtime/src/set.rs | 9 ++- scripts/addr_class_allowlist.txt | 1 + 9 files changed, 164 insertions(+), 23 deletions(-) diff --git a/crates/perry-runtime/src/array/collection_tag_tests.rs b/crates/perry-runtime/src/array/collection_tag_tests.rs index 65bf9ff26c..37f5cca666 100644 --- a/crates/perry-runtime/src/array/collection_tag_tests.rs +++ b/crates/perry-runtime/src/array/collection_tag_tests.rs @@ -216,6 +216,65 @@ fn a_stale_registry_entry_over_recycled_bytes_does_not_read_as_a_map() { assert_eq!(js_map_size(map), 3); } +/// `keys_array_slot` must be the general getter, minus the work the field-get +/// funnel already did — and must refuse every shape it cannot serve on those +/// terms rather than guess. Both directions are asserted against the fallback +/// counter, so "stopped applying" and "started swallowing" are equally red. +#[test] +fn keys_array_slot_matches_the_general_getter_and_delegates_what_it_cannot_serve() { + let dense_keys = dense(&[10.0, 20.0, 30.0]); + + let before = crate::array::test_keys_array_slot_fallbacks(); + for i in 0..3u32 { + let fast = unsafe { crate::array::keys_array_slot(dense_keys, i) }; + let general = crate::array::js_array_get(dense_keys, i); + assert_eq!( + fast.bits(), + general.bits(), + "slot {i} must read identically through both paths" + ); + } + assert_eq!( + crate::array::test_keys_array_slot_fallbacks(), + before, + "a dense, descriptor-free keys array is exactly what the fast path \ + exists for — it must not delegate" + ); + + // Out of range, and a hole, both delegate: the general getter walks the + // prototype chain for those and the dense words cannot answer. + let before = crate::array::test_keys_array_slot_fallbacks(); + let oob = unsafe { crate::array::keys_array_slot(dense_keys, 7) }; + assert_eq!(oob.bits(), crate::array::js_array_get(dense_keys, 7).bits()); + assert_eq!( + crate::array::test_keys_array_slot_fallbacks(), + before + 1, + "an out-of-range index must reach the general getter" + ); + + let holey = js_array_alloc_with_length(3); + js_array_set_f64(holey, 1, 42.0); + let before = crate::array::test_keys_array_slot_fallbacks(); + let hole = unsafe { crate::array::keys_array_slot(holey, 0) }; + assert_eq!(hole.bits(), crate::array::js_array_get(holey, 0).bits()); + assert_eq!( + crate::array::test_keys_array_slot_fallbacks(), + before + 1, + "a HOLE reads through the prototype chain, so it must delegate" + ); + let filled = unsafe { crate::array::keys_array_slot(holey, 1) }; + assert_eq!(filled.bits(), crate::array::js_array_get(holey, 1).bits()); + + // A null / low pointer must delegate rather than dereference. + let before = crate::array::test_keys_array_slot_fallbacks(); + let _ = unsafe { crate::array::keys_array_slot(std::ptr::null(), 0) }; + assert_eq!( + crate::array::test_keys_array_slot_fallbacks(), + before + 1, + "a null keys pointer must delegate, never be dereferenced" + ); +} + /// The invariant the whole gate rests on: a registered collection's address IS /// its `arena_alloc_gc` header, so its `obj_type` is a complete answer. /// diff --git a/crates/perry-runtime/src/array/header.rs b/crates/perry-runtime/src/array/header.rs index 925ebab811..4ba6bdec85 100644 --- a/crates/perry-runtime/src/array/header.rs +++ b/crates/perry-runtime/src/array/header.rs @@ -94,16 +94,15 @@ pub(crate) fn array_object_flags(arr: *const ArrayHeader) -> u16 { /// stopped being true when they moved into the managed arena. #[inline] pub(crate) fn array_receiver_gc_tag(arr: *const ArrayHeader) -> (u8, u16) { - if (arr as usize) < crate::gc::GC_HEADER_SIZE + 0x1000 { - return (0, 0); - } - // SAFETY: the same `arr - GC_HEADER_SIZE` read `clean_arr_ptr` performs on - // this pointer (forwarding chain, lazy/object rejection), under the same - // magnitude guard. - unsafe { - let gc_header = - (arr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - ((*gc_header).obj_type, (*gc_header)._reserved) + // `try_read_gc_header` rather than this file's usual + // `>= GC_HEADER_SIZE + 0x1000` floor: that floor sits BELOW the handle + // band, and `js_array_length` reaches here before its proxy/handle + // receivers have been routed. The canonical predicate rejects the bands + // without touching memory, and rejecting an address the old floor would + // have read costs nothing — a handle is not a Map either way. + match unsafe { crate::value::addr_class::try_read_gc_header(arr as usize) } { + Some(header) => (header.obj_type, header._reserved), + None => (0, 0), } } diff --git a/crates/perry-runtime/src/array/indexing.rs b/crates/perry-runtime/src/array/indexing.rs index 6da5937b26..d3095676cf 100644 --- a/crates/perry-runtime/src/array/indexing.rs +++ b/crates/perry-runtime/src/array/indexing.rs @@ -545,6 +545,20 @@ fn array_get_property_by_key(arr: *const ArrayHeader, key: *const crate::StringH /// FOR DENSE KEYS/PROPERTY ARRAYS ONLY — general JS arrays may have /// `length > capacity` (sparse), where this cap would be incorrect. pub(crate) unsafe fn keys_array_len_capped_to_capacity(arr: *const ArrayHeader) -> usize { + // #7768: a well-formed dense keys array answers from its own two words. + // `js_array_length` re-derives the same number through a proxy probe, a + // second header read for its lazy/object arms, and a `clean_arr_ptr` + // forwarding walk — once per property read on the field-get funnel. + // `length <= capacity` is exactly the well-formed case; the sparse and + // corrupted shapes this cap exists for fall through unchanged. + if let Some(header) = crate::value::addr_class::try_read_gc_header(arr as usize) { + if header.obj_type == crate::gc::GC_TYPE_ARRAY + && header.gc_flags & crate::gc::GC_FLAG_FORWARDED == 0 + && (*arr).length <= (*arr).capacity + { + return (*arr).length as usize; + } + } let raw = js_array_length(arr) as usize; if arr.is_null() { raw @@ -553,6 +567,66 @@ pub(crate) unsafe fn keys_array_len_capped_to_capacity(arr: *const ArrayHeader) } } +/// Read slot `index` of a dense internal keys/property array. +/// +/// The object field-get funnel has already proved `keys` is a live +/// `GC_TYPE_ARRAY` — it reads the `GcHeader` and returns `undefined` otherwise +/// — and has capped `index` below the array's own capacity (see +/// [`keys_array_len_capped_to_capacity`]). Those are precisely the two facts +/// [`js_array_get_f64`] re-establishes from scratch on every call: a +/// `clean_arr_ptr` forwarding walk, a lazy-header probe, the exotic-receiver +/// classifications and a descriptor-flag read — per key examined, per property +/// read. On `gc-handoff/apps/asyncpipe_big.ts` that one funnel was 78% of all +/// `js_array_get_f64` samples. +/// +/// Falls back to the general getter for anything it cannot serve on those +/// terms — a forwarded array (which `clean_arr_ptr` would relocate), one +/// carrying index descriptors, an out-of-range index, or a hole (which reads +/// through the prototype chain) — so no general semantics move. Keys arrays +/// are dense and descriptor-free, so the fallback is the cold arm. +#[inline] +pub(crate) unsafe fn keys_array_slot( + keys: *const ArrayHeader, + index: u32, +) -> crate::value::JSValue { + if let Some(header) = crate::value::addr_class::try_read_gc_header(keys as usize) { + if header.obj_type == crate::gc::GC_TYPE_ARRAY + && header.gc_flags & crate::gc::GC_FLAG_FORWARDED == 0 + && header._reserved & crate::gc::OBJ_FLAG_ARRAY_DESCRIPTORS == 0 + && index < (*keys).length + && index < (*keys).capacity + { + let elements = + (keys as *const u8).add(std::mem::size_of::()) as *const f64; + let raw = std::ptr::read(elements.add(index as usize)); + if raw.to_bits() != crate::value::TAG_HOLE { + return crate::value::JSValue::from_bits(raw.to_bits()); + } + } + } + #[cfg(test)] + KEYS_ARRAY_SLOT_FALLBACKS.with(|c| c.set(c.get().wrapping_add(1))); + crate::array::js_array_get(keys, index) +} + +/// Times [`keys_array_slot`] could NOT serve a slot from the dense words and +/// had to delegate. Asserted in both directions by +/// `array::collection_tag_tests` — zero for the dense keys arrays the fast path +/// exists for, non-zero for every shape it must refuse — so a fast path that +/// silently stopped applying, or one that started swallowing a shape it should +/// have delegated, both go red. +/// Per THREAD — `cargo test` runs every case on its own thread in one process, +/// so a process-global counter would be moved by whatever else is running. +#[cfg(test)] +thread_local! { + static KEYS_ARRAY_SLOT_FALLBACKS: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +#[cfg(test)] +pub(crate) fn test_keys_array_slot_fallbacks() -> u64 { + KEYS_ARRAY_SLOT_FALLBACKS.with(|c| c.get()) +} + /// Auto-opt dead-strip anchor: codegen emits a bare `js_array_length` symbol in /// native-region wrappers (`__perry_wrap_*`) and elsewhere, so it must be a /// `#[no_mangle]` C export AND survive dead-stripping even when no Rust caller diff --git a/crates/perry-runtime/src/array/mod.rs b/crates/perry-runtime/src/array/mod.rs index 15476bb72d..f70bcbe693 100644 --- a/crates/perry-runtime/src/array/mod.rs +++ b/crates/perry-runtime/src/array/mod.rs @@ -105,7 +105,7 @@ pub use self::immutable::{ pub(crate) use self::indexing::{ array_has_own_index, array_iteration_is_exotic, array_proto_iterator_modified, array_prototype_addr, array_prototype_has_index_flag, array_spec_get, array_spec_has_index, - invalidate_array_index_fast_path, keys_array_len_capped_to_capacity, + invalidate_array_index_fast_path, keys_array_len_capped_to_capacity, keys_array_slot, note_array_proto_iterator_write, note_object_prototype_index_write, object_prototype_addr, object_prototype_addr_matches, object_prototype_has_index_flag, PERRY_ARRAY_INDEX_FAST_PATH_INVALIDATED, @@ -120,7 +120,9 @@ pub use self::indexing::{ scan_prototype_addr_cache_roots_mut, }; #[cfg(test)] -pub(crate) use self::indexing::{test_array_proto_addr_cache, test_object_proto_addr_cache}; +pub(crate) use self::indexing::{ + test_array_proto_addr_cache, test_keys_array_slot_fallbacks, test_object_proto_addr_cache, +}; pub use self::is_array::js_array_is_array; pub(crate) use self::iter_methods::throw_reduce_of_empty; pub use self::iter_methods::{ diff --git a/crates/perry-runtime/src/map.rs b/crates/perry-runtime/src/map.rs index d6fef958ce..d59f7b7f75 100644 --- a/crates/perry-runtime/src/map.rs +++ b/crates/perry-runtime/src/map.rs @@ -196,18 +196,23 @@ fn register_map(ptr: *mut MapHeader, entries: *mut f64, capacity: usize) { /// receiver-tag gates (#7768) are asserted against this: a plain-array element /// read must not move it. Remove those gates and the assertion fails, which is /// the point — a fast path nobody can prove ran is not a fast path. +/// +/// Per THREAD, not per process: the registries themselves are thread-local, and +/// `cargo test` runs every case on its own thread in one process, so a global +/// counter would be moved by whatever else happens to be running. #[cfg(test)] -pub(crate) static TEST_MAP_REGISTRY_PROBES: std::sync::atomic::AtomicU64 = - std::sync::atomic::AtomicU64::new(0); +thread_local! { + static TEST_MAP_REGISTRY_PROBES: std::cell::Cell = const { std::cell::Cell::new(0) }; +} #[cfg(test)] pub(crate) fn test_map_registry_probe_count() -> u64 { - TEST_MAP_REGISTRY_PROBES.load(std::sync::atomic::Ordering::Relaxed) + TEST_MAP_REGISTRY_PROBES.with(|c| c.get()) } pub fn is_registered_map(addr: usize) -> bool { #[cfg(test)] - TEST_MAP_REGISTRY_PROBES.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + TEST_MAP_REGISTRY_PROBES.with(|c| c.set(c.get().wrapping_add(1))); // #7469: nothing has ever been registered ⟹ nothing can be found. Checked // first because it is the only arm that costs neither a thread-local // resolution nor a hash. diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs index 1d0d70c8f5..f817ca035b 100644 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs @@ -175,7 +175,7 @@ pub extern "C" fn js_object_get_field_by_name( crate::array::keys_array_len_capped_to_capacity(keys); if key_count <= 4096 { for i in 0..key_count { - let kv = crate::array::js_array_get(keys, i as u32); + let kv = crate::array::keys_array_slot(keys, i as u32); if crate::string::js_string_key_matches(kv, key) { super::super::prop_plan::read_plan_record( keys as usize, diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs index ba2efc5cea..a21e8fae88 100644 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs @@ -1601,7 +1601,7 @@ pub(crate) fn get_field_by_name_object_tail( if let Some(field_idx) = cached { let idx = field_idx as usize; let cache_hit_valid = if idx < key_count { - let key_val = crate::array::js_array_get(keys, field_idx); + let key_val = crate::array::keys_array_slot(keys, field_idx); // #1781: SSO-aware match — pre-fix the `is_string()` here // false-invalidated cache hits for ≤5-byte keys stored // as SHORT_STRING_TAG values. @@ -1676,7 +1676,7 @@ pub(crate) fn get_field_by_name_object_tail( } for i in 0..key_count { - let key_val = crate::array::js_array_get(keys, i as u32); + let key_val = crate::array::keys_array_slot(keys, i as u32); // #1781: accept inline SSO short keys here too — the // slow-path lookup is what backs `obj[k]` for ≤5-byte // keys after a field-cache miss. diff --git a/crates/perry-runtime/src/set.rs b/crates/perry-runtime/src/set.rs index 850455b09b..66b806d60d 100644 --- a/crates/perry-runtime/src/set.rs +++ b/crates/perry-runtime/src/set.rs @@ -224,17 +224,18 @@ fn register_set(ptr: *mut SetHeader, elements: *mut f64, capacity: usize) { /// Every entry into [`is_registered_set`]. Twin of /// `map::TEST_MAP_REGISTRY_PROBES` — see that counter for what it pins down. #[cfg(test)] -pub(crate) static TEST_SET_REGISTRY_PROBES: std::sync::atomic::AtomicU64 = - std::sync::atomic::AtomicU64::new(0); +thread_local! { + static TEST_SET_REGISTRY_PROBES: std::cell::Cell = const { std::cell::Cell::new(0) }; +} #[cfg(test)] pub(crate) fn test_set_registry_probe_count() -> u64 { - TEST_SET_REGISTRY_PROBES.load(std::sync::atomic::Ordering::Relaxed) + TEST_SET_REGISTRY_PROBES.with(|c| c.get()) } pub fn is_registered_set(addr: usize) -> bool { #[cfg(test)] - TEST_SET_REGISTRY_PROBES.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + TEST_SET_REGISTRY_PROBES.with(|c| c.set(c.get().wrapping_add(1))); // #7469: nothing registered ⟹ nothing to find, without a thread-local // resolution or a hash. See `map::is_registered_map` for the pairing. if set_registry_never_used() { diff --git a/scripts/addr_class_allowlist.txt b/scripts/addr_class_allowlist.txt index 205647490a..b644b440dc 100644 --- a/scripts/addr_class_allowlist.txt +++ b/scripts/addr_class_allowlist.txt @@ -151,3 +151,4 @@ crates/perry-runtime/src/child_process/value_util.rs | * | pre-existing GcHeader crates/perry-runtime/src/closure/dispatch/ | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up (split of closure/dispatch.rs) crates/perry-runtime/src/bun_compat/string_width.rs | 0xE0000..=0xE007F | Unicode "Tags" codepoint block (U+E0000..U+E007F) tested against a char, not a handle-band address crates/perry-runtime/src/bun_compat/width_tables.rs | * | pure Unicode East-Asian-width codepoint-range table; every hex literal is a Unicode code point (e.g. U+F0000 / U+100000 SPUA-A/B planes), never a handle-band address +crates/perry-runtime/src/array/collection_tag_tests.rs | * | unit tests for the #7768 receiver-tag gate: they read AND re-stamp `GcHeader.obj_type` on an address they allocated themselves, which is the whole subject under test. `try_read_gc_header` cannot serve them (it hands out a shared reference, and the recycling test must WRITE the tag to model `arena_alloc_gc` handing the bytes to the next owner). From 3234ed98edfec916dde4581c9de2d35ac3510912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 10 Aug 2026 14:43:37 +0200 Subject: [PATCH 3/8] docs(array): point the #7768 placeholders at the real PR number (#7765) --- ...d => 7765-array-receiver-tag-gates-collection-probes.md} | 2 +- crates/perry-runtime/src/array/collection_tag_tests.rs | 2 +- crates/perry-runtime/src/array/indexing.rs | 6 +++--- crates/perry-runtime/src/map.rs | 2 +- .../src/object/field_get_set/get_field_by_name_tail.rs | 2 +- scripts/addr_class_allowlist.txt | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) rename changelog.d/{7768-array-receiver-tag-gates-collection-probes.md => 7765-array-receiver-tag-gates-collection-probes.md} (99%) diff --git a/changelog.d/7768-array-receiver-tag-gates-collection-probes.md b/changelog.d/7765-array-receiver-tag-gates-collection-probes.md similarity index 99% rename from changelog.d/7768-array-receiver-tag-gates-collection-probes.md rename to changelog.d/7765-array-receiver-tag-gates-collection-probes.md index f0ebfb0d8b..fb795ddf4b 100644 --- a/changelog.d/7768-array-receiver-tag-gates-collection-probes.md +++ b/changelog.d/7765-array-receiver-tag-gates-collection-probes.md @@ -1,4 +1,4 @@ -### Array element reads stop asking whether an array is a Map (#7768) +### Array element reads stop asking whether an array is a Map (#7765) `gc-handoff/apps/asyncpipe.ts` — an async service pipeline, and the worst gap in the corpus at 13x node — spent **13.5% of its run in `set::is_registered_set` + diff --git a/crates/perry-runtime/src/array/collection_tag_tests.rs b/crates/perry-runtime/src/array/collection_tag_tests.rs index 37f5cca666..80297bbae5 100644 --- a/crates/perry-runtime/src/array/collection_tag_tests.rs +++ b/crates/perry-runtime/src/array/collection_tag_tests.rs @@ -1,4 +1,4 @@ -//! Receiver-tag gating of the `Map`/`Set` registry probes (#7768). +//! Receiver-tag gating of the `Map`/`Set` registry probes (#7765). //! //! `js_array_get_f64` and `js_array_length` used to ask both collection //! registries "is this receiver a Set? a Map?" on every element read of an diff --git a/crates/perry-runtime/src/array/indexing.rs b/crates/perry-runtime/src/array/indexing.rs index d3095676cf..277b145cfd 100644 --- a/crates/perry-runtime/src/array/indexing.rs +++ b/crates/perry-runtime/src/array/indexing.rs @@ -545,7 +545,7 @@ fn array_get_property_by_key(arr: *const ArrayHeader, key: *const crate::StringH /// FOR DENSE KEYS/PROPERTY ARRAYS ONLY — general JS arrays may have /// `length > capacity` (sparse), where this cap would be incorrect. pub(crate) unsafe fn keys_array_len_capped_to_capacity(arr: *const ArrayHeader) -> usize { - // #7768: a well-formed dense keys array answers from its own two words. + // #7765: a well-formed dense keys array answers from its own two words. // `js_array_length` re-derives the same number through a proxy probe, a // second header read for its lazy/object arms, and a `clean_arr_ptr` // forwarding walk — once per property read on the field-get funnel. @@ -664,7 +664,7 @@ pub extern "C" fn js_array_length(arr: *const ArrayHeader) -> u32 { }; if !arr.is_null() { let addr = arr as usize; - // #7768: gate both probes on the receiver's own type tag — see + // #7765: gate both probes on the receiver's own type tag — see // `js_array_get_f64` for why the tag answers, why it is ABA-proof, and // why a header-less buffer receiver still lands on the same result. // This reads the byte the `GC_TYPE_LAZY_ARRAY` / `GC_TYPE_OBJECT` block @@ -895,7 +895,7 @@ pub extern "C" fn js_array_get_f64(arr: *const ArrayHeader, index: u32) -> f64 { crate::buffer::js_buffer_get(arr as *const crate::buffer::BufferHeader, index as i32); return byte_val as f64; } - // #7768: ONE `GcHeader` read now gates both collection probes below and + // #7765: ONE `GcHeader` read now gates both collection probes below and // supplies the descriptor flags further down, which `array_object_flags` // used to re-derive through a second `clean_arr_ptr` and a second header // read. On `gc-handoff/apps/asyncpipe_big.ts` this call site was 76% of all diff --git a/crates/perry-runtime/src/map.rs b/crates/perry-runtime/src/map.rs index d59f7b7f75..7a0daf976c 100644 --- a/crates/perry-runtime/src/map.rs +++ b/crates/perry-runtime/src/map.rs @@ -193,7 +193,7 @@ fn register_map(ptr: *mut MapHeader, entries: *mut f64, capacity: usize) { /// Every entry into [`is_registered_map`], i.e. every caller that could not /// rule a `Map` out more cheaply. The `js_array_get_f64` / `js_array_length` -/// receiver-tag gates (#7768) are asserted against this: a plain-array element +/// receiver-tag gates (#7765) are asserted against this: a plain-array element /// read must not move it. Remove those gates and the assertion fails, which is /// the point — a fast path nobody can prove ran is not a fast path. /// diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs index a21e8fae88..2ba040bea5 100644 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs @@ -461,7 +461,7 @@ pub(crate) fn get_field_by_name_object_tail( // been true since `js_set_alloc` moved to // `arena_alloc_gc(_, _, GC_TYPE_SET)`: a registered Set IS a GC // allocation and its `obj_type` classifies it. `js_array_get_f64` and - // `js_array_length` gate their probes on exactly that byte (#7768); + // `js_array_length` gate their probes on exactly that byte (#7765); // this receiver is not proven to carry a header at this point, so it // still asks the registry.) if crate::set::is_registered_set(obj as usize) { diff --git a/scripts/addr_class_allowlist.txt b/scripts/addr_class_allowlist.txt index b644b440dc..6399fe472a 100644 --- a/scripts/addr_class_allowlist.txt +++ b/scripts/addr_class_allowlist.txt @@ -151,4 +151,4 @@ crates/perry-runtime/src/child_process/value_util.rs | * | pre-existing GcHeader crates/perry-runtime/src/closure/dispatch/ | * | pre-existing GcHeader probe predating addr_class; address validated by call-site guards (magnitude/registry/is_valid_obj_ptr) -- migrate to addr_class::try_read_gc_header in a follow-up (split of closure/dispatch.rs) crates/perry-runtime/src/bun_compat/string_width.rs | 0xE0000..=0xE007F | Unicode "Tags" codepoint block (U+E0000..U+E007F) tested against a char, not a handle-band address crates/perry-runtime/src/bun_compat/width_tables.rs | * | pure Unicode East-Asian-width codepoint-range table; every hex literal is a Unicode code point (e.g. U+F0000 / U+100000 SPUA-A/B planes), never a handle-band address -crates/perry-runtime/src/array/collection_tag_tests.rs | * | unit tests for the #7768 receiver-tag gate: they read AND re-stamp `GcHeader.obj_type` on an address they allocated themselves, which is the whole subject under test. `try_read_gc_header` cannot serve them (it hands out a shared reference, and the recycling test must WRITE the tag to model `arena_alloc_gc` handing the bytes to the next owner). +crates/perry-runtime/src/array/collection_tag_tests.rs | * | unit tests for the #7765 receiver-tag gate: they read AND re-stamp `GcHeader.obj_type` on an address they allocated themselves, which is the whole subject under test. `try_read_gc_header` cannot serve them (it hands out a shared reference, and the recycling test must WRITE the tag to model `arena_alloc_gc` handing the bytes to the next owner). From 4c0a689573314755f7a4b69b58532e838dede977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 10 Aug 2026 14:45:34 +0200 Subject: [PATCH 4/8] docs: fold the keys-walk pass and the measured numbers into the #7765 fragment --- ...ay-receiver-tag-gates-collection-probes.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/changelog.d/7765-array-receiver-tag-gates-collection-probes.md b/changelog.d/7765-array-receiver-tag-gates-collection-probes.md index fb795ddf4b..0e3cca4fd7 100644 --- a/changelog.d/7765-array-receiver-tag-gates-collection-probes.md +++ b/changelog.d/7765-array-receiver-tag-gates-collection-probes.md @@ -42,6 +42,30 @@ The same one header read also feeds the descriptor-flag check further down second `clean_arr_ptr` and a second header read (3.1% of the profile on its own). +**The adjacent cluster falls to the same argument.** `js_array_get_f64` was +6.3%, and 78% of that came from one caller: the object field-get funnel walking +an object's `keys_array`. That funnel has *already* proved `keys` is a live +`GC_TYPE_ARRAY` — it reads the `GcHeader` and returns `undefined` otherwise — +and capped the index below the array's capacity, which is precisely the pair of +facts `js_array_get` re-established per key, per property read, through a +`clean_arr_ptr` forwarding walk, a lazy-header probe, the exotic-receiver +classifications and a descriptor-flag read. `keys_array_slot` serves the dense, +descriptor-free, non-forwarded case from the array's own two words and delegates +everything it cannot serve on those terms — a hole (which reads through the +prototype chain), an out-of-range index, a forwarded or descriptor-carrying +array, a null pointer — so no general semantics move. +`keys_array_len_capped_to_capacity` stops paying the same toll through +`js_array_length` once per property read. + +Measured on the pinned mini, both arms timed back to back, min of 5 (the five +benchmarks closest to their floor re-measured interleaved at 9): +**`asyncpipe.ts` 0.9065 s → 0.7143 s, −21.2%** — it stops being the corpus's +worst gap. `is_registered_map` + `is_registered_set` fall from **13.51% to +1.2–1.4%** of the `asyncpipe_big.ts` profile (two agreeing runs), and +`array_object_flags`, `js_array_get_f64` and `js_array_length` all leave the top +of it. `shapes.ts` (−4.5%) and `interp.ts` improve as a side effect — same +funnel. No protected benchmark regresses beyond run-to-run noise. + `crates/perry-runtime/src/array/collection_tag_tests.rs` asserts THE SUBJECT, not just the answer — the registry is a correct fallback, so a test that only compared values would still pass with the gates deleted (CLAUDE.md, "four ways a @@ -56,6 +80,14 @@ confirmation at the end of `is_registered_map` is removed. `every_registered_collection_address_carries_its_own_type_tag` pins the invariant the gates rest on, across capacity growth, so a future registration path that forgot the tag goes red here rather than silently. +`keys_array_slot` gets the same treatment from both sides: a per-thread +fallback counter asserted at zero for the dense arrays the fast path exists for +and at exactly one per refusal for every shape it must delegate, so "stopped +applying" and "started swallowing something it should have delegated" are +equally red. (Per-*thread*, because `cargo test` runs every case on its own +thread in one process — a process-global counter is moved by whatever else +happens to be running, which is how the first version of these assertions +passed for the wrong reason.) Two comments claiming Map/Set headers are `alloc()`-backed with no `GcHeader` — the stated reason the registries are consulted before any header read — are From 5c93be22f79086f56643c882e6bcb4689b685a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 10 Aug 2026 14:48:35 +0200 Subject: [PATCH 5/8] docs(array): separate the per-thread note in the fallback-counter doc --- crates/perry-runtime/src/array/indexing.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/perry-runtime/src/array/indexing.rs b/crates/perry-runtime/src/array/indexing.rs index 277b145cfd..8cd272aa02 100644 --- a/crates/perry-runtime/src/array/indexing.rs +++ b/crates/perry-runtime/src/array/indexing.rs @@ -615,6 +615,7 @@ pub(crate) unsafe fn keys_array_slot( /// exists for, non-zero for every shape it must refuse — so a fast path that /// silently stopped applying, or one that started swallowing a shape it should /// have delegated, both go red. +/// /// Per THREAD — `cargo test` runs every case on its own thread in one process, /// so a process-global counter would be moved by whatever else is running. #[cfg(test)] From aa427357c71797fde0f7367d6f1c601898f303d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 10 Aug 2026 17:23:39 +0200 Subject: [PATCH 6/8] docs: rebase the #7765 fragment's numbers onto c2a96b638 and record the interleaving requirement --- ...ay-receiver-tag-gates-collection-probes.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/changelog.d/7765-array-receiver-tag-gates-collection-probes.md b/changelog.d/7765-array-receiver-tag-gates-collection-probes.md index 0e3cca4fd7..c67fb4a751 100644 --- a/changelog.d/7765-array-receiver-tag-gates-collection-probes.md +++ b/changelog.d/7765-array-receiver-tag-gates-collection-probes.md @@ -57,14 +57,23 @@ array, a null pointer — so no general semantics move. `keys_array_len_capped_to_capacity` stops paying the same toll through `js_array_length` once per property read. -Measured on the pinned mini, both arms timed back to back, min of 5 (the five -benchmarks closest to their floor re-measured interleaved at 9): -**`asyncpipe.ts` 0.9065 s → 0.7143 s, −21.2%** — it stops being the corpus's +Measured on the pinned mini, both arms rebuilt from the same merge-base and run +**interleaved** — 4 passes × best-of-5, alternating arms per benchmark per pass: +**`asyncpipe.ts` 0.9211 s → 0.7284 s, −20.9%** — it stops being the corpus's worst gap. `is_registered_map` + `is_registered_set` fall from **13.51% to 1.2–1.4%** of the `asyncpipe_big.ts` profile (two agreeing runs), and `array_object_flags`, `js_array_get_f64` and `js_array_length` all leave the top -of it. `shapes.ts` (−4.5%) and `interp.ts` improve as a side effect — same -funnel. No protected benchmark regresses beyond run-to-run noise. +of it. `shapes.ts` (−4.7%) improves as a side effect — same funnel. No protected +benchmark moves: the largest is `push_num` at +0.4%, and `churn_alloc`, `tree`, +`retain` and `fib40` are flat to four decimals. + +Interleaving is not a nicety here. The mini oscillates ~3% between passes, so a +block-sequential A/B (all of one arm, then the other) manufactured a +7.3% +`interp` "regression" and a uniform ~3% shift on eight benchmarks purely from +which phase each block landed in — visible only because the per-pass series +showed passes 1 and 3 slow on *both* arms. An earlier +1.2% on `churn_alloc`, +measured that way against a different base, likewise disappears (+0.0%) once the +arms are interleaved. `crates/perry-runtime/src/array/collection_tag_tests.rs` asserts THE SUBJECT, not just the answer — the registry is a correct fallback, so a test that only From 72f5acd0cd3ec0d1efa9cd0cce95678034a8fc57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 10 Aug 2026 18:44:54 +0200 Subject: [PATCH 7/8] test: classify the recycled-bytes stores for the GC store-site inventory Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix --- crates/perry-runtime/src/array/collection_tag_tests.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/perry-runtime/src/array/collection_tag_tests.rs b/crates/perry-runtime/src/array/collection_tag_tests.rs index 80297bbae5..dc62f7e5a0 100644 --- a/crates/perry-runtime/src/array/collection_tag_tests.rs +++ b/crates/perry-runtime/src/array/collection_tag_tests.rs @@ -186,7 +186,10 @@ fn a_stale_registry_entry_over_recycled_bytes_does_not_read_as_a_map() { (*recycled).length = 2; (*recycled).capacity = 2; let elements = (addr as *mut u8).add(std::mem::size_of::()) as *mut f64; + // GC_STORE_AUDIT(POINTER_FREE): raw f64 numerics into a buffer this + // test allocated and re-stamped itself; no heap pointer is stored. std::ptr::write(elements, 111.0); + // GC_STORE_AUDIT(POINTER_FREE): second slot of the same test-owned buffer. std::ptr::write(elements.add(1), 222.0); } From 2048bb0e96ad6dd3a4bab6f80cf23c7d794b3047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 10 Aug 2026 18:44:54 +0200 Subject: [PATCH 8/8] chore: bump version to 0.5.1455 Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix --- CLAUDE.md | 2 +- Cargo.lock | 152 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 3 files changed, 78 insertions(+), 78 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9edb9eb983..6752f7d41f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1454 +**Current Version:** 0.5.1455 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index b7c461e04b..a4c1ec47c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1454" +version = "0.5.1455" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1454" +version = "0.5.1455" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1454" +version = "0.5.1455" [[package]] name = "perry-ui-tvos" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1454" +version = "0.5.1455" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 35de506fe9..d8fd276816 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1454" +version = "0.5.1455" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry"