ReviewOS

zig-utils/zig-js

Memory: bound heap, cache, and executable-code footprint

#480
Open chris opened this 9 days ago ·
9 days ago

Parent: #459 Depends on: #461, #476, #477, #479

Goal

Treat memory footprint, fragmentation, executable-code lifetime, and cache retention as first-class performance gates alongside throughput.

Scope

  • Add retained/peak RSS and owned-byte accounting by GC generation/slab/cell kind, strings/backing, arrays/buffers, shapes/ICs, parser/AST/bytecode, JIT artifacts/metadata/maps, modules, inspector, and caches.
  • Define bounded policies for code cache, retired epochs, compilation metadata, atoms/strings, regex plans, formatter caches, realm templates, allocator slabs, and thread-local reserves.
  • Trigger safe GC/compaction/code eviction under memory pressure without thrash, invalid entry, latency cliffs, or repeated compilation storms.
  • Improve object/cell/layout density only with ABI and exact allocation-class evidence; preserve stable IDs and relocation.
  • Add fragmentation and long-lived mixed-graph benchmarks, not just explicit compaction's retained tail.

No-workaround rules

  • No throughput win purchased with unbounded retention, process-global leak, disabled reclamation, or opaque allocator cache.
  • Do not delete reproducible build caches as a runtime memory “fix”; .zig-cache/zig-out are out of process scope.

Acceptance

  • Machine-readable memory inventory reconciles owned allocations/bytes at checkpoints and teardown.
  • Long-running workloads reach a bounded plateau; forced pressure safely reclaims GC and code/cache memory.
  • Compaction retains its 90.8% fragmented-backing reduction and exact checksum/fixed-point contract.
  • Latency/throughput/RSS evidence is published together for every memory-policy change.
  • Leak, fault/OOM, moving GC, no-GIL, TSan, ABI, and code-retirement gates pass.

Foundation

#144/#433; explicit compaction report; GC object finalizer optimization 14d94ec0.

9 days ago

Heap-plateau work advanced through the Promise benchmark blocker #512 in four main commits, culminating in perf(gc): reclaim quiescent promotion debt (#512).

The memory-policy defect was general: a full collection during a large live burst raised the old-generation threshold from a transient live set. After the burst became unreachable, promoted garbage remained below that inflated threshold across later host checkpoints. Shared GcState now records the promoted total and live baseline from the last quiescent full sweep. A host boundary requests a heap-level full sweep when promotion debt reaches that trusted live baseline, while explicit thresholds, forced collection, moving-GC barriers, and sibling-realm coordination remain intact.

Focused coverage promotes a protected linked graph, drops its sole root, sets the ordinary old threshold to its maximum, and proves the quiescent debt policy reclaims the graph. Full units pass 1,537 with one expected skip and no failures/leaks.

On the exact eight-lane bounded async workload, the final slice changes:

  • median elapsed: 1.540351500 s → 0.980488667 s (36.3% faster)
  • maximum RSS: 2,567,405,568 → 2,005,286,912 bytes (21.9% lower)
  • peak physical footprint: 2,555,972,560 → 1,333,791,264 bytes (47.8% lower)
  • checksum: 91956096 in all samples before and after

The full 60,000-job × 7-sample × 8-lane row now completes in 217.76 s process real time with median measured workload 26.409281125 s, maximum RSS 8,001,585,152 bytes, peak physical footprint 18,089,916,888 bytes, and frozen checksum 920578560 in all seven samples. The preceding implementation remained GC-bound after 1,248.43 s with a 19.2 GB footprint and produced no complete row.

This is one concrete bounded-plateau/reclamation slice for #480, not closure: per-owner memory inventory, forced OS-pressure policy, cache/code eviction, fragmentation panels, OOM/no-GIL/TSan breadth, and combined publication evidence remain open.

9 days ago

Collection side-storage ownership advanced in c797973a: collision chains contain positions only (no managed pointers), survive moving GC without relocation, deinitialize with the existing collection backing, retain capacity only proportionally to historical ordered entries/tombstones, and fall back correctly after allocation failure. #480 remains open for the broader heap/cache/code footprint and plateau acceptance work.

9 days ago

#514 is complete on main and advances the bounded-memory scope: repeated Map/Set delete/reinsert churn now compacts at a deterministic threshold, clear and empty compaction release backing capacity, and teardown/capacity witnesses cover the ordered/index/cursor side structures. Exact-parent raw/report evidence is in 363a643; peak RSS and deterministic retained-capacity evidence are recorded without making a cache claim.

