Skip to content

Latest commit

 

History

History
956 lines (907 loc) · 59.9 KB

File metadata and controls

956 lines (907 loc) · 59.9 KB

Thread Testing

Thread support is verified with Zig 0.17-dev. The package declares this in build.zig.zon, and the build options below use the Zig 0.17 build API.

Required Checks

Run the fast local gates before changing thread behavior or docs:

zig build test
zig build threads-test
zig build threads-test -Dthreads-shard-index=0 -Dthreads-shard-count=4
zig build threads-test -Dthreads-case=atomics/property-waitasync-timeout.js
zig build threads-test -Dthreads-parallel-js=true -Dthreads-case=sync/condition-wait-notify.js
zig build threads-reference-audit
python3 tools/threads-reference-audit.py --run-probes --expect-current-blockers --probe-timeout 60
zig build test -Dtsan=true
zig build test -Dtsan=true -Dtest-filter=parallel_js
zig build threadfuzz -Dfuzz-iters=20
THREADFUZZ_SEED_TIMEOUT_MS=300000 zig build threadfuzz -Dtsan=true -Dfuzz-midgc=true -Dfuzz-iters=2
zig build threadfuzz -Dtsan=true -Dfuzz-lifecycle=true -Dfuzz-iters=2
THREADFUZZ_SEED_TIMEOUT_MS=300000 zig build threadfuzz -Dfuzz-midgc=true -Dfuzz-iters=5
zig build threadfuzz -Dfuzz-lifecycle=true -Dfuzz-iters=20
zig build threadfuzz -Dfuzz-verify=true -Dfuzz-iters=300
bun run docs:build

For performance work, also run:

zig build threads-profile
zig build threads-profile -Dthreads-profile-case='global binding churn' -Dthreads-profile-max-workers=1
zig build threads-profile -Dthreads-profile-case='condition asyncWait'
zig build threads-profile -Dthreads-profile-case='condition asyncWait parked'
zig build threads-profile -Dthreads-profile-case='condition asyncWait multi-lock'
zig build threads-profile -Dthreads-profile-debug=true -Dthreads-profile-case='condition asyncWait'
zig build midgc-profile
zig build gc-profile
zig build gc-profile -Dgc-profile-case='nursery'

These profiles are not correctness gates. threads-profile is the local contention baseline for comparing the no-GIL default against .gil = true across the hot shared structures named in the production roadmap. Its mixed Object/Function/Promise allocation row enables GC in both modes to isolate the cell-backing lock path; shared-realm rows also split LockedArena and Environment binding-lock acquisitions/contention/spins, plus Object backing, property, and element lock acquisitions/contention/spins. The profile prints internal native-wait microsecond attribution for join, lock, condition, and property waits alongside the existing contention event counters. gc-profile is the local allocation/lifecycle baseline for comparing arena, explicit-GC, no-GIL threaded GC, and .gil = true context modes, including the reusable GC-cell slab backing.

-Dgc-profile-case='<exact table name>' runs one GC allocation/lifecycle table without spending the full profile matrix. Current exact cases are lifecycle, task lifecycle, nursery, nursery drift, workload destroy, allocation, explicit gc, gc backing baseline, gc backing baseline buckets, gc backing, gc backing buckets, gc churn reuse, and gc finalizers.

-Dthreads-profile-case='<exact scenario name>' runs one shared-realm row across the host's 1/2/4/8-thread matrix and skips Worker tables. The profiler prints the exact shared-realm scenario names from its scenario table at startup, so newly added focused rows cannot silently drift out of the command help. -Dthreads-profile-max-workers=N caps worker-count rows for broad or focused smoke checks and debugging, while full local baselines should omit the cap. Add -Dthreads-profile-debug=true for a symbolized safety-check build when diagnosing a crash; timing from that mode is not a performance baseline.

midgc-profile isolates the internal parallel_midscript_gc testing policy so its collector behavior is not hidden inside the broader contention matrix. It reports elected attempts, finishing sweeps, publication-timeout versus round-limit aborts, deferred-cell blocked aborts, opened root-publication generations, failed publication poll totals and worst-generation poll counts, allocation-race finish retries and worst-attempt retry counts, born-growth extension rounds, deferred-work rounds, running-peer requests, parked-peer observations, actual peer publications, post-abort retry backoff skips, and collector-side total/maximum pause time. That pause is time the collector mutator spends in the driver; it is not a stop-the-world pause because peer mutators continue running. The profile is an attribution tool, not a stable embedder API or correctness gate.

CI additionally runs heavier no-GIL production gates on every pull request and push to main:

zig build threadfuzz -Dfuzz-iters=400
zig build threadfuzz -Dtsan=true -Dfuzz-iters=60
THREADFUZZ_SEED_TIMEOUT_MS=300000 zig build threadfuzz -Dtsan=true -Dfuzz-midgc=true -Dfuzz-iters=2
zig build threadfuzz -Dtsan=true -Dfuzz-lifecycle=true -Dfuzz-iters=2
zig build threadfuzz -Dfuzz-amplify=true -Dfuzz-iters=30
zig build threadfuzz -Dfuzz-broad=true -Dfuzz-iters=80
THREADFUZZ_SEED_TIMEOUT_MS=300000 zig build threadfuzz -Dfuzz-midgc=true -Dfuzz-iters=20
zig build threadfuzz -Dfuzz-lifecycle=true -Dfuzz-iters=60
zig build threadfuzz -Doptimize=ReleaseSafe -Dfuzz-iters=400
zig build threadfuzz -Dfuzz-verify=true -Dfuzz-iters=300
zig build threads-test-bin -Dtsan=true
./zig-out/bin/threads-test parallel-js one <allowlisted-case>

Nightly and manual CI also run deeper TSan fuzz sweeps:

zig build threadfuzz -Dtsan=true -Dfuzz-iters=120
zig build threadfuzz -Dtsan=true -Dfuzz-midgc=true -Dfuzz-iters=5
zig build threadfuzz -Dtsan=true -Dfuzz-lifecycle=true -Dfuzz-iters=5

Manual workflow_dispatch runs can override those three nightly TSan iteration counts without changing the scheduled defaults. Use the nightly_tsan_default_iters, nightly_tsan_midgc_iters, and nightly_tsan_lifecycle_iters inputs to collect wall-ms and flake evidence for #13 before promoting a larger depth into the nightly schedule.

