fix(asyncify): consume-once root suspensions — replaces the wake-window deferral

The deferral (9ca2ac1) modeled the wrong condition and taxed every parked
fiber completion with a macrotask hop: under CI load that stretched
three-client apply chains and flaked drift-trio S4 twice consecutively
(26/26 green locally under stress) — retracted.

The actual fatal state, readable in all four prod stacks once seen: a SECOND
rewind of the same root suspension. Root suspends once per fiber_swap out of
it; two parked fibers completing against one root suspension epoch (a tool
fiber + a collab fiber both waking around open:settled) each drive
finishContextSwitch(root) — the second rewinds already-consumed data →
"unreachable executed" → poisoned runtime, with the wake-side "index out of
bounds" as the sibling symptom.

Cure: stop exempting root from the validity check the shim already keeps.
First consumption proceeds synchronously — zero added latency anywhere; the
second is refused ([wx-asyncify] "root suspension already consumed") — the
yielded fiber stays properly suspended and resumable, root continues via its
real pending resume, libcontext's ghost-epoch contract enforced one layer
lower. Root remains exempt only from the internally-parked quarantine (its
yield park is routine).

.ci-cache-epoch 4→5 (the epoch-4 cache holds the retracted deferral shim).

Local: fiber 2/2 + timer 1/1, firefox sweep 20 passed, drift-trio-scenarios
kicad-chromium 26/26 under 3-worker stress, web fatal+follow 2/2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
This commit is contained in:
Gergő Törcsvári 2026-08-01 15:53:18 +02:00
commit ae33a100c2
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
3 changed files with 40 additions and 42 deletions

View file

@ -1 +1 @@
4
5

View file

@ -127,12 +127,26 @@ the fiber's `finishContextSwitch` rewind of `main+20` — replaying frames
over live state. The 8 ms-earlier "index out of bounds" is the wake side of
the same collision.
Root entry is legal and constant in healthy flow; ONLY the wake-window
overlap is fatal. **Layer 3: serialize, don't refuse.** The shim marks the
synchronous wake window (`Asyncify.__inSleepWake` around `wakeUp()`);
`finishContextSwitch(root)` inside that window is DEFERRED one macrotask
(`[wx-asyncify] root-entry-deferred` beacon) and re-fired via the trampoline
once the wake has settled. Ordering change only — nothing is dropped.
**Layer 3, first attempt (wake-window deferral) — RETRACTED same day:**
deferring every root entry inside a sleep-wake window taxed EVERY parked
fiber completion with a macrotask hop; under CI load that stretched
three-client apply chains and flaked drift-trio S4 twice in a row (green
26/26 locally). It also modeled the wrong condition.
**Layer 3, final: consume-once root suspensions.** The actual fatal state is
a SECOND rewind of the same root suspension: root suspends once per
`fiber_swap` out of it, but two parked fibers completing against one root
suspension epoch (a tool fiber + a collab fiber both waking around
`open:settled`) each trigger `finishContextSwitch(root)` — the second rewinds
already-consumed data → "unreachable executed". The shim already records
every suspension (including root's); the fix is simply to stop exempting
root from the validity check: first consumption proceeds synchronously (zero
added latency anywhere), the second is refused
(`[wx-asyncify] fiber-resume-refused: root suspension already consumed`) —
the yielded fiber stays properly suspended and resumable, root continues via
its real pending resume, same contract as libcontext's ghost epochs enforced
one layer lower. Root stays exempt ONLY from the internally-parked
quarantine (its yield park is routine — the 19-red lesson).
## The white screen, round 3 (fixed at the DOM level)

View file

@ -297,48 +297,32 @@ if (typeof Fibers !== "undefined"
}
var isRoot = newFiber === Fibers.__rootFiber;
// The prod killer (four identical trap stacks, v0.1.19–22): a fiber
// completes and re-enters the ROOT while a sleep wake's own rewind is
// still on the stack — two "resume main" paths interleaved in one tick,
// doRewind(root) replays over live state, "unreachable executed",
// poisoned runtime. Root entry is legal and constant in healthy flow;
// ONLY the wake-window overlap is fatal. Defer it by one macrotask so
// the wake settles first — ordering change only, nothing is dropped.
if (isRoot && (Asyncify.__inSleepWake || 0) > 0) {
var deferred = newFiber;
Fibers.__rootDeferrals = (Fibers.__rootDeferrals || 0) + 1;
if (Fibers.__rootDeferrals <= 10 || Fibers.__rootDeferrals % 100 === 0) {
console.warn("[wx-asyncify] root-entry-deferred: fiber completion landed inside a "
+ "sleep-wake window; retrying next tick (occurrence "
+ Fibers.__rootDeferrals + ")");
}
__fcsRec("defer root new=" + deferred);
var retry = function() {
if (Fibers.trampolineRunning || Fibers.nextFiber) {
setTimeout(retry, 0); // another switch in flight — wait our turn
return;
}
__fcsRec("defer-retry new=" + deferred);
Fibers.nextFiber = deferred;
Fibers.trampoline();
};
setTimeout(retry, 0);
return;
}
var HEAPU32v = (typeof GROWABLE_HEAP_U32 === "function") ? GROWABLE_HEAP_U32() : HEAPU32;
var entryPoint = HEAPU32v[((newFiber + 12) >>> 2) >>> 0];
if (!isRoot && Fibers.__internallyParked.has(newFiber)) {
// Root is exempt from THIS check only: it "parks" in the main loop's
// yield as a matter of course (quarantining it starved every coroutine
// return — 19 collab e2e reds on the first guard build).
__refuseFiber(newFiber, "is asyncify-parked mid-body (sleep in flight)");
return;
}
if (!isRoot && entryPoint === 0) {
// Suspended-fiber path: about to rewind newFiber+20. (The root is
// exempt: re-entering it with an older suspension is the long-standing
// ghost-resume flow, resolved by libcontext's epoch machinery.)
if (entryPoint === 0) {
// Suspended-fiber path: about to rewind newFiber+20 — root INCLUDED.
// Consume-once semantics are the actual prod killer's cure (all four
// trap stacks, v0.1.19–22): each fiber_swap suspension is rewindable
// exactly once. Two fibers completing against ONE root suspension
// epoch (a tool fiber and a collab fiber both waking around
// open:settled) makes the second finishContextSwitch(root) rewind
// already-consumed data → "unreachable executed" → poisoned runtime.
// Refusing the second consumption loses nothing: the fiber that
// yielded stays properly suspended (recorded above), and the root
// continues via its real pending resume (its own sleep wake or the
// next fresh JS entry) — the same contract as libcontext's
// ghost-resume epochs, enforced one layer lower.
if (!Fibers.__validSuspensions.has(newFiber)) {
__refuseFiber(newFiber, "has no live suspension - rewinding would replay stale data");
__refuseFiber(newFiber, isRoot
? "root suspension already consumed - a second rewind would replay stale frames"
: "has no live suspension - rewinding would replay stale data");
return;
}
Fibers.__validSuspensions.delete(newFiber);