ReviewOS

zig-utils/zig-js

Threads: mature mid-script parallel GC

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

Parent: #1

Goal

Move mid-script parallel collection from a heavily tested internal capability toward a generally supportable policy.

Current baseline

The collector can trace running peers through root-publication generations, directly trace frozen parked peers, and abort without sweeping when publication or heap stability does not converge. Sync property waits, condition waits, and contended locks service publication requests at bounded pump points. Specialized fuzzer programs cover parked waiters, async jobs, ThreadLocal roots, finalization, joins, Workers, teardown, termination, and native-callback closure entry.

zig build midgc-profile now isolates this internal policy and reports attempts, finishing sweeps, publication-timeout versus round-limit aborts, deferred-cell aborts, generations, bounded born-growth extension rounds (ext-rnd), deferred-work rounds, post-abort retry backoff skips (backoff), total and worst-generation publication polls (wait-poll / wait-max), total and worst-attempt finish retries (fin-retry / retry-max), running/parked peer observations, actual peer publications, and collector-side total/maximum pause. These counters and parallel_midscript_gc remain testing/profile implementation details, not stable embedder API.

The current profile evidence says convergence pain is mostly round-limit fallback under sustained allocation, not publication-timeout failure. The internal policy now suppresses immediate retry churn after failed attempts and can extend the base generation cap only while a born-cell progress signal remains bounded and safe. Worst-generation, extension/backoff, and deferred-abort counters keep those decisions visible before changing budgets or wake/pump policy further.

Completed milestones

  • Add pause, abort-reason, retry, generation, and peer-participation metrics.
  • Add accounting invariants for attempt outcomes, abort classification, generations, pause totals, and max-versus-total telemetry.
  • Prove both running-peer root publication and direct parked-peer observation paths execute.
  • Add a focused repeatable convergence profile independent of the broad contention matrix.
  • Add bounded post-abort retry backoff and born-growth convergence extension telemetry.
  • Make the waiter-table reclamation witness deterministic at a truly quiescent point and add arm/GC/thread failure diagnostics.
  • Add deferred generator/iterator abort telemetry so fail-closed no-GIL finishes are visible separately from generic round-limit churn.
  • Add native-callback closure-root coverage for VM-lowered callback entry during mid-script collection.

Remaining work

  • Define the supported collection/convergence policy for sync-wait parked peers.
  • Continue reducing reliance on bounded publication polling, wake/pump retries, round-limit aborts, and quiescent fallback using the new backoff/extension/deferred-abort profile evidence.
  • Expand nested native callback, host re-entry, deep VM/tree-walker, and multi-context coverage.
  • Prove termination/teardown behavior when a collection is active at greater sustained depth.
  • Decide when the internal parallel_midscript_gc path can become production policy; do not expose the test knob as public API.

Acceptance criteria

  • TSan-clean higher-depth mid-GC sweeps and deterministic lifecycle oracles.
  • Explicit root-publication and lock-order documentation.
  • No hangs, lost cleanup jobs, or collection-time UAFs under termination and sync-wait stress.
  • Profile evidence supports any convergence-policy or retry-budget change.
9 days ago

Landed 3b17a78 test(threads): measure mid-script GC convergence on main.

Implemented

  • Added internal atomic telemetry for publication generations/polls, allocation-race finish retries, publication-timeout and round-limit aborts, running-peer requests, parked-peer observations, actual peer root publications, and collector-mutator total/maximum pause.
  • Added focused accounting invariants: every elected attempt ends in one finishing sweep or classified abort; abort reasons sum to total aborts; every attempt opens at least one generation; maximum pause cannot exceed total pause.
  • Existing end-to-end witnesses now require both a directly observed parked peer and actual roots published by running sync-wait peers.
  • Added zig build midgc-profile, separate from threads-profile, so collector convergence remains measurable even when unrelated contention scenarios fail.

A representative local profile completed 20 attempts with 11 sweeps, 9 round-limit aborts, 0 publication-timeout aborts, 396 generations, 8,016,071 failed publication polls, 5 finish retries, 1,145 running-peer requests, 396 parked-peer observations, and 1,150 actual publications. This points the next optimization work at generation/poll convergence under sustained allocation rather than weakening finish safety.

Deterministic lifecycle oracle

