ci(threadfuzz): parameterize nightly depth #966
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| # Runs the docs/threads/testing.md "Required Checks" — crucially the | |
| # ThreadSanitizer gates that surface the no-GIL races blocking the execution-path | |
| # GIL drop. The toolchain comes from pantry's own action (`zig@0.17.0-dev`), | |
| # which resolves the current 0.17.0-dev master (≥0.17.0-dev.956) from | |
| # ziglang.org/download/index.json — so the pin tracks upstream automatically | |
| # instead of going stale. At ≥dev.956 TSan now builds on macOS too; CI keeps | |
| # running it on Linux as the shared gate. (The older dev.131 pin could not build | |
| # TSan on darwin — bundled libcxx failed with `undeclared identifier 'INFINITY'` | |
| # — which is why this was Linux-only before.) | |
| # | |
| # Triggers: pull_request + push-to-main + manual dispatch + nightly schedule. The | |
| # no-GIL TSan corpus gate and test262-parallel leg now run on | |
| # every PR and main push (production gating), not just nightly. | |
| on: | |
| pull_request: | |
| push: | |
| branches: [main] # this project lands directly on main, so gate main pushes too | |
| workflow_dispatch: | |
| inputs: | |
| nightly_tsan_default_iters: | |
| description: 'Manual nightly-threadfuzz-tsan default-profile iterations' | |
| required: false | |
| default: '120' | |
| nightly_tsan_midgc_iters: | |
| description: 'Manual nightly-threadfuzz-tsan mid-GC profile iterations' | |
| required: false | |
| default: '5' | |
| nightly_tsan_lifecycle_iters: | |
| description: 'Manual nightly-threadfuzz-tsan lifecycle profile iterations' | |
| required: false | |
| default: '5' | |
| schedule: | |
| - cron: '0 7 * * *' # nightly continuous validation (incl. the race-hunt probe) | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # Each Required Check runs as its own matrix leg (fail-fast: false), so an | |
| # unrelated failure in one (e.g. the full unit suite) does not mask the | |
| # threading / ThreadSanitizer gates. | |
| gate: | |
| name: ${{ matrix.name }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - name: unit (shard 0/4) | |
| cmd: zig build test -Dunit-shard-index=0 -Dunit-shard-count=4 | |
| - name: unit (shard 1/4) | |
| cmd: zig build test -Dunit-shard-index=1 -Dunit-shard-count=4 | |
| - name: unit (shard 2/4) | |
| cmd: zig build test -Dunit-shard-index=2 -Dunit-shard-count=4 | |
| - name: unit (shard 3/4) | |
| cmd: zig build test -Dunit-shard-index=3 -Dunit-shard-count=4 | |
| - name: threads-gil (shard 0/4) | |
| cmd: zig build threads-test -Dthreads-shard-index=0 -Dthreads-shard-count=4 | |
| - name: threads-gil (shard 1/4) | |
| cmd: zig build threads-test -Dthreads-shard-index=1 -Dthreads-shard-count=4 | |
| - name: threads-gil (shard 2/4) | |
| cmd: zig build threads-test -Dthreads-shard-index=2 -Dthreads-shard-count=4 | |
| - name: threads-gil (shard 3/4) | |
| cmd: zig build threads-test -Dthreads-shard-index=3 -Dthreads-shard-count=4 | |
| - name: threads-nogil-witness | |
| cmd: zig build threads-test -Dthreads-parallel-js=true -Dthreads-case=sync/condition-wait-notify.js | |
| # Reference-only corpus hygiene: fail if a non-promoted PR-249 file | |
| # loses its blocker classification or if one of the nearest | |
| # promotion probes starts passing or changes failure shape without the | |
| # allowlist/docs being updated in the same change. | |
| - name: threads-reference-audit | |
| cmd: zig build threads-reference-audit && python3 tools/threads-reference-audit.py --run-probes --expect-current-blockers --probe-timeout 60 | |
| - name: tsan-unit (shard 0/4) | |
| cmd: zig build test -Dtsan=true -Dunit-shard-index=0 -Dunit-shard-count=4 | |
| - name: tsan-unit (shard 1/4) | |
| cmd: zig build test -Dtsan=true -Dunit-shard-index=1 -Dunit-shard-count=4 | |
| - name: tsan-unit (shard 2/4) | |
| cmd: zig build test -Dtsan=true -Dunit-shard-index=2 -Dunit-shard-count=4 | |
| - name: tsan-unit (shard 3/4) | |
| cmd: zig build test -Dtsan=true -Dunit-shard-index=3 -Dunit-shard-count=4 | |
| - name: tsan-parallel-js | |
| cmd: zig build test -Dtsan=true -Dtest-filter=parallel_js | |
| # Concurrent-JS fuzzer: random programs sharing objects/arrays/closures/ | |
| # typed-arrays across JS Threads in GIL-free parallel contexts. Seeded, | |
| # so it is a deterministic gate (no flake): a throw/UAF fails the leg. | |
| # It found the call-dispatch + frame-slot-rooting bugs (commit b462c90). | |
| - name: threadfuzz | |
| cmd: zig build threadfuzz -Dfuzz-iters=400 | |
| # A smaller batch under ThreadSanitizer surfaces any unsynchronized | |
| # engine access the seeded programs exercise (TSan is ~10× slower). | |
| - name: tsan-threadfuzz | |
| cmd: zig build threadfuzz -Dtsan=true -Dfuzz-iters=60 | |
| # Specialized TSan smoke for the mid-script-GC profile. The larger | |
| # non-TSan leg owns breadth; this leg proves the hidden root/parked | |
| # waiter/microtask paths are also free of unsuppressed engine races. | |
| # TSan makes this 45-case profile exceed the default 120s per-seed | |
| # watchdog on loaded runners, so keep the watchdog local to this leg. | |
| - name: tsan-threadfuzz-midgc | |
| cmd: THREADFUZZ_SEED_TIMEOUT_MS=300000 zig build threadfuzz -Dtsan=true -Dfuzz-midgc=true -Dfuzz-iters=2 | |
| # Specialized TSan smoke for lifecycle teardown. It keeps the Worker, | |
| # asyncJoin, waitAsync, cleanup, and termination combinations under | |
| # sanitizer instead of relying only on the default-profile TSan leg. | |
| - name: tsan-threadfuzz-lifecycle | |
| cmd: zig build threadfuzz -Dtsan=true -Dfuzz-lifecycle=true -Dfuzz-iters=2 | |
| # Stress amplification: the high-contention profile (6–14 threads, | |
| # 5000-iter loops) drives rare interleavings far harder — a throw, UAF, | |
| # hang, or torn read fails the leg. Run without TSan (its ~10× slowdown | |
| # makes amplified contention impractical; the default-profile | |
| # `tsan-threadfuzz` above covers race detection). | |
| - name: threadfuzz-amplified | |
| cmd: zig build threadfuzz -Dfuzz-amplify=true -Dfuzz-iters=30 | |
| # Breadth profile: cheaper than amplified, but deliberately covers | |
| # caught exceptions/finally, nested thread lifecycle, asyncJoin, | |
| # property wait/waitAsync, Condition wakeups, Thread.restrict, and | |
| # FinalizationRegistry cleanup under GC-backed parallel contexts. | |
| - name: threadfuzz-broad | |
| cmd: zig build threadfuzz -Dfuzz-broad=true -Dfuzz-iters=80 | |
| # Mid-script GC wait-pump profile: blocks peers in property | |
| # Atomics.wait, Condition.wait, and contended Lock acquisition while | |
| # allocation pressure drives the experimental parallel collector. It | |
| # also keeps ThreadLocal-only roots and completed-but-unjoined Thread | |
| # results live across that pressure window. | |
| # Each seed must complete exactly and finish at least one parallel sweep. | |
| - name: threadfuzz-midgc | |
| cmd: zig build threadfuzz -Dfuzz-midgc=true -Dfuzz-iters=20 | |
| # Lifecycle profile: deterministic termination storms (main throws | |
| # with parked/unjoined Threads), cross-realm Worker + shared-realm | |
| # Thread overlap on one SharedArrayBuffer with an exact counter oracle, | |
| # Worker/thread/finalization scheduling on one retained SAB, | |
| # Worker handler exception recovery, Thread.restrict lifecycle | |
| # isolation, deterministic asyncHold barging, exact Worker FIFO | |
| # close/terminate drain/drop ordering, and cross-thread | |
| # finalization cleanup count/sum oracles including parked-waiter | |
| # delivery and unregister-token suppression. | |
| - name: threadfuzz-lifecycle | |
| cmd: zig build threadfuzz -Dfuzz-lifecycle=true -Dfuzz-iters=60 | |
| # Sanitizer combo: ReleaseSafe keeps Zig's safety checks (UB, bounds, | |
| # overflow, unreachable) on but under the optimizer, catching codegen- | |
| # dependent faults the Debug build hides — a different bug class than TSan. | |
| - name: releasesafe-threadfuzz | |
| cmd: zig build threadfuzz -Doptimize=ReleaseSafe -Dfuzz-iters=400 | |
| # Deterministic-correctness oracle: each program's exact result is | |
| # predicted and checked, so a wrong VALUE (lost/torn atomic update) fails | |
| # the leg — a bug class the no-throw/no-race gates above can't see. | |
| - name: threadfuzz-verify | |
| cmd: zig build threadfuzz -Dfuzz-verify=true -Dfuzz-iters=300 | |
| steps: | |
| # build.zig.zon uses local-path deps (`../zig-regex`, `../zig-gc`), so the | |
| # sibling repos are checked out next to this one. | |
| - uses: actions/checkout@v6 | |
| with: | |
| path: zig-js | |
| - uses: actions/checkout@v6 | |
| with: | |
| repository: zig-utils/zig-regex | |
| path: zig-regex | |
| - uses: actions/checkout@v6 | |
| with: | |
| repository: zig-utils/zig-gc | |
| path: zig-gc | |
| - uses: pantry-pm/pantry/packages/action@main | |
| with: | |
| packages: zig@0.17.0-dev | |
| - name: ${{ matrix.name }} | |
| working-directory: zig-js | |
| run: ${{ matrix.cmd }} | |
| # Whole-corpus no-GIL ThreadSanitizer sweep — the issue #1 GIL-drop gate | |
| # (drive the no-GIL corpus to zero unsuppressed races, then the GIL can flip | |
| # off as the default). Runs EVERY allowlist case under `parallel_js` + TSan, | |
| # each in its OWN process: per-case isolation sidesteps the cumulative-load OOM | |
| # of a single all-in-one-process TSan run (TSan shadow memory grows across the | |
| # whole allowlist). Sharded 4 ways so the ~5h serial sweep fits the | |
| # runner time limit. Both *named* no-GIL frontier races are now closed — the | |
| # api/blocking-gate settlement race (re-derived its GIL-specific assertion) and | |
| # the semantics-batch StringHashMap-grow panic (accessor-snapshot in seal/freeze | |
| # + locked getAccessor in property-mode Atomics) — so this sweep hunts any | |
| # remaining *un-named* residual. continue-on-error while still being driven to | |
| # zero: a TSan report names the next fix. | |
| tsan-nogil-corpus: | |
| name: tsan-nogil-corpus (shard ${{ matrix.shard }}/4) | |
| # Gates every PR + main push (production no-GIL safety), plus nightly. Now at | |
| # zero engine-state races, so it hard-blocks: a TSan report fails the run. | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [0, 1, 2, 3] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| path: zig-js | |
| - uses: actions/checkout@v6 | |
| with: | |
| repository: zig-utils/zig-regex | |
| path: zig-regex | |
| - uses: actions/checkout@v6 | |
| with: | |
| repository: zig-utils/zig-gc | |
| path: zig-gc | |
| - uses: pantry-pm/pantry/packages/action@main | |
| with: | |
| packages: zig@0.17.0-dev | |
| - name: Build the TSan-instrumented threads-test binary | |
| working-directory: zig-js | |
| run: zig build threads-test-bin -Dtsan=true | |
| - name: Sweep shard under parallel_js + TSan | |
| working-directory: zig-js | |
| run: | | |
| set -uo pipefail | |
| # Halt each case at its first race so the report is the case-log tail. | |
| # The engine currently keeps even JS program-byte paths TSan-clean via | |
| # locks/atomics, so the production corpus runs with no suppressions. | |
| export TSAN_OPTIONS="halt_on_error=1" | |
| BIN=./zig-out/bin/threads-test | |
| # This shard's slice of the allowlist (NR % 4 == shard), each case run | |
| # in its own process. `timeout` caps a heavy stress case (the GC/CAS | |
| # storms run minutes under TSan's ~10x slowdown) so a shard can't hang. | |
| # `list` prints to stderr (the harness's only output stream), so 2>&1. | |
| "$BIN" list 2>&1 | awk -v S=${{ matrix.shard }} 'NR % 4 == S' > shard.txt | |
| echo "shard ${{ matrix.shard }}: $(wc -l < shard.txt) cases" | |
| races=0; fails=0; passes=0; n=0 | |
| while IFS= read -r c; do | |
| [ -z "$c" ] && continue | |
| n=$((n+1)) | |
| if timeout 600 "$BIN" parallel-js one "$c" > case.log 2>&1; then | |
| passes=$((passes+1)) | |
| elif grep -q "ThreadSanitizer:" case.log; then | |
| races=$((races+1)); echo "::error::TSan race in $c"; echo "----- $c -----"; cat case.log | |
| else | |
| fails=$((fails+1)); echo "::warning::non-race failure/timeout in $c"; tail -8 case.log | |
| fi | |
| done < shard.txt | |
| echo "=== shard ${{ matrix.shard }} done: pass=$passes fail=$fails race=$races (n=$n) ===" | |
| # The gate is races==0; surface a race as a leg failure (non-race | |
| # functional fails/timeouts under TSan slowness do not fail the gate). | |
| [ "$races" -eq 0 ] | |
| nightly-threadfuzz-tsan: | |
| name: nightly-threadfuzz-tsan (${{ matrix.name }}) | |
| # Higher-iteration sanitizer expansion for issue #1's remaining roadmap. | |
| # PRs/main pushes run the fast TSan smoke legs above; nightly/manual runs | |
| # spend more time on the same default, mid-GC, and lifecycle profiles. | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - name: default | |
| profile: default | |
| - name: midgc | |
| profile: midgc | |
| - name: lifecycle | |
| profile: lifecycle | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| path: zig-js | |
| - uses: actions/checkout@v6 | |
| with: | |
| repository: zig-utils/zig-regex | |
| path: zig-regex | |
| - uses: actions/checkout@v6 | |
| with: | |
| repository: zig-utils/zig-gc | |
| path: zig-gc | |
| - uses: pantry-pm/pantry/packages/action@main | |
| with: | |
| packages: zig@0.17.0-dev | |
| - name: ${{ matrix.name }} | |
| working-directory: zig-js | |
| env: | |
| DEFAULT_ITERS: ${{ github.event.inputs.nightly_tsan_default_iters || '120' }} | |
| MIDGC_ITERS: ${{ github.event.inputs.nightly_tsan_midgc_iters || '5' }} | |
| LIFECYCLE_ITERS: ${{ github.event.inputs.nightly_tsan_lifecycle_iters || '5' }} | |
| run: | | |
| case "${{ matrix.profile }}" in | |
| default) | |
| zig build threadfuzz -Dtsan=true -Dfuzz-iters="${DEFAULT_ITERS}" | |
| ;; | |
| midgc) | |
| THREADFUZZ_SEED_TIMEOUT_MS=300000 zig build threadfuzz -Dtsan=true -Dfuzz-midgc=true -Dfuzz-iters="${MIDGC_ITERS}" | |
| ;; | |
| lifecycle) | |
| zig build threadfuzz -Dtsan=true -Dfuzz-lifecycle=true -Dfuzz-iters="${LIFECYCLE_ITERS}" | |
| ;; | |
| esac | |
| # Runs a curated test262 cross-section in GIL-free parallel contexts and asserts | |
| # it introduces no NEW failures vs the baseline (arena) engine — i.e. the | |
| # parallel-mode locked paths + GC-managed allocator are correct across a broad | |
| # slice of the language, not just the threads corpus. (The full corpus is too | |
| # slow per-context under GC for CI; this is the representative gate.) | |
| test262-parallel: | |
| name: test262-parallel | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| path: zig-js | |
| - uses: actions/checkout@v6 | |
| with: | |
| repository: zig-utils/zig-regex | |
| path: zig-regex | |
| - uses: actions/checkout@v6 | |
| with: | |
| repository: zig-utils/zig-gc | |
| path: zig-gc | |
| - uses: pantry-pm/pantry/packages/action@main | |
| with: | |
| packages: zig@0.17.0-dev | |
| - name: Parallel test262 introduces no new failures | |
| working-directory: zig-js | |
| run: | | |
| set -uo pipefail | |
| SUBS="test/language/types/boolean test/language/types/number test/language/statements/class/subclass-builtins test/built-ins/Object/keys test/built-ins/Map/prototype/set" | |
| zig build test262-bin >/dev/null | |
| BASE=./zig-out/bin/test262 | |
| fails_base=0 | |
| for s in $SUBS; do | |
| f=$("$BASE" --diag "$s" 2>&1 | grep -oE '[0-9]+ fail' | grep -oE '[0-9]+' | head -1 || echo 0) | |
| fails_base=$((fails_base + ${f:-0})) | |
| done | |
| echo "baseline (arena) total fails: $fails_base" | |
| zig build test262-bin -Dtest262-parallel-js=true >/dev/null | |
| PAR=./zig-out/bin/test262 | |
| fails_par=0 | |
| for s in $SUBS; do | |
| line=$("$PAR" --diag "$s" 2>&1 | tail -1); echo "$line" | |
| f=$(echo "$line" | grep -oE '[0-9]+ fail' | grep -oE '[0-9]+' | head -1 || echo 0) | |
| fails_par=$((fails_par + ${f:-0})) | |
| done | |
| echo "parallel total fails: $fails_par (baseline: $fails_base)" | |
| if [ "$fails_par" -gt "$fails_base" ]; then | |
| echo "::error::parallel mode introduced $((fails_par - fails_base)) new test262 failures"; exit 1 | |
| fi | |
| docs: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| path: zig-js | |
| - uses: oven-sh/setup-bun@v2 | |
| - name: docs:build | |
| working-directory: zig-js | |
| run: | | |
| bun install | |
| bun run docs:build |