The corpus TSan sweep is sharded in CI and runs each allowlisted case in its own process to avoid TSan shadow-memory growth across a single long run. CI also runs TSan smoke seeds for the specialized mid-script-GC and lifecycle fuzzer profiles, so their hidden-root, parked-waiter, Worker, cleanup, termination, and async-join paths are covered by sanitizer instead of only by non-TSan breadth runs; the nightly/manual sweeps extend that same sanitizer coverage to more generated programs without making every PR pay the full cost. These gates fail on every reported race. CI currently runs without a suppression file: plain typed-array paths take the buffer lock, and Atomics paths use hardware atomics, so even JS-defined program-byte access stays TSan-clean. If a future program-byte false positive genuinely needs a suppression, add a deterministic load-bearing witness in the same change; do not add broad suppressions or suppress engine-state frames.

What Each Gate Covers

zig build test runs unit and C-API tests, including agents, workers, shared buffers, property-mode Atomics, Thread, Lock, Condition, ThreadLocal, parallel-GC witnesses, C embedder threading, the main can-block gate, and the public Context.Options.heap_limit_bytes allocator-cap and Context.heapBudgetStats() pressure-diagnostic smoke coverage, plus the shared-realm Thread OOM survivor/recovery witnesses that keep a sibling joinable after one peer exhausts the context cap and prove GC-backed capped contexts can recover after unreachable pressure is collected. The focused heap-cap witnesses also cover no-GIL ArrayBuffer byte-slab recovery while a real peer thread is running and publishing roots to the abort-safe parallel collector, plus the trace-sensitive realm task queue, property waiter tables, MicrotaskQueue, Lock/Condition, async-generator request, thread API, realm-root, active-interpreter, and pending-join lock guards that keep allocation-failure recovery out of mutable promise/thread queue, property/lock/condition waiter, ThreadLocal map, root-registry, and generator side-store critical sections. Separate deferred-generator, deferred-generator-handler, deferred-async-generator-request, and deferred-iterator-helper witnesses root suspended stack, resumable handler, pending-request, and mid-helper state, then check that no-GIL allocation-failure recovery aborts instead of sweeping while parallel tracing has deferred mutable execution/helper buffers to a world-stopped finish.

zig build threads-test runs the green WebKit PR-249 allowlist from reference/webkit-249/threads-tests. CI shards the serialized/GIL leg with -Dthreads-shard-index=N -Dthreads-shard-count=4; local full runs still use the plain command, while a stuck shard prints the active RUN case before executing it. The required matrix gate is also bounded so a true hang becomes an archived, diagnosable failed job instead of an opaque spinner. The current coverage contains 236 promoted files out of 259 executable PR-249 files: 234 in the default zig build threads-test allowlist plus 2 parallel_js-only witnesses. It covers:

  • api/ and lifecycle/: constructor shape, lifecycle, ids, constructor errors, exceptions, restriction, return values, join semantics, blocking gates, lock/condition basics, async lock/condition behavior, thread-local storage, termination watchdogs, and Thread.restrict.
  • arrays/ and shared-objects/: shared identity, dense elements, holes, push/resize interleavings, typed arrays over SharedArrayBuffer, property reads/writes/adds/deletes, accessors, prototype chains, frozen/sealed objects, and dictionary-mode objects.
  • atomics/ and sync/: property load/store/RMW/CAS, wait/notify, waitAsync timeout behavior, typed-array lane guardrails, mutex-style counters, condition handshakes, notify-all behavior, and thread-local isolation.
  • races/, invariants/, objectmodel/, and semantics/: transition interleavings, lost-property/element prevention, shape/storage invariants, private fields, regexp/date/string/symbol shared state, IC-vs-transition cases, and termination storms.
  • heap-*, gc-stress/, and cve/: heap option/epoch/deferral/stress drivers, parked-frame/root witnesses, teardown/lifecycle hazards, waiter-table reclamation, FinalizationRegistry delivery, buffer/SAB lifetime, and no-WebAssembly premise/refusal witnesses.
  • bench/, scaling/, jit/, and vmstate/: deterministic checksum coverage, independent-work scaling witnesses, tree-walker-compatible JIT audit files, per-thread exception/regexp/stack/structure state, and flag identity checks.

zig build threads-test -Dthreads-parallel-js=true runs the allowlist through the same no-GIL path that enable_threads uses by default. CI's TSan sweep uses threads-test-bin -Dtsan=true and invokes each case with parallel-js one. cve/mc-dos-waiter-table-storm.js is a focused no-GIL lifecycle witness for property Atomics.waitAsync tickets that are removed from the global table by a peer just as their owning spawned thread tears down its stack-local microtask queue; keep the focused -Dthreads-parallel-js=true case green when changing property waiter settlement, thread queue transfer, or microtask teardown. Its three completion arms are label-aware in runner failures, and the reclamation arm waits for the independent wide-key arm's native thread teardown before requesting a realm-quiescent full collection. This keeps the WeakRef oracle focused on waiter-root removal instead of racing asyncJoin publication against final OS-thread queue/park-record cleanup. Failure output also includes the last reclamation turn/count, GC cycle counts, pending property tickets, and not-yet-exited thread records. The api/lock-async-hold.js barging witness now starts its child Thread inside the setup lock.hold, so the async ticket is deterministically queued against an already-active sync hold instead of racing with immediate no-fn asyncHold() grant delivery under true parallel scheduling.

