You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Perry's version-stable packed-loop optimization does not admit immutable arrays held in closure-capture slots. That excludes the actual wolf-ecs/simple_iter shape even after #8755 and the forwarded-array follow-up #8767: each system closes over a Query extends Array, loads an Archetype extends Array from it, then performs a nested indexed loop.
Add guarded loop versioning for immutable closure-captured packed Arrays/Array subclasses, including the nested case where the inner array is obtained from a guarded outer indexed read. Keep the generic loop as the side exit whenever capture storage, identity, layout, version, or mutation proofs fail.
Node and Perry must both finish with checksum: 2000. The public integration case is noctjs/ecs-benchmark at 7b53a36606118e8b2a450a2ba4919939c86bbd2e, src/cases/wolf-ecs/simple_iter.js, with wolf-ecs from that lockfile.
Measured on an Apple M1 Mac mini (8 GiB, macOS 26.5.1, Node 26.5.1), AC power, after a 60-sample CPU quiet gate, three discarded process warmups, and 11 alternating Node/Perry process pairs:
Runtime
median
Node
0.004938 ms/op
Perry 20a388974d7333ccaec9da9175fbcee54ad49902
0.780749 ms/op
Perry is 158.10x slower. Every process completed successfully.
A symbolized one-second profile synchronized to the timed phase collected 759 main-thread samples. js_packed_arraylike_index_get was the exclusive leaf in 413/759 samples (54.4%). The three system closures accounted for another 81, 76, and 69 exclusive samples.
The same full Wolf executable compiled from #8767 head 7e223e9dcaa87ad6415d85aec7ff201428e5f716 measured 0.780296 ms/op in a confirmation run: effectively unchanged. Its retained LLVM module contains 21 calls to js_packed_arraylike_index_get across the three systems (seven indexed reads per system) and zerostable_packed.loop.fast / stable_packed.loop.slow blocks.
This is expected from the current admission rule: packed_loop_array_binding_storage_is_addressable explicitly rejects ctx.closure_captures, and stable_packed_loop::match_candidate separately rejects a captured array_id. #8767 follows one forwarding edge after a candidate has been admitted; it does not make closure captures candidates and is not a duplicate of this issue.
Proposed direction
Represent an immutable closure-capture slot as addressable versioned-loop storage, or copy its tagged value into a shadow-bound compiler-private local at the loop preheader.
Reload the capture/root after any collecting operation; never retain a raw movable pointer across a safepoint.
Let the guarded outer indexed read nominate the resulting element for a second guarded packed-loop version, so const current = query[i] can feed the inner loop without reverting to per-index helpers.
Revalidate the relevant compact fingerprints before effects at the same points as the existing versioned loop and side-exit to the unchanged generic iteration on failure.
Record selection and rejection reasons for closure-capture and nested-derived candidates in --explain-lowering; the current silent absence of a loop record makes this gap harder to diagnose.
Semantic constraints
Preserve closure binding identity and rebinding semantics; mutable captures, boxed captures, aliases that can rebind, and unknown capture layouts must remain generic.
Preserve mutation and exception ordering. A callback/getter/proxy or loop-body effect that can invalidate the layout must revalidate or side-exit before the next indexed effect.
Preserve moving/forced-GC correctness: capture and derived-element roots must be rewritten by GC, and raw addresses must not survive safepoints.
Acceptance criteria
Register the reproduction as semantic and compiler-output coverage; Node and Perry print checksum 2000, including with PERRY_GC_FORCE_EVACUATE=1.
The stable fast arms for both query[i] and current[j] contain no js_packed_arraylike_index_get; explicit generic fallback/continuation arms remain present and are exercised.
--explain-lowering records that the closure-captured outer binding and nested derived array were selected, including guard/fallback identities.
Re-run the full upstream wolf-ecs/simple_iter case with a state/checksum oracle added to the harness. On the quiet M1 alternating protocol, require at least a 1.5x Perry median improvement and at least 9/11 paired wins; report helper samples, RSS, and executable-size delta.
Summary
Perry's version-stable packed-loop optimization does not admit immutable arrays held in closure-capture slots. That excludes the actual
wolf-ecs/simple_itershape even after #8755 and the forwarded-array follow-up #8767: each system closes over aQuery extends Array, loads anArchetype extends Arrayfrom it, then performs a nested indexed loop.Add guarded loop versioning for immutable closure-captured packed Arrays/Array subclasses, including the nested case where the inner array is obtained from a guarded outer indexed read. Keep the generic loop as the side exit whenever capture storage, identity, layout, version, or mutation proofs fail.
Self-contained reproduction
Node and Perry must both finish with
checksum: 2000. The public integration case isnoctjs/ecs-benchmarkat7b53a36606118e8b2a450a2ba4919939c86bbd2e,src/cases/wolf-ecs/simple_iter.js, withwolf-ecsfrom that lockfile.Build and retain evidence:
Current evidence
Measured on an Apple M1 Mac mini (8 GiB, macOS 26.5.1, Node 26.5.1), AC power, after a 60-sample CPU quiet gate, three discarded process warmups, and 11 alternating Node/Perry process pairs:
20a388974d7333ccaec9da9175fbcee54ad49902Perry is 158.10x slower. Every process completed successfully.
A symbolized one-second profile synchronized to the timed phase collected 759 main-thread samples.
js_packed_arraylike_index_getwas the exclusive leaf in 413/759 samples (54.4%). The three system closures accounted for another 81, 76, and 69 exclusive samples.The same full Wolf executable compiled from #8767 head
7e223e9dcaa87ad6415d85aec7ff201428e5f716measured 0.780296 ms/op in a confirmation run: effectively unchanged. Its retained LLVM module contains 21 calls tojs_packed_arraylike_index_getacross the three systems (seven indexed reads per system) and zerostable_packed.loop.fast/stable_packed.loop.slowblocks.This is expected from the current admission rule:
packed_loop_array_binding_storage_is_addressableexplicitly rejectsctx.closure_captures, andstable_packed_loop::match_candidateseparately rejects a capturedarray_id. #8767 follows one forwarding edge after a candidate has been admitted; it does not make closure captures candidates and is not a duplicate of this issue.Proposed direction
const current = query[i]can feed the inner loop without reverting to per-index helpers.--explain-lowering; the current silent absence of a loop record makes this gap harder to diagnose.Semantic constraints
Acceptance criteria
2000, including withPERRY_GC_FORCE_EVACUATE=1.query[i]andcurrent[j]contain nojs_packed_arraylike_index_get; explicit generic fallback/continuation arms remain present and are exercised.--explain-loweringrecords that the closure-captured outer binding and nested derived array were selected, including guard/fallback identities.wolf-ecs/simple_itercase with a state/checksum oracle added to the harness. On the quiet M1 alternating protocol, require at least a 1.5x Perry median improvement and at least 9/11 paired wins; report helper samples, RSS, and executable-size delta.Related work
Uint32ArrayRMW, which should become the inner value-update path once the packed index reads stop falling back.