diag(asyncify): write-time instrumentation + local warm-load repro findings
The prod differential ladder finished: staged byte VOLUME on a warm load is the only trigger left (V1a siblings-without-lib-tables dies, V1b +120 files survives, V1c sibling KiCad files renamed byte-for-byte dies, V1d Leonardo + 123MB of inert markdown dies on loads 3-4; 14MB never dies). 3D models, collab/ydoc/presence, lib tables, sibling KiCad handling and file count are all exonerated — volume only loads the dice on the underlying race. That made the crash reproducible locally for the first time in six campaigns: a persistent browser profile + a 110MB project fails every warm load with the exact prod signature. Iteration is now ~12 minutes instead of a release cycle. Shim: every fiber switch now records the departing side's remaining asyncify buffer and its recorded rewind entry (rem=/rf=), which is what identified the unrewindable capture and disproved buffer overflow. The deferral family is closed for good — a microtask-deferred retry on a clean empty stack died identically to the nested rewind, because the suspension is broken at write time, not by nesting. Shell: log the origin stack when wx reports the top window destroyed. That notification fires from ~wxTopLevelWindowWasm for ANY top-level window, so a transient frame dying mid-load navigates the user out of the editor — a real bug in its own right, found while chasing the empty flight-recorder dumps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
This commit is contained in:
parent
11da3fce19
commit
84a40d4492
4 changed files with 137 additions and 14 deletions
|
|
@ -294,10 +294,26 @@ if (typeof Fibers !== "undefined"
|
|||
if (newFiber === Fibers.__rootFiber && (Asyncify.__inSleepWake || 0) > 0) {
|
||||
Fibers.__rootHotTotal = (Fibers.__rootHotTotal || 0) + 1;
|
||||
}
|
||||
// rem = free bytes left in the old side's asyncify buffer after its unwind
|
||||
// finished (asyncify_data layout: [data]=write ptr, [data+4]=buffer end).
|
||||
// A deep capture that exhausts the 512K fiber buffer overflows SILENTLY in
|
||||
// release — rem at/below 0 here is the smoking gun for an unrewindable
|
||||
// suspension (the 2026-08-03 deferred-retry trap hypothesis).
|
||||
var __remStr = "";
|
||||
if (Asyncify.currData) {
|
||||
var __H = (typeof GROWABLE_HEAP_U32 === "function") ? GROWABLE_HEAP_U32() : HEAPU32;
|
||||
__remStr = " rem=" + (__H[((Asyncify.currData + 4) >>> 2) >>> 0] - __H[(Asyncify.currData >>> 2) >>> 0])
|
||||
// The rewind entry recorded for this suspension (setDataRewindFunc
|
||||
// wrote exportCallStack[0] at unwind start) vs the export stack NOW —
|
||||
// a capture taken under a NESTED export whose recorded entry is the
|
||||
// outer _main-style bottom is unrewindable (the 2026-08-03 trap).
|
||||
+ " rf=" + (Asyncify.getDataRewindFuncName ? Asyncify.getDataRewindFuncName(Asyncify.currData) : "?")
|
||||
+ " es=[" + (Asyncify.exportCallStack || []).join("|") + "]";
|
||||
}
|
||||
__fcsRec("fcs old=" + (Asyncify.currData ? Asyncify.currData - 20 : 0)
|
||||
+ " new=" + newFiber
|
||||
+ (newFiber === Fibers.__rootFiber ? " ROOT" : "")
|
||||
+ " w=" + (Asyncify.__inSleepWake || 0));
|
||||
+ " w=" + (Asyncify.__inSleepWake || 0) + __remStr);
|
||||
// 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
|
||||
|
|
@ -331,18 +347,19 @@ if (typeof Fibers !== "undefined"
|
|||
|
||||
var isRoot = newFiber === Fibers.__rootFiber;
|
||||
|
||||
// DEFERRAL RETIRED (2026-08-02, second retraction — see async/16 round 5).
|
||||
// Both deferral variants are unsound: the main loop's every iteration runs
|
||||
// INSIDE its yield-wake's synchronous extent, so "root re-entry during a
|
||||
// root-owned wake" also matches every legit nested coroutine Call/return
|
||||
// in a board open — v0.1.24 deferred thousands of them per load and the
|
||||
// open crawled/hung (open:settled result=failed at the 60s escape,
|
||||
// "hung forever" with a throttled background tab). The fatal interleave
|
||||
// and the benign bulk share the same observable signature at this layer;
|
||||
// the discriminator does not exist here. The rare nested-rewind crash is
|
||||
// accepted until the fiber-first runtime (design B) removes the dual
|
||||
// suspension protocols altogether; the recorder keeps every occurrence
|
||||
// fully observable.
|
||||
// DEFERRAL FAMILY CLOSED (2026-08-03, round 6 — the definitive finding).
|
||||
// Deferral at this layer can never work: by the time finishContextSwitch
|
||||
// runs, _asyncify_start_unwind has ALREADY written the suspension, and a
|
||||
// root suspension captured inside main's own live wake window is broken
|
||||
// AT WRITE TIME — emscripten_fiber_swap records the rewind entry as
|
||||
// exportCallStack[0] (the re-invoked main export) while the capture only
|
||||
// spans the swap-site frames (1.3KB vs a valid fresh-entry's ~10KB). A
|
||||
// microtask-deferred retry on a clean empty stack trapped IDENTICALLY to
|
||||
// the nested rewind. Prevention lives where it must: the libcontext C++
|
||||
// guard refuses the jump BEFORE the unwind starts (jump-refused-hot-main,
|
||||
// keyed on __wakingRoot via EM_JS) — same ghost contract as the parked
|
||||
// guard. The consume-once/quarantine checks below remain the backstop
|
||||
// for laundered attributions the C++ layer cannot see.
|
||||
|
||||
var HEAPU32v = (typeof GROWABLE_HEAP_U32 === "function") ? GROWABLE_HEAP_U32() : HEAPU32;
|
||||
var entryPoint = HEAPU32v[((newFiber + 12) >>> 2) >>> 0];
|
||||
|
|
|
|||
Loading…
Reference in a new issue