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
{{ message }}
Repository navigation
gc: the JSON tape's large per-parse block interacts badly with the generational collector (3.2x RSS, 25x variance vs mark-sweep) #7539
Split out of #7478's decomposition (see #7537, merged). This is now the dominant term in field_access, and it is not the tape's intrinsic cost.
The measurement
The public baseline's idiomatic row flips two switches at once (PERRY_JSON_TAPE=0andPERRY_GEN_GC=0), so "tape on, mark-sweep" had never been measured. Decomposing all four combinations on the pinned quiet host (M1 mini, 11 runs, taskpolicy -t 0 -l 0) at origin/main @ fc5e56e:
variant
tape + genGC
tape + markSweep
parse+scan
2729 ms · σ 214.9 · 208 MB
2957 ms · σ 8.8 · 66 MB
field_access
2942 ms · σ 101.8 · 222 MB
3247 ms · σ 7.7 · 67 MB
Identical tape, different collector. σ collapses from 214.9 to 8.8 and RSS from 208 MB to 66 MB — a 3.2× memory difference and a 25× variance difference, with the tape doing exactly the same work.
Neither factor alone explains it: tape-off under gen-GC is σ 17.6. It is specifically the interaction.
Mechanism
Each parse allocates header + tape as one ~2.4 MB block (200,002 TapeEntrys for this fixture). That rounds up to a dedicated oversized arena block and is retained; the resulting old-gen growth fires full collections at data-dependent points, which is both the variance and the retained footprint.
#7537 took field_access from 2981 ms to 2016 ms by flipping scans to the batch parser early, and parse+scan from 2729 to 1339. With the element-wise materializer no longer the bottleneck, this interaction is what is left: full - scan is 707 ms under gen-GC but only 309 ms under mark-sweep for the identical tree.
The strongest single data point: tape + markSweep field_access after #7537 is 1759 ms at σ 2.1 and 76 MB — within 282 ms of the idiomatic floor and completely stable. The same build under gen-GC is 2016 ms at σ 219 and 193 MB. The tape is not the problem any more; the collector's handling of its large per-parse block is.
Suggested direction
Treat a large, immediately-tenured, pointer-free block as a special case rather than as ordinary old-gen growth — it never needs scanning (a tape is TapeEntrys, not JSValues) and never needs copying. Options worth measuring: allocate it outside the arena entirely; mark the block pointer-free so full collections skip it; or exempt oversized single-allocation blocks from the trigger arithmetic that is currently firing fulls.
Acceptance: field_access under the default (gen-GC) configuration within noise of the tape + markSweep number — i.e. ~1760 ms at low σ — without regressing roundtrip (201 ms, the memcpy path).
Activity
perf(gc): move the JSON tape out of the old generation (#7539) (#7553)
test(gc): tape-materialisation rooting tests updated to the born-old …
test(gc): tape-materialisation rooting tests updated to the born-old …