9 days ago

#515 is complete on main in e7d430f. Hostile JSON nesting is now bounded by the engine stack contract across parse, raw JSON, and reviver traversal; partial parse results never publish, and non-syntax resource failures are no longer masked as SyntaxError. This prevents process-stack exhaustion under untrusted input and adds normal/ReleaseSafe/TSan subprocess coverage.

9 days ago

The #517 parse-record redesign also reduces median peak RSS on the 8,192-element callback-observable JSON row from 46.3 MB to 37.2 MB while removing the quadratic scan. See the seven-pair raw/report evidence; identities/checksums matched and no samples were discarded.

9 days ago

Negative memory evidence from #518: moving escaped decode buffers from parser arena growth to managed string backing increased median peak RSS from 29.5 MB to 30.8 MB on the dedicated row. The attempt was fully reverted by 5a86e04; raw schema-v1 evidence is preserved in 500e67a so this allocator tradeoff is not repeated without a different builder design.

9 days ago

#519 found a concrete temporary-allocation lifetime defect during exact-parent measurement: the new replacer membership table was initially arena-retained across persistent-context warmups. https://github.com/zig-utils/zig-js/commit/2e48c838 moves only its buckets to the active realm allocator and deinitializes them after each stringify invocation, while ordered keys remain arena-owned. Median peak RSS fell from 45,465,600 to 34,258,944 bytes at 0.999x wall time. Evidence: https://github.com/zig-utils/zig-js/commit/11833827.

9 days ago

Exact-parent measurement for #520 caught parser hash buckets retained by the persistent Context arena. https://github.com/zig-utils/zig-js/commit/e11bec9c separates AST/token ownership from non-owning parser scratch and deinitializes the index through Context backing. The correction lowers median peak RSS from 52,150,272 to 48,758,784 bytes at 0.9968x instructions versus the indexed parent, leaving final memory near the original 48,398,336-byte median. Evidence: https://github.com/zig-utils/zig-js/commit/39c617e4.

9 days ago

Opened #521 for the next measured lifetime boundary: own-key enumeration and Proxy invariant checks allocate non-owning membership buckets from the realm arena on every call. The focused issue requires ordinary/array/proxy plateau witnesses, Context-owned freeable scratch, abrupt-path cleanup, no-GIL/TSan coverage, and exact ordering/invariant preservation.

9 days ago

Child #521 is complete and closed. Own-key membership indexes now use freeable Context scratch across reflection/enumeration paths, with exact ordering and Proxy invariants preserved. The 4096-key exact-parent row improved 58.745 -> 0.670 ms and 600.5M -> 11.2M instructions; the live retained-RSS witness improved 31.41 -> 27.36 MB after ten warmups. Full details and commit/evidence links: /zig-utils/zig-js/issues/521#issuecomment-5245487200. The distinct deletion-rebuild cost remains #522.

9 days ago

Memory ownership update from completed #522: bounded named-deletion compaction and stable slot reuse reduced the 4K live retained-RSS row from 45,137,920 to 27,197,440 bytes for delete and from 45,154,304 to 27,262,976 bytes for delete/re-add. Retained backing growth fell 55.1–55.5%; the checked-in attribution intentionally makes no allocation-call-count claim because call counts were similar. Evidence and implementation links: /zig-utils/zig-js/issues/522#issuecomment-5246336679.

9 days ago

Linked memory child #523: dead-key pruning currently retains WeakMap/WeakSet historical list and index capacity even when the live set collapses to zero. The child requires bounded post-GC backing with failure-safe shrink behavior and retained-memory evidence.

9 days ago

Closed #523 with bounded weak-collection backing and failure-safe post-prune shrink; see 6989099b.

The 4K WeakMap + 4K WeakSet fixture adds 86,312 bytes of bounded backing, no allocation calls, and no GC cells relative to the exact parent. Post-move retained and peak RSS are both 1.009×. Pruning shrinks a 512-entry high-water mark to bounded survivor capacity and releases empty list/index backing to capacity 0; an injected shrink OOM preserves the exact old index for retry. Evidence: 39eeb362.

9 days ago

Closed #524 with bounded Date setter coercion storage. Exact-parent attribution removes 9,273,552 bytes of warm high-water backing and 70 growth events; observed 4K retained RSS is 0.9468x versus 1.0043x for the identical setTime control. Evidence: 0e2ed155.

