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
|
|
@ -24,6 +24,59 @@ if (typeof Asyncify !== "undefined") {
|
|||
// swaps don't allocate through allocateData). Make every repair and every
|
||||
// concurrent-park window loud, so a saved console dump answers that.
|
||||
// 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 counts = {};
|
||||
return function(kind, msg, withStack) {
|
||||
|
|
@ -64,6 +117,8 @@ if (typeof Asyncify !== "undefined") {
|
|||
// re-enters handleSleep while rewinding with currData set (verified
|
||||
// empirically 2026-07-31 — the timer-park e2e produced ~100/s of them
|
||||
// on a healthy run).
|
||||
__rec("sleep s=" + Asyncify.state + " cd=" + (Asyncify.currData || 0)
|
||||
+ " w=" + (Asyncify.__inSleepWake || 0));
|
||||
if (Asyncify.state === 0 && Asyncify.currData) {
|
||||
__wxAsyncifyReport(
|
||||
"concurrent-park",
|
||||
|
|
@ -94,6 +149,7 @@ if (typeof Asyncify !== "undefined") {
|
|||
// wakeUp runs from pure JS on Promise resolution. Fiber swaps during
|
||||
// the await may have overwritten Asyncify.currData. Restore OUR buffer
|
||||
// so handleSleep's _asyncify_start_rewind and doRewind use it.
|
||||
__rec("wake buf=" + (sleepCtx.capturedData || 0) + " cdWas=" + (Asyncify.currData || 0));
|
||||
if (sleepCtx.capturedData) {
|
||||
if (Asyncify.currData !== sleepCtx.capturedData) {
|
||||
// The repair firing. currData=null → the overlapping chain
|
||||
|
|
@ -111,6 +167,15 @@ if (typeof Asyncify !== "undefined") {
|
|||
Asyncify.currData = sleepCtx.capturedData;
|
||||
}
|
||||
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 {
|
||||
return wakeUp(result);
|
||||
} catch (e) {
|
||||
|
|
@ -124,6 +189,8 @@ if (typeof Asyncify !== "undefined") {
|
|||
return;
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
Asyncify.__inSleepWake -= 1;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -177,6 +244,7 @@ if (typeof Fibers !== "undefined"
|
|||
var __fiberRefusals = 0;
|
||||
|
||||
var __refuseFiber = function(newFiber, why) {
|
||||
__fcsRec("refuse new=" + newFiber);
|
||||
++__fiberRefusals;
|
||||
if (__fiberRefusals <= 10 || __fiberRefusals % 100 === 0) {
|
||||
console.warn("[wx-asyncify] fiber-resume-refused: fiber=" + newFiber + " " + why
|
||||
|
|
@ -188,7 +256,15 @@ if (typeof Fibers !== "undefined"
|
|||
Asyncify.currData = null;
|
||||
};
|
||||
|
||||
var __fcsRec = (typeof Asyncify !== "undefined" && Asyncify.__recPush)
|
||||
? Asyncify.__recPush
|
||||
: function() {};
|
||||
|
||||
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
|
||||
// left currData = oldFiber+20 (fiber_swap's unwind path); record that
|
||||
// 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;
|
||||
|
||||
// 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)) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue