ReviewOS

zig-utils/zig-js

JSON.stringify: bound retained continuation-frame cache capacity

#1022
Closed glennmichael123 opened this 10 days ago ·
10 days ago

Parent: #480 Related: #473 Follows: #1019

Measured problem

Current main 9453de45 retains the largest completed JSON.stringify continuation buffer until Interpreter.deinit. A focused direct FrameStack witness used the production append/deinit path and observed:

  • 4,096 active frames: logical length returns to 0, capacity remains 5,900 frames, retaining 566,456 bytes including cache metadata.
  • 50,000 active frames: logical length returns to 0, capacity remains 67,247 frames, retaining 6,455,768 bytes including cache metadata.

Normal Context.evaluate teardown releases the cache, but a long-running evaluation or embedding-held interpreter keeps that high-water allocation after a one-off deep stringify.

Goal

Account and bound reusable JSON.stringify continuation storage without undoing repeated-deep traversal reuse or changing JavaScript semantics.

Scope

  • Add exact current/peak cache count, frame-capacity, and owned-byte gauges to the machine-readable Context memory inventory.
  • Use a deterministic working-set policy: a large frame allocation is retained only after consecutive invocations demonstrate comparable occupancy; one-off high-water demand is released at invocation unwind.
  • Preserve the reentrant lease boundary: nested stringify calls never alias an outer active stack.
  • Keep allocation failure, abrupt completion, moving GC, no-GIL, and teardown failure-atomic.
  • Version and fail-closed validate the representative memory-inventory schema.

Acceptance

  • A deterministic 4,096/50,000-frame witness records the current baseline and proves one-off backing is released.
  • Repeated deep calls reuse one retained buffer without repeated growth; a later shallow call releases oversized backing.
  • Memory inventory exposes exact current/peak cache bytes and frame capacity and reconciles at teardown.
  • Reentrant and exhaustive allocation-failure witnesses pass without stale frames, lease leaks, or allocator leaks.
  • Existing 50,000-level object/array limit witnesses throw catchable RangeError in tree and required-VM modes.
  • Moving-GC, no-GIL, and unsuppressed TSan coverage pass.
  • JSON test262 remains 165/165 and the forced tree/required-VM sweep remains exact.
  • Deterministic allocation/capacity evidence is published; no noisy wall-time claim is used for acceptance.

Guardrails

No reduced nesting, new semantic limit, cached final JSON, benchmark recognition, global visited set, arena-only shortcut, or rejection based on source/input identity.

10 days ago

First runtime slice landed directly on main in aa806be.

The continuation cache now uses a deterministic two-observation working-set policy: backing is retained only when two consecutive invocations each occupy at least half of the allocation. A one-off deep traversal releases its frame buffer at unwind, repeated comparable deep calls retain and reuse one allocation, and a later shallow call releases an oversized retained buffer. Reentrant stringify fallbacks remain isolated behind the existing lease.

Focused module verification:

  • exhaustive allocation-failure / lease-unwind witness: pass
  • retained-working-set and accounting witness: pass
  • 4,096-frame first observation releases; second confirms and retains; third performs no capacity growth
  • 50,000-frame one-off observation releases at unwind
  • focused builtins result: 3 passed, 0 failed

The public Context memory-inventory schema, representative validator, Test262, forced-tier, moving-GC/no-GIL/TSan, and published deterministic evidence remain in progress.

10 days ago

The public inventory/tooling slice landed directly on main in 48a3bf7.

  • Context MemoryInventorySnapshot is now schema 4.
  • Representative attribution is schema 16 under frozen matrix V45.
  • Exact current/peak cache count, frame capacity, bytes, growths, releases, and released-frame bytes are published as named overlaps within Context backing; the disjoint owned-byte subtotal is unchanged.
  • Context-created interpreters attach the opt-in concurrent cache gauge only when execution attribution is enabled.
  • The validator fails closed on missing fields, non-integer values, impossible peak/current relationships, and nonzero bytes with zero live caches.

Verification:

  • real zig-gc 0285ef8 Context memory inventory + teardown: 2/2 passed
  • representative matrix V45: 18 implemented families, 0 deferred
  • schema-16 attribution self-test: passed, including incoherent-cache rejection
  • docs link gate: 375 files, all links resolve

No wall-time claim is being made. JSON/Test262, forced-tier, moving-GC/no-GIL/TSan, and final deterministic evidence remain in progress.

10 days ago

Third slice is now on main as 3e31886 (test(json): cover retained frame cache boundaries). It adds forced tree-walker/required-VM 50,000-level object+array limit coverage, verifies cache release after callback-triggered moving collection, and asserts shared no-GIL inventory returns to zero. Against the collecting zig-gc revision 0285ef8: all three focused production-module cases pass, and the no-GIL case also passes with direct Zig -fsanitize-thread (2/2, zero TSan reports).

10 days ago

Completed directly on main in four logical commits: aa806be, 48a3bf7, 3e31886, and 2723311.\n\nFinal receipts:\n- deterministic raw counters: https://github.com/zig-utils/zig-js/blob/2723311/docs/.data/json-stringify-frame-cache-retention-2026-10-02.json\n- dated interpretation: https://github.com/zig-utils/zig-js/blob/2723311/docs/.data/json-stringify-frame-cache-retention-2026-10-02.md\n- one-off retention: 566,456 B -> 72 B at 4,096 frames; 6,455,768 B -> 72 B at 50,000 frames\n- confirmed repeated 4,096-frame working set: third invocation adds 0 capacity growths; shallow invocation releases it\n- teardown: 0 current caches, 0 frame capacity, 0 current bytes\n- focused allocation/reentrancy/accounting: 3/3 pass\n- 50k forced tree + required VM, moving GC, shared no-GIL, and unsuppressed TSan: pass\n- pinned JSON test262: 165 pass, 0 fail\n- forced tree/required-VM witness: 165 agree, 0 diverge\n- docs link gate: 376 files; BunPress strict build: 51 pages\n\nNo wall-time or throughput claim was used. The issue acceptance checklist is fully checked.

Sign in to comment on this issue.