9 days ago

#526 is complete on main. Date formatting no longer leaves proportional year/date/time/ISO intermediates in Context arena high-water storage: exact 4K attribution drops invocation retained backing by 158,851,286 bytes (97.5%) and growth events from 215 to zero, with identical managed-cell counts/bytes. Stable ordinary peak RSS is 0.9670x; the separate retained-RSS observer was noisy and is published without a directional claim. Evidence: c7705224.

9 days ago

#528 closes a concrete Intl retention path. On the scored 4K locale-list invocation, newly retained Context backing falls from 60,710,606 to 4,191,682 bytes (0.069x), peak increase from 59,429,908 to 2,449,643 bytes, while the bounded 4 MiB heap regression passes. Raw allocation chain: exact-parent-locale-list-allocation-2026-08-10.json.

9 days ago

Closed #529 with exact-parent memory evidence. Generic Array traversal now keeps decimal index keys in bounded invocation-local storage: the 100 x 1K attribution delta reduces requested backing bytes 59,423,930 -> 38,534,342, growth events 215 -> 28, and retained backing increase 35,070,438 -> 14,164,680. Full units, bounded heap, moving GC, no-GIL TSan, and 2,810 Array-prototype test262 cases pass. Evidence. The independent deep-Shape complexity issue remains #530.

9 days ago

#530 now has explicit retained-memory evidence in 21a33f13: the 4,096-property persistent Shape index raises median post-invocation RSS from 38,322,176 to 40,501,248 bytes (+2,179,072; 1.057x). The construction and traversal phase deltas are separately recorded, and a real moving-compaction test stays within a 32 MiB JavaScript heap limit.

This is a bounded, Context-arena-owned tradeoff rather than hidden retention; #480 should include the +2.18 MiB 4K fixture cost in its broader footprint accounting.

9 days ago

New child #559 links memory bounds to shared allocation scaling. Its acceptance requires bounded/reclaimable mutator-local capacity, exact OOM/teardown/compaction behavior, and RSS evidence before any cell-slab sharding can land; unbounded per-thread reserves are explicitly prohibited.

9 days ago

Completed bounded-lifetime child #562: 034e431c removes retained no-op UTF-8 conversion buffers and bounds actual WTF-8 transformation to one input-sized arena allocation. Zero-capacity OOM and exact surrogate output are covered; full units have zero leaks.

9 days ago

New child #656 tracks a post-sweep retention gap found while reviewing #655: a concurrent mark completed by the evaluation-exit defer can miss Context-level Wasm reclamation, cell-tail trimming, and compaction scheduling because the next quiescent boundary sees no active marker and returns below threshold. The child requires exact completion identity and once-only maintenance; it explicitly forbids a redundant forced full collection.

9 days ago

Bounded publication chain #636, #637, #645, #655, and #657 is complete. Fresh Linux normal and TSan shard 3 both pass the exact 4 MiB pressure/recovery oracle with zero leaks. The final policy preserves healthy warm slabs, adapts the last refill, releases fully empty tails on real allocation failure, reserves host completion roots before attacker code, and scores reclamation only after a genuinely quiescent host boundary.

Follow-up #656 is implemented in cd217904: concurrent full sweeps now hand once-only Wasm/slab/compaction maintenance to the first safe boundary instead of retaining it until an unrelated collection. Its fresh Linux matrix is in progress.

9 days ago

Concurrent post-sweep retention child #656 is complete in cd217904. Fresh Linux normal/TSan unit witnesses, normal/TSan mid-GC fuzz, and normal/TSan lifecycle fuzz cover exact once-only maintenance; normal mid-GC scored 900 programs with 0 failures. Completed sweeps now reclaim Wasm garbage and eligible empty slab tails and schedule compaction at the first safe boundary without forcing another full collection. The only TSan unit-shard reports were the separate user-owned #643 Map iterator race.

9 days ago

Related retention evidence child #661 is implemented in b815b3e7. Cold Context samples now record baseline/live/peak/post-destroy RSS in one Mach accounting domain plus 25/50/75/100% soak checkpoints and every GC finalizer field. Eight-or-more-iteration collection rejects material monotonic growth and final growth above the frozen 8 MiB/5% bound. The local 16-context diagnostic reached an identical 75%/100% checkpoint, but no public memory-policy claim is made without dated quiet-host evidence.

9 days ago

Related child #666 removes an unconditional StringCell allocation from every successful String.prototype.toString/valueOf call by returning the existing StringData Value, as the specification requires. It also prevents flat-Latin-1 physical bytes from entering the canonical WTF-8 constructor.

9 days ago

Related memory win #666 completed via 4c922933 and fa4d29cd: valid String.prototype.toString/valueOf calls return the existing StringData Value and allocate zero StringCells/backing bytes. A fail-first allocator recorded zero allocations across 30 identity assertions; Test262 14/14 and full units 1,756 passed, 1 skipped, 0 failed/leaked.

9 days ago

Related child #667 will remove the need for any relational-comparison transcode by streaming directly over representation-aware UTF-16 units. It carries a zero-allocation contract and explicit physical flat-Latin-1 validation.

9 days ago

Related zero-allocation child #667 is complete via 75e56136. Relational String comparison consumes physical storage directly through a representation-aware iterator: no eager canonicalization, backing copy, or comparator scratch. Canonical and flat-layout witnesses plus the 1,759-test full inventory are clean.

9 days ago

Related child #668 keeps dynamic-source conversion transient and demand-driven: canonical/ASCII source remains borrowed, while physical flat Latin-1 receives one bounded parser-ingress conversion with no retained duplicate backing.

9 days ago

Related transient-conversion child #668 is complete via 355329dc. Dynamic source borrows canonical/ASCII storage and materializes a bounded WTF-8 view only for physical flat Latin-1; no permanent duplicate backing is retained.

9 days ago

Opened related child #669 to keep custom Object.prototype.toString output at one backing-buffer allocation. Flat Latin-1 will encode directly into the final canonical buffer rather than retaining or allocating conversion scratch.

9 days ago

Child #669 is complete in 170aa4a8: custom Object.prototype.toString tags now compose into one exact final representation-aware StringCell backing, including managed-GC accounting and flat-Latin-1 canonical hashing. Test262 is 41/41 with zero flips; full units are 1,764 pass + 1 skip with zero failures/leaks; focused managed-GC TSan is clean.

9 days ago

Related allocation cleanup landed through #671 in da430db1: structured clone no longer needs an arena transcode before the final wire copy for physical flat-Latin-1 String fields. The direct encoder is covered by exact one-allocation and OOM-atomicity tests.

9 days ago

String-footprint progress from #777 landed in 7544c504: production non-ASCII Latin-1 StringCells now use one byte per UTF-16 unit. The checked café fixture reduces owned backing from 5 canonical WTF-8 bytes to 4 physical bytes while exact canonical egress and 3,291 relevant Test262 files remain unchanged. This is a workload-local owned-byte result, not a process RSS or throughput claim.

9 days ago

Allocation progress from #778 landed in 7071d650: JSON.stringify now expands flat Latin-1 String values directly into its authoritative output, and a pre-capacitated fail-first witness proves zero intermediate canonical transcode allocations. Exact-parent JSON semantics remain 165/165. This is exact allocation evidence, not RSS or throughput evidence.

9 days ago

Footprint child #779 landed in a0e184e9: called String now retains existing StringData with zero allocations, while construction allocates only the required wrapper state in the checked flat Latin-1 path. Exact-parent String Test262 remains 1,223/1,223. #779 awaits a clean-host full unit gate.

9 days ago

Footprint child #780 landed in 0c464096: plain String methods now allocate zero duplicate protocol receivers in the checked path, while protocol methods reuse existing StringData. Exact-parent String Test262 remains 1,223/1,223. #780 awaits the shared clean-host full unit gate.

9 days ago

Follow-up a85154ed extends #780: charAt/charCodeAt/at/codePointAt now use physical StringCell access without a whole flat Latin-1 transcode, while preserving original protocol receiver semantics. Exact-parent String Test262 remains 1,223/1,223.

9 days ago

Footprint child #781 landed in a42cf61e: repeat retains count-one StringData, sizes once before allocation, avoids flat Latin-1 source snapshots, and fills ordinary results with logarithmic doubling copies. Exact-parent String Test262 remains 1,223/1,223.

9 days ago

Flat Latin-1 follow-up #782 landed on main in 872dbbb5. Binary + and common String.prototype.concat now stream flat bytes directly into one canonical result instead of allocating whole-input WTF-8 snapshots and a concatenation scratch buffer. Exact-parent concat/addition inventories are unchanged and threadfuzz passes 400/0.

9 days ago

#853 landed in 0a5d1f70: the keyed sparse-hole seed lives in the existing cold allocation, so hot Object size and the 256-byte cold GC allocation class remain unchanged. Full backing-finalization and bounded-lifecycle units pass with 0 leaks.

9 days ago

Related hardening landed: #873 securely seeds the exact-thread-ID ThreadLocal value index, preserves lifecycle/GC semantics, and is implemented by 3cf4429e.

9 days ago

Opened child #974 and landed its first machine-readable ownership slice in 77b38325. The versioned snapshot reconciles Context backing, allocator controls/reserve, and owned native mappings without double-counting shared heap/code or logical GC subsets. It deliberately reports precise collector auxiliary scratch as untracked, so #974 and #480 remain open until that domain and teardown reconciliation are exact.

9 days ago

Memory inventory progress: dcfbc7ff publishes #974 schema-v3 owned-memory checkpoints in benchmark attribution JSON, freezes matrix V40/sidecar schema 14, and adds fail-closed reconciliation validation. These records remain diagnostic and do not alter scored performance or allocation-replay contracts.

9 days ago

#974 is complete through dcfbc7ff. Owned Context backing, collector auxiliary scratch, controls/recovery reserve, native live/retired mappings, shared owner/observer roles, generation partitions, budgets, and teardown-zero proofs are now versioned and emitted in benchmark attribution JSON. Final gates: full units 2,483 pass/1 skip; TSan no-GIL 74/74; focused JIT/retirement 430 pass/1 platform skip; relocation inventory, 117/117 public C API audit plus hosts, matrix/sidecar validators, and docs build all passed.

9 days ago

#975 has its first implementation commit in b635141. The public Zig host API now coordinates exact precise-heap compaction and native-code retirement under the existing conductor, with explicit shared/unsafe/OOM statuses and active-reader-safe epoch retirement. Focused normal and TSan tests pass with no leaks, and the docs manifest is current.

I am continuing #975 with unscored, machine-readable representative-benchmark diagnostics before running the broad closing gates.

9 days ago

#975 is complete and closed in b635141, 7a887d3, and ee2c184.

The Context now has an exact coordinated pressure checkpoint across precise heap/slab and native-code ownership, including shared observer boundaries, active-reader epoch retirement, fail-closed movement/OOM, zeroed teardown, and a non-scored V41 representative diagnostic. Full unit, no-GIL TSan, focused JIT/relocation/ABI, complete quick benchmark sidecar, and docs gates pass.

9 days ago

Focused child #990 now tracks the measured RegExp compiled-program and matcher-scratch retention gap after #989. Exact-main evidence at 32,768 unique patterns: 51.0 MB string control, 316.3 MB eager construction, 557.3 MB after one match each. Implementation and raw before/after evidence will land there.

9 days ago

#990 has landed on main through 8de5d5f9. The RegExp cache now owns and bounds compiled graphs, persistent matcher scratch, and resettable match results instead of leaving evicted programs in the Context arena.

At 32,768 distinct patterns, exact-parent → candidate peak RSS is 301.64 → 73.34 MiB for construction and 531.52 → 108.28 MiB for construction plus first match; the source-only control and all checksums are unchanged. All 2,456 affected test262 cases and two real-collector no-GIL TSan witnesses pass.

Evidence: #990 · report · raw.

9 days ago

Child #1027 has landed the temporary-allocation ownership slice relevant to this memory umbrella.

GC auxiliary, native JIT analysis/lowering, and Wasm decode/validation scratch now share a process-wide fail-closed byte contract with schema-v8 current/peak/requested/rejected/released counters by domain and a reconciled total. Published executable mappings/metadata, decoded Wasm modules, and precise-heap cells remain in their existing long-lived ownership domains.

The exact-parent report and raw samples record 1- and 20-lane RSS/CPU/wall/scratch evidence with exact checksums. Candidate native scratch requested/released medians reconcile at 24.30 MiB (1 lane) and 486.03 MiB (20 lanes), current bytes return to zero after teardown, and no policy rejection or allocator failure occurred under the default unlimited policy.

The final full unit gate is 2,567 pass, 1 skip, 0 fail, 0 leak. This advances, but does not close, #480: retained-heap/cache/code inventory, pressure reclamation, plateau, and fragmentation work remain.

Sign in to comment on this issue.