ReviewOS

zig-utils/zig-js

JIT: order Class-A invalidation against shared-GC waits

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

Parent: #433 Unblocked: #429 Child: #457

Integrate optimizer invalidation/jettison with the shared-GC stop protocol so a Class-A stop cannot publish behind a mutator waiting for collection completion. Dependent code must be invalid before mutators resume.

Acceptance

  • Serialize stop publication with shared-GC conductor/completion ordering.
  • Bump the optimizer invalidation generation inside the stop window before resume.
  • Jettison dependent tiers while native polls preserve exact resumable state and roots.
  • Prove bounded convergence under concurrent allocation/collection pressure.
  • Add poll/resume and both GC-wait-vs-Class-A normal + TSan witnesses.

Completion

Implemented by 26ef6127, 01306740, and the inherited-property prerequisite in #457. Both pinned GC-wait/Class-A executables were promoted by 3f2fe88b only after normal and TSan runs proved nonzero optimizer publication, Class-A invalidation, and cooperative collection. Final closure evidence is recorded in docs/.data/jit-concurrency-2026-07-22.md by ead94ad8.

9 days ago

26ef612/d845cc4 completes the shared conductor and core evidence. The heap-owned gate is shared by precise sibling realms; cooperative, abort-safe M3, allocation-failure, quiescent, and moving collectors enter it before publication, while Class-A invalidation takes it before Owner.clear bumps the generation and jettisons tiers. Focused normal/TSan results: conductor 3/3, cooperative rendezvous 3/3, exit cleanup 3/3, M3 wait-peer publication 3/3, and allocation recovery 4/4. The first four acceptance items are checked. This issue stays open for explicit promotion of the two pinned mc-safe-gcwait-vs-classa-stop executables rather than treating internal witnesses as corpus completion.

9 days ago

Checkpoint 01306740 / 427ba867 wires real parallel-realm named-property mutation into Class-A invalidation. VM fast-slot and canonical writes rotate the executable generation only after native heap assumptions exist; a waiting mutator remains a cooperative-GC participant while contending for the shared conductor. Tree-walker→VM function calls now acquire the same outer execution epoch as top-level VM runs. Focused Class-A evidence passes 4/4 normally and under TSan; README and detailed evidence are current.

The exact two PR-249 witnesses remain intentionally unpromoted: diagnosis proved their return o.y + 1 victim depends on inherited-property specialization/watchpoints that zig-js does not yet publish. The mechanism-only case completed seven real cooperative collections but zero Class-A fires before this batch; the rope-bearing case additionally exposed its existing concurrent ReferenceError: i. #457 now owns the non-vacuous prototype-chain/watchpoint prerequisite and exact promotion gate.

9 days ago

Final acceptance item ("poll/resume and both GC-wait-vs-Class-A normal + TSan witnesses") is now met, so this closes.

The prerequisite that kept it open was the non-vacuous inherited-property assumption, now delivered under #457 (16b2a1a2/a757cd1a/2bdb47be). With it in place both pinned executables run a real published assumption through the shared conductor and were promoted in 3f2fe88b:

cve/mc-safe-gcwait-vs-classa-stop.js                publications=18 invalidations=13 collections=1
cve/mc-safe-gcwait-vs-classa-stop-noropevariant.js  publications=18 invalidations=13 collections=1

Identical under ThreadSanitizer with no race reported. That is the discrimination this issue asked for: the mechanism-only case previously completed real cooperative collections but zero Class-A fires, and promoting it on the green assertion alone would have been exactly the bookkeeping promotion #429 forbids. Poll/resume evidence is the 89fc073e cooperative-stop / expired-watchdog / shell-timeout set, where an optimized loop reconstructs before the boundary and bytecode services each request at step 1,024 in normal and TSan runs.

Conductor evidence from the earlier batch stands: conductor 3/3, cooperative rendezvous 3/3, exit cleanup 3/3, M3 wait-peer publication 3/3, allocation recovery 4/4, focused Class-A 4/4 normal and TSan.

Inventory: 243 → 245 promoted, 10 → 8 blocked of 259. Remaining #433 work is the corpus evidence for the still-blocked optimizer cases, tracked on #429/#433.

9 days ago

Follow-up defect from the shared conductor landed here, found while investigating why a full zig build test took 2h13m without finishing.

Context.collectGarbage takes the JIT/GC conductor and then runs the collection, and the sweep's post-sweep drain invokes host finalizers. The gate is a plain non-reentrant mutex, so a finalizer that calls back into collection spun against the lock its own thread already held — an infinite loop at 100% CPU, not a slow path.

This is not a hypothetical re-entry. runDeferredPostSweepCallbacks documents it as supported ("drained to a fixed point so reentrant collections are harmless"), the C API lets a JSObjectMakeArrayBufferWithBytesNoCopy deallocator call JSGarbageCollect, and one of the two tests that hung is literally named "C-API: class finalizers run once after sweep and may reenter collection". Sampled stack at the hang:

JSGarbageCollect -> collectGarbage -> collect -> finishMarking -> runAfterSweep
  -> afterSweep -> runDeferredPostSweepCallbacks -> ExternalBufferOwner.release
    -> <host deallocator> -> JSGarbageCollect -> collectGarbage
      -> enterJitGcConductor   [spinning, 100% CPU]

The conductor now records its holding thread, and collectGarbage declines a re-entry from a thread already inside it — the same shape as the two checks already above it, which decline when JS threads are running or a parallel collector is elected. Declining is the correct answer rather than making the gate reentrant: a nested collection would run against a heap that is mid-sweep, and the outer collection sweeps everything the nested one would have. Ownership is recorded in enterJitGcConductor/leaveJitGcConductor rather than at the one call site, so re-entry from allocation-failure and quiescent collections — which also run finalizers — is covered too.

Both tests now pass instead of hanging: 35 ms and 2,329 ms. A focused regression test pins the guard directly so a future conductor change cannot reintroduce this without a failure that names the cause.

Fixed on main; the two-hour suite this was hiding inside is separately addressed by 5c7c4225 (zig build test-parallel), which is also what made the two hung tests visible as the long pole rather than an opaque wait.

Sign in to comment on this issue.