zig build threadfuzz generates random programs that share objects, arrays, closures, constructors, Maps/Sets, accessors, and typed arrays across JS Threads in a parallel context. The default oracle is "no unexpected throw, deadlock, UAF, or engine race"; -Dtsan=true turns unsynchronized engine access into a race report; -Dfuzz-amplify=true raises contention; -Doptimize=ReleaseSafe keeps safety checks under optimization; -Dfuzz-verify=true generates deterministic atomic programs whose exact result is predicted. Long sweep profiles also run with a per-seed watchdog by default; set THREADFUZZ_SEED_TIMEOUT_MS=0 to disable it, or set a larger millisecond value for slow local/TSan machines. When it fires, the watchdog prints the active profile and seed so the stalled program is still one-command reproducible. Aggregate profiles print wall-ms alongside program count, seed, and failure count; use those CI/nightly timings as runtime evidence for any future depth increase instead of raising iteration counts blindly. The broad profile (-Dfuzz-broad=true) enables GC and adds caught exception/finally paths, nested thread lifecycle, asyncJoin, property wait / waitAsync, Condition wakeups, Thread.restrict, and FinalizationRegistry cleanup sidecars. The focused unit suite also locks in property-mode Atomics.notify queue compaction: matching sync waiters are unlinked before signal, unmatched waiters keep FIFO order, and async tickets are collected without repeated removals. Property waitAsync timeout polling now has a direct unit witness for one-pass expired-ticket compaction that preserves unexpired FIFO order. The mid-script GC profile (-Dfuzz-midgc=true) uses the internal testing context to enable parallel_midscript_gc, blocks peers in property Atomics.wait, Condition.wait, and contended Lock acquisition, queues a FIFO async-hold grant chain including a root-bearing rejected grant plus async condition reacquire grants through those pump points, keeps a typed-array waitAsync promise/reaction graph reachable only through the native waiter queue until notification, keeps pending Thread.asyncJoin fulfillment/rejection promise reactions reachable only through native completion records until the child threads are released, keeps a registered object reachable only through ThreadLocal.value while the owning thread is parked, keeps a completed-but-unjoined Thread result object and a completed-but-unjoined thrown exception object reachable only through the thread completion record, adds a promise-publication subprogram that leaves a child-returned typed-array waitAsync promise, a child-returned rejected promise, a child-returned user thenable, and a child-thrown object rooted through thread completion/native waiter state until after a finishing sweep, then verifies join() / asyncJoin() fulfillment, rejection, thenable assimilation, and thrown-object publication from observers registered both before and after child completion, adds a property Atomics.waitAsync late-settlement subprogram that registers finite-timeout property tickets in child threads, drives a finishing mid-script sweep after the child local queues have closed, then requires rerouted timeout settlement to reach both asyncJoin() and join() promise observers with exact root/score oracles, adds a sync-wait cleanup subprogram that parks peers in property Atomics.wait, Condition.wait, and contended Lock.hold acquisition through a finishing sweep before verifying their stack roots after resume plus exact FinalizationRegistry cleanup count/sum delivery, settles expired property waitAsync tickets while those peers are still parked, keeps a live property waitAsync ticket rooted through the finishing sweep, keeps isolated script and module Workers parked on retained SharedArrayBuffers through the same sweep, and then notifies the live waiter and both Workers with exact captured-root and Worker-reply oracles, adds a sync-timeout subprogram that parks property Atomics.wait peers and static Atomics.Condition.waitFor peers through a finishing sweep, rejects early cleanup while their stack roots are still live, then requires timeout results, Atomics.Mutex.UnlockToken reacquisition/unlock, and exact FinalizationRegistry cleanup after quiescence, adds a sync-wait burst subprogram that parks multiple waiters on the same property, the same Condition, and the same contended Lock through a finishing sweep, rejects early cleanup while those stack roots are still live, then releases all three wait sets and verifies exact cleanup after quiescence, adds an Atomics.Mutex.lockIfAvailable subprogram that keeps acquire-after-release waiters parked behind a holder through a finishing sweep, allows timeout waiters to expire independently while those acquire peers remain rooted, then requires reused-token acquire and timeout results plus exact FinalizationRegistry cleanup after quiescence, adds a static Atomics.Condition.wait subprogram that parks notify/reacquire token waiters through a finishing sweep, rejects early cleanup while their stack roots are live, then requires exact notify counts, token reacquisition, asyncJoin observers, and cleanup after quiescence, adds a ThreadLocal lifecycle subprogram that parks owner threads with per-thread ThreadLocal.value objects through a finishing sweep before checking per-thread isolation, nested-thread isolation, thrown-object identity, and asyncJoin observers, adds a ThreadLocal-finalization subprogram that parks owner threads with targets reachable only through ThreadLocal.value, drives a finishing mid-script sweep, verifies cleanup is not delivered while those hidden roots are live, then clears the values and requires exact cleanup count/sum delivery, adds a ThreadLocal-termination cleanup subprogram that keeps ThreadLocal-only cleanup targets live through a finishing sweep, forces top-level-failure teardown, requires blocking joins to observe termination, then verifies exact cleanup after owner-thread entries are released, adds a Thread.restrict lifecycle subprogram that parks restricted owner-local objects through a finishing sweep before checking owner isolation, nested foreign access rejection, thrown-object identity, and asyncJoin observers, adds a Thread.restrict-finalization subprogram that parks owner threads with restricted owner-local objects registered for finalization, verifies nested foreign reads still throw ConcurrentAccessError, drives a finishing mid-script sweep, rejects early cleanup while those owner-thread roots are live, then releases the owners and requires exact asyncJoin and cleanup oracles, adds a pending-microtask subprogram that queues Promise, typed-array waitAsync, Thread.asyncJoin, with-fn Lock.asyncHold, no-fn release-function, and FinalizationRegistry cleanup roots through a finishing mid-script sweep before draining the realm run loop and verifying exact reaction/cleanup oracles, adds late-asyncJoin fulfillment and rejection cleanup subprograms that first observe child completion or thrown-object identity through blocking join(), then keep post-completion Thread.asyncJoin() promises plus sibling cleanup roots live while property waiters stay parked through a finishing sweep, reject early cleanup during that window, attach late observers, and require exact cleanup after those records are released, adds a finalization/asyncJoin subprogram that registers child-thread cleanup targets with unregister tokens, mixes fulfilled and rejected asyncJoin observers, parks sync-wait peers through the finishing sweep, and requires exact cleanup plus unregister-token suppression, adds a typed-array waitAsync/finalization subprogram that keeps native waiter reaction roots pending while notifying child threads stay parked through a finishing sweep, then verifies asyncJoin settlement and exact cleanup, adds a Condition.asyncWait/finalization subprogram that keeps async reacquire tickets and child asyncJoin observers pending through a finishing sweep, then verifies exact reacquire, asyncJoin, and cleanup oracles, adds an asyncHold throw/finalization subprogram that keeps queued Lock.asyncHold(fn) fulfillment/throw callbacks plus no-fn release grants pending while the lock is held through a finishing sweep before exact reaction and cleanup verification, adds a creator-owned buffer subprogram that leaves child-created SharedArrayBuffer and ArrayBuffer storage rooted through unjoined Thread completion records and delayed asyncJoin observers across a finishing sweep, then verifies blocking join(), post-sweep asyncJoin(), and ArrayBuffer.transfer() observers see exact contents after the creating thread has exited, adds 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 a finishing sweep, adds script Worker/SAB and module Worker/SAB cleanup subprograms that run isolated Workers on the same retained SharedArrayBuffer while shared-realm Threads register cleanup targets and park stack roots through a finishing sweep, then verify exact Worker progress, joined thread roots, asyncJoin reactions, and cleanup count/sum, adds script and module Worker handler- exception cleanup subprograms that first recover from an expected thrown onmessage delivery and then prove the same Worker/SAB progress plus shared-realm cleanup oracle through the finishing sweep, adds script and module Worker close/terminate subprograms that keep exact FIFO drain/drop ordering, post-close drop, post-terminate receive silence, shared-realm joined roots, asyncJoin reactions, and cleanup count/sum live across the finishing sweep, adds script and module Worker terminate/finalization subprograms where spinning Workers share one retained SAB with shared-realm Threads that publish cleanup roots, asyncJoin observers, joined roots, and exact cleanup count/sum through a finishing sweep before Worker termination, adds script and module Worker/thread teardown subprograms that keep shared-realm Threads, pending asyncJoin rejection reactions, and cleanup jobs live through a finishing sweep while isolated Workers spin, then force top-level failure teardown and verify exact rejection and cleanup oracles, adds script and module Worker/Condition.asyncWait teardown subprograms that keep a condition async reacquire ticket, parked Thread, isolated Worker progress, and cleanup jobs live through a finishing sweep before notification and top-level failure teardown, adds script and module Worker/waitAsync teardown subprograms that keep child-owned typed-array waitAsync tickets pending through a finishing sweep while isolated Workers spin, then force top-level failure teardown and require pending asyncJoin rejection reactions plus zero leaked child waiter tickets, adds script and module Worker/ThreadLocal/asyncHold teardown subprograms that compose isolated Worker termination with ThreadLocal hidden roots, no-fn Lock.asyncHold() release-function delivery, parked property/condition waiters, post-sweep rejection release, top-level failure, rejected asyncJoin observers, and exact cleanup through a finishing sweep, adds a weak-collection subprogram that parks property Atomics.wait, Condition.wait, and contended Lock.hold peers while live WeakMap values are reachable only through live weak keys, dead WeakMap/WeakSet targets are reachable only through weak structures and WeakRefs, and FinalizationRegistry unregister-token records are compacted through a finishing sweep, then verifies live ephemeron values, cleared dead refs, exact cleanup count/sum, and exact unregister suppression, and adds an expected-termination subprogram that parks children after installing child-owned typed-array waitAsync tickets, drives a finishing mid-script parallel sweep, then verifies teardown asyncJoin rejection reactions and zero leaked waitAsync tickets. It also has a focused join-termination unit witness that checks parked-state/mutex cleanup, then requires exact script completion or exact expected termination plus at least one finishing parallel sweep and exact FinalizationRegistry cleanup count/sum delivery plus unregister-token suppression after a quiescent collect, and parks script and module Workers on a retained SAB while shared-realm Threads publish FinalizationRegistry cleanup roots and asyncJoin observers through a finishing sweep. Each seed currently runs 45 deterministic mid-GC subprograms. The lifecycle profile (-Dfuzz-lifecycle=true) adds simultaneous host-thread-owned no-GIL contexts that repeatedly bootstrap, run exact join/exception-identity oracles, abruptly tear down parked children, and destroy before recreating (threadfuzz multictx is the focused seed-reproduction mode), plus deterministic resizable ArrayBuffer / DataView constructor, DataView access, and sliceToImmutable races where one no-GIL peer repeatedly resizes a backing buffer while another constructs views or performs locked view/bulk-copy access, expected-throw termination storms for parked/unjoined shared-realm Threads, exact Atomics counter oracles for script Worker plus simple-import, diamond-shaped, and fanout/rejoin module Worker overlap with shared-realm Threads on one retained SharedArrayBuffer; the fanout profile composes the seven-module Worker graph with parent/child Thread trees and exact child-plus-parent asyncJoin reaction publication, script/module Worker/thread/finalization scheduling on one retained SAB, exact cleanup after terminating spinning script and module Workers that share the retained SAB, Worker termination while top-level failure tears down parked shared-realm Threads, pending asyncJoin rejection 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 close / terminate / postMessage lifecycles, plus worker handler-exception recovery after a thrown onmessage, 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, Thread.restrict lifecycle isolation plus Thread.restrict-owned FinalizationRegistry cleanup after owner-thread exit, Thread exception identity through join() / asyncJoin() while property and condition waiters are parked, thread-returned typed-array waitAsync promise assimilation through join() / asyncJoin() while waiters are parked, property Atomics.waitAsync late settlement where a peer removes timeout tickets from the global table while the owning thread closes its stack-local microtask queue, a mixed property/typed-array waitAsync race 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, direct FinalizationRegistry targets, WeakRefs, and unregister-suppressed records, typed-array waitAsync settlement interleaved with asyncJoin reactions and exact FinalizationRegistry cleanup delivery, deterministic Condition.asyncWait reacquire delivery interleaved with join() / asyncJoin() reactions and exact FinalizationRegistry cleanup delivery, proposal-style Atomics.Mutex / Atomics.Condition.waitFor token waiters that take both notify and timeout paths while asyncJoin observers and exact cleanup share the same lifecycle window, Atomics.Mutex.lockIfAvailable token waiters that take both acquire-after-release and timeout paths with reused tokens in that same cleanup window, Lock.asyncHold() barging where a sync hold legally overtakes a queued no-fn async ticket before await delivers its release function, no-fn Lock.asyncHold() release-function delivery while property and condition waiters stay parked before exact cleanup after they resume, teardown termination with pending asyncJoin rejection reactions and child-owned typed-array waitAsync tickets that must be abandoned before the child's stack-owned waiter token disappears; already-completed sibling Threads in that teardown window must also preserve thrown-object identity and user- thenable assimilation through blocking join() and asyncJoin, cross-thread FinalizationRegistry cleanup count/sum oracles, teardown termination while property waitAsync timeout compaction, async condition reacquire, a pending asyncJoin rejection reaction, and already-ready FinalizationRegistry cleanup jobs share the same realm turn, module Worker termination composed with the same condition async reacquire oracle, plus module Worker termination composed with the same child-owned typed-array waitAsync ticket abandonment, pending asyncJoin rejection cleanup, and exact FinalizationRegistry cleanup, Promise reaction queue churn from with-fn Lock.asyncHold, no-fn release functions, typed-array waitAsync, Thread.asyncJoin, and exact FinalizationRegistry cleanup, post-completion Thread.asyncJoin() observers settling after blocking joins while property waiters stay parked, followed by exact FinalizationRegistry cleanup, post-completion Thread.asyncJoin() rejection observers preserving thrown-object identity after blocking joins while property waiters stay parked, followed by exact child and reaction cleanup, Lock.asyncHold(fn) throw/release ordering with queued no-fn release grants and exact FinalizationRegistry cleanup, creator-owned SharedArrayBuffer and ArrayBuffer storage that survives the creating Thread's exit, sibling-thread reads, GC pressure, and post-creator ArrayBuffer.transfer(), child-created SAB/ArrayBuffer storage crossing isolated Worker structured-clone after the creator Thread exits, plus sibling script Worker and module Worker clone/finalization cleanup/transfer observer variants, cleanup delivery interleaved with join() / asyncJoin() and unregister-token suppression, cleanup delivery after parked property/condition waiters resume, child-returned fulfilled/rejected promises and user thenables published through both join() and asyncJoin(), plus ThreadLocal roots kept live while no-fn Lock.asyncHold() release functions deliver with property and condition waiters parked, followed by exact cleanup, plus ThreadLocal isolation across normal, throwing, nested, and async-joined thread lifecycles, plus ThreadLocal values registered with FinalizationRegistry across park/resume/clear/join cleanup lifecycles with exact cleanup count/sum delivery after quiescent collection, plus ThreadLocal-only targets held by owners that are forcibly terminated by top-level failure and must be released before exact cleanup after teardown. It also parks child Threads behind parent-created asyncJoin() promises that outlive the parent Thread's local microtask queue, then verifies child release, nested ThreadLocal roots, rerouted async settlement, and exact finalization cleanup after both thread layers exit. It now also composes isolated Worker termination with shared-realm teardown that abandons child-owned typed-array waitAsync tickets, rejects pending asyncJoin reactions, and delivers exact cleanup, plus isolated Worker termination overlapping ThreadLocal hidden roots, no-fn Lock.asyncHold() release-function delivery, parked property/condition waiters, top-level teardown, rejected asyncJoin observers, and exact cleanup. Each seed The mid-script variants keep those deliberately released waiters on indefinite waits, bounded by the host-side per-case watchdog, and assert that no rejection settles before the release point. This keeps the semantic oracle independent of TSan runner speed. Each seed currently runs 56 deterministic lifecycle subprograms.

