2026-05-25 15:07:10 +02:00
|
|
|
// === Asyncify / fiber / modal diagnostics (LOGGING ONLY — no behavior change) ===
|
|
|
|
|
//
|
|
|
|
|
// Injected only when inject-dyncall-shims.sh runs with SHIM_DIAGNOSTICS=1.
|
|
|
|
|
// Observability for the Chrome/V8 renderer crash on the first coroutine resume
|
|
|
|
|
// (the main-context Asyncify rewind). Does NOT swallow or alter any call — it
|
|
|
|
|
// logs and traces, then delegates, so the real crash still happens and can be
|
|
|
|
|
// observed right up to the faulting point.
|
|
|
|
|
(function() {
|
|
|
|
|
var modalActive = false;
|
2026-05-27 16:14:03 +02:00
|
|
|
var glTraceActive = false; // armed at the first main rewind (rewindId===0) below
|
|
|
|
|
var glCallSeq = 0;
|
|
|
|
|
var glTraceCap = 8000; // safety cap so a non-crashing run can't log forever
|
2026-05-25 15:07:10 +02:00
|
|
|
var tableLen = function() { return (typeof wasmTable !== "undefined" && wasmTable) ? wasmTable.length : -1; };
|
|
|
|
|
var asyncState = function() { return (typeof Asyncify !== "undefined") ? Asyncify.state : "N/A"; };
|
|
|
|
|
|
|
|
|
|
// 1. Timer scheduling — flag function pointers already out of bounds at schedule time.
|
|
|
|
|
if (typeof _emscripten_async_call !== "undefined") {
|
|
|
|
|
var __origAsyncCall = _emscripten_async_call;
|
|
|
|
|
_emscripten_async_call = function(func, arg, millis) {
|
|
|
|
|
var inBounds = func >= 0 && func < tableLen();
|
2026-05-27 16:14:03 +02:00
|
|
|
console.log("[DIAG_ASYNC_CALL] func=" + func + " arg=" + arg + " millis=" + millis +
|
2026-05-25 15:07:10 +02:00
|
|
|
" inBounds=" + inBounds + " modalActive=" + modalActive + " state=" + asyncState());
|
2026-05-27 16:14:03 +02:00
|
|
|
if (!inBounds) { console.log("[DIAG_ASYNC_CALL] OUT OF BOUNDS at schedule time! func=" + func); console.trace(); }
|
2026-05-25 15:07:10 +02:00
|
|
|
return __origAsyncCall(func, arg, millis);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Rewind target selection — which export Asyncify will re-enter on rewind.
|
|
|
|
|
if (typeof Asyncify !== "undefined" && Asyncify.setDataRewindFunc) {
|
|
|
|
|
var __origSetRewind = Asyncify.setDataRewindFunc.bind(Asyncify);
|
|
|
|
|
Asyncify.setDataRewindFunc = function(ptr, forced) {
|
2026-05-27 16:14:03 +02:00
|
|
|
console.log("[DIAG_REWIND_FUNC] ptr=" + ptr + " forced=" + forced + " state=" + Asyncify.state +
|
2026-05-25 15:07:10 +02:00
|
|
|
" modalActive=" + modalActive + " callStack=" + JSON.stringify(Asyncify.exportCallStack));
|
|
|
|
|
return __origSetRewind(ptr, forced);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. doRewind — the actual rewind that crashes V8. Log the buffer + saved rewind id
|
|
|
|
|
// immediately before re-entering wasm, so the last line before the crash names it.
|
|
|
|
|
if (typeof Asyncify !== "undefined" && typeof Asyncify.doRewind === "function") {
|
|
|
|
|
var __origDoRewind = Asyncify.doRewind.bind(Asyncify);
|
2026-05-27 16:14:03 +02:00
|
|
|
var heap32 = function () { return (typeof GROWABLE_HEAP_I32 === "function") ? GROWABLE_HEAP_I32() : HEAP32; };
|
2026-05-25 15:07:10 +02:00
|
|
|
Asyncify.doRewind = function(ptr) {
|
2026-05-27 16:14:03 +02:00
|
|
|
var H = heap32();
|
|
|
|
|
var rd = function (off) { try { return H[((ptr + off) >> 2)]; } catch (e) { return -999; } };
|
|
|
|
|
// asyncify_data layout: [ptr+0]=current stack pos (top of saved data),
|
|
|
|
|
// [ptr+4]=stack end, [ptr+8]=rewindId. Saved call-index/locals live below [ptr+0].
|
|
|
|
|
var curPos = rd(0), stackEnd = rd(4), rewindId = rd(8);
|
|
|
|
|
var name = (Asyncify.callStackIdToName && Asyncify.callStackIdToName[rewindId]) || "?";
|
|
|
|
|
var usedBytes = curPos - (ptr + 12);
|
|
|
|
|
console.log("[DIAG_DOREWIND] ptr=" + ptr + " rewindId=" + rewindId + " (" + name + ")" +
|
|
|
|
|
" curPos=" + curPos + " stackEnd=" + stackEnd + " usedBytes=" + usedBytes +
|
|
|
|
|
" state=" + asyncState() + " — re-entering wasm now");
|
|
|
|
|
// Arm the WebGL tracer exactly at the main rewind (the crash window: the silent V8
|
|
|
|
|
// abort happens right after this rewind returns to main, before coroutine #2/first paint).
|
|
|
|
|
if (rewindId === 0 && !glTraceActive) {
|
|
|
|
|
glTraceActive = true;
|
|
|
|
|
console.log("[DIAG_GL] tracing ARMED at main rewind (rewindId=0)");
|
|
|
|
|
}
|
|
|
|
|
// Dump the saved call-index chain (first words of the buffer) so we can see the
|
|
|
|
|
// depth/shape of what the rewind replays at the crash.
|
|
|
|
|
try {
|
|
|
|
|
var words = [];
|
|
|
|
|
var start = ptr + 12;
|
|
|
|
|
for (var a = start; a < curPos && a < start + 256; a += 4) words.push(H[(a >> 2)]);
|
|
|
|
|
console.log("[DIAG_DOREWIND] saved-data[" + words.length + "w]: " + JSON.stringify(words));
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
try {
|
|
|
|
|
return __origDoRewind(ptr);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.log("[DIAG_DOREWIND] EXCEPTION during rewind: " + e + " | " + (e && e.stack));
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
2026-05-25 15:07:10 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. dynCall_vi — log (and trace) out-of-bounds pointers but DO NOT swallow; call through.
|
|
|
|
|
if (typeof dynCall_vi === "function") {
|
|
|
|
|
var __origDynCallVi = dynCall_vi;
|
|
|
|
|
dynCall_vi = function(index, a0) {
|
|
|
|
|
if (index < 0 || index >= tableLen()) {
|
2026-05-27 16:14:03 +02:00
|
|
|
console.log("[DIAG_DYNCALL_VI] OUT OF BOUNDS index=" + index + " tableLen=" + tableLen() +
|
2026-05-25 15:07:10 +02:00
|
|
|
" modalActive=" + modalActive + " state=" + asyncState());
|
|
|
|
|
console.trace();
|
|
|
|
|
}
|
|
|
|
|
return __origDynCallVi(index, a0);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 20:10:33 +02:00
|
|
|
// 5. Modal lifecycle: poll the scheduler wait registry ("modal" waits).
|
|
|
|
|
// (The legacy Module._endModal hook was deleted at doc 20 D-1.)
|
2026-05-25 15:07:10 +02:00
|
|
|
if (typeof Module !== "undefined") {
|
2026-08-05 20:10:33 +02:00
|
|
|
var lastModalWaits = 0;
|
2026-05-25 15:07:10 +02:00
|
|
|
setInterval(function() {
|
2026-08-05 20:10:33 +02:00
|
|
|
var S = globalThis.__wxScheduler;
|
|
|
|
|
if (!S || typeof S.pendingWaits !== "function") return;
|
|
|
|
|
var n = S.pendingWaits("modal");
|
|
|
|
|
if (n !== lastModalWaits) {
|
|
|
|
|
console.log("[DIAG_MODAL] modal waits " + lastModalWaits + " -> " + n +
|
|
|
|
|
", state=" + asyncState());
|
|
|
|
|
modalActive = n > 0;
|
|
|
|
|
lastModalWaits = n;
|
2026-05-25 15:07:10 +02:00
|
|
|
}
|
|
|
|
|
}, 100);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 20:10:33 +02:00
|
|
|
// 6. EM_ASYNC_JS sleeps (wxWasmYieldUntilJs, js_enumerateFonts, clipboard, etc.) — log
|
test(wasm): coroutine crash reproduction harness + per-engine runner
Investigation scaffolding for the Chrome-only KiCad coroutine renderer crash.
Adds isolated reproduction probes exercising the coroutine/Asyncify/fiber layer
under KiCad-like conditions, runnable in BOTH Firefox and system Chrome.
- tests/playwright-coroutine.config.ts + test:coroutine:firefox|chrome npm
scripts: run the coroutine specs in Firefox AND system Chrome (the old e2e
config only used bundled Chromium, which never reproduced the crash).
- tests/apps/standalone/coroutine-pthread/: no-wx + pthreads reproduction probes
(fiber-in-main, nested invoke_/dynCall boundaries, RunMainStack, embind,
main-loop/rAF activation) + worker_dom_stub.js for wx+pthreads builds.
- tests/apps/Makefile.wasm: coroutine-pthread{,-main,-nested,-nested-ex,-wx,
-embind,-mainloop} targets.
- scripts/common/shims/diagnostics.js: add EM_ASYNC_JS handleSleep enter/wake
tracking (DIAG_SLEEP) to detect nested-async at the crash.
Findings (details in research notes): every isolated factor so far — direct /
nested / RunMainStack fiber, wx event loop + all 13 scenarios incl EM_ASYNC_JS,
pthreads, and main-loop/rAF activation — runs CLEAN in system Chrome. The
coroutine/Asyncify layer is exonerated; GL/WebGL is the remaining untested factor
(next). The reliable FF-pass/Chrome-fail repro is still the KiCad pcbnew e2e.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 18:44:12 +02:00
|
|
|
// enter/wake so we can see whether an async sleep is NESTED with a fiber swap
|
|
|
|
|
// at the crash (the #9153 collision). Logging only; delegates unchanged.
|
|
|
|
|
if (typeof Asyncify !== "undefined" && typeof Asyncify.handleSleep === "function") {
|
|
|
|
|
var __diagOrigHandleSleep = Asyncify.handleSleep.bind(Asyncify);
|
|
|
|
|
var diagSleepId = 0;
|
|
|
|
|
Asyncify.handleSleep = function(startAsync) {
|
|
|
|
|
var id = ++diagSleepId;
|
2026-05-27 16:14:03 +02:00
|
|
|
console.log("[DIAG_SLEEP] ENTER id=" + id + " state=" + asyncState() +
|
test(wasm): coroutine crash reproduction harness + per-engine runner
Investigation scaffolding for the Chrome-only KiCad coroutine renderer crash.
Adds isolated reproduction probes exercising the coroutine/Asyncify/fiber layer
under KiCad-like conditions, runnable in BOTH Firefox and system Chrome.
- tests/playwright-coroutine.config.ts + test:coroutine:firefox|chrome npm
scripts: run the coroutine specs in Firefox AND system Chrome (the old e2e
config only used bundled Chromium, which never reproduced the crash).
- tests/apps/standalone/coroutine-pthread/: no-wx + pthreads reproduction probes
(fiber-in-main, nested invoke_/dynCall boundaries, RunMainStack, embind,
main-loop/rAF activation) + worker_dom_stub.js for wx+pthreads builds.
- tests/apps/Makefile.wasm: coroutine-pthread{,-main,-nested,-nested-ex,-wx,
-embind,-mainloop} targets.
- scripts/common/shims/diagnostics.js: add EM_ASYNC_JS handleSleep enter/wake
tracking (DIAG_SLEEP) to detect nested-async at the crash.
Findings (details in research notes): every isolated factor so far — direct /
nested / RunMainStack fiber, wx event loop + all 13 scenarios incl EM_ASYNC_JS,
pthreads, and main-loop/rAF activation — runs CLEAN in system Chrome. The
coroutine/Asyncify layer is exonerated; GL/WebGL is the remaining untested factor
(next). The reliable FF-pass/Chrome-fail repro is still the KiCad pcbnew e2e.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 18:44:12 +02:00
|
|
|
" currData=" + ((typeof Asyncify.currData !== "undefined" && Asyncify.currData) || "null"));
|
|
|
|
|
return __diagOrigHandleSleep(function(wakeUp) {
|
|
|
|
|
return startAsync(function(result) {
|
2026-05-27 16:14:03 +02:00
|
|
|
console.log("[DIAG_SLEEP] WAKE id=" + id + " state=" + asyncState() +
|
test(wasm): coroutine crash reproduction harness + per-engine runner
Investigation scaffolding for the Chrome-only KiCad coroutine renderer crash.
Adds isolated reproduction probes exercising the coroutine/Asyncify/fiber layer
under KiCad-like conditions, runnable in BOTH Firefox and system Chrome.
- tests/playwright-coroutine.config.ts + test:coroutine:firefox|chrome npm
scripts: run the coroutine specs in Firefox AND system Chrome (the old e2e
config only used bundled Chromium, which never reproduced the crash).
- tests/apps/standalone/coroutine-pthread/: no-wx + pthreads reproduction probes
(fiber-in-main, nested invoke_/dynCall boundaries, RunMainStack, embind,
main-loop/rAF activation) + worker_dom_stub.js for wx+pthreads builds.
- tests/apps/Makefile.wasm: coroutine-pthread{,-main,-nested,-nested-ex,-wx,
-embind,-mainloop} targets.
- scripts/common/shims/diagnostics.js: add EM_ASYNC_JS handleSleep enter/wake
tracking (DIAG_SLEEP) to detect nested-async at the crash.
Findings (details in research notes): every isolated factor so far — direct /
nested / RunMainStack fiber, wx event loop + all 13 scenarios incl EM_ASYNC_JS,
pthreads, and main-loop/rAF activation — runs CLEAN in system Chrome. The
coroutine/Asyncify layer is exonerated; GL/WebGL is the remaining untested factor
(next). The reliable FF-pass/Chrome-fail repro is still the KiCad pcbnew e2e.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 18:44:12 +02:00
|
|
|
" currData=" + ((typeof Asyncify.currData !== "undefined" && Asyncify.currData) || "null"));
|
|
|
|
|
return wakeUp(result);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-27 16:14:03 +02:00
|
|
|
// 7. WebGL call tracer — pinpoint the exact GL op that crashes Chrome's renderer.
|
|
|
|
|
// KiCad runs the GAL on an OffscreenCanvas in the pthread worker (PROXY_TO_PTHREAD +
|
|
|
|
|
// OFFSCREENCANVAS_SUPPORT), and this diagnostics code runs in that same worker, so we
|
|
|
|
|
// hook getContext where the context is actually created. Each call logs via
|
|
|
|
|
// console.error (immediate flush → captured even just before a hard V8 abort), but
|
|
|
|
|
// only once glTraceActive is set (at the main rewind), so volume = the crash window.
|
|
|
|
|
function wrapGLContext(ctx, kind) {
|
|
|
|
|
if (!ctx) return ctx;
|
|
|
|
|
try { if (ctx.__diagWrapped) return ctx; ctx.__diagWrapped = true; } catch (e) {}
|
|
|
|
|
console.log("[DIAG_GL] context created kind=" + kind);
|
|
|
|
|
return new Proxy(ctx, {
|
|
|
|
|
get: function(target, prop) {
|
|
|
|
|
var val = target[prop];
|
|
|
|
|
if (typeof val === "function") {
|
|
|
|
|
return function() {
|
|
|
|
|
if (glTraceActive && glCallSeq < glTraceCap) {
|
|
|
|
|
console.log("[DIAG_GL] #" + (++glCallSeq) + " " + String(prop));
|
|
|
|
|
}
|
|
|
|
|
return val.apply(target, arguments);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return val;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
function hookGetContext(proto, kind) {
|
|
|
|
|
if (!proto || typeof proto.getContext !== "function" || proto.__diagGCHooked) return;
|
|
|
|
|
proto.__diagGCHooked = true;
|
|
|
|
|
var orig = proto.getContext;
|
|
|
|
|
proto.getContext = function(type) {
|
|
|
|
|
// Log the ATTEMPT before calling through, so if getContext itself crashes the
|
|
|
|
|
// renderer (e.g. a Chrome/ANGLE WebGL-context bug) this is the last line we see.
|
|
|
|
|
if (type === "webgl2" || type === "webgl" || type === "experimental-webgl") {
|
|
|
|
|
var attrs = "";
|
|
|
|
|
try { attrs = JSON.stringify(arguments[1] || {}); } catch (e) {}
|
|
|
|
|
console.log("[DIAG_GL] getContext(" + kind + ":" + type + ") attrs=" + attrs + " — calling through now");
|
|
|
|
|
}
|
|
|
|
|
var ctx = orig.apply(this, arguments);
|
|
|
|
|
if (type === "webgl2" || type === "webgl" || type === "experimental-webgl") {
|
|
|
|
|
console.log("[DIAG_GL] getContext returned " + (ctx ? "a context" : "NULL"));
|
|
|
|
|
try { return wrapGLContext(ctx, kind + ":" + type); } catch (e) { return ctx; }
|
|
|
|
|
}
|
|
|
|
|
return ctx;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
if (typeof OffscreenCanvas !== "undefined") hookGetContext(OffscreenCanvas.prototype, "offscreen");
|
|
|
|
|
if (typeof HTMLCanvasElement !== "undefined") hookGetContext(HTMLCanvasElement.prototype, "html");
|
|
|
|
|
|
|
|
|
|
// 8. dynCall_ii / dynCall_vi invocation tracer (logging only). The shim routes the
|
|
|
|
|
// pthread-entry (ii) and fiber-entry/signal/timer (vi) callbacks through these bound
|
|
|
|
|
// instrumented dynCall_<sig>. Wrap them to log each invocation + the function pointer,
|
|
|
|
|
// armed at the main rewind (glTraceActive) so volume = the crash window. The LAST line
|
|
|
|
|
// before the silent crash names the faulting dispatch + its ptr. Tag thread for context.
|
|
|
|
|
var __thr = (typeof ENVIRONMENT_IS_PTHREAD !== "undefined" && ENVIRONMENT_IS_PTHREAD) ? "worker" : "main";
|
|
|
|
|
var __fnName = function(ptr) {
|
|
|
|
|
try { var f = getWasmTableEntry(ptr); return (f && f.name) ? f.name : "?"; } catch (e) { return "?err"; }
|
|
|
|
|
};
|
|
|
|
|
try {
|
|
|
|
|
if (typeof dynCall_ii === "function") {
|
|
|
|
|
var __origDCii = dynCall_ii;
|
|
|
|
|
dynCall_ii = function(ptr, a0) {
|
|
|
|
|
if (glTraceActive && glCallSeq < glTraceCap)
|
|
|
|
|
console.log("[DIAG_DC] " + __thr + " dynCall_ii ptr=" + ptr + " name=" + __fnName(ptr) + " #" + (++glCallSeq));
|
|
|
|
|
return __origDCii(ptr, a0);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
try {
|
|
|
|
|
if (typeof dynCall_vi === "function") {
|
|
|
|
|
var __origDCvi = dynCall_vi;
|
|
|
|
|
var __asy = function() {
|
|
|
|
|
if (typeof Asyncify === "undefined") return "noAsyncify";
|
|
|
|
|
var st = Asyncify.state;
|
|
|
|
|
var cd = (Asyncify.currData || 0);
|
|
|
|
|
return "state=" + st + " currData=" + cd;
|
|
|
|
|
};
|
|
|
|
|
dynCall_vi = function(ptr, a0) {
|
|
|
|
|
var big = glTraceActive && ptr > 15000; // the rare large-index 'vi' dispatches (incl. the stalling 20078)
|
|
|
|
|
if (glTraceActive && glCallSeq < glTraceCap) {
|
|
|
|
|
console.log("[DIAG_DC] " + __thr + " dynCall_vi ptr=" + ptr + " name=" + __fnName(ptr) + " arg=" + a0 + " #" + (++glCallSeq));
|
|
|
|
|
}
|
|
|
|
|
if (big) {
|
|
|
|
|
// Asyncify state going IN: if it's non-NORMAL (1=unwinding, 2=rewinding) the
|
|
|
|
|
// leftover coroutine state is making the instrumented dispatch misbehave.
|
|
|
|
|
console.log("[DIAG_DC_VI] ENTER ptr=" + ptr + " " + __asy());
|
|
|
|
|
var r = __origDCvi(ptr, a0);
|
|
|
|
|
// If this RETURNED line never appears, the dispatch unwound/stalled and never came back.
|
|
|
|
|
console.log("[DIAG_DC_VI] RETURNED ptr=" + ptr + " " + __asy());
|
|
|
|
|
return r;
|
|
|
|
|
}
|
|
|
|
|
return __origDCvi(ptr, a0);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
|
|
|
|
|
// 9. Periodic asyncify-state monitor (main thread). After dynCall_vi(20078)=
|
|
|
|
|
// setupUIConditions appears to unwind-and-never-rewind, this timer (which still runs
|
|
|
|
|
// on the idle event loop) reveals the post-stall Asyncify.state: if it's stuck at
|
|
|
|
|
// 1 (UNWINDING) or 2 (REWINDING) with a fixed currData, the app yielded and the
|
|
|
|
|
// rewind was never scheduled. Logs only on change + a heartbeat.
|
|
|
|
|
if (typeof Asyncify !== "undefined" && __thr === "main") {
|
|
|
|
|
var __lastSt = -999, __lastCd = -999, __hb = 0;
|
|
|
|
|
setInterval(function() {
|
|
|
|
|
var st = Asyncify.state, cd = (Asyncify.currData || 0);
|
|
|
|
|
if (st !== __lastSt || cd !== __lastCd) {
|
|
|
|
|
console.log("[DIAG_ASTATE] change -> state=" + st + " currData=" + cd);
|
|
|
|
|
__lastSt = st; __lastCd = cd;
|
|
|
|
|
} else if (st !== 0 && (++__hb % 6 === 0)) {
|
|
|
|
|
console.log("[DIAG_ASTATE] STILL state=" + st + " currData=" + cd + " (stuck?)");
|
|
|
|
|
}
|
|
|
|
|
}, 500);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log("[DIAG] Asyncify/fiber/modal diagnostics installed (logging only) [" + __thr + "]");
|
2026-05-25 15:07:10 +02:00
|
|
|
})();
|
|
|
|
|
// === End diagnostics ===
|