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.
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:buildFor 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=5Manual 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.
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/andlifecycle/: 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, andThread.restrict.arrays/andshared-objects/: shared identity, dense elements, holes, push/resize interleavings, typed arrays overSharedArrayBuffer, property reads/writes/adds/deletes, accessors, prototype chains, frozen/sealed objects, and dictionary-mode objects.atomics/andsync/: 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/, andsemantics/: 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/, andcve/: 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/, andvmstate/: 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.
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.jsAdd -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.jsUse -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=trueFor 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 1The 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.jsremains 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 documentedRangeErrorblocker. The portable zig-js contract is covered byTypedArray set snapshots array-like source lengthandparallel_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.jsstill depends on a reference-shell detach race that constructs freshInt32Arrayviews after the sourceArrayBufferhas been transferred. zig-js keeps the normal detached-buffer constructorTypeErrorthere instead of weakeningTypedArrayvalidation. The portable creator-owned storage subset is covered by the unit witnessthreads: creator-owned ArrayBuffer storage survives creator exit, GC, resize, and transferplusthreadfuzz creatorbuffers. Together they check child-createdSharedArrayBuffer/ArrayBufferstorage after creator exit, sibling reads, GC pressure, post-creator resize, and post-creatorArrayBuffer.transfer().dw2-marklistset-storm.jsandw16-c1-prevent-collection.jsremain out because they target JSC shared-GC mark-list and heap-snapshotpreventCollectionhooks rather than portable zig-js behavior.- Helper/preload files such as
harness.js,bench/harness.js,scaling/harness.js,resources/assert.js, andvmstate/resources/workload.jsare 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.
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.tsThe search should find no stale partial allowlist counts and no removed
thread-test pass-through command syntax. Use -Dthreads-case and
-Dthreads-sweep.
- 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.