pcbjam/scripts/common/shims/diagnostics.js
Viktor Vaczi a4ad69412e fix(wasm): bind dynCall_* to real DYNCALLS=1 exports; refactor shim into files
The shim bound bare dynCall_* names to JS getWasmTableEntry() calls, bypassing
the asyncify-instrumented dynCall_* wasm trampolines that -sDYNCALLS=1 provides.
That broke Asyncify unwind/rewind through indirect calls -> "indirect call
signature mismatch" (caught every frame in Firefox; fatal renderer crash in
Chrome). Bind the bare names to wasmExports["dynCall_<sig>"] instead.

Result: the PCBnew "select draw lines" e2e is green in Firefox (tool selects and
draws, zero page errors). Dropped the fiber-stabilization block, the shipped
diagnostic block, and the exportCallStack JS hack (all compensated for the wrong
binding); shim shrank 521 -> ~250 lines.

- scripts/common/inject-dyncall-shims.sh: orchestrator only; injected JS extracted
  to scripts/common/shims/
- scripts/common/shims/dyncall-binding.js.tmpl: per-signature binding template
- scripts/common/shims/handlesleep.js: nested-Asyncify handleSleep fix (#9153)
- scripts/common/shims/diagnostics.js: logging-only, opt-in via SHIM_DIAGNOSTICS=1
- tests/package.json: add test:kicad:firefox / test:kicad:chrome scripts

Known issue (tracked separately): Chrome still renderer-crashes on the first
coroutine resume. Asyncify.doRewind replays the deep main-context call stack and
exceeds V8's execution-stack limit (Firefox tolerates the same wasm). Proper fix
is JSPI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:07:10 +02:00

83 lines
4 KiB
JavaScript

// === 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;
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();
console.warn("[DIAG_ASYNC_CALL] func=" + func + " arg=" + arg + " millis=" + millis +
" inBounds=" + inBounds + " modalActive=" + modalActive + " state=" + asyncState());
if (!inBounds) { console.error("[DIAG_ASYNC_CALL] OUT OF BOUNDS at schedule time! func=" + func); console.trace(); }
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) {
console.warn("[DIAG_REWIND_FUNC] ptr=" + ptr + " forced=" + forced + " state=" + Asyncify.state +
" 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);
Asyncify.doRewind = function(ptr) {
var rewindId = -1;
try { rewindId = (typeof GROWABLE_HEAP_I32 === "function") ? GROWABLE_HEAP_I32()[((ptr + 8) >> 2)] : HEAP32[((ptr + 8) >> 2)]; } catch (e) {}
console.warn("[DIAG_DOREWIND] ptr=" + ptr + " rewindId=" + rewindId + " state=" + asyncState() +
" modalActive=" + modalActive + " — re-entering wasm now");
return __origDoRewind(ptr);
};
}
// 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()) {
console.error("[DIAG_DYNCALL_VI] OUT OF BOUNDS index=" + index + " tableLen=" + tableLen() +
" modalActive=" + modalActive + " state=" + asyncState());
console.trace();
}
return __origDynCallVi(index, a0);
};
}
// 5. Modal lifecycle (startModal sets Module._endModal; detect appear/disappear).
if (typeof Module !== "undefined") {
var seen = false;
setInterval(function() {
if (Module._endModal && !seen) {
seen = true; modalActive = true;
console.warn("[DIAG_MODAL] modal started, state=" + asyncState());
var __origEnd = Module._endModal;
Module._endModal = function(code) {
console.warn("[DIAG_MODAL] EndModal code=" + code + " state=" + asyncState());
modalActive = false;
return __origEnd(code);
};
} else if (!Module._endModal && seen) {
seen = false;
console.warn("[DIAG_MODAL] modal cleanup, state=" + asyncState());
}
}, 100);
}
console.warn("[DIAG] Asyncify/fiber/modal diagnostics installed (logging only)");
})();
// === End diagnostics ===