ReviewOS

zig-utils/zig-js

Interpreter: recycle uncaptured block environments

#634
Closed chris opened this 9 days ago ·
9 days ago

Problem

Fresh Linux x86-64 validation of #628 and #633 reaches past both prior allocation fixes, then exhausts the unchanged 4 MiB recovery witness while duplicating the binding name for the loop body’s const o: unit shard 0.

The tree walker allocates a new GC Environment, binding map, and duplicated names every time a lexical block is entered. Fresh lexical-environment identity is observable only when a closure captures that environment. For an uncaptured repeated block, retaining each dead side store until the next successful collection needlessly consumes the fail-closed realm budget; on Linux the next tiny binding-name allocation is the first request after no recoverable slack remains.

Required implementation

  • Recycle an exited block environment only when it was not captured and has no pending disposable state.
  • Fully reset and reclaim its binding tables, duplicated names, side tables, values, flags, and parent before reuse; do not retain prior lexical state.
  • Keep the reusable environment per interpreter, root it precisely while cached, and relocate its pointer at moving GC checkpoints.
  • Preserve fresh environments for captured closures, nested captures, function-body scopes, using/abrupt disposal, TDZ, Annex B block functions, and no-GIL ownership.
  • Do not raise the heap cap, bypass accounting, force a collection on every block, or use a process-global/AST-mutating cache.

Acceptance

  • The unchanged Context heap_limit_bytes never publishes a torn object under recovery pressure witness passes on exact Zig dev.1818 Linux and under TSan.
  • Focused tests prove an uncaptured repeated lexical block reuses bounded storage while closures from distinct entries retain distinct bindings.
  • Existing block-scope, TDZ, Annex B, disposal, interpreter/VM identity, and moving-GC witnesses pass.
  • Matching full unit shards report zero failures/leaks attributable to this change.

Parent: #628 Roadmap parent: #480 Related: #633, #100

9 days ago

The matching TSan unit shard 0 reproduces the same unchanged witness and same Environment.put / dupeBindingName boundary after both parent fixes, with 400 passed, 32 skipped, 1 failed, and 0 leaked. This gives #634 an ordinary + sanitizer Linux baseline; the partially published map-entry ordering revealed by that stack is separately tracked in #636.

9 days ago

Implemented by 12078f3e.

Evidence:

  • 2,000 repeated lexical block entries stay below 64 total Environment allocations instead of requiring at least 2,000 (>31x bounded reduction).
  • Captured closure entries remain distinct (0,1,2).
  • Exact Zig dev.1818 unit shard 0: 435/435 passed, 0 leaked.
  • Focused normal and TSan reuse, torn-object recovery, moving-root relocation, and mid-loop nursery probes pass; the heavy TSan nursery witness completed in 142.8 s.
  • Test262 test/language/statements/block: 21 -> 21 passing, 0 failures.
  • Test262 test/language/statements/for: 385 -> 385 passing, 0 failures.

The cache handoff remains continuously rooted, relocation rewrites the cached cell, and reuse installs the exact managed parent barrier under the Environment lock. Keeping this issue open for fresh Linux unit-shard and TSan CI confirmation.

9 days ago

Fresh Linux CI exposed a deterministic witness calibration consequence of this optimization. Both the prior run and the current run report zero cooperative attempts in parallel_js: cooperative shared nursery rendezvous bounds object churn. The test keeps a 512 KiB production tranche but creates only 2 × (64 + 1,200) Object-class cells; after #634 removes per-iteration Environment cells, the object workload itself is below that byte boundary. The follow-up will retain the 512 KiB policy and increase object churn beyond it, preserving every collection/park/timeout assertion.

9 days ago

Fresh TSan CI also identified one synchronization omission in the reuse handoff: Environment.resetForBlockReuse clears with_object under the binding lock while gc.traceEnv read that edge immediately after releasing the same lock (unit shard 2, parallel-js). The follow-up moves the trace read into the existing Environment critical section; it does not widen a context/global lock. The same change recalibrates the bounded mid-script witness to the collector's byte accounting and keeps the 512 KiB cooperative production tranche while making the Object-only churn exceed it.

9 days ago

Linux/TSan follow-up landed in fc203490.

The Environment marker now reads with_object inside the same per-Environment binding critical section used by resetForBlockReuse; no context-wide lock was added. The cooperative witness retains the production 512 KiB tranche and generates enough Object cells to cross it independently of Environment churn. The bounded mid-script assertion now uses the collector's byte currency, and the recovery probe predeclares its loop counter so its body catch measures transactional Object publication.

Exact dev.1818 focused evidence:

  • normal: 4/4 passed, 0 leaked;
  • TSan: 4/4 passed, 0 leaked, 0 sanitizer reports;
  • the exact native-callback reproducer that emitted four Linux races now passes under TSan;
  • Test262 flips: 0 (trace synchronization and witness calibration only).

Keeping #634 open for the fresh pushed Linux shards.

9 days ago

Closing with fresh Linux evidence from run 32388315489: the exact uncaptured block-environment witness passed in unit shard 1 and TSan unit shard 1, both with zero leaks. Exact interpreter-root relocation also passed normally and under TSan. The unrelated failures in that superseded run are tracked separately.

Sign in to comment on this issue.