The core JavaScript multithreading architecture for issue #1 is implemented:
isolated agents/workers, shared memory, structured clone, Workers, and
shared-realm Threads are all present, and shared-realm threads run
true-parallel by default. The remaining work is production hardening:
performance, documentation, stress breadth, and promotion of reference-only
tests as the matching engine features land.
- Agent and worker isolation: one
Contextper OS thread, values crossing by structured clone or retainedSharedArrayBufferstorage. - Worker inboxes/outboxes reject nonblockingly at configurable per-frame,
live-byte, and live-message caps. Defaults are 64 MiB, 256 MiB, and 1024;
successful
postMessagemeans exactly one queued frame, while rejection is synchronous and releases frame/SAB ownership exactly once. The per-frame cap includes framing and SAB manifests and is enforced during serialization, not only after allocation. - Structured clone has a shared 256-level nesting ceiling across serialization, wire preflight, and deserialization. Excessive graphs fail with a catchable clone error, and framed SAB manifests keep rejection cleanup non-recursive. Graph IDs, strings, counts, and host-size conversions are checked; impossible wire counts and noncanonical references/flags fail before large loops or allocations.
- Shared-realm
Thread: oneContext, one heap, one global object, real OS threads, same-realm identity, and no-GIL parallel execution by default. - Serialized shared-realm fallback:
Context.createWith(.{ .enable_threads = true, .gil = true }). - C embedder surface:
ZJSGlobalContextCreateThreaded(gil). - Typed-array
Atomics.wait/notify/waitAsyncover shared buffers. - Property-mode
Atomics.load/store/exchange/compareExchange/ RMW /wait/waitAsync/notify. Thread,Lock,Condition,ThreadLocal,ConcurrentAccessError,Atomics.Mutex, andAtomics.Condition.- GC-managed parallel contexts with thread-safe allocation, write barriers, root tracing for active VM frames, conservative stack scanning where sound, and abort-safe mid-script collection experiments.
- Optional per-context allocator pressure cap:
Context.createWith(.{ .heap_limit_bytes = n })applies a thread-safe budget to Context-owned outstanding allocator bytes. Arena-backed contexts fail closed witherror.OutOfMemory; GC-backed contexts can collect and retry at safe allocation-recovery points. Single-threaded and GIL-capable GC-backed contexts also retry reclaimedArrayBufferbyte-slab pressure. Live no-GIL peer recovery is proven for GC-cell slab failures and safepoint-ownedArrayBufferbyte-slab pressure where an active interpreter can drive the abort-safe parallel collector. Realm task queues, propertyAtomics.waitwaiter tables,Lock.asyncHoldqueues, condition waiter queues/tickets, ThreadLocal maps, thread API bookkeeping, active-interpreter and realm root registries, and other trace-sensitive side-store locks fail closed while held, because the tracer may need the same lock to find hidden JS roots. Mutable generator / iterator-helper side stores are a distinct fail-closed class today: the parallel tracer defers them to a world-stopped finish, so allocation-failure recovery aborts rather than claiming a sweep while deferred generator/iterator edges remain. Broader side-store no-GIL recovery remains tracked under #30 / #36.ctx.heapBudgetStats()reportslimit_bytes,used_bytes, lifetimepeak_bytes, andremaining_bytesfor capped contexts. - Test-shell helpers such as
print,setTimeout,drainMicrotasks,gc, and supported$vmcompatibility hooks for conformance coverage.
- Sharing ordinary JS values between isolated agents or workers without structured clone.
- Treating test-shell helpers or
$vmas an embedder event-loop/API surface. - Treating
Context.TestingOptions.parallel_js,Context.TestingOptions.parallel_midscript_gc, or other testing knobs as public API. - Assuming unsupported JSC
$vmhooks exist:sharedHeapTest, dictionary conversion, code deletion, disassembly, stop counters, and related JIT artifact controls are absent until backed by real engine behavior. - Treating JavaScript-level data races as engine-level synchronization. For example, racing accesses to shared buffer program bytes are JS program races; the engine currently keeps those paths TSan-clean with locks/atomics instead of suppressions. See Memory Model.
- Treating deep recursive call behavior as a finished VM-stack architecture.
VM and tree-walker calls both throw catchable
RangeErrors before native stack overflow, and the promoted PR-249 stack-overflow witness is green, but calls still consume native stack per call until a future iterative or trampolined call path exists. - Treating remaining WebAssembly/JIT-specific PR-249 files as implemented when the required WebAssembly surface, JIT artifact hooks, or JSC shell controls do not exist in this engine.
The C API keeps the original rule for non-threaded contexts: handles are
affine to the thread that owns their JSContextRef.
Threaded contexts should be created with ZJSGlobalContextCreateThreaded(gil).
With gil == false, the context uses the same no-GIL parallel path as
Context.createWith(.{ .enable_threads = true }). With gil == true, the
context uses the serialized fallback and embedders must respect the GIL model.
JSStringRef values are immutable and use atomic retain/release. GC-enabled
JSValueRef wrappers use counted JSValueProtect / JSValueUnprotect roots.
Do not infer cross-thread handle semantics beyond the documented threaded
context APIs.
Detailed acceptance criteria are tracked in GC/lifecycle #16, contention #15, mid-script GC #14, fuzzing #13, memory model #12, heap/OOM resource control #24, and PR-249 promotions #11. Issue #1 remains the umbrella status page.
- GC allocation fast path / nursery. The first generational policy has
landed: new GC cells enter a non-moving, one-cycle nursery; quiescent minor
collection reclaims unreachable young cells and immediately tenures every
survivor. Owner-aware strong barriers remember dirty old Object/Environment
containers, weak-container barriers preserve exact WeakRef/WeakMap/
FinalizationRegistry semantics, and mutable type-erased side-cell kinds are
conservatively rescanned. Remembered-set allocation failure falls back to the
existing precise full collector. Explicit
collectGarbage()remains full-heap, and parallel mid-script collection remains on the full concurrent protocol. GC cells also use a reusable size-class slab backing instead of calling the backing allocator for every cell. Fresh chunks now use lazy bump cursors with a per-bucket bump hint instead of pre-linking every unused slot during short-lived context setup, and a per-bucket fresh-chunk cursor skips chunks whose bump range is already exhausted. Freed-cell, capacity, and issued-slot accounting is maintained per bucket, so GC cell-backing stats no longer walk every free-list node or slab chunk after a collection. Slab ownership checks are bucket-local with an address-span reject before the recent-chunk hint and sorted per-bucket chunk address index, so frees do not scan unrelated size-class chunks or linearly walk a large bucket during collection or teardown. New slab creation reserves per-bucket metadata in fixed-size capacity chunks before allocating the backing chunk and uses a binary lower-bound insertion into the sorted address index, cutting allocator churn and linear metadata scans from GC context lifecycle growth. Context teardown also skips rebuilding slab freelists for owned cells that will be released by the following whole-chunk free, and the backing leaves parallel mode for that single-owner destroy phase so owned live-cell frees skip the per-free spinlock while chunks are being drained. The collector's explicit bulk teardown path still runs every cell finalizer and releases collector side buffers, but skips one backing-allocator free per cell beforeGcCellBackingreleases the slabs wholesale. Bucket-shaped delegated side allocations still classify once and free through the wrapped allocator while finalizers run. After an explicit quiescentcollectGarbage(), the backing now uses per-slab live counters to release fully unused tail chunks while retaining non-empty and inner chunks for reuse, so one-off allocation spikes can return slab memory beforeContext.destroy()without changing the collector API. Non-owned bucket-shaped resize/remap/free paths also reuse the classification lock instead of retaking it before delegation. Single-mutator GC object side stores bypass the cell-slab classifier and allocate from the context allocator directly; true-parallel JS contexts still route side stores through the synchronized backing wrapper because an embedder allocator may not be thread-safe. GC-enabled context creation now groups the heap, root-tracing binding, and cell backing in one stable lifecycle state allocation instead of three separate GPA objects, reducing create/destroy allocator churn while keeping the existing internal pointers stable. No-GIL context bootstrap also defers GC heap and cell-backing parallel locking until the fully initialized context is about to be returned; the context is private during bootstrap, and the returned no-GIL context still has parallel heap/backing mode enabled. LiveSharedArrayBufferretain teardown is covered for arena, no-GIL threaded, and.gil = truecontexts. Correctness is gated, but tight-loop block-scope allocation and create/destroy-heavy context lifecycles are still slower under the GC path than under the old arena model.zig build gc-profileremains the repeatable baseline for nursery tuning, deeper generational policy, and lifecycle pooling.-Dgc-profile-case='<exact table name>'runs one focused profile table when a full local matrix would be too expensive for a small change. It splits context lifecycle time into create and destroy columns while also including a create-per-task versus long-lived-context reuse table with periodic collection to quantify the embedder lifecycle tradeoff, plus a workload destroy table that compares live object-heavy destroy against quiescent pre-collection and post-collection destroy. That keeps finalizer draining visible separately from teardown that remains after a collection. Current pooling guidance is intentionally conservative: keep a bounded pool per isolation domain, run one task at a time per context unless the host is deliberately sharing a realm across parallel JS tasks, and callcollectGarbage()at quiescent task boundaries. The profile's reuse row uses a 40-task loop with collection every 10 tasks; on the 2026-07-10 local run, recreate-per-task was 6.67x slower than reuse+periodic-GC for explicit GC and 6.69x slower for threaded no-GIL GC. Destroy rather than pool when realm globals/modules must reset, host handles cannot be released cleanly, untrusted code should not share state with the next task, or Worker/Thread activity is still live. The profile also prints GC cell-backing attribution for both the intrinsic empty-context footprint and an object-heavy allocation run: chunk count, total cell-slot capacity, live cells at context creation, live cells after allocation, free slots after collection, and live cells after collection. The same profile now follows with per-size-class bucket tables for the empty context and the object workload, so nursery and context-lifecycle work can separate global setup pressure from workload pressure while targeting the slot sizes that dominate chunk count, issued cells, fresh allocation, reused allocation, freed cells, free cells, and surviving live cells. The same profile now includes a repeated allocate-plus-collect churn table that reports fresh/reused/freed cells, final chunk/live counts, and reuse percentage for GC modes. A quiescent nursery row additionally reports boundary pause time, young input, reclaimed cells, promoted cells/bytes, and minor/full cycle deltas. Explicit-GC tail-slab trimming keeps the post-collection chunk/free-slot columns honest after one-off spikes while still preserving reuse inside retained chunks. Chunk metadata reserve-before-slab allocation keeps this profile focused on cell-slab pressure rather than avoidable metadata allocation churn. The parallel cell allocator now protects each size class with its own fast-path lock; unrelated 64/128/256/512/1024/2048-byte allocations no longer contend on one global cell-backing lock. Chunk growth, metadata allocation, and non-cell side storage still pass through a separate inner-allocator lock because embedder allocators are not required to be thread-safe. This is a contention reduction for the existing slab allocator. The object-sized 1024/2048-byte buckets now use 384 KiB chunks: the local profile keeps the intrinsic empty context at three object-cell chunks with 1152 slots, while explicit collection trims fully unused object-heavy spike chunks back to that retained baseline instead of carrying the older 83-chunk post-collect footprint. Multi-slab tail trimming compacts the sorted address index and freelist metadata once for the whole trimmed range instead of rescanning those structures once per released slab. It also splits GC finalizer attribution between empty-context destroy and destroy after the object workload. Theskipfreecolumn reports the exact number of finalized cell-storage frees elided by whole-slab teardown, keeping that lifecycle improvement visible even when wall-clock profile rows are noisy. - Context lifecycle cost. Long-lived embedders amortize the GC setup and teardown costs, but create-per-unit-of-work embedders need either cheaper context lifecycle or clearer guidance.
- Heap cap and OOM contract. The first production resource-control primitive
is public:
Context.Options.heap_limit_byteswraps the context allocator with a thread-safe outstanding-byte budget. It covers arena chunks, GC cell slabs and side stores, shared-buffer retain tables, thread records, and other Context-owned allocations routed throughContext.gpa, then reports allocator pressure as ordinary Zigerror.OutOfMemory.Context.heapBudgetStats()returns current outstanding bytes, lifetime peak bytes, the configured limit, and remaining headroom for capped contexts; uncapped contexts returnnull. The peak is monotonic for the context lifetime, so embedders can tune limits and alert on near-cap scripts without installing a custom allocator wrapper. Top-level host evaluation reports cap pressure as Zigerror.OutOfMemory. A capped context prebuilds an immutableOutOfMemoryErrorinstance during bootstrap; a spawned shared-realmThreadthat hits the cap publishes that reserved object, sojoin()rethrows a deterministicErrorvalue even when there is no heap headroom left to allocate a fresh object, and siblingThreads can still join. ExistingasyncJoin()waiters reject with the same reserved object when their already-created promise reaction can be delivered. This is intentionally an embedder allocator boundary rather than a JavaScript heap-shape oracle. Arena-backed caps remain fail-closed and non-reclaimable. GC-backed capped contexts can now reclaim at safe GC cell allocation failures and reclaimedArrayBufferbyte pressure, then retry. In no-GIL shared-realm contexts, GC-cell slab andArrayBufferbyte-slab recovery are safepoint-owned by the active interpreter and use the abort-safe parallel root-publication collector; allocation still fails closed when recovery is attempted outside active JS execution or while holding side-store/root-list locks that the tracer may need. Object/property, promise-state, iterator-helper, async-generator request, realm-root, active-interpreter, thread API, and pending-join locks all participate in that guard. Remaining emergency-recovery work is tracked in #30: lock-aware side-store pressure coverage under live no-GIL peers without deadlocking GC tracing. - Parallel scaling optimization. Benchmarks show real speedup, but scaling
is sub-linear.
zig build threads-profilenow provides a repeatable baseline against the.gil = truefallback for independent compute, shared object properties, global lexical binding churn, mixed Object/Function/Promise GC-cell allocation, shared array append, typed-array Atomics, propertyAtomics.wait/notify, propertyAtomics.waitAsynctimeout settlement,Condition.wait/notifyAll, single-lock and multi-lockCondition.asyncWait,Lock.asyncHolddelivery, observedLock.asyncHoldcallback settlement, no-fnLock.asyncHoldrelease-function delivery, and lifecycle churn. Use it to drive contention reductions in global/environment bindings, property/element locks, sync waiters, propertywaitAsynctimeout settlement, async condition regrant delivery, unobserved async-hold grant delivery, promise-observed callback settlement, no-fn release-function delivery, Worker/agent queues, shared-buffer lifetime churn, collection helpers, and GC allocation. Itslcntandaqcolumns split direct contendedLock.holdattempts from queuedLock.asyncHoldgrants inside the aggregateeventstotal, whileshape/newsh/syldsplit hidden-class transition requests, newly-created child shapes, and transition-lock yields so object/property rows can separate cached shape convergence from later slot/element work. Theaacq/acnt/aspn,eacq/ecnt/espn, andob*/op*/oe*lock triplets split arena, Environment binding, and Object backing/property/element lock traffic before optimizing those paths. Itsjoinscolumns splitThread.joinparks from aggregate park pressure, itslock/cond/propcolumns split the remaining sync park pressure by contendedLock.hold,Condition.wait, and propertyAtomics.wait, itswaitus/jus/lus/cus/puscolumns split total native wait microseconds plus join/lock/condition/property wait microseconds, itsasync/donecolumns now split async condition/property-waitAsync registration from completed async-condition reacquires plus settled propertywaitAsynctickets, and itsempty/jobscolumns show whether run-loop task-pump overhead is empty fast-path churn or real grant delivery, withhold/cjobsplitting the delivered jobs into ordinaryLock.asyncHoldgrants versusCondition.asyncWaitreacquire grants. Its Worker message rows split structured-clone channelpush/popoperations from empty receivenullpolls, and its Worker teardown rows reportopstotals for channel push, pop, empty-pop, and close work in self-close, host-close, and terminate modes. Empty sync-wait task pumps no longer take the shared run-loop task lock, reducing one measured cost in contended lock/lifecycle paths; task-queue writers publish the pending-count hint from the locked queue length instead of writer-side atomic RMW and reserve realm task-queue capacity in fixed chunks before capacity-assumed appends, so async grant storms pay fewer allocator-growth trips while holding the shared API lock; real async-hold delivery now uses FIFO head cursors for both per-lock pending grants and realm task delivery, and retry-front grants use an amortized O(1) front stash when no consumed head slot is available, so failed grant delivery does not fall back to shifting the whole pending list. The per-lock pending and retry-front queues also reserve fixed-size capacity chunks before capacity-assumed appends, reducing allocator-growth trips inside the lock-held grant queues. Realm task delivery copies larger bounded FIFO bursts under the shared API lock before running grants outside it, so queue drains do not front-shift remaining jobs and already-queued grant storms need fewer shared-lock acquisitions. The async-hold task pump also snapshots the microtask enqueue generation around each grant, so unobserved grants that settle without queued reactions skip an otherwise-empty no-GIL microtask drain while preserving checkpoint order for grants that do enqueue reactions. No-fn async-hold grants embed their once-only release state in the already arena-lived hold job, so delivered release functions avoid a second small state allocation. Condition notify/notifyAll also dequeues the mixed sync/async waiter queue through a FIFO head cursor instead of shifting every notified waiter. Timed-out or terminated sync condition waiters are marked canceled and skipped by that cursor instead of being removed from the middle of the queue. The condition waiter queue reserves fixed-size capacity chunks before capacity-assumed appends while holdingCondRecord.mutex. Sync notifyAll handoff now waits on the condition ack signal instead of a fixed 1ms polling sleep, reducing ready-waiter latency without changing the timeout fallback. Async-only condition notifications now move no-fn async regrant preparation outside the condition queue mutex; mixed sync/async wakeups keep the existing sync handoff ordering. Notify records woken sync/async entries in one FIFO wake list; small notifications use a fixed stack buffer and only larger notifications allocate a pre-sized heap list. Contiguous async condition regrants for the same lock are prepared in fixed-size stack batches and applied under one lock acquisition per batch, sonotifyAll()no longer retakes that lock once per async waiter. Ready async-condition reacquire jobs are appended to the realm task queue in FIFO bursts, amortizing the shared API lock when a notification wakes multiple lock groups, and sync handoff completion uses a pending-waiter countdown instead of rescanning that wake list until every ticket acknowledges. Representative.gil = trueguidance is now part of the profile workflow: on the 2026-07-10 local 11-core run, independent compute favored no-GIL at 2, 4, and 8 threads (2.56x, 8.42x, and 16.63x faster than serialized), whilecondition asyncWaitfavored.gil = truefor coordination-heavy handoff work (8 threads: 128.60 ms no-GIL versus 5.95 ms serialized). Treat those as workload-shape examples, not portable thresholds; rerun the exact focused row and prefer.gil = truewhen thevs gilcolumn remains below 1.0x. Promise microtask drains use a FIFO head cursor, so observed async-hold callback settlement and no-fn release-function reactions preserve FIFO order without shifting the remaining reaction queue on each job. Microtask enqueues and abandoned-thread queue transfers reserve fixed-size capacity chunks before capacity-assumed appends, reducing allocator-growth trips under each targetMicrotaskQueue's trace-sensitive queue-local lock during promise/thread lifecycle bursts. Per-promise fulfill/reject reaction lists reserve fixed-size capacity chunks before capacity-assumed appends underPromise.lock, reducing allocator-growth trips while many.then()observers register on one pending promise. Async-generator request queues now drain queued.next()/.return()/.throw()promises through a FIFO head cursor, compact consumed head slots before growing, reserve fixed-size capacity chunks before capacity-assumed appends, and expose only pending requests to GC tracing. Property-modeAtomics.notifynow stable-compacts matching sync and async waiters in one pass: notified heap-owned sync tickets leave the realm waiter table before signal, and matchingwaitAsynctickets are collected without repeated middle removals. Individual sync wait timeout/termination cleanup also stable-compacts the waiter table in one pass instead of shifting the remaining waiters. Timeout polling now uses the same one-pass compaction shape for expired propertywaitAsynctickets, and realm teardown frees abandoned propertywaitAsynctickets by linear scan. Property sync waiter and waitAsync ticket tables reserve fixed-size capacity chunks before capacity-assumed appends, so waiter storms grow those tables less often while holdingGil.prop_mutex. Typed-arrayAtomics.notifyunlinks notified sync stack tickets before signal. Typed-arrayAtomics.wait/waitAsyncticket list appends reserve fixed-size capacity chunks before capacity-assumed writes under the process-wide waiter mutex, and typed-arraywaitAsyncharvest/abandon paths stable-compact settled or owner tickets in one pass while preserving FIFO order for remaining waiters. Context-owned typed-arraywaitAsyncpromise roots takerealm_lockfor list-header mutation, settlement removal, clearing, and interpreter-root tracing, and reserve fixed-size capacity chunks before capacity-assumed appends. Worker inbox/outbox channels now drain structured-clone messages with FIFO head cursors as well, avoiding front shifts in receive-heavy Worker loops, and reserve fixed-size queue capacity chunks before capacity-assumed appends so message bursts grow the queues less often under the channel mutex.$262.agentreport delivery uses the same FIFO head-cursor shape and reserves fixed-size queue capacity chunks before capacity-assumed appends, so report-heavy Atomics/test262 agent cases do not shift the whole report queue on eachgetReport()and grow that queue less often under the group mutex. SharedArrayBuffer retain lists reserve fixed-size capacity chunks before capacity-assumed appends too, reducing allocator-growth trips while a realm records SAB backing storage under the retain-list spin lock. FinalizationRegistry cleanup jobs reserve fixed-size capacity chunks before capacity-assumed appends after duplicate suppression, reducing allocator-growth trips while ready cleanup registries are queued underrealm_lock. C-API protected-handle entries reserve fixed-size capacity chunks before capacity-assumed appends after counted-handle deduplication, reducing allocator-growth trips whileJSValueProtectqueues GC roots underrealm_lock. Shared-realmThreadrecords reserve fixed-size capacity chunks before capacity-assumed appends, reducing allocator-growth trips while the main record is installed and spawned records are appended under the GIL/API lock.Thread.asyncJoin()pending observer lists reserve fixed-size capacity chunks before capacity-assumed appends, reducing allocator-growth trips while pending join promises are registered under the target thread'sjoin_mutex. Active-interpreter root entries reserve fixed-size capacity chunks before capacity-assumed appends, reducing allocator-growth trips while evaluate/drain paths register GC roots under the active-interpreter lock. GIL park records reserve fixed-size capacity chunks before capacity-assumed appends, reducing allocator-growth trips while threads register their mid-script-GC stack-scan records under the GIL. Internal module-graph queues for top-level-await parent resumption,import deferstartup, and dynamic-import namespace waiters reserve fixed-size capacity chunks before capacity-assumed appends, reducing allocator-growth trips in module Worker/import-graph lifecycle bursts. The completed-parent resumption queue drains with a FIFO head cursor instead of shifting the queue for every completed parent. Empty internalWorker.receive(..., 0)polls return under the channel lock without entering a timed condition wait or touching drained-queue compaction. Active interpreter roots, protected C-API handles, and GIL park records are unordered root sets, so removal now uses swap semantics instead of order-preserving list shifts on evaluate, handle-unprotect, and thread teardown paths. WeakMap/WeakSet delete and GC dead-key pruning now use unordered tail removal too, and FinalizationRegistryunregisterremoves matching records with one stable compaction pass so survivor cleanup order is preserved without repeated middle shifts. The profile now includes isolatedWorkersections for structured-clone inbox/outbox round-trips, empty receive polling, and teardown. The teardown table splits handler-driven self-close, owner-driven host-close drain of queued messages, and hardterminate()of spinning code, with separate script and module Worker rows so import-graph startup and teardown pressure has its own baseline instead of being inferred from shared-realmThreadrows. - Memory model maintenance. Keep Memory Model aligned with the no-suppression TSan policy, new synchronization primitives, and any promoted PR-249 coverage that exercises JS-defined races.
- Mid-script parallel GC maturity. The abort-safe collector now has
cooperative root publication at sync-wait pump points, covering property
Atomics.wait,Condition.wait, and contendedLockacquisition without tracing those peers as frozen parked stacks. Host-side thread queues are part of the root set too:Gil.tasks,LockRecord.pending, async condition waiters, typed-arraywaitAsyncwaiter/reaction roots, pendingThread.asyncJoinpromise/reaction roots, ThreadLocal maps, thread completion results, protected C-API handles, release-function lock records, and contendedLock.holdreceiver/callback pairs now trace, barrier, or temp-root their hidden JS values. A focused C-API unit witness now protects an otherwise-unrooted object while shared-realmThreads drive a finishing mid-script parallel sweep, then proves thatJSValueProtectkeeps the object alive until the finalJSValueUnprotect. The mid-GC fuzzer now queues a FIFOLock.asyncHoldgrant chain including a root-bearing rejected grant, an asyncCondition.waitreacquire path with captured JS roots, a typed-arraywaitAsyncreaction graph reachable only through the native waiter queue, and pendingThread.asyncJoinfulfillment/rejection reaction graphs reachable only through native completion records; it also runs a sibling expected-termination subprogram where parked children hold child-owned typed-arraywaitAsynctickets through a finishing mid-script sweep, then teardownasyncJoinrejection reactions run and post-termination notify sees zero leaked waitAsync tickets, plus a sibling ThreadLocal lifecycle subprogram where per-threadThreadLocal.valueobjects stay parked through a finishing sweep before per-thread isolation, nested-thread isolation, thrown-object identity, andasyncJoinobservers are verified, plus a sibling ThreadLocal-termination cleanup subprogram where ThreadLocal-only cleanup targets stay live through a finishing sweep until top-level-failure teardown releases the owner-thread entries and exact cleanup is verified, plus a sibling Thread.restrict lifecycle subprogram where restricted owner-local objects stay parked through a finishing sweep before owner isolation, nested foreign access rejection, thrown-object identity, andasyncJoinobservers are verified, plus a sibling promise-publication subprogram where a child-returned typed-arraywaitAsyncpromise, a child-returned rejected promise, a child-returned user thenable, and a child-thrown object remain rooted through thread completion/native waiter state until post-sweepjoin()/asyncJoin()fulfillment, rejection, thenable assimilation, and thrown-object publication, plus a sibling propertyAtomics.waitAsynclate-settlement subprogram where finite-timeout property tickets are removed after the registering child queue has closed, then rerouted through realm draining to exactjoin()/asyncJoin()observers after a finishing sweep, plus a sibling sync-wait cleanup subprogram where propertyAtomics.wait,Condition.wait, and contendedLock.holdpeers stay parked through a finishing sweep before their stack roots and exactFinalizationRegistrycleanup count/sum are verified, where expired propertywaitAsynctickets compact while those peers are parked and a live propertywaitAsyncticket stays rooted through the sweep until notification, and where isolated script and module Workers stay parked on retainedSharedArrayBuffers through the same sweep before their replies are verified, plus a sibling sync-wait burst subprogram where multiple waiters on the same property, the sameCondition, and the same contendedLockstay parked through a finishing sweep before burst release and exact finalization cleanup, plus a sibling sync-timeout subprogram where propertyAtomics.waitand staticAtomics.Condition.waitForpeers stay parked through a finishing sweep, reject early cleanup while their stack roots are live, then time out with exactUnlockTokenreacquisition/unlock and finalization cleanup, plus a siblingAtomics.Mutex.lockIfAvailablesubprogram where acquire-after-release waiters stay parked behind a holder through a finishing sweep, timeout waiters may expire independently while those acquire peers remain rooted, and reused-token acquire/timeout results plus exact finalization cleanup are verified, plus a sibling staticAtomics.Condition.waitsubprogram where notify/reacquire token waiters stay parked through a finishing sweep, reject early cleanup while their roots are live, then verify exact notify counts, token reacquisition,asyncJoinobservers, and finalization cleanup, plus a sibling async-hold release cleanup subprogram where no-fnLock.asyncHold()release functions are delivered while property and condition waiters stay parked through a finishing sweep before exact cleanup count/sum is verified, plus a sibling nested parent/childThread.asyncJoincleanup subprogram where parentThreadLocalroots, childThreadLocalroots, child completion records, and asyncJoin reactions stay live through a finishing sweep before exact cleanup count/sum is verified, plus a sibling pending-microtask subprogram where Promise, typed-arraywaitAsync,Thread.asyncJoin, with-fnLock.asyncHold, no-fn release-function, andFinalizationRegistrycleanup roots stay queued through a finishing sweep until exact post-drain reaction/cleanup checks pass, plus a sibling creator-owned buffer subprogram where child-createdSharedArrayBufferandArrayBufferstorage stays rooted through unjoinedThreadcompletion records and delayedasyncJoinobservers until blockingjoin(), post-sweepasyncJoin(), andArrayBuffer.transfer()observers verify exact contents after the creator exits, plus script and module Worker creator-owned cleanup subprograms where child-created SAB/ArrayBuffer storage crosses Worker structured-clone while sibling cleanup roots and transfer observers survive the finishing sweep, plus a weak-collection subprogram where live WeakMap values stay reachable only through live weak keys, dead WeakMap/WeakSet targets are reachable only through weak structures and WeakRefs, and FinalizationRegistry unregister-token records compact while propertyAtomics.wait,Condition.wait, and contendedLock.holdpeers stay parked through a finishing sweep, then live ephemeron values, cleared dead refs, exact cleanup count/sum, and exact unregister suppression are verified, plus script Worker/SAB and module Worker/SAB cleanup subprograms where isolated Workers keep progressing on a retainedSharedArrayBufferwhile shared-realmThreads publish cleanup targets and parked stack roots through a finishing sweep, plus script and module Worker/thread finalization subprograms where isolated Workers park on a retained SAB while shared-realmThreads publishFinalizationRegistrycleanup roots andasyncJoinobservers through a finishing sweep before Worker release, plus sibling script/module Worker handler-exception cleanup subprograms that recover from an expected thrownonmessagebefore proving the same Worker progress and cleanup oracle, plus script/module Worker close/terminate subprograms that preserve exact FIFO drain/drop, post-close drop, post-terminate silence, joined roots, asyncJoin reactions, and cleanup count/sum through the finishing sweep, plus script/module Worker terminate/finalization subprograms where spinning Workers share one retained SAB with shared-realm cleanup roots, asyncJoin observers, joined roots, and exact cleanup count/sum through the finishing sweep, plus script and module Worker/Condition.asyncWait teardown subprograms where a condition async reacquire ticket, parkedThread, isolated Worker progress, and cleanup jobs stay live through a finishing sweep before notification and top-level failure teardown, plus script and module Worker/ThreadLocal/asyncHold teardown subprograms where isolated Worker termination overlapsThreadLocalhidden roots, no-fnLock.asyncHold()release delivery, parked property/condition waiters, post-sweep rejection release, top-level failure, rejectedasyncJoinobservers, and exact cleanup through a finishing sweep. Sync-wait pump points must execute the async grants during the same allocation-pressure window that produces a finishing parallel sweep, and thewaitAsyncreaction must run intact after notification. Join-sidegc_parkedstate is balanced on termination/error unwinds and scoped to the actual native condition wait rather than join-time task pumping, so a failed or activeThread.join()cannot leave the interpreter looking like a stale or moving frozen parked peer. Requested shell/host GC does not disturb an elected mid-script parallel collector while threads are live; later quiescent collection aborts stale parallel mark state before a fresh precise mark. Policy boundary: sync-wait/condition/contended-lock peers are "running" for root publication while they pump tasks and safepoints, and become directly traceable frozen peers only inside the bounded native wait that setsgc_parkedand is pinned bygc_root_lockon wake. The collector spends at most 50,000 mark/poll iterations per publication generation and applies a caller-specific time floor: 25 ms for opportunistic mid-script collection, or 100 ms for an allocation-failure recovery attempt that cannot otherwise make progress. A 32-generation base cap plus bounded extension rounds applies only while born-cell progress continues and no deferred work is pending; failure still aborts without sweep and falls back to quiescent full collection. Those budgets are implementation/testing policy rather than public API. Keep quiescent collection as the fallback for cycles that still cannot converge, and keep widening wait/cleanup stress around this protocol. - Fuzzer breadth. The broad
threadfuzzprofile now covers caught exceptions/finally, nested thread lifecycle,asyncJoin, propertywait/waitAsync,Condition,Thread.restrict, andFinalizationRegistrycleanup under GC-backed parallel contexts. The mid-GC profile now hammers sync-wait root publication during finishingparallel_midscript_gcsweeps, executes a queued async-hold grant chain including rejected grant reactions and async condition reacquire grants from those pump points, keeps a typed-arraywaitAsyncpromise/reaction graph live only through the native waiter queue, keeps pendingThread.asyncJoinfulfillment/rejection reactions live only through native completion records, keeps a ThreadLocal-only hidden root live in a parked peer, parks ThreadLocal-onlyFinalizationRegistrytargets through a finishing sweep before clearing them with an exact cleanup oracle, keeps ThreadLocal-only cleanup targets live through a finishing sweep until top-level-failure teardown releases their owner-thread entries, registers child-thread cleanup targets with unregister tokens while fulfilled and rejectedThread.asyncJoinobservers plus sync-wait peers stay live through a finishing sweep, keeps typed-arraywaitAsyncreaction roots pending while notifying child threads stay parked through a finishing sweep before exactasyncJoinand cleanup verification, keepsCondition.asyncWaitreacquire tickets and childasyncJoinobservers pending through a finishing sweep before exact reacquire, asyncJoin, and cleanup verification, keeps queuedLock.asyncHold(fn)fulfillment/throw callbacks plus no-fn release grants pending while the lock remains held through a finishing sweep before exact reaction and cleanup verification, and parks Thread.restrict-owned finalization targets through a finishing sweep while nested foreign access still throwsConcurrentAccessErrorbefore owner release, keeps pending Promise/microtask roots live across asyncHold callback/release delivery, typed-arraywaitAsync,Thread.asyncJoin, and cleanup reactions, keeps completed-but-unjoined Thread result and thrown exception objects live through the thread completion record, keeps creator-ownedSharedArrayBufferandArrayBufferstorage live through unjoined Thread completion records and delayedasyncJoinobservers, verifies isolated script/module Worker/SAB progress plus script/module Worker handler-exception recovery, close/terminate drain/drop, script/module Worker/Condition.asyncWait teardown, and script/module Worker/ThreadLocal/asyncHold teardown while shared-realm cleanup roots are swept, and verifies exactFinalizationRegistrycleanup count/sum delivery afterward. The lifecycle profile now adds concurrent multi-context bootstrap/execute/abrupt-teardown/ destroy loops from independent host threads, deterministic termination storms, script Worker/thread retained-SharedArrayBufferoverlap, simple-import, diamond-shaped, and fanout/rejoin module Worker/thread overlap with exact Atomics counter oracles; the fanout case runs nested parent/child Threads and verifies child and parentasyncJoinrerouting while the Worker import graph executes, script/module Worker/thread/finalization scheduling on one retained SAB, script and module Worker termination interleaved with exact shared-realm finalization cleanup on a retained SAB, Worker termination while top-level failure tears down parked shared-realmThreads, pendingasyncJoinrejection reactions, and already-ready cleanup jobs on the same retained SAB, module Worker termination with the same shared-realm teardown/reaction/cleanup oracle, exact FIFO drain/drop ordering for mixed script and module Worker terminate/close/postMessage lifecycles, worker handler-exception recovery, and Worker handler-exception recovery composed with shared-realm Thread finalization cleanup on one retained SAB, module Worker handler-exception recovery composed with the same retained-SAB cleanup oracle, plusThread.restrictlifecycle isolation plusThread.restrict-ownedFinalizationRegistrycleanup after owner-thread exit, Thread exception identity throughjoin()/asyncJoin()while property and condition waiters are parked, thread-returned typed-arraywaitAsyncpromise assimilation throughjoin()/asyncJoin()while waiters are parked, typed-arraywaitAsyncsettlement interleaved withasyncJoinreactions and exactFinalizationRegistrycleanup delivery,Condition.asyncWaitreacquire delivery interleaved withjoin()/asyncJoin()reactions and exactFinalizationRegistrycleanup delivery, proposal-styleAtomics.Mutex/Atomics.Condition.waitFortoken waiters that take both notify and timeout paths whileasyncJoinobservers and exact cleanup share the same lifecycle window,Atomics.Mutex.lockIfAvailabletoken waiters that take both acquire-after-release and timeout paths with reused tokens in that same cleanup window, teardown termination with pendingasyncJoinrejection reactions and child-owned typed-arraywaitAsynctickets that must be abandoned before the child exits while completed siblings preserve thrown- object identity and user-thenable publication throughjoin()andasyncJoin, cross-threadFinalizationRegistrycleanup count/sum oracles, teardown termination while propertywaitAsynctimeout compaction, async condition reacquire, a pendingasyncJoinrejection reaction, and already-readyFinalizationRegistrycleanup jobs share the same realm turn, Worker termination composed with condition async reacquire, pendingasyncJoinrejection cleanup, and exactFinalizationRegistrycleanup, Worker termination composed with child-owned typed-arraywaitAsyncticket abandonment, pendingasyncJoinrejection cleanup, and exactFinalizationRegistrycleanup, module Worker termination composed with the same child-owned typed-arraywaitAsyncticket abandonment, pendingasyncJoinrejection cleanup, and exactFinalizationRegistrycleanup, Worker termination composed withThreadLocalhidden roots, no-fnLock.asyncHold()release-function delivery, parked property/condition waiters, top-level teardown, rejectedasyncJoinobservers, and exactFinalizationRegistrycleanup, deterministicLock.asyncHold()barging where a sync hold legally overtakes a queued no-fn async ticket beforeawaitdelivers its release function, no-fnLock.asyncHold()release-function delivery while property and condition waiters stay parked before exact cleanup after they resume, Promise reaction queue churn from with-fnLock.asyncHold, no-fn release functions, typed-arraywaitAsync,Thread.asyncJoin, and exactFinalizationRegistrycleanup, propertyAtomics.waitAsynclate settlement where peer timeout polling races an owning Thread closing its stack-local microtask queue, a mixed property/typed-arraywaitAsyncrace where notify and timeout tickets settle before top-level failure abandons sibling property and typed-array tickets, exact cleanup ordering across WeakMap values, WeakSet values, directFinalizationRegistrytargets, WeakRefs, and unregister-suppressed records, post-completionThread.asyncJoin()fulfillment and rejection promises plus sibling cleanup roots staying live after blocking joins while property waiters stay parked through a finishing sweep, creator-ownedSharedArrayBufferandArrayBufferstorage that survives the creating Thread's exit, sibling-thread reads, GC pressure, and post-creator resize/ArrayBuffer.transfer()(also covered by the focused unit witness), child-created SAB/ArrayBuffer storage crossing isolated Worker structured-clone after creator exit plus sibling script and module Worker clone/finalization cleanup/transfer observer variants, and cleanup delivery interleaved withjoin()/asyncJoin()plus unregister-token suppression, plus cleanup delivery after parked property/condition waiters resume, plusThreadLocalvalues registered withFinalizationRegistryacross park/resume/clear/join cleanup lifecycles with exact cleanup count/sum delivery after quiescent collection, plusThreadLocal-only cleanup targets released when top-level failure forcibly terminates their owner threads, plus parent-created childThreads whoseasyncJoin()promises outlive the parent Thread's local queue before child release, nestedThreadLocalroots, rerouted async settlement, and exact finalization cleanup after both thread layers exit, plus post-completionThread.asyncJoin()fulfillment and rejection observers settling after blocking joins while property waiters stay parked before exact cleanup. CI now runs small TSan smoke seeds for both the mid-script-GC and lifecycle profiles in addition to their larger non-TSan breadth gates, and nightly/manual CI extends sanitizer depth with higher-iteration default, mid-script-GC, and lifecycle fuzz sweeps. That keeps hidden-root, parked-waiter, Worker, cleanup, termination, and async-join combinations race-gated while deeper sanitizer expansion continues. Keep extending it toward more teardown ordering, broader cross-realm scheduling, and richer cleanup/finalization interleavings. - Reference-only PR-249 files. Promote only when the engine implements the
behavior and the file is reliable under Zig
0.17-dev, especially the WebAssembly-required files, JIT/shell-hook witnesses, JSC-specific mark-list or heap-snapshot/preventCollection probes, detached-buffer fresh-view reference assumptions, and typed-array race-shape probes. Runpython3 tools/threads-reference-audit.py --run-probes --expect-current-blockers --probe-timeout 60to keep the nearest-probe negative baseline honest: it passes only while those files still fail or time out with their documented blocker evidence, and fails when a candidate starts passing or changes failure shape. Usepython3 tools/threads-reference-audit.py --format jsonwhen automation needs the same counts, blocker categories, promotion probes, and expected blocker evidence without scraping human-readable output. - TC39 structs tracking. Keep
proposal-structstracking in P8-structs.md and this issue; do not split it into a parallel tracker.
No new process-global mutable state may land without a ruling in bindings.md. Use one of:
per-thread: each agent or shared-realm thread gets its own state,locked: shared state is protected by a mutex or atomics,refused: the state must never be reachable from a second thread.
This rule applies to file-scope var, pub var, threadlocal, and
container-scope mutable statics.