JSON.stringify: give the DynamicBuffer fast path a depth limit so cyclic values fail fast
Problem
JSON.stringifyon a cyclic value is pathologically slow in Bun (oven-sh/bun#40974). Flat: ~3 ms per call, about 640x Node. With an indent: ~1.5 s and ~2 GB of RSS per call, about 100,000x Node. Acyclic values are unaffected.FastStringifierhas no recursion or cycle check. The design counts on the buffer limit to catch cycles. InDynamicBuffermode that limit is the 2 GB string length cap, so a cyclic value with a gap doubles the buffer up to 2 GB before the attempt fails. Only then does the generalStringifierrun and throw theTypeError.
Fix
-
Values nested deeper than 512 now fail the
DynamicBufferfast path with a newDepthLimitfailure reason. The generalStringifierhandles them: it detects cycles at once and serializes any depth iteratively. -
m_depthis now also tracked in gap-lessDynamicBuffermode (it was gap-only).StaticBuffermode is unchanged, its 8 KB buffer already bounds the waste. -
jsc shell, Linux x64 release, min of 9, 8 keys of 50 bytes plus a self reference:
case before after cyclic flat 3.15 ms/op 0.13 ms/op cyclic indent 2 1464 ms/op 1.5 ms/op acyclic 8 keys 0.0027 ms/op 0.0027 ms/op 200k-object array, flat 25.8 ms 26.8 ms 200k-object array, indent 2 24.5 ms 23.8 ms -
JSTests/stress/json-stringify-cyclic-depth-limit.jscovers the new bail path: cycles throwTypeErrorflat and with gaps, through objects and arrays, direct and indirect, and 1000-deep acyclic values round-trip through the general path with 8-bit and 16-bit content. Note: fork CI does not run JSTests. The file runs underrun-jsc-stress-testslocally, and the Bun PR runs it in CI as a jsc-stress fixture.
Notes
- Known residual: a cycle that carries several MB of payload per revolution still reaches the 2 GB cap within 512 levels (a 3 MB payload cycle takes ~1.5 s). I measured exact cycle detection to fix this: a visit stack (WTF::Vector, then a flat array, then stack-frame-linked chain nodes gated behind a depth threshold). Every design cost 7-53% on large acyclic documents (for example 24.5 ms to 37 ms on a 19 MB indented stringify, on both GCC and Clang), because any per-container work in
append()disturbs the hot path. The depth limit is free, and it fully fixes the reported shapes, which carry bytes per revolution in the hundreds, not millions. - Upstream WebKit
mainhas the identicalDynamicBufferpathology. This patch applies there as is.