zig build test262 -Dtest262-parallel-js=true runs test262 programs in GIL-free parallel contexts. The full corpus is too slow for every PR, so CI uses a curated representative slice and asserts no new failures versus the baseline arena engine.

zig build threads-reference-audit scans the vendored PR-249 corpus and fails if any non-allowlisted executable file lacks an explicit reference-only blocker classification. This keeps shell-hook, WebAssembly, JIT, and heap-cap gaps visible without inflating the green allowlist with no-op passes.

zig build threads-profile is not a pass/fail correctness gate. It is the local scaling and contention profiler for issue #1. The wall-clock columns compare the no-GIL default with .gil = true across independent compute, shared object properties, global lexical binding churn, shared array append, typed-array Atomics, property Atomics.wait / notify, property Atomics.waitAsync timeout settlement, Condition.wait / notifyAll, single-lock and multi-lock Condition.asyncWait, contended Lock.hold, Lock.asyncHold delivery, observed Lock.asyncHold callback settlement, no-fn Lock.asyncHold release-function delivery, and thread lifecycle churn. Its opt-in counters let events count logical contention in Lock/Condition/property waits and queued asyncHold grants. The shape/newsh/syld columns report hidden-class transition requests, newly-created child shapes, and transition-lock yields, so object/property rows can distinguish cached shape convergence from slot/element work. The aacq/acnt/aspn columns report LockedArena acquisitions, contended acquisitions, and failed spin attempts; the eacq/ecnt/espn columns report the same for Environment binding-table locks; obacq/obcnt/ obspn, opacq/opcnt/opspn, and oeacq/oecnt/oespn report the same for Object backing, property, and element locks. This lets allocation-heavy, binding-heavy, shared-property, and shared-array rows separate allocator pressure, global/environment pressure, object-shape pressure, object-storage lock pressure, and waiter pressure. The lcnt and aq columns split direct contended Lock.hold attempts from queued Lock.asyncHold grants inside that total, and parks count timed wait/pump iterations including Thread.join. The joins columns split the Thread.join subset out of aggregate parks so lifecycle churn can be attributed separately from lock, condition, and property wait pressure. The lock/cond/prop columns split the remaining sync park pressure by contended Lock.hold, Condition.wait, and property Atomics.wait, so source-specific waiter regressions do not hide inside aggregate parks. The async/done columns aggregate Condition.asyncWait plus property waitAsync registrations against completed async-condition reacquires plus settled property waitAsync tickets; caw/cad and paw/pad split those same async sources into condition-async wait/done and property-waitAsync wait/done pairs, so timeout-settlement parity and async condition regrant pressure stay visible without hiding behind a combined total. The empty/jobs columns split the run-loop task pump into empty atomic fast-path hits and real grant-job delivery, and the paired hold/cjob columns split those delivered jobs into ordinary Lock.asyncHold grants versus Condition.asyncWait reacquire grants. The cqgrow/cqcomp columns count condition waiter-queue backing growth and consumed-head compaction, so condition asyncWait and notify-heavy rows can distinguish allocation pressure from amortized FIFO churn. Run it before and after synchronization or lifecycle changes so performance work has an attributed baseline instead of only elapsed time. The profile also prints isolated Worker tables for structured-clone inbox/outbox round-trips, empty receive polling, and teardown churn. The message table now prints push/pop channel operations for the timed round trips and null empty receives for the polling row, so a timing change can be checked against actual inbox/outbox work. The teardown table splits handler-driven self-close, owner-driven host-close drain after queuing messages, and hard terminate() of spinning code; its self ops/host ops/term ops columns count total Worker channel push, pop, empty-pop, and close operations for each teardown mode. Both tables emit separate script and module Worker rows so import-graph startup and teardown cost can be compared with plain source Workers. It intentionally has no .gil = true column because each Worker owns its own Context.

