From a17a836f00a90f0bd6b2d68afc38a423404eac67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 14 Sep 2026 04:57:25 +0200 Subject: [PATCH 1/2] perf(json): count a compiled loop's whole-array materialization as traversal evidence Traversal feedback (#10150) learns that a program scans its parsed arrays from element reads in lazy_get_rooted. The element-shape loop clone (#10171) materializes a lazy array whole in its preheader through js_array_refresh_local_head before any element is read lazily, so a scan loop served by the clone never produced evidence: every parse built the tape and then materialized every record anyway. On the quiet bench mini records_array_16k:scan and records_array_1m:scan read 1.11x and 1.18x the better of Node and Bun, with a third of the parse samples in build_tape_into. js_array_refresh_local_head now notes one flip's worth of evidence the first time it materializes a lazy array. Its emitters are all cold arms that run about once per receiver, so the added tracked-header probe is not on a hot path. --- crates/perry-runtime/src/array/header.rs | 7 +++ crates/perry-runtime/src/array/mod.rs | 2 + .../array/refresh_traversal_evidence_tests.rs | 55 +++++++++++++++++++ .../src/json/traversal_feedback.rs | 48 ++++++++++++++-- 4 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 crates/perry-runtime/src/array/refresh_traversal_evidence_tests.rs diff --git a/crates/perry-runtime/src/array/header.rs b/crates/perry-runtime/src/array/header.rs index 08c965df62..b832d073a5 100644 --- a/crates/perry-runtime/src/array/header.rs +++ b/crates/perry-runtime/src/array/header.rs @@ -1363,10 +1363,17 @@ pub extern "C" fn js_array_refresh_local_head(value: f64) -> f64 { if !crate::value::addr_class::is_plausible_heap_addr(raw) { return value; } + // Cold arms only (loop-clone preheader, guarded read repair): see + // `traversal_feedback::note_compiled_materialization`. + let materializes_lazy = + unsafe { crate::json::traversal_feedback::lazy_array_unmaterialized(raw) }; let cleaned = clean_arr_ptr(raw as *const ArrayHeader); if cleaned.is_null() || cleaned as usize == raw { return value; } + if materializes_lazy { + crate::json::traversal_feedback::note_compiled_materialization(); + } f64::from_bits(crate::value::POINTER_TAG | (cleaned as u64 & crate::value::POINTER_MASK)) } diff --git a/crates/perry-runtime/src/array/mod.rs b/crates/perry-runtime/src/array/mod.rs index dd08760e23..d3816eca98 100644 --- a/crates/perry-runtime/src/array/mod.rs +++ b/crates/perry-runtime/src/array/mod.rs @@ -64,6 +64,8 @@ mod index_get_exit_tests; #[cfg(test)] mod push_pop_tests; #[cfg(test)] +mod refresh_traversal_evidence_tests; +#[cfg(test)] mod spread_dense_tests; #[cfg(test)] mod strict_store_tests; diff --git a/crates/perry-runtime/src/array/refresh_traversal_evidence_tests.rs b/crates/perry-runtime/src/array/refresh_traversal_evidence_tests.rs new file mode 100644 index 0000000000..a930eda60a --- /dev/null +++ b/crates/perry-runtime/src/array/refresh_traversal_evidence_tests.rs @@ -0,0 +1,55 @@ +//! A lazy JSON array materialized whole by compiled code refreshing its local +//! head is traversal evidence, exactly once per array, and a plain array is +//! never evidence. Without this the element-shape loop clone (#10171) served +//! every scan loop through its preheader and traversal feedback never learned +//! that the tape was being wasted. + +fn lazy_array_box(bytes: &[u8], len: u32) -> f64 { + let tape = crate::json_tape::build_tape(bytes).unwrap(); + let text = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + let lazy = unsafe { crate::json_tape::alloc_lazy_array(&tape.entries, 0, len, text) }; + let header = unsafe { crate::value::addr_class::try_read_gc_header(lazy as usize) }.unwrap(); + assert_eq!( + header.obj_type, + crate::gc::GC_TYPE_LAZY_ARRAY, + "fixture must be a real lazy array or the assertions below are vacuous" + ); + crate::value::js_nanbox_pointer(lazy as i64) +} + +#[test] +fn compiled_materialization_of_a_lazy_array_counts_once_and_a_plain_array_never() { + crate::json::traversal_feedback::reset_for_tests(); + let scope = crate::gc::RuntimeHandleScope::new(); + + let mut plain = crate::array::js_array_alloc(2); + plain = crate::array::js_array_push_f64(plain, 1.0); + let plain_box = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(plain as i64)); + let _ = crate::array::header::js_array_refresh_local_head(plain_box.get_nanbox_f64()); + assert_eq!( + crate::json::traversal_feedback::score_for_tests(), + 0, + "a plain array is not traversal evidence" + ); + + let lazy = scope.root_nanbox_f64(lazy_array_box(br#"[{"id":1},{"id":2},{"id":3}]"#, 3)); + let first = crate::array::header::js_array_refresh_local_head(lazy.get_nanbox_f64()); + assert_ne!( + first.to_bits(), + lazy.get_nanbox_f64().to_bits(), + "the head was refreshed" + ); + assert_eq!( + crate::json::traversal_feedback::score_for_tests(), + 2, + "materializing a lazy array whole is one flip's worth of evidence" + ); + + let _ = crate::array::header::js_array_refresh_local_head(lazy.get_nanbox_f64()); + assert_eq!( + crate::json::traversal_feedback::score_for_tests(), + 2, + "an already materialized lazy array is not evidence again" + ); + crate::json::traversal_feedback::reset_for_tests(); +} diff --git a/crates/perry-runtime/src/json/traversal_feedback.rs b/crates/perry-runtime/src/json/traversal_feedback.rs index 55f3ea2d2f..bca5cd65a2 100644 --- a/crates/perry-runtime/src/json/traversal_feedback.rs +++ b/crates/perry-runtime/src/json/traversal_feedback.rs @@ -24,10 +24,16 @@ //! 1.11x -> 0.98x, records_array_8m:scan CPU 0.93x -> 0.77x and RSS 190 -> //! 163 MiB; every other cell unchanged. //! -//! Only element-by-element reads in `lazy_get_rooted` count (the flip, or an -//! in-order read of the last element). Stringify, -//! revivers, array methods and mutation also force materialization, but none of -//! them is evidence that a scan would have been cheaper eagerly. +//! Two kinds of event count as traversal evidence: element-by-element reads in +//! `lazy_get_rooted` (the flip, or an in-order read of the last element), and a +//! compiled loop or indexed read materializing the whole array through +//! `js_array_refresh_local_head` — the element-shape loop clone's preheader +//! (#10171) and the guarded indexed-read repair both do that before a single +//! element is read lazily, so without it a program whose scan loop is served by +//! the clone never produced evidence and paid for the tape AND the eager +//! materialization on every parse. Stringify, revivers, array methods and +//! mutation also force materialization, but none of them is evidence that a +//! scan would have been cheaper eagerly. use std::cell::Cell; @@ -78,6 +84,35 @@ pub(crate) unsafe fn after_cold_read( } } +/// A lazy array was materialized whole by compiled code refreshing its local +/// head (`js_array_refresh_local_head`) — the element-shape loop clone's +/// preheader or a guarded indexed read's repair. Every element is now eagerly +/// built, so the tape this array was parsed onto was wasted: the same evidence +/// as a flip. +pub(crate) fn note_compiled_materialization() { + SCORE.with(|s| s.set(s.get().saturating_add(2).min(SCORE_MAX))); +} + +/// Is `addr` a lazy JSON array whose elements have not been built yet? Read by +/// `js_array_refresh_local_head` before it materializes, so evidence is noted +/// once per array. Its emitters are cold arms that run about once per +/// receiver, so the tracked-header probe costs nothing measurable. +/// +/// # Safety +/// +/// `addr` must be a plausible heap address; the tracked-header read validates +/// ownership before anything is dereferenced. +pub(crate) unsafe fn lazy_array_unmaterialized(addr: usize) -> bool { + let Some(header) = crate::value::addr_class::try_read_tracked_gc_header(addr) else { + return false; + }; + if (*header.as_ptr()).obj_type != crate::gc::GC_TYPE_LAZY_ARRAY { + return false; + } + let lazy = addr as *const crate::json_tape::LazyArrayHeader; + (*lazy).magic == crate::json_tape::LAZY_ARRAY_MAGIC && (*lazy).materialized.is_null() +} + /// Should an otherwise tape-eligible parse go eagerly instead? pub(crate) fn prefer_eager() -> bool { if SCORE.with(Cell::get) < PREFER_EAGER_AT { @@ -96,6 +131,11 @@ pub(crate) fn prefer_eager() -> bool { }) } +#[cfg(test)] +pub(crate) fn score_for_tests() -> u8 { + SCORE.with(Cell::get) +} + #[cfg(test)] pub(crate) fn reset_for_tests() { SCORE.with(|s| s.set(0)); From 372af78ddab76431b3cfdaf2b3ecbf4c96abb034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 14 Sep 2026 06:15:45 +0200 Subject: [PATCH 2/2] docs(changelog): fragment for #10249 --- changelog.d/10249-json-clone-traversal-evidence.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog.d/10249-json-clone-traversal-evidence.md diff --git a/changelog.d/10249-json-clone-traversal-evidence.md b/changelog.d/10249-json-clone-traversal-evidence.md new file mode 100644 index 0000000000..b1dd622120 --- /dev/null +++ b/changelog.d/10249-json-clone-traversal-evidence.md @@ -0,0 +1,3 @@ +### perf(json): count a compiled loop's whole-array materialization as traversal evidence + +Traversal feedback (#10150) switches a program's `JSON.parse` calls to eager parsing once its lazy arrays keep being fully traversed, but it only counted element reads in `lazy_get_rooted`. The element-shape loop clone (#10171) materializes a lazy array whole in its preheader through `js_array_refresh_local_head`, so scan loops served by the clone never produced evidence and paid for both the tape and the eager materialization on every parse. `js_array_refresh_local_head` now counts the first materialization of each lazy array as evidence. Measured on the bench mini: `records_array_16k:scan` 149.6 → 109.7 ms, `records_array_1m:scan` 180.4 → 132.6 ms, `records_array_8m:scan` 171.5 → 122.6 ms, other JSON rows unchanged.