jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove the inventory; every deletion verified by grep closure + full gates. Broken-right-now fixes: - deploy-staging.yml passed the retired opt_level input — the workflow could not even start. Removed. - env.sh carried dead exports with a live -sASYNCIFY=1 inside (WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason. - docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone. Dead weight removed: - binaryen submodule (nothing builds or invokes it), wasm-opt-bench workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess scaffolding (existed to parallelize the deleted wasm-opt phase; the postprocess is a seconds-long node script and now runs inline), build-monitor's dead asyncify rows, sched-context orphan build output, dead .gitignore entries, the .jspi-assets spike dir (the two wf-result research JSONs moved to docs/features/async/migration-evidence/). - bindings: fiber_park.h + its 12 embind registrations (broken-if- called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route, main_stack_runner.h + 5 includes, the always-null context-sleep weak hook in nanosleep_yield.c. - shim: the backend field (installed-flag idempotency instead), noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the WasmTool fallback and string-dump normalize branch). - web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts (gerber-demo keeps it: it loads the deployed CDN release, which predates emscripten 6 — noted inline). Conditionals: all 'backend === jspi' checks reduced to scheduler- presence checks; races_quiescent re-keyed from Asyncify.state (vacuous) to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive, which is the probing activation's own window by definition). Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→ JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS, kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests), collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→ wasmTrapSignatures (lists byte-identical). Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused asserts re-keyed to live JSPI beacons; eeschema-load's failure message no longer sends the developer to a deleted script; wait-beacons' dead families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is unconstructible); the embind test.fail re-gated with the JSPI reason (plain embind invokers cannot suspend — verified still failing); lint-determinism now scans tests/jspi (166 files clean); eeschema-collab local-move gated to chromium (~50% flaky on FF even solo; pcbnew twin covers both engines). Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md describes the single-phase build; docs/features/async/README.md banner-marked historical and repointed at the NEW 23-jspi-runtime.md (current architecture: export census, turnstile, libcontext ownership + refusal contract, embind call shapes, the em-pthread service-wrapper trick, exception policy, known gaps). Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the quiescence-probe fix; the 3 other reds were verified contention flakes solo-green or the documented FF gate), web 76/0, jspi 18/18 both engines, vitest 295/295 + 17/17, all lints green, live-app census clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
This commit is contained in:
parent
3ee174e9b4
commit
9c475a804e
120 changed files with 1522 additions and 2974 deletions
|
|
@ -41,8 +41,8 @@ export EMSDK_QUIET=1
|
|||
# WebAssembly exceptions (legacy binary encoding) are the only build mode. -sSUPPORT_LONGJMP=wasm is
|
||||
# required because the deps that use setjmp/longjmp (freetype, cairo, OpenCASCADE) must use wasm
|
||||
# setjmp — emscripten's JS-longjmp implementation cannot coexist with -fwasm-exceptions (it would
|
||||
# leave emscripten_longjmp undefined). -sWASM_LEGACY_EXCEPTIONS=1 selects the EH binary encoding our
|
||||
# post-link Asyncify + catch-arm-hoisting pass can consume (Asyncify can't handle exnref).
|
||||
# leave emscripten_longjmp undefined). -sWASM_LEGACY_EXCEPTIONS=1 pins the legacy EH binary encoding:
|
||||
# the exnref encoding is not adopted across our pinned emsdk toolchain and target browsers.
|
||||
export DEPS_EH_FLAGS="-fwasm-exceptions -sSUPPORT_LONGJMP=wasm -sWASM_LEGACY_EXCEPTIONS=1"
|
||||
|
||||
# Emscripten SDK setup
|
||||
|
|
@ -123,23 +123,6 @@ elif [ -z "$JOBS" ]; then
|
|||
export JOBS=1
|
||||
fi
|
||||
|
||||
# Common linker flags for WASM
|
||||
export WASM_LDFLAGS="\
|
||||
-sALLOW_MEMORY_GROWTH=1 \
|
||||
-sINITIAL_MEMORY=256MB \
|
||||
-sSTACK_SIZE=5MB \
|
||||
-sASYNCIFY=1 \
|
||||
-sASYNCIFY_STACK_SIZE=16384 \
|
||||
-sLEGACY_GL_EMULATION \
|
||||
-sMAX_WEBGL_VERSION=2"
|
||||
|
||||
# Threading flags (when enabled)
|
||||
export PTHREAD_LDFLAGS="\
|
||||
-pthread \
|
||||
-sPROXY_TO_PTHREAD=1 \
|
||||
-sPTHREAD_POOL_SIZE=navigator.hardwareConcurrency \
|
||||
-sOFFSCREENCANVAS_SUPPORT=1"
|
||||
|
||||
# Create output directories
|
||||
mkdir -p "$BUILD_ROOT" "$DEPS_ROOT" "$SYSROOT"/{lib,include,share} "$STAMPS_DIR"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,141 +0,0 @@
|
|||
#!/bin/bash
|
||||
# Get path to Binaryen wasm-opt
|
||||
#
|
||||
# Usage: ./scripts/common/get-wasm-opt.sh
|
||||
# Output: Prints path to wasm-opt executable
|
||||
#
|
||||
# Prefers the emsdk-bundled Binaryen (tools/emsdk/upstream/bin/) so that the
|
||||
# wasm-opt and wasm-emscripten-finalize versions match the Emscripten that
|
||||
# generated the JS glue. A version mismatch between the compiler's Binaryen
|
||||
# (e.g. v121+72 dev) and a standalone release (v121) corrupts asyncify
|
||||
# metadata, causing "func is not a function" errors at runtime.
|
||||
#
|
||||
# Falls back to downloading standalone Binaryen v130 if emsdk is not installed
|
||||
# locally (e.g. CI environments that only use Docker).
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
|
||||
# --- Prefer emsdk-bundled Binaryen (matches the Emscripten that compiled the WASM) ---
|
||||
# Override: set BINARYEN_VERSION in the env to FORCE a specific standalone release
|
||||
# (skips the emsdk preference). Used to A/B Binaryen versions — e.g. v121 carries a
|
||||
# wasm::Type lock-contention bug that makes the host-side -O2 pass ~9x slower than
|
||||
# v130 (see docs/ci-build-slowness-findings.md). Validate any bump with the e2e
|
||||
# suite: a Binaryen/emsdk skew can corrupt asyncify metadata ("func is not a function").
|
||||
EMSDK_WASM_OPT="${PROJECT_ROOT}/tools/emsdk/upstream/bin/wasm-opt"
|
||||
# build-kicad-target.sh replaces emsdk's wasm-opt with a no-op stub (moving the
|
||||
# real binary to wasm-opt.real) so emcc skips the in-link asyncify. That's meant
|
||||
# for the container emsdk, but a host-mode run leaves the HOST emsdk stubbed —
|
||||
# and the stub fakes --version and exits 0, so the host-side asyncify/-O2
|
||||
# "succeed" while doing NOTHING (broken wasm: "asyncify_stop_unwind is not a
|
||||
# function"). Always prefer the preserved real binary when it exists.
|
||||
if [ -x "${EMSDK_WASM_OPT}.real" ]; then
|
||||
EMSDK_WASM_OPT="${EMSDK_WASM_OPT}.real"
|
||||
fi
|
||||
if [ -z "${BINARYEN_VERSION:-}" ] && [ -x "${EMSDK_WASM_OPT}" ]; then
|
||||
EMSDK_VERSION=$("${EMSDK_WASM_OPT}" --version 2>&1 || true)
|
||||
echo "Using emsdk-bundled Binaryen: ${EMSDK_VERSION}" >&2
|
||||
echo "${EMSDK_WASM_OPT}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Fallback: download standalone Binaryen (for CI or environments without local emsdk) ---
|
||||
echo "emsdk Binaryen not found at ${EMSDK_WASM_OPT}, falling back to standalone download..." >&2
|
||||
|
||||
# Default v130: v121 has a wasm::Type lock convoy that makes -O2 ~9x slower on
|
||||
# many-core Linux (docs/ci-build-slowness-findings.md). v130 output validated by
|
||||
# the full e2e suite locally (31/31) and Chromium-green on CI run 27226030304.
|
||||
BINARYEN_VERSION="${BINARYEN_VERSION:-130}"
|
||||
|
||||
# BINARYEN_BUILD_FROM_SOURCE=1: compile wasm-opt ourselves instead of using the
|
||||
# official Linux release tarballs, which are badly built — measured on the
|
||||
# calculator fixture, identical output sha256: asyncify 4x faster on x86
|
||||
# (CI run 27276830256: 3:50 -> 0:58) and 13x on aarch64; -O2 equal. The macOS
|
||||
# tarball is well-built (self-build is ~12% SLOWER there), so this is only
|
||||
# worth enabling on Linux CI. Needs cmake, ninja, g++, git. ~5 min on 32 cores,
|
||||
# cached in build-wasm/tools after the first call.
|
||||
if [[ "${BINARYEN_BUILD_FROM_SOURCE:-0}" == "1" ]]; then
|
||||
SELF_DIR="${PROJECT_ROOT}/build-wasm/tools/binaryen-${BINARYEN_VERSION}-selfbuilt"
|
||||
SELF_WASM_OPT="${SELF_DIR}/bin/wasm-opt"
|
||||
if [ ! -x "${SELF_WASM_OPT}" ]; then
|
||||
SRC_DIR="${PROJECT_ROOT}/build-wasm/tools/binaryen-src-${BINARYEN_VERSION}"
|
||||
BUILD_DIR="${PROJECT_ROOT}/build-wasm/tools/binaryen-build-${BINARYEN_VERSION}"
|
||||
echo "Building Binaryen v${BINARYEN_VERSION} from source (one-time, ~5 min)..." >&2
|
||||
if [ ! -d "${SRC_DIR}" ]; then
|
||||
git clone -q --depth 1 --branch "version_${BINARYEN_VERSION}" \
|
||||
--recurse-submodules --shallow-submodules \
|
||||
https://github.com/WebAssembly/binaryen.git "${SRC_DIR}" >&2
|
||||
fi
|
||||
cmake -S "${SRC_DIR}" -B "${BUILD_DIR}" -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_CXX_FLAGS="-Wno-maybe-uninitialized" \
|
||||
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON -DBUILD_TESTS=OFF >&2
|
||||
ninja -C "${BUILD_DIR}" wasm-opt wasm-emscripten-finalize >&2
|
||||
# wasm-opt links lib/libbinaryen.so via rpath $ORIGIN/../lib.
|
||||
mkdir -p "${SELF_DIR}/bin" "${SELF_DIR}/lib"
|
||||
cp "${BUILD_DIR}/bin/wasm-opt" "${BUILD_DIR}/bin/wasm-emscripten-finalize" "${SELF_DIR}/bin/"
|
||||
cp "${BUILD_DIR}"/lib/libbinaryen.* "${SELF_DIR}/lib/" 2>/dev/null || true
|
||||
echo "Self-built Binaryen installed to ${SELF_DIR}" >&2
|
||||
fi
|
||||
SELF_VERSION=$("${SELF_WASM_OPT}" --version 2>&1 | grep -o '[0-9]\+' | head -1)
|
||||
if [ "${SELF_VERSION}" != "${BINARYEN_VERSION}" ]; then
|
||||
echo "ERROR: self-built wasm-opt version mismatch (got ${SELF_VERSION}, expected ${BINARYEN_VERSION})" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Using self-built Binaryen v${BINARYEN_VERSION} (${SELF_WASM_OPT})" >&2
|
||||
echo "${SELF_WASM_OPT}"
|
||||
exit 0
|
||||
fi
|
||||
BINARYEN_DIR="${PROJECT_ROOT}/build-wasm/tools/binaryen-${BINARYEN_VERSION}"
|
||||
WASM_OPT="${BINARYEN_DIR}/bin/wasm-opt"
|
||||
|
||||
download_binaryen() {
|
||||
# Detect platform
|
||||
local os=$(uname -s)
|
||||
local arch=$(uname -m)
|
||||
local platform=""
|
||||
|
||||
case "${os}-${arch}" in
|
||||
Darwin-arm64) platform="arm64-macos" ;;
|
||||
Darwin-x86_64) platform="x86_64-macos" ;;
|
||||
Linux-aarch64) platform="aarch64-linux" ;;
|
||||
Linux-x86_64) platform="x86_64-linux" ;;
|
||||
*)
|
||||
echo "ERROR: Unsupported platform: ${os}-${arch}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
local url="https://github.com/WebAssembly/binaryen/releases/download/version_${BINARYEN_VERSION}/binaryen-version_${BINARYEN_VERSION}-${platform}.tar.gz"
|
||||
local tarball="${PROJECT_ROOT}/build-wasm/tools/binaryen-${BINARYEN_VERSION}.tar.gz"
|
||||
|
||||
echo "Downloading Binaryen v${BINARYEN_VERSION} for ${platform}..." >&2
|
||||
mkdir -p "${PROJECT_ROOT}/build-wasm/tools"
|
||||
curl -L -o "${tarball}" "${url}"
|
||||
|
||||
echo "Extracting..." >&2
|
||||
tar -xzf "${tarball}" -C "${PROJECT_ROOT}/build-wasm/tools"
|
||||
mv "${PROJECT_ROOT}/build-wasm/tools/binaryen-version_${BINARYEN_VERSION}" "${BINARYEN_DIR}"
|
||||
rm "${tarball}"
|
||||
|
||||
echo "Binaryen v${BINARYEN_VERSION} installed to ${BINARYEN_DIR}" >&2
|
||||
}
|
||||
|
||||
# Download Binaryen if not cached
|
||||
if [ ! -x "${WASM_OPT}" ]; then
|
||||
download_binaryen
|
||||
fi
|
||||
|
||||
# Verify version
|
||||
INSTALLED_VERSION=$("${WASM_OPT}" --version 2>&1 | grep -o '[0-9]\+' | head -1)
|
||||
if [ "${INSTALLED_VERSION}" != "${BINARYEN_VERSION}" ]; then
|
||||
echo "WARNING: wasm-opt version mismatch (got ${INSTALLED_VERSION}, expected ${BINARYEN_VERSION})" >&2
|
||||
echo "Re-downloading..." >&2
|
||||
rm -rf "${BINARYEN_DIR}"
|
||||
download_binaryen
|
||||
fi
|
||||
|
||||
# Output path to wasm-opt (this is the only stdout output)
|
||||
echo "${WASM_OPT}"
|
||||
|
|
@ -1,3 +1,15 @@
|
|||
# Promising-export census for the KiCad browser apps: every wasm entry export
|
||||
# whose body can reach a suspension (park) under JSPI. Consumed as
|
||||
# -sJSPI_EXPORTS=@thisfile by scripts/kicad/build-kicad-target.sh; emscripten's
|
||||
# symbol-list parser strips lines starting with '#'. Do NOT add blank lines
|
||||
# (they would parse as empty symbol entries).
|
||||
# Regenerate by grep, not memory: the wx entries are the EMSCRIPTEN_KEEPALIVE
|
||||
# exports in wxwidgets/src/wasm/ that can park; pcbjam_libctx_entry is
|
||||
# libcontext's coroutine entry (kicad/thirdparty/libcontext).
|
||||
# Kept in sync BY HAND with two siblings:
|
||||
# - scripts/common/shims/jspi-scheduler.js installExportWraps list (same set
|
||||
# minus main — the runtime calls main before the wraps exist)
|
||||
# - tests/apps/Makefile.wasm WX_JSPI_EXPORTS (comma-joined copy)
|
||||
main
|
||||
wx_dom_event
|
||||
wx_dom_mouse
|
||||
|
|
|
|||
|
|
@ -1,242 +0,0 @@
|
|||
// === 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 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
|
||||
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.log("[DIAG_ASYNC_CALL] func=" + func + " arg=" + arg + " millis=" + millis +
|
||||
" inBounds=" + inBounds + " modalActive=" + modalActive + " state=" + asyncState());
|
||||
if (!inBounds) { console.log("[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.log("[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);
|
||||
var heap32 = function () { return (typeof GROWABLE_HEAP_I32 === "function") ? GROWABLE_HEAP_I32() : HEAP32; };
|
||||
Asyncify.doRewind = function(ptr) {
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 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.log("[DIAG_DYNCALL_VI] OUT OF BOUNDS index=" + index + " tableLen=" + tableLen() +
|
||||
" modalActive=" + modalActive + " state=" + asyncState());
|
||||
console.trace();
|
||||
}
|
||||
return __origDynCallVi(index, a0);
|
||||
};
|
||||
}
|
||||
|
||||
// 5. Modal lifecycle: poll the scheduler wait registry ("modal" waits).
|
||||
// (The legacy Module._endModal hook was deleted at doc 20 D-1.)
|
||||
if (typeof Module !== "undefined") {
|
||||
var lastModalWaits = 0;
|
||||
setInterval(function() {
|
||||
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;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 6. EM_ASYNC_JS sleeps (wxWasmYieldUntilJs, js_enumerateFonts, clipboard, etc.) — log
|
||||
// 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;
|
||||
console.log("[DIAG_SLEEP] ENTER id=" + id + " state=" + asyncState() +
|
||||
" currData=" + ((typeof Asyncify.currData !== "undefined" && Asyncify.currData) || "null"));
|
||||
return __diagOrigHandleSleep(function(wakeUp) {
|
||||
return startAsync(function(result) {
|
||||
console.log("[DIAG_SLEEP] WAKE id=" + id + " state=" + asyncState() +
|
||||
" currData=" + ((typeof Asyncify.currData !== "undefined" && Asyncify.currData) || "null"));
|
||||
return wakeUp(result);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// 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 + "]");
|
||||
})();
|
||||
// === End diagnostics ===
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
// jspi-scheduler.js — the JSPI-era successor of asyncify-scheduler.js.
|
||||
// jspi-scheduler.js — the wx scheduler shim for the JSPI runtime.
|
||||
//
|
||||
// Ships as a --pre-js. Keeps the S4 token-wait registry contract byte-for-byte
|
||||
// Ships as a --pre-js. Provides the S4 token-wait registry
|
||||
// (beginWait/waitPromise/resolveWait/resolveTopWait/waitEarlyResolved/
|
||||
// takeWaitResult/pendingWaits/noteContextWait/shutdown) so every C++ bridge
|
||||
// and web caller keeps working, and adds the two things JSPI needs:
|
||||
// takeWaitResult/pendingWaits/shutdown) that every C++ bridge and web
|
||||
// caller relies on, plus the two things JSPI needs:
|
||||
//
|
||||
// 1. ACTIVATION TRACKING. Every promising export the app declares is wrapped
|
||||
// so the shim always knows which activation is executing synchronously
|
||||
|
|
@ -20,30 +20,21 @@
|
|||
// into parked frames' locals (stack-allocated wxDialog members mutated
|
||||
// by a cross-tick EndModal), resurrecting dead state at resume.
|
||||
//
|
||||
// Observability: an event ring + live activation table via __wxWaitDump()
|
||||
// (alias __wxAsyncifyDump kept one release for crash-report consumers).
|
||||
//
|
||||
// Asyncify-era machinery that has NO successor here, by design: currData
|
||||
// single-writer tripwire, deferred-wake queue, stale-fiber quarantine,
|
||||
// trampoline heal, dyncall shims — the states they policed are
|
||||
// unrepresentable under JSPI.
|
||||
// Observability: an event ring + live activation table via __wxWaitDump().
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
if (globalThis.__wxScheduler && globalThis.__wxScheduler.backend === "jspi") {
|
||||
if (globalThis.__wxSchedulerInstalled) {
|
||||
return; // idempotent under double injection
|
||||
}
|
||||
|
||||
var RING_CAP = 256;
|
||||
|
||||
var S = {
|
||||
backend: "jspi",
|
||||
|
||||
// --- mailbox lane (timers/wheel; ordering machinery, mechanism-free) ----
|
||||
// Same contract as the asyncify shim: enqueueAfter queues a C callback,
|
||||
// delivery happens through the dedicated _wxWasmMailboxTick export from a
|
||||
// fresh task, in order. Under JSPI the tick is a promising export — a
|
||||
// enqueueAfter queues a C callback; delivery happens through the dedicated
|
||||
// _wxWasmMailboxTick export from a fresh task, in order. The tick is a
|
||||
// suspension inside a delivered handler parks the tick's own activation,
|
||||
// and the rejection path carries the same containment (a throwing handler
|
||||
// must not leave a parked quasi-modal unresolved).
|
||||
|
|
@ -94,17 +85,12 @@
|
|||
}, 0);
|
||||
},
|
||||
|
||||
// --- S1 embind lane (contract-identical port from asyncify-scheduler) --
|
||||
// --- S1 embind lane --
|
||||
// Mutators (doc 18 classification) must not enter wasm while a load is in
|
||||
// flight: the open activation is suspended mid-load and a collab-apply /
|
||||
// save / theme flip entering between its parks would mutate the board
|
||||
// under it. Semantic exclusion — nothing asyncify-specific about it.
|
||||
// The FIFO drains, in order, once kicadOpenFileBusy clears.
|
||||
//
|
||||
// Retired here, by design: _wrapOpenFile / kicadOpenFileStart (the
|
||||
// asyncify Phase F starter route). Under JSPI kicadOpenFile is an embind
|
||||
// async() export — its own promising activation parks legally and the
|
||||
// call returns a real Promise; no dispatch-context detour exists.
|
||||
// under it. The exclusion is semantic, independent of the suspension
|
||||
// mechanism. The FIFO drains, in order, once kicadOpenFileBusy clears.
|
||||
MUTATOR_NAMES: [
|
||||
"kicadSetChrome", "kicadSetReadOnly",
|
||||
"kicadCollabApply", "kicadCollabApplyItems",
|
||||
|
|
@ -181,16 +167,11 @@
|
|||
},
|
||||
|
||||
// The embind PARKERs (kicadOpenFile / kicadOpenFiles / kicadLibsReload,
|
||||
// registered emscripten::async() under PCBJAM_JSPI): wrap them with the
|
||||
// registered emscripten::async()): wrap them with the
|
||||
// same activation tracking as the raw promising exports, so their parks
|
||||
// (wxWasmYieldUntil inside the load) find a tracked record and get the
|
||||
// green-copy spill-stack discipline. Embind names live on Module WITHOUT
|
||||
// green-region spill-stack discipline. Embind names live on Module WITHOUT
|
||||
// the underscore prefix, hence the separate installer.
|
||||
// NOT here: the kicadTestFiberPark* levers. They are emscripten::async()
|
||||
// (a plain embind call into a suspending body throws on strict-JSPI
|
||||
// Firefox), but the parker wrap's turnstile queueing would DEFER a
|
||||
// mid-park poke until the park drains — the exact race the levers exist
|
||||
// to stage. Their suspensions ride the untracked-anon-record path.
|
||||
PARKER_NAMES: ["kicadOpenFile", "kicadOpenFiles", "kicadLibsReload"],
|
||||
_wrapParkers: function () {
|
||||
var wrapped = 0;
|
||||
|
|
@ -252,12 +233,6 @@
|
|||
return entry.result | 0;
|
||||
},
|
||||
|
||||
// JSPI: context waits do not exist; kept as a loud no-op for transition
|
||||
// callers (nothing registers them — wxWasmYieldUntil suspends in place).
|
||||
noteContextWait: function (token) {
|
||||
console.warn("[wx-scheduler] noteContextWait(" + token + ") under jspi backend");
|
||||
},
|
||||
|
||||
resolveWait: function (token, result) {
|
||||
var entry = this.waits.get(token);
|
||||
if (!entry || entry.resolved) return false;
|
||||
|
|
@ -294,7 +269,7 @@
|
|||
dead: false,
|
||||
shutdown: function (why) {
|
||||
this.dead = true;
|
||||
// S6 teardown contract (same as the asyncify shim): queued-but-
|
||||
// S6 teardown contract: queued-but-
|
||||
// undelivered mutators FAIL LOUDLY instead of hanging their callers,
|
||||
// and undelivered mailbox messages drop — the pumps stop themselves on
|
||||
// the dead flag.
|
||||
|
|
@ -307,8 +282,8 @@
|
|||
if (stranded) {
|
||||
console.warn("[wx-scheduler] shutdown (" + why + ") stranded:" + stranded);
|
||||
} else {
|
||||
// teardown-gate contract (e2e/app-quit.spec.ts, ported from the
|
||||
// asyncify shim): a clean exit must SAY so on the console
|
||||
// teardown-gate contract (e2e/app-quit.spec.ts): a clean exit must
|
||||
// SAY so on the console
|
||||
console.log("[wx-scheduler] shutdown (" + why + ") clean");
|
||||
}
|
||||
this._note("shutdown", why, stranded);
|
||||
|
|
@ -751,7 +726,7 @@
|
|||
return wrapped;
|
||||
},
|
||||
|
||||
// --- observability skeleton (finalized in Phase 7) ---------------------
|
||||
// --- observability ------------------------------------------------------
|
||||
_ring: [],
|
||||
_note: function (ev, a, b) {
|
||||
this._ring.push([Date.now(), ev, String(a), b | 0]);
|
||||
|
|
@ -768,7 +743,6 @@
|
|||
});
|
||||
});
|
||||
return {
|
||||
backend: "jspi",
|
||||
dead: this.dead,
|
||||
waitsBegun: this.waitsBegun,
|
||||
waitsResolved: this.waitsResolved,
|
||||
|
|
@ -787,7 +761,7 @@
|
|||
globalThis.__wxScheduler = S;
|
||||
globalThis.__wxSchedulerInstalled = true; // wxWasmSchedulerAssertInstalled probe
|
||||
|
||||
// --- Phase 7 signals ------------------------------------------------------
|
||||
// --- diagnostic signals ---------------------------------------------------
|
||||
// SuspendError attributor: a SuspendError means a PLAIN (non-promising)
|
||||
// wasm entry tried to park — a missed -sJSPI_EXPORTS/installExportWraps
|
||||
// entry. The engine cannot say WHICH export, but the live dump (what was
|
||||
|
|
@ -825,8 +799,6 @@
|
|||
});
|
||||
}, 10000);
|
||||
globalThis.__wxWaitDump = function () { return S.dump(); };
|
||||
// transition alias: crash-report consumers read __wxAsyncifyDump
|
||||
globalThis.__wxAsyncifyDump = globalThis.__wxWaitDump;
|
||||
|
||||
// Self-install the activation wraps once the runtime is up (this file ships
|
||||
// as a --pre-js, so Module exists here). The name set mirrors the
|
||||
|
|
|
|||
Loading…
Reference in a new issue