zig build midgc-profile is the corresponding focused convergence profile for the internal mid-script parallel collector. Its accounting invariants are also asserted by focused unit tests: every elected attempt ends in exactly one sweep or classified abort, abort reasons sum to total aborts, each attempt opens at least one publication generation, and maximum collector-side pause does not exceed total pause. Publication generations use 0 only as the idle sentinel and have a focused wraparound witness, so a stale high generation cannot satisfy a fresh request after counter wrap. The same coherence check keeps worst-generation publication polls and worst-attempt finish retries bounded by their aggregate totals. End-to-end witnesses require both directly observed parked peers and roots actually published by running sync-wait peers. A focused native-callback witness also runs VM-lowered closures through Array.prototype.map inside no-GIL workers while mid-script collection runs, so tree-walker callback entry, captured upvalues, root publication, and the mid-GC barrier are checked together.

The focused condition asyncWait profile is also a nursery lifetime gate. A Debug run exposed a GC-poisoned Promise stored only in the native condition queue. Lock, Condition, and ThreadLocal side records now retain their wrapper as the owner for generational barriers: queued lock jobs, async condition waiters, condition-to-lock edges, and ThreadLocal map values are remembered when an old wrapper receives a young target. The unit test parallel_js nursery remembers native synchronization side-record edges tenures those wrappers, stores young values only in native records, forces a minor collection, and requires condition reacquire plus ThreadLocal reads to survive. Run the focused profile in Debug once and ReleaseFast repeatedly after changing these queues. Empty sync-wait task pumps now have a lock-free fast path; real async-hold delivery drains larger bounded FIFO bursts from the realm task queue under one API-lock acquisition before running grants outside that lock, task-queue writers publish the atomic pending hint from the locked queue length instead of writer-side atomic RMW, task-queue writers reserve capacity in fixed chunks before capacity-assumed appends, and retry-front async-hold grants use a front stash instead of shifting the per-lock pending list when no consumed head slot is available; condition notify/notifyAll uses a FIFO head cursor for the mixed sync/async waiter queue; 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; sync notifyAll handoff now waits on the waiter's condition ack signal instead of a fixed 1ms polling sleep; per-lock async grant queues and the condition waiter queue reserve fixed-size capacity chunks before capacity-assumed appends; property-mode Atomics.wait timeout/termination cleanup stable-compacts the sync waiter table in one pass instead of shifting the remaining waiters; property-mode sync waiter and waitAsync ticket tables reserve fixed-size capacity chunks before capacity-assumed appends; typed-array Atomics.notify unlinks sync stack tickets before signal, and typed-array Atomics.wait / waitAsync ticket-list appends reserve fixed-size capacity chunks before capacity-assumed writes; typed-array waitAsync harvest/abandon paths stable-compact matching tickets in one pass while preserving FIFO order for other waiters; Atomics.waitAsync reserves async waiter capacity chunks guards context-owned typed-array waitAsync root-list reserve growth and post-settlement clearing; Worker inbox/outbox channels use the same shape for structured-clone message delivery, and empty internal Worker.receive(..., 0) polls skip timed condition wait setup and drained-queue compaction. Active interpreter roots, protected C-API handles, and GIL park records remove with swap semantics because those root sets have no observable order. C-API: JSValueProtect roots survive mid-script parallel GC protects an otherwise-unrooted C-API object while shared-realm Threads drive a finishing mid-script parallel sweep, verifies the object and nested child survive while protected, then proves the final JSValueUnprotect releases it. C-API: JSValueProtect reserves handle capacity chunks guards counted-handle deduplication and fixed-chunk protected-handle table growth. worker channel pops FIFO without front shifts keeps that queue shape and zero-timeout polling behavior under a direct unit guard, while condition queue head cursor skips canceled sync waiters covers the condition timeout/termination queue shape directly, and condition sync handoff countdown tracks acknowledged tickets covers the no-rescan sync notify handoff counter. condition queue reserves capacity chunks guards the fixed-chunk waiter-queue growth invariant under CondRecord.mutex, and condition queue compacts consumed head before growing guards steady notify/re-wait churn by compacting a large consumed head before a capacity growth would be needed. Contention-profile stats expose those paths as cqgrow/cqcomp, keeping the allocator/churn side of async-condition rows visible next to elapsed timing and task-pump counts. jsthread lock pending async jobs are cursor FIFO covers FIFO pop, consumed-slot retry, and front-stash retry without front shifts, while jsthread lock pending queues reserve capacity chunks and jsthread lock retry-front queue reserves capacity chunks guard fixed-chunk growth for both per-lock async grant queues. jsthread traces queued async hold task roots covers the GC roots behind both queued realm tasks and retry-front lock grants. The public condition corpus cases exercise the stack-buffered wake-list notify path for async-only and sync notify-all wakes: api/condition-async-wait.js, sync/condition-wait-notify.js, and sync/condition-notify-all-multi-waiter.js. The same notify path batches contiguous same-lock async condition regrants under one lock acquisition per fixed-size stack batch instead of retaking that lock once per async waiter. property waiter removal stable-compacts timed-out sync ticket covers the property waiter cleanup shape, property waiter queues reserve capacity chunks guards fixed-chunk growth for both property-mode sync waiters and property waitAsync tickets, waiter table notify unlinks sync tickets and preserves async FIFO tail plus waiter table harvestAsync stable-compacts settled owner tickets cover the typed-array waiter-table compaction shapes, waiter table tickets reserve fixed-size capacity chunks covers typed-array waiter-list reserve growth, and api/condition-wait-termination.js keeps the JS termination path exercised. Promise microtask drains now use the same FIFO head-cursor pattern, with microtask queue is FIFO with a head cursor guarding the direct queue shape, fixed-chunk reserve growth, pending-queue transfer, and generation counter behavior, and the asyncHold corpus case exercising observed callback/release-function reactions through the public API. The async-hold task pump snapshots the microtask enqueue generation before and after each delivered grant, so unobserved grants that settle without queuing reactions keep the required task turn while skipping an otherwise-empty no-GIL microtask drain. No-fn async-hold release states are embedded in their already arena-lived hold jobs, so the same public asyncHold corpus case also covers the release-function path after that allocation reduction. Promise reaction lists reserve capacity chunks guards per-promise fulfill/reject reaction-list reserve growth plus GC-owned live-entry accounting. Async-generator request queues use the same direct queue shape: async generator request queue uses a head cursor and compacting reserve guards FIFO pop, consumed-slot compaction before growth, fixed-size reserve growth, and GC pending-request tracing through Generator.pendingRequests(). The property waitAsync timeout row should keep async and done equal after finite tickets settle; the single-lock Condition.asyncWait row intentionally keeps its notifier in a hot property-spin rendezvous to stress task delivery under scheduler pressure. The condition asyncWait parked row keeps the same single-lock async-condition shape but parks the notifier with property Atomics.wait, so it is the cleaner control row when deciding whether a change helped condition reacquire delivery or merely changed spin-loop interference. The multi-lock row exercises FIFO-bursted realm task enqueue across lock groups and the paired run-loop job delivery pressure separately through the hold versus cjob split; it is also listed in the startup filter output so it can be run directly without rediscovering its exact scenario name from source. Worker channel unit tests cover both FIFO head-cursor draining and fixed-size capacity chunk reservation before inbox/outbox appends; the script/module Worker message rows in threads-profile are the local signal for whether those queue growth reductions help or regress Worker-heavy traffic. threads-profile remains the check that this kind of targeted optimization does not merely move overhead elsewhere.

