fix(asyncify): layer 3 — serialize root re-entry out of sleep-wake windows + flight recorder
v0.1.22 still trapped with BOTH guards silent: the fatal rewind's target is the ROOT context, which layer 2 exempted. All four prod stacks are the same collision — a fiber completes its yield-back to main while main's sleep-wake rewind is still on the stack (maybeStopUnwind → trampoline → finishContextSwitch → doRewind(root) → unreachable), two "resume main" paths interleaved in one tick; the 8ms-earlier "index out of bounds" is the wake side of the same event. Root entry is legal and constant in healthy flow; only the wake-window overlap is fatal. So: serialize, don't refuse. The shim marks the synchronous wake window (Asyncify.__inSleepWake around wakeUp) and DEFERS a root finishContextSwitch landing inside it by one macrotask ([wx-asyncify] root-entry-deferred beacon, trampoline retry) — an ordering change only, nothing dropped. Suspension recording happens before the deferral branch, so the yielding fiber's validity survives the wake chain nulling currData. Plus a flight recorder: a 96-entry ring of asyncify/fiber events (sleeps, wakes, every context switch with ROOT/wake-depth, refusals, deferrals), silent in normal operation, auto-dumped with full machine state next to the first trap signature in the console; window.__wxAsyncifyDump() on demand. The next prod export reads like a black box, not a stack-shape puzzle. .ci-cache-epoch 3→4 (wasm cache key omits scripts/**). Local: fiber-resume-park 2/2 (one refusal beacon), timer-park 1/1, sweep 20 passed, 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:
parent
f488744797
commit
9ca2ac1e52
2 changed files with 107 additions and 1 deletions
|
|
@ -1 +1 @@
|
||||||
3
|
4
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,59 @@ if (typeof Asyncify !== "undefined") {
|
||||||
// swaps don't allocate through allocateData). Make every repair and every
|
// swaps don't allocate through allocateData). Make every repair and every
|
||||||
// concurrent-park window loud, so a saved console dump answers that.
|
// concurrent-park window loud, so a saved console dump answers that.
|
||||||
// Rate-limited per kind: first 10 in full, then every 100th.
|
// Rate-limited per kind: first 10 in full, then every 100th.
|
||||||
|
// Flight recorder: a capped ring of asyncify/fiber events (never printed
|
||||||
|
// during normal operation), dumped to the console ONCE when a trap
|
||||||
|
// signature surfaces — so a prod console export carries the exact event
|
||||||
|
// sequence and machine state at death instead of just stack shapes.
|
||||||
|
// window.__wxAsyncifyDump() returns it on demand.
|
||||||
|
var __recMax = 96;
|
||||||
|
Asyncify.__rec = [];
|
||||||
|
var __rec = function(ev) {
|
||||||
|
var r = Asyncify.__rec;
|
||||||
|
r.push(((typeof performance !== "undefined" ? performance.now() : 0) | 0) + " " + ev);
|
||||||
|
if (r.length > __recMax) r.shift();
|
||||||
|
};
|
||||||
|
Asyncify.__recPush = __rec;
|
||||||
|
|
||||||
|
var __dumpState = function() {
|
||||||
|
var F = (typeof Fibers !== "undefined") ? Fibers : null;
|
||||||
|
var pend = Array.isArray(Asyncify.__pendingSleepContexts)
|
||||||
|
? Asyncify.__pendingSleepContexts.map(function(c) { return c.capturedData || 0; }).join(",")
|
||||||
|
: "n/a";
|
||||||
|
var head = "[wx-asyncify] STATE"
|
||||||
|
+ " state=" + Asyncify.state
|
||||||
|
+ " currData=" + (Asyncify.currData || 0)
|
||||||
|
+ " inSleepWake=" + (Asyncify.__inSleepWake || 0)
|
||||||
|
+ " exportStack=" + (Asyncify.exportCallStack ? Asyncify.exportCallStack.length : -1)
|
||||||
|
+ " pendingSleeps=[" + pend + "]"
|
||||||
|
+ (F ? (" nextFiber=" + F.nextFiber
|
||||||
|
+ " trampolining=" + F.trampolineRunning
|
||||||
|
+ " root=" + F.__rootFiber
|
||||||
|
+ " valid=[" + (F.__validSuspensions ? Array.from(F.__validSuspensions).join(",") : "") + "]"
|
||||||
|
+ " parked=[" + (F.__internallyParked ? Array.from(F.__internallyParked).join(",") : "") + "]"
|
||||||
|
+ " deferrals=" + (F.__rootDeferrals || 0))
|
||||||
|
: " (no Fibers)");
|
||||||
|
return head + "\n[wx-asyncify] RECORDER (oldest first):\n " + Asyncify.__rec.join("\n ");
|
||||||
|
};
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.__wxAsyncifyDump = __dumpState;
|
||||||
|
// Auto-dump beside the first trap signatures in the console — the one
|
||||||
|
// artifact prod reports reliably contain.
|
||||||
|
var __dumps = 0;
|
||||||
|
var __onTrap = function(msg) {
|
||||||
|
if (__dumps >= 2) return;
|
||||||
|
if (!/index out of bounds|unreachable executed|table index|indirect call signature|null function or function signature|memory access out of bounds/i.test(msg)) return;
|
||||||
|
++__dumps;
|
||||||
|
try { console.error(__dumpState()); } catch (e) {}
|
||||||
|
};
|
||||||
|
window.addEventListener("error", function(e) {
|
||||||
|
__onTrap(e && e.error instanceof Error ? e.error.message : String((e && e.message) || ""));
|
||||||
|
});
|
||||||
|
window.addEventListener("unhandledrejection", function(e) {
|
||||||
|
__onTrap(e && e.reason instanceof Error ? e.reason.message : String((e && e.reason) || ""));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var __wxAsyncifyReport = (function() {
|
var __wxAsyncifyReport = (function() {
|
||||||
var counts = {};
|
var counts = {};
|
||||||
return function(kind, msg, withStack) {
|
return function(kind, msg, withStack) {
|
||||||
|
|
@ -64,6 +117,8 @@ if (typeof Asyncify !== "undefined") {
|
||||||
// re-enters handleSleep while rewinding with currData set (verified
|
// re-enters handleSleep while rewinding with currData set (verified
|
||||||
// empirically 2026-07-31 — the timer-park e2e produced ~100/s of them
|
// empirically 2026-07-31 — the timer-park e2e produced ~100/s of them
|
||||||
// on a healthy run).
|
// on a healthy run).
|
||||||
|
__rec("sleep s=" + Asyncify.state + " cd=" + (Asyncify.currData || 0)
|
||||||
|
+ " w=" + (Asyncify.__inSleepWake || 0));
|
||||||
if (Asyncify.state === 0 && Asyncify.currData) {
|
if (Asyncify.state === 0 && Asyncify.currData) {
|
||||||
__wxAsyncifyReport(
|
__wxAsyncifyReport(
|
||||||
"concurrent-park",
|
"concurrent-park",
|
||||||
|
|
@ -94,6 +149,7 @@ if (typeof Asyncify !== "undefined") {
|
||||||
// wakeUp runs from pure JS on Promise resolution. Fiber swaps during
|
// wakeUp runs from pure JS on Promise resolution. Fiber swaps during
|
||||||
// the await may have overwritten Asyncify.currData. Restore OUR buffer
|
// the await may have overwritten Asyncify.currData. Restore OUR buffer
|
||||||
// so handleSleep's _asyncify_start_rewind and doRewind use it.
|
// so handleSleep's _asyncify_start_rewind and doRewind use it.
|
||||||
|
__rec("wake buf=" + (sleepCtx.capturedData || 0) + " cdWas=" + (Asyncify.currData || 0));
|
||||||
if (sleepCtx.capturedData) {
|
if (sleepCtx.capturedData) {
|
||||||
if (Asyncify.currData !== sleepCtx.capturedData) {
|
if (Asyncify.currData !== sleepCtx.capturedData) {
|
||||||
// The repair firing. currData=null → the overlapping chain
|
// The repair firing. currData=null → the overlapping chain
|
||||||
|
|
@ -111,6 +167,15 @@ if (typeof Asyncify !== "undefined") {
|
||||||
Asyncify.currData = sleepCtx.capturedData;
|
Asyncify.currData = sleepCtx.capturedData;
|
||||||
}
|
}
|
||||||
cleanup();
|
cleanup();
|
||||||
|
// Mark the synchronous wake window: everything below wakeUp() —
|
||||||
|
// the rewind, the resumed code running forward, its next unwind —
|
||||||
|
// executes inside it. A fiber completion whose root-entry lands
|
||||||
|
// in this window rewinds the root WHILE the wake's own rewind is
|
||||||
|
// in flight (the four identical prod trap stacks:
|
||||||
|
// maybeStopUnwind → trampoline → finishContextSwitch →
|
||||||
|
// doRewind(root) → unreachable). The stale-fiber guard below
|
||||||
|
// defers such root entries by one macrotask.
|
||||||
|
Asyncify.__inSleepWake = (Asyncify.__inSleepWake || 0) + 1;
|
||||||
try {
|
try {
|
||||||
return wakeUp(result);
|
return wakeUp(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -124,6 +189,8 @@ if (typeof Asyncify !== "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
|
} finally {
|
||||||
|
Asyncify.__inSleepWake -= 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -177,6 +244,7 @@ if (typeof Fibers !== "undefined"
|
||||||
var __fiberRefusals = 0;
|
var __fiberRefusals = 0;
|
||||||
|
|
||||||
var __refuseFiber = function(newFiber, why) {
|
var __refuseFiber = function(newFiber, why) {
|
||||||
|
__fcsRec("refuse new=" + newFiber);
|
||||||
++__fiberRefusals;
|
++__fiberRefusals;
|
||||||
if (__fiberRefusals <= 10 || __fiberRefusals % 100 === 0) {
|
if (__fiberRefusals <= 10 || __fiberRefusals % 100 === 0) {
|
||||||
console.warn("[wx-asyncify] fiber-resume-refused: fiber=" + newFiber + " " + why
|
console.warn("[wx-asyncify] fiber-resume-refused: fiber=" + newFiber + " " + why
|
||||||
|
|
@ -188,7 +256,15 @@ if (typeof Fibers !== "undefined"
|
||||||
Asyncify.currData = null;
|
Asyncify.currData = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var __fcsRec = (typeof Asyncify !== "undefined" && Asyncify.__recPush)
|
||||||
|
? Asyncify.__recPush
|
||||||
|
: function() {};
|
||||||
|
|
||||||
Fibers.finishContextSwitch = function(newFiber) {
|
Fibers.finishContextSwitch = function(newFiber) {
|
||||||
|
__fcsRec("fcs old=" + (Asyncify.currData ? Asyncify.currData - 20 : 0)
|
||||||
|
+ " new=" + newFiber
|
||||||
|
+ (newFiber === Fibers.__rootFiber ? " ROOT" : "")
|
||||||
|
+ " w=" + (Asyncify.__inSleepWake || 0));
|
||||||
// The swap that scheduled this switch just suspended its old fiber and
|
// The swap that scheduled this switch just suspended its old fiber and
|
||||||
// left currData = oldFiber+20 (fiber_swap's unwind path); record that
|
// left currData = oldFiber+20 (fiber_swap's unwind path); record that
|
||||||
// suspension as live — and a GENUINE swap-out also ends any internal
|
// suspension as live — and a GENUINE swap-out also ends any internal
|
||||||
|
|
@ -221,6 +297,36 @@ if (typeof Fibers !== "undefined"
|
||||||
}
|
}
|
||||||
|
|
||||||
var isRoot = newFiber === Fibers.__rootFiber;
|
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 HEAPU32v = (typeof GROWABLE_HEAP_U32 === "function") ? GROWABLE_HEAP_U32() : HEAPU32;
|
||||||
var entryPoint = HEAPU32v[((newFiber + 12) >>> 2) >>> 0];
|
var entryPoint = HEAPU32v[((newFiber + 12) >>> 2) >>> 0];
|
||||||
if (!isRoot && Fibers.__internallyParked.has(newFiber)) {
|
if (!isRoot && Fibers.__internallyParked.has(newFiber)) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue