@@ -42,6 +42,30 @@ The same one header read also feeds the descriptor-flag check further down
4242second ` clean_arr_ptr ` and a second header read (3.1% of the profile on its
4343own).
4444
45+ ** The adjacent cluster falls to the same argument.** ` js_array_get_f64 ` was
46+ 6.3%, and 78% of that came from one caller: the object field-get funnel walking
47+ an object's ` keys_array ` . That funnel has * already* proved ` keys ` is a live
48+ ` GC_TYPE_ARRAY ` — it reads the ` GcHeader ` and returns ` undefined ` otherwise —
49+ and capped the index below the array's capacity, which is precisely the pair of
50+ facts ` js_array_get ` re-established per key, per property read, through a
51+ ` clean_arr_ptr ` forwarding walk, a lazy-header probe, the exotic-receiver
52+ classifications and a descriptor-flag read. ` keys_array_slot ` serves the dense,
53+ descriptor-free, non-forwarded case from the array's own two words and delegates
54+ everything it cannot serve on those terms — a hole (which reads through the
55+ prototype chain), an out-of-range index, a forwarded or descriptor-carrying
56+ array, a null pointer — so no general semantics move.
57+ ` keys_array_len_capped_to_capacity ` stops paying the same toll through
58+ ` js_array_length ` once per property read.
59+
60+ Measured on the pinned mini, both arms timed back to back, min of 5 (the five
61+ benchmarks closest to their floor re-measured interleaved at 9):
62+ ** ` asyncpipe.ts ` 0.9065 s → 0.7143 s, −21.2%** — it stops being the corpus's
63+ worst gap. ` is_registered_map ` + ` is_registered_set ` fall from ** 13.51% to
64+ 1.2–1.4%** of the ` asyncpipe_big.ts ` profile (two agreeing runs), and
65+ ` array_object_flags ` , ` js_array_get_f64 ` and ` js_array_length ` all leave the top
66+ of it. ` shapes.ts ` (−4.5%) and ` interp.ts ` improve as a side effect — same
67+ funnel. No protected benchmark regresses beyond run-to-run noise.
68+
4569` crates/perry-runtime/src/array/collection_tag_tests.rs ` asserts THE SUBJECT,
4670not just the answer — the registry is a correct fallback, so a test that only
4771compared 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.
5680` every_registered_collection_address_carries_its_own_type_tag ` pins the
5781invariant the gates rest on, across capacity growth, so a future registration
5882path that forgot the tag goes red here rather than silently.
83+ ` keys_array_slot ` gets the same treatment from both sides: a per-thread
84+ fallback counter asserted at zero for the dense arrays the fast path exists for
85+ and at exactly one per refusal for every shape it must delegate, so "stopped
86+ applying" and "started swallowing something it should have delegated" are
87+ equally red. (Per-* thread* , because ` cargo test ` runs every case on its own
88+ thread in one process — a process-global counter is moved by whatever else
89+ happens to be running, which is how the first version of these assertions
90+ passed for the wrong reason.)
5991
6092Two comments claiming Map/Set headers are ` alloc() ` -backed with no ` GcHeader `
6193— the stated reason the registries are consulted before any header read — are
0 commit comments