The clean corpus exposed a real intermittent cve/mc-dos-waiter-table-storm.js 2/3 completion. Arm labels and failure-only GC/waiter/thread diagnostics proved that notify and dead-registrant timeout cleanup completed, but the WeakRef reclamation arm could consume all 2,000 turns before the independent wide-key threads finished native teardown. The witness now gates reclamation on final synchronous joins after verifying async results, preserving its reclamation threshold while requiring a genuinely quiescent full collection. It passed three consecutive isolated runs and the subsequent clean corpus.

Verification

  • zig build test
  • zig build threads-test — 227/227
  • zig build threads-test -Dthreads-parallel-js=true -Dthreads-case=sync/condition-wait-notify.js
  • focused collector witnesses in normal and TSan builds
  • waiter-table storm: three consecutive process-isolated passes
  • zig build threadfuzz -Dfuzz-midgc=true -Dfuzz-iters=5 — 225 programs, 0 failures
  • zig build threadfuzz -Dtsan=true -Dfuzz-midgc=true -Dfuzz-iters=2 — 90 programs, 0 failures
  • zig build threadfuzz -Dfuzz-verify=true -Dfuzz-iters=300 — 300 programs, 0 failures
  • zig build test -Dtsan=true -Dtest-filter=parallel_js
  • zig build midgc-profile
  • bun run docs:build
  • git diff --check

The internal collector remains opt-in/test-only. This milestone adds evidence and determinism; it does not claim production policy graduation.

9 days ago

