From 58005a3f52bad3b18ff7fa7b01b0b2488d8a40ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 8 Aug 2026 09:38:03 +0200 Subject: [PATCH 1/4] perf(gc): defer the JSON materialiser's per-slot layout notes to one finalize (#7630) Step-zero profiling on the pinned mini put the per-slot layout machinery at the top of json_pipeline's cost families (~52 samples: layout_note_slot, descriptor visits, layout_transfer, layout_forget_object). The payer is the parse cohort: records are born POINTER_FREE, the first string field builds a per-object side-table pointer mask, and every subsequent field store pays a hashmap round-trip -- then layout_transfer moves the mask on promotion and layout_forget_object drops it at death. The materialiser owns each object's whole construction, so its store loops now use runtime_store_jsvalue_slot_layout_deferred -- bit-for-bit the shared helper minus the layout note (canonicalization, string addref demote, and the write barrier with its SATB shade all kept) -- returning the one fact the notes were computing (pointer-bearing), which the loop accumulates and settles ONCE via layout_finish_deferred_boxed_object: - no pointer stored: the POINTER_FREE birth state is still the truth and keeps its whole-payload trace skip (number-only records); - any pointer stored: GC_LAYOUT_UNKNOWN, the tag-checked scan-all state. A pointer mask can never skip anything for a cohort whose every slot is a NaN-boxed JSValue, so the mask machinery bought nothing here. Routed through layout_mark_unknown so a mask created by the shaped path's by-name fallback mid-construction is removed, not stranded. The shaped path's finalize runs on the live pointer re-read from the parse root, so a mid-parse collection cannot leave it on a stale copy. --- crates/perry-runtime/src/gc/barrier.rs | 28 ++++++++++++++++++++++ crates/perry-runtime/src/gc/layout.rs | 25 +++++++++++++++++++- crates/perry-runtime/src/json/parser.rs | 31 +++++++++++++++++++------ crates/perry-runtime/src/object/mod.rs | 20 ++++++++++++++++ 4 files changed, 96 insertions(+), 8 deletions(-) diff --git a/crates/perry-runtime/src/gc/barrier.rs b/crates/perry-runtime/src/gc/barrier.rs index e92ef4deec..6fddbc0420 100644 --- a/crates/perry-runtime/src/gc/barrier.rs +++ b/crates/perry-runtime/src/gc/barrier.rs @@ -1503,6 +1503,34 @@ fn canonicalize_typed_slot_store_bits( } } +/// #7630: `runtime_store_jsvalue_slot` minus the per-slot layout note, for a +/// caller that OWNS the object's whole construction and settles its layout +/// state once at the end (`layout_finish_deferred_boxed_object`). The JSON +/// materialiser is the caller: per record it performed ~13 `layout_note_slot` +/// calls whose only net effect was to build a per-object side-table pointer +/// mask — the profile's top cost family. Everything else is kept bit-for-bit: +/// the typed-slot canonicalization, the string addref demote, and the write +/// barrier (whose SATB shade must never be dropped — the #7602 lesson). +/// Returns whether the stored bits carry a heap pointer, so the caller can +/// accumulate the one fact the elided notes were computing. +#[inline] +pub(crate) fn runtime_store_jsvalue_slot_layout_deferred( + parent_user: usize, + slot_addr: usize, + slot_index: usize, + value_bits: u64, +) -> bool { + let value_bits = canonicalize_typed_slot_store_bits(parent_user, slot_index, value_bits); + unsafe { + std::ptr::write(slot_addr as *mut u64, value_bits); + } + if value_bits & TAG_MASK == STRING_TAG { + crate::string::js_string_addref((value_bits & POINTER_MASK) as *mut crate::StringHeader); + } + runtime_write_barrier_slot(parent_user, slot_addr, value_bits); + super::layout::layout_pointer_bearing_bits(value_bits) +} + #[inline] pub(crate) fn runtime_store_jsvalue_slot( parent_user: usize, diff --git a/crates/perry-runtime/src/gc/layout.rs b/crates/perry-runtime/src/gc/layout.rs index 95788e0dc4..5c50db7531 100644 --- a/crates/perry-runtime/src/gc/layout.rs +++ b/crates/perry-runtime/src/gc/layout.rs @@ -551,7 +551,7 @@ pub(super) fn strip_nanbox_user_ptr(bits: u64) -> usize { } #[inline] -pub(super) fn layout_pointer_bearing_bits(bits: u64) -> bool { +pub(in crate::gc) fn layout_pointer_bearing_bits(bits: u64) -> bool { let tag = bits & TAG_MASK; if tag == POINTER_TAG || tag == STRING_TAG || tag == BIGINT_TAG { return bits & POINTER_MASK != 0; @@ -609,6 +609,29 @@ pub(crate) unsafe fn layout_init_all_pointer_slots(user_ptr: *mut u8) { (*header)._reserved |= GC_LAYOUT_ALL_POINTERS; } +/// #7630: settle a materialiser-built object's layout state ONCE, after its +/// construction loop elided the per-slot notes +/// (`runtime_store_jsvalue_slot_layout_deferred`). Two exact outcomes: +/// +/// - **No pointer was stored**: the `layout_init_pointer_free` birth state is +/// still the truth, and it is the valuable one — the tracer skips the whole +/// payload. Nothing to do. +/// - **Any pointer was stored**: `GC_LAYOUT_UNKNOWN`, the tag-checked +/// scan-all-slots state. For a cohort whose every slot is a NaN-boxed +/// `JSValue`, a pointer mask can never skip anything a tag check would not +/// reject anyway — the mask machinery (per-object side-table entry, hashmap +/// round-trip per store, `layout_transfer` per promotion, +/// `layout_forget_object` per death) buys nothing here. Routed through +/// `layout_mark_unknown`, not a bare state store, so a mask that a +/// slow-path by-name store DID create mid-construction (shape-overflow +/// records) is removed with the state change rather than stranded. +pub(crate) unsafe fn layout_finish_deferred_boxed_object(user_ptr: usize, saw_pointer: bool) { + if !saw_pointer { + return; + } + layout_mark_unknown(user_ptr as *mut u8); +} + pub(crate) unsafe fn layout_mark_unknown(user_ptr: *mut u8) { let Some(header) = layout_header_for_user(user_ptr as usize) else { return; diff --git a/crates/perry-runtime/src/json/parser.rs b/crates/perry-runtime/src/json/parser.rs index 4e8f2a9848..0c4965b34a 100644 --- a/crates/perry-runtime/src/json/parser.rs +++ b/crates/perry-runtime/src/json/parser.rs @@ -359,6 +359,7 @@ impl<'a> DirectParser<'a> { // Pre-allocate with the known keys_array + field count. No // shape cache lookup — the shape is already in the cache from // the one-time build at parse entry. + let mut saw_pointer = false; let mut js_obj = crate::object::js_object_alloc_class_inline_keys( 0, // class_id 0 = plain object (not a class instance) 0, // parent_class_id @@ -429,8 +430,10 @@ impl<'a> DirectParser<'a> { if fast_idx < alloc_limit { let slot_idx = fast_idx; let value_bits = value.bits(); - // GC_STORE_AUDIT(BARRIERED): shaped JSON field write uses the shared object slot-store helper. - crate::object::store_object_field_slot( + // GC_STORE_AUDIT(BARRIERED): shaped JSON field write uses the + // layout-deferred slot-store helper (#7630); the layout state + // is settled once at the tail of this function. + saw_pointer |= crate::object::store_object_field_slot_layout_deferred( js_obj, slot_idx, value_bits, ); fast_idx += 1; @@ -453,6 +456,11 @@ impl<'a> DirectParser<'a> { // path as generic parse_object). let key_ptr = cached_parse_key_ptr(key_bytes); js_obj = parse_root_object_ptr(obj_slot); + // The by-name path stores through the noting helper and may + // build a mask mid-construction; treat it as pointer-bearing so + // the tail's finalize (which routes through layout_mark_unknown) + // removes whatever it recorded (#7630). + saw_pointer = true; crate::object::js_object_set_field_by_name( js_obj, key_ptr as *mut StringHeader, @@ -471,6 +479,10 @@ impl<'a> DirectParser<'a> { } self.expect(b'}'); js_obj = parse_root_object_ptr(obj_slot); + // #7630: the construction loop elided per-slot layout notes; settle the + // layout state once, on the LIVE pointer (re-read from the parse root + // above, so a mid-parse collection cannot leave this on a stale copy). + crate::gc::layout_finish_deferred_boxed_object(js_obj as usize, saw_pointer); parse_root_restore(saved_roots); JSValue::object_ptr(js_obj as *mut u8) } @@ -665,22 +677,27 @@ impl<'a> DirectParser<'a> { for i in 0..alloc_field_count { std::ptr::write(fields_ptr.add(i), JSValue::undefined()); } - let write_field = |i: usize, value: JSValue| { + let write_field = |i: usize, value: JSValue| -> bool { let value_bits = value.bits(); unsafe { - // GC_STORE_AUDIT(BARRIERED): JSON object field write uses the shared object slot-store helper. - crate::object::store_object_field_slot(js_obj, i, value_bits); + // GC_STORE_AUDIT(BARRIERED): JSON object field write uses the + // layout-deferred slot-store helper (#7630); the layout state is + // settled once below. No allocation happens between the writes + // and the finalize, so `js_obj` cannot move in between. + crate::object::store_object_field_slot_layout_deferred(js_obj, i, value_bits) } }; + let mut saw_pointer = false; if let Some((_, values)) = heap_fields.as_ref() { for (i, value) in values.iter().copied().enumerate() { - write_field(i, value); + saw_pointer |= write_field(i, value); } } else { for (i, value) in inline_values[..inline_len].iter().copied().enumerate() { - write_field(i, value); + saw_pointer |= write_field(i, value); } } + crate::gc::layout_finish_deferred_boxed_object(js_obj as usize, saw_pointer); parse_root_restore(saved_roots); JSValue::object_ptr(js_obj as *mut u8) } diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index 255847bcc5..d7ff54d129 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -1666,6 +1666,26 @@ pub(crate) unsafe fn store_object_field_slot( crate::gc::runtime_store_jsvalue_slot(obj as usize, slot as usize, field_index, value_bits); } +/// #7630: `store_object_field_slot` without the per-slot layout note, for the +/// JSON materialiser's construction loops. Returns whether the value carries a +/// heap pointer; the caller accumulates that and settles the object's layout +/// state once via `layout_finish_deferred_boxed_object`. +#[inline] +pub(crate) unsafe fn store_object_field_slot_layout_deferred( + obj: *mut ObjectHeader, + field_index: usize, + value_bits: u64, +) -> bool { + let fields_ptr = (obj as *mut u8).add(std::mem::size_of::()) as *mut u64; + let slot = fields_ptr.add(field_index); + crate::gc::runtime_store_jsvalue_slot_layout_deferred( + obj as usize, + slot as usize, + field_index, + value_bits, + ) +} + #[inline] pub(super) unsafe fn mark_object_dynamic_shape_unknown(obj: *mut ObjectHeader) { if obj.is_null() || (obj as usize) < crate::gc::GC_HEADER_SIZE + 0x1000 { From 8e321ee2c0c4ac51e0fc421e8061e573ff6b4376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 8 Aug 2026 09:53:44 +0200 Subject: [PATCH 2/4] refactor(gc): split the runtime slot-store helpers into barrier_store.rs (2000-line cap) --- crates/perry-runtime/src/gc/barrier.rs | 132 ----------------- crates/perry-runtime/src/gc/barrier_store.rs | 143 +++++++++++++++++++ crates/perry-runtime/src/gc/mod.rs | 3 + crates/perry-runtime/src/json/parser.rs | 7 +- 4 files changed, 150 insertions(+), 135 deletions(-) create mode 100644 crates/perry-runtime/src/gc/barrier_store.rs diff --git a/crates/perry-runtime/src/gc/barrier.rs b/crates/perry-runtime/src/gc/barrier.rs index 6fddbc0420..e9e88b2a11 100644 --- a/crates/perry-runtime/src/gc/barrier.rs +++ b/crates/perry-runtime/src/gc/barrier.rs @@ -1455,138 +1455,6 @@ pub(super) fn mark_dirty_external_slot_page(header_addr: usize, page: usize) -> }) } -pub(crate) fn runtime_write_barrier_slot(parent_addr: usize, slot_addr: usize, child_bits: u64) { - if !write_barriers_enabled() { - incremental_mark_barrier_value(child_bits); - return; - } - write_barrier_slot_decoded(parent_addr, slot_addr, child_bits, false); -} - -/// Canonicalize an **INT32-boxed** numeric store into a raw-f64-masked slot of -/// an intact typed-shape descriptor (`0x7FFE…` → the plain IEEE bits of the same -/// number). The object twin of `canonicalize_array_numeric_store_bits` -/// (`array/header.rs`), and needed for the same reason. -/// -/// `layout_note_slot` treats any non-raw-f64 bit pattern landing in a raw-f64 -/// slot as a representation change and calls `layout_set_typed_unknown`, which -/// evicts the object's `TypedLayoutDescriptor` **permanently and one-way**. -/// INT32 boxes genuinely reach object fields from FFI / native modules (sqlite -/// row columns, `v8` deserialization, …), and unlike codegen's guarded class- -/// field store — which canonicalizes inline behind a plain-finite check — this -/// runtime choke point wrote the bits verbatim. One FFI integer therefore cost -/// the object its typed fast path forever. -/// -/// There is no observable behavior change: an INT32 box and its f64 are `===` -/// and print identically. `value_bits_to_number` supplies the class-ref -/// exclusion (a `ClassRef` shares INT32_TAG and must keep its tag), so a class -/// value still downgrades the descriptor rather than being stripped to a bare -/// number. -/// -/// Ordered tag-first so the (hot) non-INT32 store never pays the thread-local -/// descriptor probe. -#[inline] -fn canonicalize_typed_slot_store_bits( - parent_user: usize, - slot_index: usize, - value_bits: u64, -) -> u64 { - if value_bits & TAG_MASK != crate::value::INT32_TAG { - return value_bits; - } - if !crate::gc::layout_slot_is_raw_f64_typed(parent_user, slot_index) { - return value_bits; - } - match crate::array::value_bits_to_number(value_bits) { - Some(number) => number.to_bits(), - None => value_bits, - } -} - -/// #7630: `runtime_store_jsvalue_slot` minus the per-slot layout note, for a -/// caller that OWNS the object's whole construction and settles its layout -/// state once at the end (`layout_finish_deferred_boxed_object`). The JSON -/// materialiser is the caller: per record it performed ~13 `layout_note_slot` -/// calls whose only net effect was to build a per-object side-table pointer -/// mask — the profile's top cost family. Everything else is kept bit-for-bit: -/// the typed-slot canonicalization, the string addref demote, and the write -/// barrier (whose SATB shade must never be dropped — the #7602 lesson). -/// Returns whether the stored bits carry a heap pointer, so the caller can -/// accumulate the one fact the elided notes were computing. -#[inline] -pub(crate) fn runtime_store_jsvalue_slot_layout_deferred( - parent_user: usize, - slot_addr: usize, - slot_index: usize, - value_bits: u64, -) -> bool { - let value_bits = canonicalize_typed_slot_store_bits(parent_user, slot_index, value_bits); - unsafe { - std::ptr::write(slot_addr as *mut u64, value_bits); - } - if value_bits & TAG_MASK == STRING_TAG { - crate::string::js_string_addref((value_bits & POINTER_MASK) as *mut crate::StringHeader); - } - runtime_write_barrier_slot(parent_user, slot_addr, value_bits); - super::layout::layout_pointer_bearing_bits(value_bits) -} - -#[inline] -pub(crate) fn runtime_store_jsvalue_slot( - parent_user: usize, - slot_addr: usize, - slot_index: usize, - value_bits: u64, -) { - let value_bits = canonicalize_typed_slot_store_bits(parent_user, slot_index, value_bits); - unsafe { - std::ptr::write(slot_addr as *mut u64, value_bits); - } - // A heap string stored into an object field / array element is now aliased - // from the heap, so a later `js_string_append` must NOT mutate its buffer - // in place while this slot still references it. Demote it from "uniquely - // owned" (refcount==1) to shared (refcount==0). This realizes the - // documented `js_string_addref` contract ("stored into an array/object" — - // see string/alloc.rs): codegen wires the local-copy alias case (`let y = - // x`) but never the heap-store case, so refcount=1 strings leaked into - // object/array slots and were corrupted by the in-place append fast path. - // Concretely: code that snapshots a string into a heap slot - // (`slot = newState`) and later grows the same buffer via `+=` would have - // the in-place append silently rewrite the stored slot, so a later equality - // check against the snapshot wrongly saw the two as identical. Every dynamic - // object-field and array-element write funnels through here, so this is the - // single complete choke point. - if value_bits & TAG_MASK == STRING_TAG { - crate::string::js_string_addref((value_bits & POINTER_MASK) as *mut crate::StringHeader); - } - layout_note_slot(parent_user, slot_index, value_bits); - runtime_write_barrier_slot(parent_user, slot_addr, value_bits); -} - -pub(crate) fn runtime_write_barrier_external_slot( - parent_addr: usize, - slot_addr: usize, - child_bits: u64, -) { - if !write_barriers_enabled() { - incremental_mark_barrier_value(child_bits); - return; - } - write_barrier_slot_decoded(parent_addr, slot_addr, child_bits, true); -} - -pub(crate) fn runtime_write_barrier_gc_slot(parent_addr: usize, slot_addr: usize, child_bits: u64) { - if !write_barriers_enabled() { - incremental_mark_barrier_value(child_bits); - return; - } - let parent_is_malloc_gc = matches!( - crate::arena::classify_heap_generation(parent_addr), - crate::arena::HeapGeneration::Unknown - ) && malloc_gc_parent_addr(parent_addr); - write_barrier_slot_decoded(parent_addr, slot_addr, child_bits, parent_is_malloc_gc); -} - #[inline] pub(crate) fn runtime_write_barrier_root_heap_word(value_bits: u64) { incremental_mark_barrier_value(value_bits); diff --git a/crates/perry-runtime/src/gc/barrier_store.rs b/crates/perry-runtime/src/gc/barrier_store.rs new file mode 100644 index 0000000000..792d909d9e --- /dev/null +++ b/crates/perry-runtime/src/gc/barrier_store.rs @@ -0,0 +1,143 @@ +//! The runtime's exact slot-store helpers — the choke point every dynamic +//! object-field and array-element write funnels through, plus the slot-form +//! write-barrier wrappers they compose with. Split out of `barrier.rs` to +//! keep it under the repo's 2000-line cap (#7630); pure move except for the +//! `use` lines. + +use super::barrier::{ + incremental_mark_barrier_value, malloc_gc_parent_addr, write_barrier_slot_decoded, + write_barriers_enabled, +}; +use super::*; + +pub(crate) fn runtime_write_barrier_slot(parent_addr: usize, slot_addr: usize, child_bits: u64) { + if !write_barriers_enabled() { + incremental_mark_barrier_value(child_bits); + return; + } + write_barrier_slot_decoded(parent_addr, slot_addr, child_bits, false); +} + +/// Canonicalize an **INT32-boxed** numeric store into a raw-f64-masked slot of +/// an intact typed-shape descriptor (`0x7FFE…` → the plain IEEE bits of the same +/// number). The object twin of `canonicalize_array_numeric_store_bits` +/// (`array/header.rs`), and needed for the same reason. +/// +/// `layout_note_slot` treats any non-raw-f64 bit pattern landing in a raw-f64 +/// slot as a representation change and calls `layout_set_typed_unknown`, which +/// evicts the object's `TypedLayoutDescriptor` **permanently and one-way**. +/// INT32 boxes genuinely reach object fields from FFI / native modules (sqlite +/// row columns, `v8` deserialization, …), and unlike codegen's guarded class- +/// field store — which canonicalizes inline behind a plain-finite check — this +/// runtime choke point wrote the bits verbatim. One FFI integer therefore cost +/// the object its typed fast path forever. +/// +/// There is no observable behavior change: an INT32 box and its f64 are `===` +/// and print identically. `value_bits_to_number` supplies the class-ref +/// exclusion (a `ClassRef` shares INT32_TAG and must keep its tag), so a class +/// value still downgrades the descriptor rather than being stripped to a bare +/// number. +/// +/// Ordered tag-first so the (hot) non-INT32 store never pays the thread-local +/// descriptor probe. +#[inline] +fn canonicalize_typed_slot_store_bits( + parent_user: usize, + slot_index: usize, + value_bits: u64, +) -> u64 { + if value_bits & TAG_MASK != crate::value::INT32_TAG { + return value_bits; + } + if !crate::gc::layout_slot_is_raw_f64_typed(parent_user, slot_index) { + return value_bits; + } + match crate::array::value_bits_to_number(value_bits) { + Some(number) => number.to_bits(), + None => value_bits, + } +} + +/// #7630: `runtime_store_jsvalue_slot` minus the per-slot layout note, for a +/// caller that OWNS the object's whole construction and settles its layout +/// state once at the end (`layout_finish_deferred_boxed_object`). The JSON +/// materialiser is the caller: per record it performed ~13 `layout_note_slot` +/// calls whose only net effect was to build a per-object side-table pointer +/// mask — the profile's top cost family. Everything else is kept bit-for-bit: +/// the typed-slot canonicalization, the string addref demote, and the write +/// barrier (whose SATB shade must never be dropped — the #7602 lesson). +/// Returns whether the stored bits carry a heap pointer, so the caller can +/// accumulate the one fact the elided notes were computing. +#[inline] +pub(crate) fn runtime_store_jsvalue_slot_layout_deferred( + parent_user: usize, + slot_addr: usize, + slot_index: usize, + value_bits: u64, +) -> bool { + let value_bits = canonicalize_typed_slot_store_bits(parent_user, slot_index, value_bits); + unsafe { + std::ptr::write(slot_addr as *mut u64, value_bits); + } + if value_bits & TAG_MASK == STRING_TAG { + crate::string::js_string_addref((value_bits & POINTER_MASK) as *mut crate::StringHeader); + } + runtime_write_barrier_slot(parent_user, slot_addr, value_bits); + super::layout::layout_pointer_bearing_bits(value_bits) +} + +#[inline] +pub(crate) fn runtime_store_jsvalue_slot( + parent_user: usize, + slot_addr: usize, + slot_index: usize, + value_bits: u64, +) { + let value_bits = canonicalize_typed_slot_store_bits(parent_user, slot_index, value_bits); + unsafe { + std::ptr::write(slot_addr as *mut u64, value_bits); + } + // A heap string stored into an object field / array element is now aliased + // from the heap, so a later `js_string_append` must NOT mutate its buffer + // in place while this slot still references it. Demote it from "uniquely + // owned" (refcount==1) to shared (refcount==0). This realizes the + // documented `js_string_addref` contract ("stored into an array/object" — + // see string/alloc.rs): codegen wires the local-copy alias case (`let y = + // x`) but never the heap-store case, so refcount=1 strings leaked into + // object/array slots and were corrupted by the in-place append fast path. + // Concretely: code that snapshots a string into a heap slot + // (`slot = newState`) and later grows the same buffer via `+=` would have + // the in-place append silently rewrite the stored slot, so a later equality + // check against the snapshot wrongly saw the two as identical. Every dynamic + // object-field and array-element write funnels through here, so this is the + // single complete choke point. + if value_bits & TAG_MASK == STRING_TAG { + crate::string::js_string_addref((value_bits & POINTER_MASK) as *mut crate::StringHeader); + } + layout_note_slot(parent_user, slot_index, value_bits); + runtime_write_barrier_slot(parent_user, slot_addr, value_bits); +} + +pub(crate) fn runtime_write_barrier_external_slot( + parent_addr: usize, + slot_addr: usize, + child_bits: u64, +) { + if !write_barriers_enabled() { + incremental_mark_barrier_value(child_bits); + return; + } + write_barrier_slot_decoded(parent_addr, slot_addr, child_bits, true); +} + +pub(crate) fn runtime_write_barrier_gc_slot(parent_addr: usize, slot_addr: usize, child_bits: u64) { + if !write_barriers_enabled() { + incremental_mark_barrier_value(child_bits); + return; + } + let parent_is_malloc_gc = matches!( + crate::arena::classify_heap_generation(parent_addr), + crate::arena::HeapGeneration::Unknown + ) && malloc_gc_parent_addr(parent_addr); + write_barrier_slot_decoded(parent_addr, slot_addr, child_bits, parent_is_malloc_gc); +} diff --git a/crates/perry-runtime/src/gc/mod.rs b/crates/perry-runtime/src/gc/mod.rs index b6d9537464..0d62bd0c0f 100644 --- a/crates/perry-runtime/src/gc/mod.rs +++ b/crates/perry-runtime/src/gc/mod.rs @@ -82,6 +82,9 @@ mod trace; pub(crate) use trace::*; mod barrier; pub use barrier::*; +/// #7630: the runtime slot-store helpers, split from `barrier.rs` (2000-line cap). +mod barrier_store; +pub use barrier_store::*; mod dirty_page_cache; // #7187 Phase B: `crate::arena`'s page-metadata module invalidates the // barrier's "already dirty" page cache when it un-stamps or discards a page. diff --git a/crates/perry-runtime/src/json/parser.rs b/crates/perry-runtime/src/json/parser.rs index 0c4965b34a..a67a991b60 100644 --- a/crates/perry-runtime/src/json/parser.rs +++ b/crates/perry-runtime/src/json/parser.rs @@ -433,9 +433,10 @@ impl<'a> DirectParser<'a> { // GC_STORE_AUDIT(BARRIERED): shaped JSON field write uses the // layout-deferred slot-store helper (#7630); the layout state // is settled once at the tail of this function. - saw_pointer |= crate::object::store_object_field_slot_layout_deferred( - js_obj, slot_idx, value_bits, - ); + saw_pointer |= + crate::object::store_object_field_slot_layout_deferred( + js_obj, slot_idx, value_bits, + ); fast_idx += 1; took_fast = true; } From f9a27f728d4c8097a706d4aff00fd58580f21c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 8 Aug 2026 11:20:20 +0200 Subject: [PATCH 3/4] docs(changelog): add fragment for #7633 --- .../7633-json-materialiser-layout-deferred.md | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 changelog.d/7633-json-materialiser-layout-deferred.md diff --git a/changelog.d/7633-json-materialiser-layout-deferred.md b/changelog.d/7633-json-materialiser-layout-deferred.md new file mode 100644 index 0000000000..9b8fdb749a --- /dev/null +++ b/changelog.d/7633-json-materialiser-layout-deferred.md @@ -0,0 +1,26 @@ +**perf(gc): defer the JSON materialiser's per-slot layout notes to one finalize (#7630)** + +Step-zero profiling on the pinned bench host put the per-slot layout +machinery at the top of `json_pipeline`'s cost families: parse-built records +are born `POINTER_FREE`, the first string field builds a per-object +side-table pointer mask, every subsequent field store pays a hashmap +round-trip, `layout_transfer` moves the mask on promotion, and +`layout_forget_object` drops it at death. + +The materialiser owns each object's whole construction, so its store loops +now use `runtime_store_jsvalue_slot_layout_deferred` — the shared helper +minus the layout note, with the typed-slot canonicalization, string addref +demote, and write barrier (including its SATB shade) kept bit-for-bit — and +settle the layout state once per object: `POINTER_FREE` stays for +number-only records (keeping their whole-payload trace skip), anything with +a pointer becomes `GC_LAYOUT_UNKNOWN`, the tag-checked scan-all state — a +pointer mask can never skip anything for a cohort whose every slot is a +NaN-boxed `JSValue`. Routed through `layout_mark_unknown` so a mask created +by the shaped path's by-name fallback mid-construction is removed rather +than stranded. + +Pinned-host, interleaved, hash-identical: `json_pipeline` 200k `build_out` +934 → 730 ms (−22 %), total −17 %, GC pause 816 → 689 ms, peak RSS +451 → 422 MB; `layout_note_slot` / `layout_forget_object` vanish from the +profile. Also splits `barrier.rs`'s slot-store helpers into +`barrier_store.rs` for the 2000-line cap. From 1865edb13aff5b26b062af4349c9006bad5ad472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 8 Aug 2026 12:53:15 +0200 Subject: [PATCH 4/4] chore(version): bump to 0.5.1361 --- 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 068651578d..eeb47508d2 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.1360 +**Current Version:** 0.5.1361 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 14da3dc9fb..e6d3b7b9fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1360" +version = "0.5.1361" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1360" +version = "0.5.1361" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1360" +version = "0.5.1361" [[package]] name = "perry-ui-tvos" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1360" +version = "0.5.1361" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 4aecd2ec52..0e166b4785 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1360" +version = "0.5.1361" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry"