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
json_polyglot/field_access (parse → touch every element → stringify) is the lazy tape's worst case: 2981 ms vs bun's 223, while the same tape wins roundtrip outright (194 vs bun 221, node 384). This issue records tonight's decomposition and the two dead ends, so the next attempt starts from measurements instead of the plausible-but-wrong model I started with.
Decomposition (50 iterations, quiet host, same binary)
phase
tape on
tape off
parse only
210 ms
1220 ms
parse + full scan
3030 ms
1287 ms
parse + stringify
254 ms
1756 ms
Tape wins parse 6× (deferred materialization) and stringify 7× (unmutated-tape memcpy). The full scan is the entire loss: ~56 ms/iter for scan+materialize vs ~24 ms/iter for the direct parser's tree build. Both build the same 10k-record tree — the tape's per-element materialization path is ~2.3× the batch parser.
Two approaches tried, measured, and rejected
1. Earlier batch-flip triggers. Added distinct-element coverage (trip at half) and sequential-streak (trip at 16 consecutive) triggers alongside the existing walk-steps heuristic — the walk-steps trigger provably cannot fire on sequential access (1 step/element accumulates n, threshold is 2n). Both triggers verified firing (lldb breakpoint: 1 materialize per iteration). Result: time noise-equal (min 3050 old vs 3322 new), RSS regressed 180 → 213 MB — the full tree materializes while blob+tape+cache are still live, and total materialization work is unchanged because every element gets materialized either way. Reverted.
2. Re-parse the retained blob with the DirectParser inside force_materialize_lazy (lazy is top-level-array-only, so the blob is exactly the array's source; patch the few handed-out cached elements back over the fresh slots for identity). This is the structurally right fix — batch parse at 24 ms/iter instead of 56 — but it is blocked by #7477: the DirectParser's float parsing diverges from the tape's (and from node), so the prototype produced checksum 2552986400 where node says 2552985550. It also had an intermittent SIGSEGV in my rooting; both are why it was reverted rather than debugged into shape at 4 a.m.
Re-land the reparse-on-materialize with the identity patch loop (the prototype's shape, with the rooting done carefully: root the parsed result before re-reading the header, per-element handle across store_array_slot).
Keep a trip heuristic only as the decision of when to batch — the streak/coverage work is sound for that, it just cannot pay while the batch materializer is the same speed as the element-wise one.
Expected: field_access ≈ 210 + 50×(24 + ε) + memcpy-stringify ≈ ~1500 ms (from 2981), while roundtrip keeps its 194 ms win — the tape then wins or ties every JSON benchmark against the direct parser, and the remaining gap to bun (~223 ms) becomes a parser-speed workstream (shared with json_parse_1mb, currently 6.27× vs bun), not a tape-policy one.
One more datum
The per-element merge branch in force_materialize_lazy (fresh RuntimeHandleScope + safepoint per element, to honor a handful of cached slots) looked like the cost and is not: replacing it with root-batch + patch-cached-slots measured neutral. The copied-minor sabotage test (test_json_tape_force_materialize_sparse_cache_handles_survive_copied_minor_gc) hooks the ForceLazyArrayRooted safepoint — any rework of that function must keep firing it, or the instrument silently loses its subject.
json_polyglot/field_access(parse → touch every element → stringify) is the lazy tape's worst case: 2981 ms vs bun's 223, while the same tape wins roundtrip outright (194 vs bun 221, node 384). This issue records tonight's decomposition and the two dead ends, so the next attempt starts from measurements instead of the plausible-but-wrong model I started with.Decomposition (50 iterations, quiet host, same binary)
Tape wins parse 6× (deferred materialization) and stringify 7× (unmutated-tape memcpy). The full scan is the entire loss: ~56 ms/iter for scan+materialize vs ~24 ms/iter for the direct parser's tree build. Both build the same 10k-record tree — the tape's per-element materialization path is ~2.3× the batch parser.
Two approaches tried, measured, and rejected
1. Earlier batch-flip triggers. Added distinct-element coverage (trip at half) and sequential-streak (trip at 16 consecutive) triggers alongside the existing walk-steps heuristic — the walk-steps trigger provably cannot fire on sequential access (1 step/element accumulates
n, threshold is2n). Both triggers verified firing (lldb breakpoint: 1 materialize per iteration). Result: time noise-equal (min 3050 old vs 3322 new), RSS regressed 180 → 213 MB — the full tree materializes while blob+tape+cache are still live, and total materialization work is unchanged because every element gets materialized either way. Reverted.2. Re-parse the retained blob with the DirectParser inside
force_materialize_lazy(lazy is top-level-array-only, so the blob is exactly the array's source; patch the few handed-out cached elements back over the fresh slots for identity). This is the structurally right fix — batch parse at 24 ms/iter instead of 56 — but it is blocked by #7477: the DirectParser's float parsing diverges from the tape's (and from node), so the prototype produced checksum 2552986400 where node says 2552985550. It also had an intermittent SIGSEGV in my rooting; both are why it was reverted rather than debugged into shape at 4 a.m.The path that wins both
store_array_slot).Expected: field_access ≈ 210 + 50×(24 + ε) + memcpy-stringify ≈ ~1500 ms (from 2981), while roundtrip keeps its 194 ms win — the tape then wins or ties every JSON benchmark against the direct parser, and the remaining gap to bun (~223 ms) becomes a parser-speed workstream (shared with
json_parse_1mb, currently 6.27× vs bun), not a tape-policy one.One more datum
The per-element merge branch in
force_materialize_lazy(freshRuntimeHandleScope+ safepoint per element, to honor a handful of cached slots) looked like the cost and is not: replacing it with root-batch + patch-cached-slots measured neutral. The copied-minor sabotage test (test_json_tape_force_materialize_sparse_cache_handles_survive_copied_minor_gc) hooks theForceLazyArrayRootedsafepoint — any rework of that function must keep firing it, or the instrument silently loses its subject.