Landed 6c25d8e (docs(threads): define mid-GC sync-wait policy (#14)).

This defines the current internal convergence policy for sync-wait/condition/contended-lock peers:

  • those peers are treated as running peers while they pump tasks and GC safepoints;
  • they become directly traceable frozen peers only inside the bounded native wait region that sets gc_parked;
  • direct tracing is pinned by gc_root_lock so the owner cannot wake and mutate roots mid-read;
  • each root-publication generation gives running peers up to 50,000 mark/poll iterations and at least 25 ms to publish;
  • the collector caps convergence at 32 generations;
  • publication-timeout and round-limit failures abort without sweeping and leave reclamation to quiescent full collection;
  • these budgets remain implementation/testing policy, not stable embedder API.

Updated:

  • src/root_handshake.zig source contract comment
  • docs/threads/production-readiness.md
  • docs/threads/limits.md

Validation:

  • zig fmt src/root_handshake.zig
  • bun run docs:build
  • git diff --check

The follow-up work is still open: reduce reliance on the bounded polling/round-limit fallback using profile evidence, then raise sustained depth only after the TSan/fuzzer data supports it.

9 days ago

Advanced the convergence-policy evidence in b856b7a (bench(midgc): track worst convergence retries).

What changed:

  • Context now records max failed publication polls for any single root-publication generation (gc_par_publication_wait_iterations_max).
  • Context now records max self-checking finish retries for any single elected collection attempt (gc_par_finish_retries_max).
  • midgc-profile prints those as wait-max and retry-max next to the existing aggregate wait-poll and fin-retry totals.
  • Focused telemetry coherence now asserts the max counters remain bounded by their aggregate totals.

Why this matters for #14: before changing publication wait budgets, wake/pump behavior, or round-limit policy, we need to know whether convergence pain is broad across many generations or concentrated in a small number of worst cases. This gives that signal without exposing the internal parallel_midscript_gc knob as API.

Validation, batched:

  • zig build midgc-profile
  • zig build test -Dtest-filter='parallel_js (M3): mid-script parallel collector'
  • zig build test -Dtest-filter='parallel_js (M3): sync wait peers publish roots'
  • bun run docs:build
  • git diff --check

Local midgc-profile output after the change:

zig-js mid-script parallel GC telemetry (internal testing policy)
  runs  attempts    sweeps    aborts pub-abort rnd-abort generations wait-poll  wait-max fin-retry retry-max  run-peer park-peer   published    pause-us pause-max-us
     3        25        20         5         0         5         353   5173618     30432         1         1       980       353         990      877505       167345  wall=1028894 us

Leaving the remaining convergence-policy checkbox open: this is instrumentation needed for the next budget/policy reduction, not yet the reduction itself.

9 days ago

Landed another mid-script GC maturity slice in 4bf4e35, with dependency support in zig-gc f33d48c.

What changed:

  • Added born-growth and deferred-work round counters to the internal mid-GC driver, and exposed them in zig build midgc-profile. This distinguishes allocation churn from deferred marker work when a generation skips finishing.
  • Rooted the currently dequeued Gil.tasks HoldJob burst through Interpreter.current_hold_jobs. Queued jobs were already traced while in Gil.tasks, but once pumpTasks dequeued a burst they were temporarily invisible during runHoldJob JS/promise work. That was the likely source of the mid-GC asyncHold/promise-object reuse crash.
  • Relaxed the nested asyncJoin cleanup oracle: FinalizationRegistry targets nulled before child release may legitimately become cleanup-ready during mid-script GC. The final exact count/sum oracle remains strict.
  • CI TSan midgc legs now use THREADFUZZ_SEED_TIMEOUT_MS=300000 only for that profile; the 45-case seed exceeds the default 120s under TSan on loaded runners.

Validation:

  • zig build test -Dtest-filter="Value is an engine-wide 8-byte NaN-boxed word"
  • zig build test -Dtest-filter="parallel_js (M3): mid-script parallel collector"
  • zig build midgc-profile
  • zig build threadfuzz -Dfuzz-midgc=true -Dfuzz-iters=1
  • zig build threadfuzz -Dtsan=true -Dfuzz-lifecycle=true -Dfuzz-iters=1
  • THREADFUZZ_SEED_TIMEOUT_MS=300000 zig build threadfuzz -Dtsan=true -Dfuzz-midgc=true -Dfuzz-iters=1
  • git diff --check

Profiler evidence from the latest local run: born-grow was non-zero and deferred was 0, so this workload is currently limited by continued allocation churn rather than deferred tracing.

9 days ago

From the failed main run on a0375cb, the midgc jobs are now pointing at termination/reaction quiescence rather than a TSan data race:\n\n- plain threadfuzz-midgc failed deterministic Worker/Thread termination oracles (missed asyncJoin rejection counts/scores and thread-teardown settlements).\n- tsan-threadfuzz-midgc hit a per-seed timeout/leak report in the weak-collection stressor, with no TSan data-race summary in the log I inspected.\n\nFocused local repros on the current worktree:\n- ./zig-out/bin/threadfuzz midgcworkertermcondasync 1 11 -> still fails with bad reject score 0\n- ./zig-out/bin/threadfuzz midgcworkertermwaitasync 1 19 -> still fails with a short reject score\n- ./zig-out/bin/threadfuzz midgcworkertermteardown 1 18 -> still fails with short settled count\n- ./zig-out/bin/threadfuzz midgcmoduletermteardown 1 6 -> still fails with short settled count\n\nI’m keeping this as its own mid-script-GC/termination workstream rather than blending it with the lifecycle binding-lock fix in 87c4c8b.

9 days ago

Pushed 4d92f18 (fix(threads): preserve teardown reactions during mid-gc).

Root cause addressed: mid-script GC plus top-level failure teardown could lose asyncJoin cleanup reactions in two ways:

  • Promise settlement detached reaction lists from the Promise before copying selected reactions into the microtask queue. A parallel collector could observe neither the Promise list nor the queued microtask during that window.
  • After top-level failure, teardown_stop stayed true while the host/main interpreter drained asyncJoin rejection reactions after spawned threads had exited. If a reaction hit a step checkpoint, the host could throw worker terminated, abort the copied microtask batch, and silently drop later reactions.

Fix:

  • Keep Promise reaction lists attached until selected reactions have been queued, clearing them under the Promise lock afterward.
  • Keep a ThreadRecord settling_joins snapshot rooted while the finishing thread settles asyncJoin promises.
  • Clear teardown_stop after spawned threads exit and before host cleanup microtask/finalization/waitAsync drains.

Focused local validation:

  • midgcworkertermteardown 5 18 -> 0 failures
  • midgcmoduletermteardown 5 6 -> 0 failures
  • midgcworkertermwaitasync 5 19 -> 0 failures
  • midgcworkertermcondasync 5 10 -> 0 failures
  • midgc 1 1 -> 45 programs, 0 failures
  • Promise reaction-list and Thread asyncJoin pending-list unit tests passed.

The previous 641c5e9 CI red was tsan-threadfuzz-midgc seed 1 timeout rather than a semantic oracle line; this commit targets the semantic reaction-loss path and may also reduce the timeout if it was caused by aborted cleanup/drain churn, but CI still needs to prove the TSan budget.

9 days ago

Follow-up pushed in d7c69d6 after CI on 4d92f18 exposed a real TSan midgc race: Context.hasRunningJsThreads was scanning Context.js_threads while a peer Thread constructor appended to the list. The scan now takes the same threading API lock used by Thread construction before walking the list, while still checking each record under join_mutex. Focused local validation: zig build threadfuzz-bin -Dtsan=true && ./zig-out/bin/threadfuzz midgcrestrictlife 2 1 passed.

9 days ago

Follow-up pushed in c03bdf6 for the d7c69d6 CI result: tsan-threadfuzz-midgc no longer reported an engine race, but the 45-subcase midgc seed exceeded the 300s per-seed watchdog under TSan. The watchdog now refreshes before each independently reproducible midgc subcase, so it remains a per-subcase hang detector instead of an aggregate runtime cap for the whole seed. Focused validation: zig build threadfuzz-bin -Dtsan=true, then zig build threadfuzz-bin && ./zig-out/bin/threadfuzz midgc 1 1 (45 programs, 0 failures).

9 days ago

Update pushed after the c03bdf6 CI failure:

  • tsan-threadfuzz-midgc in run https://github.com/zig-utils/zig-js/actions/runs/29121977453 failed with a real TSan race: concurrent Array.prototype.push dense-element growth overlapped zig-gc weak-slot scratch growth during parallel marking. The later “worker terminate/ThreadLocal asyncHold cleanup did not attempt a parallel collection” was in the same failed profile after the TSan warning, so I treated the race as the root signal first.
  • Fixed in dependency zig-utils/zig-gc@a2b8cb2 (fix(gc): reserve weak scratch before marking) by reserving weak_slots before full/minor/parallel concurrent marking, matching the existing mark-stack pre-reservation so markWeak does not allocate/reuse aux pages while mutators grow object backing stores.
  • Local validation against the pushed zig-js state plus patched zig-gc:
    • zig build threadfuzz-bin -Dtsan=true --summary none
    • THREADFUZZ_SEED_TIMEOUT_MS=300000 ./zig-out/bin/threadfuzz midgc 1 1 → threadfuzz midgc: 45 programs from seed 1, 0 failures
  • Pushed zig-js@e2dfea18; new CI run is https://github.com/zig-utils/zig-js/actions/runs/29122992826.
9 days ago

CI follow-up from the 441b2c4 run: threadfuzz-midgc crashed during parallel-GC root tracing in zig-gc Visitor.isManaged(), where a mixed-ownership raw root edge could header-peek a stale/wild pointer before proving it belonged to the heap.

Fixed in dependency commit zig-utils/zig-gc@984ae51 by making isManaged() walk the live-cell list for an exact payload match under the allocation lock in parallel mode, plus a regression covering live GC, stack, and deliberately wild pointer inputs.

Validation so far:

  • zig build test --summary none in zig-gc
  • zig build threadfuzz-bin --summary none && ./zig-out/bin/threadfuzz midgcpromise 1 1 && git diff --check in zig-js

The original workflow is still running, so GitHub has not allowed rerunning the failed job yet. Once it completes, rerun threadfuzz-midgc/failed jobs against the new zig-gc/main.

9 days ago

CI follow-up: the c2b5e61 push is intended to rerun mid-GC on top of zig-utils/zig-gc@6ed2ca8. The prior 66027de run showed the next unsafe maybe-managed edge in zig-gc writeBarrierFrom()/rememberStrongStore during threadfuzz-midgc root publication. That dependency fix changes strong/owner-aware/weak/incremental barriers to prove exact heap membership before reading headers, with a regression for stack and deliberately wild pointer inputs.

Local validation before the rerun:

  • zig build test --summary none in zig-gc
  • zig build threadfuzz-bin --summary none && ./zig-out/bin/threadfuzz midgcpromise 1 1 in zig-js
9 days ago

CI follow-up from run 29126983628: threadfuzz-amplified failed because seed 1 exceeded the 120s per-seed watchdog. I reproduced locally with zig build threadfuzz -Dfuzz-amplify=true -Dfuzz-iters=1 -Dfuzz-seed=1. Sampling showed active worker execution, not a parked deadlock; hot time was in zig-gc headerForPayload via broad write barriers after the exact maybe-managed safety change.\n\nDependency fix pushed to zig-gc main as 49c7d8f (perf(gc): index live payload lookups). It keeps exact live-payload membership for wild/stale pointers but avoids walking the full live list on each barrier.\n\nValidation:\n- zig-gc: zig build test --summary none\n- zig-js focused repro: ./zig-out/bin/threadfuzz amplify 1 1 => 0 failures (~19s locally).\n\nNext zig-js push should pick up the updated zig-gc checkout in CI.

9 days ago

Pushed 0f5a438 (gc: back off failed mid-script retry churn) for the convergence-policy item.

Profile signal before the change was allocation-growth-bound rather than publication-timeout-bound (pub-abort=0, deferred=0, round-limit aborts with most generations seeing born growth). The driver now keeps the existing abort-safe behavior, but after a publication-timeout or round-limit abort it sets a tiny internal retry cooldown so later safepoints do not immediately elect another expensive collector attempt under the same sustained allocation burst. Successful finishing sweeps clear the retry marker. This remains internal parallel_midscript_gc policy, not public API, and quiescent full GC remains the reclaim fallback.

Telemetry/docs update:

  • Added gc_par_backoff_skips and gc_par_retry_after_ns internal counters/state.
  • zig build midgc-profile now reports backoff skips next to attempts/sweeps/aborts/pause.
  • Updated the mid-GC docs/testing notes to describe post-abort retry suppression.

Focused validation:

  • zig build test -Dtest-filter='parallel_js (M3): mid-script parallel collector' --summary none
  • zig build test -Dtest-filter='parallel_js (M3): sync wait peers publish roots' --summary none
  • zig build midgc-profile
  • bun run docs:build
  • git diff --check

Latest local profile after the change:

zig-js mid-script parallel GC telemetry (internal testing policy)
  runs  attempts    sweeps    aborts pub-abort rnd-abort generations wait-poll  wait-max fin-retry retry-max born-grow  deferred  run-peer park-peer   published   backoff    pause-us pause-max-us
     3        30        27         3         0         3         297   3775037     28686         3         2       267         0       807       297         819        10      814832       144442  wall=997550 us

The important new evidence is backoff=10: the profile now exposes and exercises suppression of immediate post-abort retry churn. Leaving the broader policy/depth checkboxes open until CI/nightly data supports further budget or production-policy changes.

9 days ago

Landed 13919e1 (gc(midgc): extend born-growth convergence rounds).

What changed:

  • Kept the existing 32-generation base cap, but added a bounded extension window up to 64 generations only while the attempt is still making born-cell progress or just raced allocation during the self-checking finish, and only when no deferred work is pending.
  • Kept abort-without-sweep as the fallback when the extension no longer has a safe/progress signal.
  • Added ext-rnd to zig build midgc-profile so extension use is visible instead of implicit.
  • Extended the focused telemetry coherence check to assert extension rounds stay bounded by total generations.
  • Updated the policy docs in testing/production-readiness/limits/P7 notes.

Validation:

  • zig build midgc-profile
  • zig build test -Dtest-filter='parallel_js (M3): mid-script parallel collector' --summary none
  • zig build test -Dtest-filter='parallel_js (M3): sync wait peers publish roots' --summary none
  • bun run docs:build
  • git diff --check

Local profile before the change:

runs=3 attempts=23 sweeps=15 aborts=8 pub-abort=0 rnd-abort=8 generations=414 wait-poll=9233864 wait-max=489437 fin-retry=2 retry-max=1 born-grow=397 deferred=0 backoff=133 pause-us=1058000 pause-max-us=244625 wall=1169673 us

Local profile after the change:

runs=3 attempts=23 sweeps=19 aborts=4 pub-abort=0 rnd-abort=4 generations=415 wait-poll=6287799 wait-max=295443 fin-retry=11 retry-max=3 born-grow=385 ext-rnd=128 deferred=0 backoff=186 pause-us=1023930 pause-max-us=211557 wall=1164689 us

This is a small policy reduction in reliance on round-limit fallback for the workload #14 identified: allocation/born growth, not publication timeout (pub-abort remains 0). CI is pending at https://github.com/zig-utils/zig-js/actions/runs/29135827218.

9 days ago

Follow-up landed in db57aa3 (test(threadfuzz): allow bounded midgc TLS cleanup). The mid-GC Worker/ThreadLocal asyncHold teardown oracle now distinguishes the intentionally released TLS-owner case from true premature cleanup: non-mid-GC still requires zero early cleanup, while mid-GC allows only a bounded valid subset before top-level teardown and still requires exact final cleanup count/sum.\n\nLocal focused validation:\n- zig build threadfuzz-bin --summary none\n- ./zig-out/bin/threadfuzz midgcworkertlsasyncholdcleanup 20 1\n- ./zig-out/bin/threadfuzz midgcmoduleworkertlsasyncholdcleanup 20 1\n- ./zig-out/bin/threadfuzz workertlsasyncholdcleanup 5 1\n- ./zig-out/bin/threadfuzz moduleworkertlsasyncholdcleanup 5 1\n- git diff --check

9 days ago

Nightly scheduled CI found a mid-script GC witness bug in the Atomics.Mutex.lockIfAvailable subprogram:

Root cause: the witness was over-strict. Timeout-path lockIfAvailable waiters may legitimately expire, return, and drop their registered targets while acquire-after-release peers are still parked behind the holder. The invariant we actually need is that acquire-path waiter roots do not clean up while those peers remain parked.

Landed e00d5858 (test(threadfuzz): allow lockIfAvailable timeouts during midgc):

  • Tracks acquire-path and timeout-path cleanup separately in runMidScriptAtomicsMutexLockIfAvailableGc.
  • Forbids acquire-path cleanup while parked peers are still behind the holder.
  • Allows timeout-path cleanup during that window, then still requires exact final cleanup after quiescence.
  • Updates the thread docs to describe that corrected invariant.

Local validation:

  • /Users/chris/.local/bin/zig build threadfuzz -Dfuzz-midgc=true -Dfuzz-iters=1
  • /Users/chris/.local/bin/zig build threadfuzz -Dfuzz-midgc=true -Dfuzz-iters=5 (225 programs from seed 1, 0 failures)
  • /Users/chris/.bun/bin/bun run docs:build
  • git diff --check

CI pending on the pushed fix: https://github.com/zig-utils/zig-js/actions/runs/29193634788

9 days ago

Follow-up: the pushed mid-GC invariant fix is CI-green.

Run: https://github.com/zig-utils/zig-js/actions/runs/29193634788 Result: 31 success, 1 expected skip, 0 failures.

The relevant jobs both passed:

  • threadfuzz-midgc
  • tsan-threadfuzz-midgc
9 days ago

Landed another focused #14 coverage slice in zig-js@6abd3634 (test(midgc): cover native callback closure roots).

What changed:

  • Added a mid-script parallel-GC witness where no-GIL workers repeatedly enter a VM-lowered closure through native Array.prototype.map callback entry.
  • The callback reads captured upvalues plus a nested object while workers allocate enough to elect the mid-script collector.
  • The test asserts the exact JS result and parallel-GC telemetry, including running-peer root requests and peer publications, so native-callback/tree-walker entry, captured-root publication, and the mid-GC barrier are checked together.
  • Updated the thread testing / production-readiness notes to record this new witness.

Local validation batched before push:

  • /Users/chris/.local/bin/zig build test -Dtest-filter="native callback VM closures survive mid-script parallel GC" --summary all
  • /Users/chris/.local/bin/zig build test -Dtest-filter="parallel_js (M3): mid-script parallel collector" --summary all
  • /Users/chris/.local/bin/zig build test -Dtest-filter="parallel_js (M3): thrown Thread completion roots" --summary all
  • /Users/chris/.bun/bin/bun run docs:build
  • git diff --check

CI for this push is pending: https://github.com/zig-utils/zig-js/actions/runs/29200500880

This advances the “nested native callback / deep VM-tree-walker coverage” part of the remaining #14 work. Multi-context and host re-entry depth are still separate follow-up slices.

9 days ago

CI routing note: the run for 6abd3634 was superseded by the follow-up #36 push. The current main-branch gate that includes the native-callback mid-GC witness is now: https://github.com/zig-utils/zig-js/actions/runs/29200654888

9 days ago

CI routing note: the previous current gate was superseded by the follow-up #36 iterator-helper push. The current main-branch gate that includes the native-callback mid-GC witness is now: https://github.com/zig-utils/zig-js/actions/runs/29200732250

9 days ago

CI routing note: the previous current gate was superseded by the follow-up #36 async-generator push. The current main-branch gate that includes the native-callback mid-GC witness is now: https://github.com/zig-utils/zig-js/actions/runs/29200819725

9 days ago

CI routing note: the previous current gate was superseded by the follow-up #36 generator-handler push. The current main-branch gate that includes the native-callback mid-GC witness is now: https://github.com/zig-utils/zig-js/actions/runs/29200885804

9 days ago

Landed zig-js@4b20837b (profile(midgc): count deferred aborts) as a #14/#36 observability slice.

What changed:

  • Added internal gc_par_deferred_aborts telemetry for elected parallel-GC attempts that abort because deferred generator/iterator cells prevent a safe no-GIL finish.
  • Kept the existing abort accounting identity intact: these remain round-limit aborts, and deferred_aborts <= round_aborts is now asserted by focused telemetry coherence.
  • zig build midgc-profile now prints a def-abort column next to pub-abort / rnd-abort.
  • The #36 fail-closed witnesses now assert deferred abort telemetry, not only deferred rounds.
  • Updated the mid-GC docs to describe the split.

Focused validation:

  • /Users/chris/.local/bin/zig build test -Dtest-filter="deferred generator handlers" --summary all
  • /Users/chris/.local/bin/zig build midgc-profile --summary all
  • /Users/chris/.bun/bin/bun run docs:build
  • git diff --check

Local profile output included the new column; this allocation-churn profile had def-abort=0, as expected because it does not root deferred generator/iterator cells:

runs=3 attempts=9 sweeps=5 aborts=4 pub-abort=0 rnd-abort=4 def-abort=0 generations=368 deferred=0 backoff=92

CI for this push is pending: https://github.com/zig-utils/zig-js/actions/runs/29201026809

9 days ago

Green gate note for d0092f5e: https://github.com/zig-utils/zig-js/actions/runs/29201642565

The deferred-abort telemetry/docs slice and native-callback closure-root coverage are covered by the successful combined gate. threadfuzz-midgc, threadfuzz-lifecycle, and the TSan threadfuzz variants all completed green.

9 days ago

Green gate note for c6856a28: https://github.com/zig-utils/zig-js/actions/runs/29203285907

The mid-script GC gate is green after widening only the non-TSan threadfuzz-midgc per-seed watchdog to match the existing 300s TSan mid-GC setting. The failed seed (midgc seed 5) replayed locally in ~23s with zero failures, so the change treats loaded-runner watchdog drift as CI configuration rather than weakening the semantic oracle.

9 days ago

CI observation while validating b09fd1e: the first main run hit the known low-frequency mid-script GC convergence oracle in threadfuzz-midgc:

Follow-up:

  • Local targeted repro passed: THREADFUZZ_SEED_TIMEOUT_MS=300000 zig build threadfuzz -Dfuzz-midgc=true -Dfuzz-iters=1 -Dfuzz-seed=8 --summary all → 45 programs from seed 8, 0 failures.
  • GitHub failed-job rerun passed; final run state is green (31 success, 1 skipped).

No new issue opened: this maps directly onto this issue’s remaining mid-script parallel-GC convergence/termination-reaction work.

9 days ago

Landed a small #14 hardening slice in 0f645145 (gc: service teardown safepoints around joins).

What changed:

  • During parallel-JS teardown joins, the host interpreter now services the GC safepoint before taking each child join lock and after observing that child as exited.
  • It also services one final teardown safepoint before clearing teardown_stop and draining asyncJoin rejection cleanup.

Why:

  • The previous keepalive path only published/drove the host safepoint inside while (!rec.exited). If a child had already exited, or exited after the host pump/safepoint but before the host parked, teardown could skip the host publication/drive opportunity that helps an in-flight mid-script parallel collection converge. That matched the rare threadfuzz-midgc seed-8 CI symptom from the prior run.
  • This does not change sweep accounting, retry policy, or collector safety rules; it just closes a skipped-safepoint edge in teardown.

Focused validation:

  • zig fmt src/context.zig
  • zig build threadfuzz-bin --summary all
  • ./zig-out/bin/threadfuzz midgcterm 5 8 → 5 programs, 0 failures
  • THREADFUZZ_SEED_TIMEOUT_MS=300000 zig build threadfuzz -Dfuzz-midgc=true -Dfuzz-iters=1 -Dfuzz-seed=8 --summary all → 45 programs, 0 failures
  • git diff --check

Next #14 pass I’d look at whether teardown should deliberately force/drive one bounded final parallel collection attempt when heap pressure is already over threshold, rather than only opportunistically servicing safepoints.

9 days ago

Follow-up CI fix landed in e132c6fe (gc: skip teardown safepoint after joins).

CI for 0f645145 exposed a deterministic local repro in unit (shard 2/4):

  • failing test: C-API: JSValueProtect roots survive mid-script parallel GC
  • symptom: protected object property tag expected 456, got NaN

Root cause of the previous slice:

  • The new teardown safepoints could still run after JS had manually joined every child thread, i.e. when there were no live peer records left.
  • That all-exited tail collection was not the intended #14 window; it could satisfy “a parallel collection happened” counters after the shared-realm work was already quiescent, and it made the C-API protected-handle witness fail.

Fix:

  • Keep the useful per-record teardown safepoint ticks, but guard them with hasRunningJsThreads() so they only run while at least one spawned record is still live.
  • Remove the post-loop all-exited safepoint entirely.

Focused validation after the fix:

  • zig build test -Dtest-filter="C-API: JSValueProtect roots survive mid-script parallel GC" --summary all
  • ./zig-out/bin/threadfuzz midgcterm 5 8
  • THREADFUZZ_SEED_TIMEOUT_MS=300000 zig build threadfuzz -Dfuzz-midgc=true -Dfuzz-iters=1 -Dfuzz-seed=8 --summary all
  • git diff --check
9 days ago

Pushed e9df0649 (gc: harden parallel publication generations) as a small mid-script parallel-GC handshake hardening slice.

What changed:

  • Split active root-publication request state from the generation source: gc_par_request is now 0 while idle, and gc_par_next_request mints non-zero generations.
  • Peer publication/completion checks now require exact generation equality rather than monotonic >= / < comparisons, so a stale high generation cannot satisfy a fresh low generation after counter wrap.
  • Added a focused wraparound witness: parallel_js (M3): publication generations skip idle zero at wrap.
  • Updated the testing docs to record the idle-zero/wrap invariant.

Focused validation:

  • zig build test -Dtest-filter="publication generations skip idle zero" --summary all
  • zig build test -Dtest-filter="parallel_js (M3): mid-script parallel collector reclaims garbage" --summary all
  • zig build test -Dtsan=true -Dtest-filter="publication generations skip idle zero" --summary all
  • THREADFUZZ_SEED_TIMEOUT_MS=300000 ./zig-out/bin/threadfuzz midgc 1 1 → 45 programs, 0 failures
  • bun run docs:build
  • git diff --check

The previous combined-head run for 7e0afd2e was clean at 24 success / 1 skipped / 0 failures before this push superseded it. New CI for e9df0649 is pending: https://github.com/zig-utils/zig-js/actions/runs/29216356732

9 days ago

Pushed 31313a8 (test: stabilize midgc worker-close oracle) after the prior main run exposed a narrow harness race in threadfuzz-midgc.

Root cause:

  • the worker-close profile started asyncJoin() reactions, synchronously joined every worker, then used a single Promise.resolve().then(...) before checking the async aggregate;
  • a synchronous join() may return just before its corresponding asyncJoin() reaction runs, so the check could observe asyncSum == 0 even though worker completion was correct.

Fix:

  • retain every returned asyncJoin() promise and gate the final oracle with Promise.all(asyncJoins), matching the established neighboring terminate/finalization pattern.

Focused validation:

  • zig build threadfuzz-bin --summary all
  • THREADFUZZ_SEED_TIMEOUT_MS=300000 ./zig-out/bin/threadfuzz midgcworkerclose 1 15 → 1 program, 0 failures
  • THREADFUZZ_SEED_TIMEOUT_MS=300000 ./zig-out/bin/threadfuzz midgcworkerclose 25 15 → 25 programs, 0 failures
  • zig fmt --check conformance/threadfuzz.zig
  • git diff --check

Fresh combined CI gate: https://github.com/zig-utils/zig-js/actions/runs/29218852375

9 days ago

Closing this tracker with an explicit support decision.\n\nThe internal mid-script parallel collector now has:\n- running-peer generation-based root publication and direct frozen parked-peer tracing;\n- abort-without-sweep behavior when publication, born-cell stability, or deferred generator/iterator tracing cannot converge;\n- bounded wait pumping, retry backoff, born-growth extension rounds, and detailed abort/pause/peer telemetry;\n- waiter, async job, ThreadLocal, finalization, join, Worker, termination, teardown, native-callback closure, and multi-context stress coverage;\n- deterministic lifecycle/mid-GC oracles with per-seed watchdog/replay, including the final released-waiter join correction in bdb369e7;\n- documented root-publication and lock-order invariants.\n\nProduction-policy decision: parallel_midscript_gc remains an internal testing/profile capability, not stable embedder API. The supported collector may abort this optimization without sweeping and fall back to quiescent/full collection. Bounded publication polling, round limits, deferred-cell aborts, and backoff are load-bearing safety policy, not correctness defects to remove indiscriminately. Promoting the path to public production policy would require new evidence and should get a dedicated issue.\n\nThe current higher-depth TSan mid-GC, lifecycle, no-GIL corpus, and deterministic gates are green: https://github.com/zig-utils/zig-js/actions/runs/29232877388 — 31 success, 1 expected skip, 0 failures. That meets this tracker acceptance without exposing the test knob or weakening fail-closed behavior.

Sign in to comment on this issue.