zig build gc-profile also includes an embedder task-lifecycle table. It compares create/evaluate/destroy per task against evaluating the same task repeatedly in one long-lived context with periodic collectGarbage() calls, so context-heavy embedders can quantify the cost of create-per-unit-of-work designs while the GC allocator and lifecycle paths continue to mature. The lifecycle table splits total time into create and destroy columns so teardown reductions are visible separately from global setup costs. The workload destroy table compares destroying the same object-heavy context while the workload is still live with a quiescent collectGarbage() followed by destroy, so finalizer draining and post-collection teardown costs can be tracked separately. The profile also prints GC cell-backing attribution for the intrinsic empty-context footprint and for 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. It then prints per-size-class bucket tables for the empty context and the same object workload, showing slot size, chunks, capacity, issued cells, fresh allocations, reused allocations, freed cells, free cells, and surviving live cells. GC finalizer attribution is also split between empty-context destroy and destroy after the object workload. These snapshot paths use exact per-bucket free, capacity, issued-slot, fresh-allocation, reused-allocation, and freed-slot counters rather than walking every free-list node or slab chunk. The object-sized 1024/2048-byte buckets use 384 KiB slab chunks rather than the small buckets' 64 KiB chunks, so compare chunk counts alongside wall-clock timings when evaluating GC allocation or lifecycle changes. A healthy local profile should show the empty context at three object-cell chunks; after explicit collection, a one-off object-heavy spike should trim fully unused tail slabs back toward that retained baseline instead of preserving the older 83-chunk post-collect footprint. Multi-slab tail trimming should compact freelist and sorted-address-index metadata once for the whole released tail range, not once per released slab. The same profile now prints a repeated allocate-plus-collect churn table that summarizes fresh cells, reused cells, freed cells, final chunk/live counts, and reuse percentage for GC modes. The quiescent nursery tables report the evaluation-entry pause used to collect a fixed young workload, young cells/bytes entering the cycle, reclaimed cells/bytes, promoted cells/bytes, byte survival/reclamation percentages, the next nursery threshold, minor/full cycle deltas, and a repeated batch threshold-drift row for each retention/allocation shape, including an array/object one-quarter-retained row alongside the object-graph rows. This keeps nursery tuning tied to reclamation quality, pause cost, and threshold trajectory instead of a single post-cycle number. Nursery threshold decay remains gradual after low-survival cycles, while upward growth is capped at the just-observed young batch size so high-survival bursts do not skip the next quiescent minor and carry an otherwise dead young batch to a later boundary. Direct GcCellBacking unit tests cover lazy fresh-slot bumping, free-list recycling, fresh-chunk cursor advancement, ownership span/hint classification, sorted address-index lookup with chunk/bump-offset/address metadata kept in sync, fixed-size metadata reserve growth before slab allocation, empty tail-slab trimming, multi-slab tail-range metadata compaction, non-empty tail and empty-inner-slab retention, and bulk teardown leaving parallel mode before owned live-cell frees drain without rebuilding freelists, stats accounting, cumulative fresh/reused/freed allocation counters, multi-chunk maintained-counter snapshots, bucket attribution, bulk-teardown behavior, and bucket-shaped delegated side frees during teardown. The sibling zig-gc suite covers deinitRetainingCellStorage: every live cell is finalized and heap counters reset while the backing allocator observes no per-cell frees. zig-js uses that API only after GcCellBacking enters its single-owner bulk mode, then releases the slabs wholesale. The enable_gc: bulk destroy skips one backing free per finalized cell integration test and gc-profile's skipfree column require exact agreement between finalized cells and elided cell-storage free dispatches. GC cell backing shards parallel allocation locks by size class holds one size-class lock and proves another can be acquired independently. Focused TSan coverage exercises the same allocator bookkeeping and teardown paths; the embedder allocator remains serialized separately for chunk growth and delegated side storage. enable_gc: collectGarbage trims empty GC backing tail chunks covers the public explicit-GC path that releases fully unused spike slabs before context destroy. The enable_gc nursery integration tests establish old containers before adding young edges, then cover Object/Environment/Promise retention, young-garbage reclamation and promotion counters, WeakRef clearing, WeakMap ephemerons, and FinalizationRegistry cleanup. The sibling zig-gc suite independently covers owner and child-only barriers, stale/wild maybe-managed root/barrier rejection, live-payload index maintenance, weak slots, binding-selected old-cell rescanning, ephemerons, oversized post-spike collector-scratch trimming, full fallback accounting, and normal/TSan collector builds. enable_gc: heap binding and cell backing share one lifecycle allocation covers the context-lifecycle reduction where the GC heap, root-tracing binding, and cell backing live in one stable state object instead of three separate GPA allocations. The public thread-semantics unit also asserts that Context.createWith(.{ .enable_threads = true }) returns with both the GC heap and cell backing in parallel mode, so the bootstrap fast path that delays those locks until the context is observable cannot silently weaken no-GIL semantics. The unit suite also covers live SharedArrayBuffer retain release during context teardown across arena, no-GIL threaded, and .gil = true contexts. RetainList reserves fixed-size capacity chunks guards the SAB retain-list growth invariant so structured-clone/lifecycle churn does not regress to one allocator-growth trip per tracked backing reference under the retain-list spin lock. FinalizationRegistry cleanup queue reserves capacity chunks guards cleanup job duplicate suppression and fixed-chunk queue growth under the realm lock. Thread API reserves thread record capacity chunks guards the main-record and spawned-record growth invariant for shared-realm Thread lifecycle churn. Thread asyncJoin pending list reserves capacity chunks guards pending async-join observer growth plus completion snapshot handoff. active interpreter root list reserves capacity chunks guards evaluation/GC root-list reserve growth and swap-pop cleanup. Gil park records reserve capacity chunks and unregister by swap guards threadlocal park-record registration growth, duplicate suppression, and swap-removal cleanup for mid-script GC stack-scan records. modules reserve internal queue capacity chunks guards module-list reserve growth, duplicate suppression, completed-parent head-cursor reset, and dynamic-import namespace waiter growth. Collection-helper removal witnesses live in the same unit suite: WeakMap and WeakSet entry delete is unordered tail removal, gc pruneDeadWeakEntries removes dead weak keys with unordered tail removal, and FinalizationRegistry unregister stable-compacts matching records guard the weak-entry tail-removal and stable unregister compaction shapes. agent reports drain FIFO with a head cursor and agent reports reserve fixed-size capacity chunks guard $262.agent report queue FIFO order, cursor compaction, capacity growth, and teardown cleanup for report-heavy Atomics agent tests.

Focused Runs

Use -Dthreads-case=<path> to run one vendored thread test. A comma-separated list runs a mini-sequence in one runner process, useful for order-dependent teardown or scheduler debugging:

zig build threads-test -Dthreads-case=api/thread-basic.js
zig build threads-test -Dthreads-case=atomics/property-waitasync-timeout.js
zig build threads-test -Dthreads-case=api/lock-basic.js,atomics/property-wait-notify.js

Add -Dthreads-parallel-js=true to force the no-GIL path explicitly:

zig build threads-test -Dthreads-parallel-js=true -Dthreads-case=sync/condition-wait-notify.js
zig build threads-test -Dthreads-parallel-js=true -Dthreads-case=cve/mc-dos-waiter-table-storm.js

Use -Dthreads-sweep=true to run every file in the original default-gate directories (api/, arrays/, atomics/, bench/, lifecycle/, races/, scaling/, shared-objects/, and sync/). Sweep mode is narrower than the promoted allowlist because it does not scan root files or promoted invariants/objectmodel/semantics/vmstate subsets:

zig build threads-test -Dthreads-sweep=true
zig build threads-test -Dthreads-sweep=true -Dthreads-parallel-js=true

For fuzzer reproduction:

zig build threadfuzz-bin
./zig-out/bin/threadfuzz file /path/to/repro.js
./zig-out/bin/threadfuzz propwaitasynclate 5 1
./zig-out/bin/threadfuzz waitrace 5 1
./zig-out/bin/threadfuzz weakorder 5 1
./zig-out/bin/threadfuzz workerclose 5 1
./zig-out/bin/threadfuzz moduleworkerclose 5 1

Remaining Reference-Only Areas

The default corpus is intentionally not a "run every file" mode. Remaining PR-249 files stay reference-only for concrete reasons:

  • WebAssembly-required CVE files remain out until this engine has the matching WebAssembly construction, compilation, relocation, and grow behavior.
  • JIT/CVE files that require JSC-specific code artifact hooks, ASAN controls, stop counters, disassembly controls, or retired-artifact machinery remain out until real engine behavior backs those hooks.
  • cve/mc-df-arraycopy-relabel.js remains out because it depends on JSC's butterfly verification shell option and a typed-array set length race shape whose current zig-js failure is still the documented RangeError blocker. The portable zig-js contract is covered by TypedArray set snapshots array-like source length and parallel_js: TypedArray set snapshots array-like source length under no-GIL grow: source length is snapshotted once, so growth before the snapshot can reject when it no longer fits the destination, while growth after the snapshot does not extend the copy.
  • cve/mc-life-creator-thread-dies.js still depends on a reference-shell detach race that constructs fresh Int32Array views after the source ArrayBuffer has been transferred. zig-js keeps the normal detached-buffer constructor TypeError there instead of weakening TypedArray validation. The portable creator-owned storage subset is covered by the unit witness threads: creator-owned ArrayBuffer storage survives creator exit, GC, resize, and transfer plus threadfuzz creatorbuffers. Together they check child-created SharedArrayBuffer / ArrayBuffer storage after creator exit, sibling reads, GC pressure, post-creator resize, and post-creator ArrayBuffer.transfer().
  • dw2-marklistset-storm.js and w16-c1-prevent-collection.js remain out because they target JSC shared-GC mark-list and heap-snapshot preventCollection hooks rather than portable zig-js behavior.
  • Helper/preload files such as harness.js, bench/harness.js, scaling/harness.js, resources/assert.js, and vmstate/resources/workload.js are not counted as standalone remaining tests.

Promote a reference-only file only when the engine implements the behavior, the file passes reliably under Zig 0.17-dev, and the docs/issue counts are updated in the same change.

Run the reference audit after promotion attempts:

zig build threads-reference-audit
python3 tools/threads-reference-audit.py --format markdown
python3 tools/threads-reference-audit.py --format json
python3 tools/threads-reference-audit.py --probe-candidates
python3 tools/threads-reference-audit.py --run-probes --probe-timeout 60
python3 tools/threads-reference-audit.py --run-probes --expect-current-blockers --probe-timeout 60
python3 tools/threads-reference-audit.py --scan-reference-only --probe-timeout 20

--run-probes executes the closest reference-only candidates with focused threads-test commands and returns nonzero on any failure or timeout. A timed out or failing probe is not promotion evidence; keep the file reference-only until the underlying behavior lands and the focused run passes reliably. Failed probes print focused runner evidence before the Zig build tail so the concrete JS error, corpus failure, or timeout is visible in one command. --format json emits the same allowlist counts, reference-only categories, closest probe commands, and expected current blocker evidence in a stable machine-readable form for CI reports, dashboards, or issue-tracker updates. The top-level promoted_executable, executable_total, reference_only_executable, and helper_preload fields mirror the nested sections so simple status scripts can read coverage without understanding the full category schema. --expect-current-blockers flips that maintenance check into a negative gate: it succeeds only while the nearest probes still fail or time out with the documented blocker evidence. If it starts failing because a probe passes, or because the failure shape changed, re-run that single -Dthreads-case=... probe, promote the file only when the underlying behavior is implemented, and update the docs/issue tracker in the same change. --scan-reference-only is a slower opt-in broom for issue audits: it runs every remaining executable reference-only file and fails only if one unexpectedly passes. Expected reference-only passes are machine-listed separately for cases that are known to skip JSC-only premises or that pass only in serialized mode while their no-GIL promotion arm remains too expensive.

Docs Checks

bun run docs:build
rg '[2]7/[2]7|[3]0/[3]0|[5]4/[5]4|[6]9/[6]9|13[0]/13[0]|14[0]/14[0]|16[8]/16[8]|17[6]/17[6]|18[2]/18[2]|18[3]/18[3]|18[4]/18[4]|18[6]/18[6]|18[7]/18[7]|18[8]/18[8]|19[347]/19[347]|20[3]/20[3]|threads-test -[-]' README.md docs bunpress.config.ts

The search should find no stale partial allowlist counts and no removed thread-test pass-through command syntax. Use -Dthreads-case and -Dthreads-sweep.

When Adding Thread Work

  • Add or update focused unit tests for narrow engine behavior.
  • Add or update PR-249 corpus coverage when the behavior is externally observable.
  • Add fuzzer generation or deterministic fuzzer oracles when the behavior can be randomized.
  • Update bindings.md for every new file-scope mutable var, pub var, threadlocal, or container-scope mutable static.
  • Re-run ThreadSanitizer before merging changes that affect waiters, shared buffers, workers, GC roots/barriers, task queues, C handles, or cross-thread value/state publication.
  • Keep GitHub issue #1 and these docs aligned whenever behavior, counts, or blockers change.