jspi: retire the asyncify pipeline — knob, post-link tail, binaryen hooks
Phase 8 in the parent repo. Deleted: asyncify-scheduler.js, apply-asyncify.sh, apply-finalize.sh, inject-dyncall-shims.sh, asyncify-imports/removelist.txt, the wasm-opt/finalize stub pair, scripts/binaryen-hoist-pass/ (the fork stays a dormant submodule; removal is a follow-up), bench/wasm-opt-bench.sh (README marked historical), wasm/shims/context_sleep.cpp, and the sched-context harness app + Makefile targets. PCBJAM_ASYNC_BACKEND is gone: build-wx-wasm.sh hardcodes the jspi stamp (still force-cleans pre-migration trees), build-kicad-target.sh gives editors the JSPI link surface and the CLIs nothing (they pin ASYNCIFY=0), the stub dance is replaced by an unconditional .real-restore, build-wasm-test.sh lost its whole post-link loop, docker/build.sh's postprocess is the ENV shim only, and Makefile.wasm links every app JSPI with the scheduler shim as a tracked prerequisite. pcbjam_async_policy.h keys on __EMSCRIPTEN__. jspi-scheduler.js: wxWasmMainLoopPump dropped from the wrap census (the export died with the D5 detach); inert [TRACE] instrumentation removed. CI: wasm-build.yml rewritten for the single-cache pipeline (one output cache keyed on compile inputs; post-processed bytes cached after the shim); opt_level input removed from both callers. wasm-cache-hash.mjs inputs now cover patch-env-shim.mjs + jspi-scheduler.js + jspi-exports.txt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NDeBaKKhQztd8KiVtHuyXr
This commit is contained in:
parent
3f09a46ff5
commit
e14faeca8b
36 changed files with 170 additions and 3348 deletions
|
|
@ -1,3 +1,9 @@
|
|||
> **Historical (asyncify era).** These benches timed the Binaryen
|
||||
> `apply-asyncify` post-link tail, which the JSPI migration deleted — the
|
||||
> scripts below that reference it are gone. The VM provisioning pieces
|
||||
> (setup-vm.sh, vm-build.sh, cloud-init) remain useful for any host-side
|
||||
> build benching.
|
||||
|
||||
# wasm-opt allocator/core benchmark
|
||||
|
||||
Fast, local feedback loop for the CI perf issue: the host-side `wasm-opt`/asyncify
|
||||
|
|
|
|||
|
|
@ -1,114 +0,0 @@
|
|||
#!/bin/bash
|
||||
# wasm-opt allocator/core benchmark — RUNS INSIDE THE LINUX VM.
|
||||
#
|
||||
# Times the host-side wasm-opt/asyncify pass (scripts/common/apply-asyncify.sh)
|
||||
# over a prebuilt eeschema .wasm across a matrix of {glibc, jemalloc} x core
|
||||
# counts, to find why the step is slow on glibc CI and what BINARYEN_CORES helps.
|
||||
#
|
||||
# Why this isolates the right thing: wasm-opt/asyncify is a standalone pass over
|
||||
# an already-compiled .wasm (see docker/build.sh:194). We never compile KiCad
|
||||
# here — we just replay the optimizer over a fixture built once on the host.
|
||||
#
|
||||
# Usage (in the VM, from the repo root):
|
||||
# ./scripts/bench/wasm-opt-bench.sh [fixture.wasm]
|
||||
# Env:
|
||||
# CORES="1 4 8 10" core counts to sweep (BINARYEN_CORES)
|
||||
# ALLOCS="glibc jemalloc"
|
||||
# STRACE=1 also run a syscall-count pass per allocator at max cores
|
||||
#
|
||||
# Output: a CSV table on stdout (also tee'd to bench/results.csv) plus per-cell
|
||||
# logs under bench/results/ (each holds apply-asyncify's own per-pass `time -v`).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
|
||||
FIXTURE="${1:-${REPO}/bench/eeschema.finalized.wasm}"
|
||||
CORES="${CORES:-1 4 8 10}"
|
||||
ALLOCS="${ALLOCS:-glibc jemalloc}"
|
||||
OUTDIR="${REPO}/bench/results"
|
||||
CSV="${REPO}/bench/results.csv"
|
||||
|
||||
if [[ ! -f "${FIXTURE}" ]]; then
|
||||
echo "ERROR: fixture not found: ${FIXTURE}" >&2
|
||||
echo "Create it on the host (see scripts/bench/README.md) and scp it in." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$(uname -s)" != "Linux" ]]; then
|
||||
echo "ERROR: run this inside the Linux VM (glibc is the point); host is $(uname -s)." >&2
|
||||
exit 1
|
||||
fi
|
||||
command -v /usr/bin/time >/dev/null || { echo "ERROR: install GNU time (apt-get install -y time)" >&2; exit 1; }
|
||||
|
||||
mkdir -p "${OUTDIR}"
|
||||
echo "cores,alloc,wall_clock,wall_s,peak_rss_kb,preload" > "${CSV}"
|
||||
|
||||
# Convert GNU time's "Elapsed (wall clock)" field ([h:]m:ss[.ss]) to seconds.
|
||||
to_seconds() {
|
||||
awk -F: '{ if (NF==3) print $1*3600+$2*60+$3; else if (NF==2) print $1*60+$2; else print $1 }'
|
||||
}
|
||||
|
||||
run_cell() {
|
||||
local cores="$1" alloc="$2"
|
||||
local logf="${OUTDIR}/${alloc}-c${cores}.log"
|
||||
local timef="${OUTDIR}/${alloc}-c${cores}.time"
|
||||
|
||||
cp "${FIXTURE}" /tmp/bench-in.wasm
|
||||
|
||||
# glibc baseline forces no preload; jemalloc leaves WASM_OPT_PRELOAD unset so
|
||||
# apply-asyncify.sh auto-detects the system libjemalloc.
|
||||
local -a env_prefix=(BINARYEN_CORES="${cores}")
|
||||
if [[ "${alloc}" == "glibc" ]]; then
|
||||
env_prefix+=(WASM_OPT_PRELOAD=none)
|
||||
fi
|
||||
|
||||
echo ">>> ${alloc} BINARYEN_CORES=${cores}" >&2
|
||||
if ! env "${env_prefix[@]}" /usr/bin/time -v -o "${timef}" \
|
||||
"${REPO}/scripts/common/apply-asyncify.sh" /tmp/bench-in.wasm /tmp/bench-out.wasm \
|
||||
>"${logf}" 2>&1; then
|
||||
echo " FAILED (see ${logf})" >&2
|
||||
echo "${cores},${alloc},FAILED,,," >> "${CSV}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local wall maxrss preload wall_s
|
||||
wall=$(grep -F "Elapsed (wall clock)" "${timef}" | awk '{print $NF}')
|
||||
maxrss=$(grep -F "Maximum resident set size" "${timef}" | awk '{print $NF}')
|
||||
preload=$(grep -m1 -F "LD_PRELOAD=" "${logf}" | sed 's/.*LD_PRELOAD=//' | tr -d ' ')
|
||||
wall_s=$(printf '%s' "${wall}" | to_seconds)
|
||||
echo " wall=${wall} (${wall_s}s) peakRSS=${maxrss}KB preload=${preload}" >&2
|
||||
echo "${cores},${alloc},${wall},${wall_s},${maxrss},${preload}" >> "${CSV}"
|
||||
}
|
||||
|
||||
for c in ${CORES}; do
|
||||
for a in ${ALLOCS}; do
|
||||
run_cell "${c}" "${a}"
|
||||
done
|
||||
done
|
||||
|
||||
# Optional: confirm the futex storm collapses with jemalloc. strace -c adds heavy
|
||||
# overhead, so this is a separate, single-pass-per-allocator measurement at the
|
||||
# highest core count, not part of the timing matrix above.
|
||||
if [[ "${STRACE:-0}" == "1" ]]; then
|
||||
command -v strace >/dev/null || { echo "strace not installed; skipping" >&2; STRACE=0; }
|
||||
fi
|
||||
if [[ "${STRACE:-0}" == "1" ]]; then
|
||||
maxc="$(echo ${CORES} | tr ' ' '\n' | sort -n | tail -1)"
|
||||
WASM_OPT="$("${REPO}/scripts/common/get-wasm-opt.sh" 2>/dev/null)"
|
||||
for a in ${ALLOCS}; do
|
||||
cp "${FIXTURE}" /tmp/bench-in.wasm
|
||||
local_preload=""
|
||||
[[ "${a}" == "jemalloc" ]] && local_preload="$(ls /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 2>/dev/null || true)"
|
||||
echo ">>> strace ${a} (asyncify pass, BINARYEN_CORES=${maxc})" >&2
|
||||
env BINARYEN_CORES="${maxc}" ${local_preload:+LD_PRELOAD=${local_preload}} \
|
||||
strace -f -c -e trace=futex,mmap,munmap -o "${OUTDIR}/strace-${a}.txt" \
|
||||
"${WASM_OPT}" --asyncify /tmp/bench-in.wasm -o /tmp/bench-out.wasm \
|
||||
>"${OUTDIR}/strace-${a}.log" 2>&1 || echo " strace ${a} failed (see log)" >&2
|
||||
done
|
||||
echo "strace summaries: ${OUTDIR}/strace-*.txt" >&2
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== results (${CSV}) ==="
|
||||
column -t -s, "${CSV}"
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
#!/bin/bash
|
||||
# Build the host post-process Binaryen tools from our submodule: wasm-opt (with the
|
||||
# catch-arm-hoisting pass, --hoist-cpp-catches) AND wasm-emscripten-finalize.
|
||||
#
|
||||
# Source of truth is the tracked Binaryen submodule (binaryen/, branch wasm-port =
|
||||
# upstream version_130 + src/passes/HoistCppCatches.cpp). This configures an out-of-source
|
||||
# build into the gitignored build-wasm/ tree and prints the wasm-opt path on stdout
|
||||
# (build progress to stderr); wasm-emscripten-finalize lands next to it in the same bin/
|
||||
# (apply-finalize.sh derives it from the wasm-opt dir). Building both from one submodule
|
||||
# keeps finalize and asyncify on a single Binaryen version and removes the host emsdk
|
||||
# dependency from the post-process. See docs/features/wasm-exceptions/06-spike-plan.md (Phase 1.5).
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
SRC="${PROJECT_ROOT}/binaryen"
|
||||
BUILD="${PROJECT_ROOT}/build-wasm/tools/binaryen-hoist-build"
|
||||
|
||||
if [ ! -f "${SRC}/src/passes/HoistCppCatches.cpp" ]; then
|
||||
echo "ERROR: binaryen submodule is missing the hoist pass." >&2
|
||||
echo " Run: git submodule update --init binaryen" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# CI fast-path: the workflow cache-restores bin/ + lib/ keyed on the exact submodule
|
||||
# SHA and sets this var on a hit, so the restored binaries are authoritative — skip
|
||||
# cmake+ninja. Trust only if both tools actually RUN: the binaries dynamically link
|
||||
# lib/libbinaryen.so, so an existence check alone passes on an incomplete restore
|
||||
# (bin/ without lib/ shipped a red main, run 28585074335) while --version proves the
|
||||
# loader resolves everything. Never set the var locally when iterating on the pass:
|
||||
# uncommitted source edits would be silently ignored (the SHA key can't see them).
|
||||
if [ "${BINARYEN_TRUST_PREBUILT:-0}" = "1" ] \
|
||||
&& "${BUILD}/bin/wasm-opt" --version >/dev/null 2>&1 \
|
||||
&& "${BUILD}/bin/wasm-emscripten-finalize" --version >/dev/null 2>&1; then
|
||||
echo "Using prebuilt Binaryen tools (BINARYEN_TRUST_PREBUILT=1): ${BUILD}/bin" >&2
|
||||
echo "${BUILD}/bin/wasm-opt"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Configure once (mirrors scripts/common/get-wasm-opt.sh's from-source flags).
|
||||
if [ ! -f "${BUILD}/build.ninja" ]; then
|
||||
echo "Configuring Binaryen submodule build (one-time, ~5 min to build)..." >&2
|
||||
cmake -S "${SRC}" -B "${BUILD}" -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_CXX_FLAGS="-Wno-maybe-uninitialized" \
|
||||
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON -DBUILD_TESTS=OFF >&2
|
||||
fi
|
||||
|
||||
ninja -C "${BUILD}" wasm-opt wasm-emscripten-finalize >&2
|
||||
|
||||
echo "${BUILD}/bin/wasm-opt"
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
// Minimal Asyncify harness: drive one unwind/rewind through $vt and check it yields 50.
|
||||
const fs = require('fs');
|
||||
const path = process.argv[2];
|
||||
const BUF = 16, STACK = 1024, STACK_END = 16384;
|
||||
let inst, rewinding = false, pending = 0;
|
||||
|
||||
const imports = {
|
||||
env: {
|
||||
sleep: (ms) => {
|
||||
if (!rewinding) {
|
||||
inst.exports.asyncify_start_unwind(BUF);
|
||||
pending = ms;
|
||||
return 0; // dummy; value is discarded as we unwind
|
||||
} else {
|
||||
inst.exports.asyncify_stop_rewind();
|
||||
rewinding = false;
|
||||
return pending; // real value supplied on rewind
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mod = new WebAssembly.Module(fs.readFileSync(path));
|
||||
inst = new WebAssembly.Instance(mod, imports);
|
||||
// asyncify buffer struct: [current, end]
|
||||
const mem = new Int32Array(inst.exports.memory.buffer);
|
||||
mem[BUF >> 2] = STACK;
|
||||
mem[(BUF + 4) >> 2] = STACK_END;
|
||||
|
||||
inst.exports.vt(); // runs, throws, catch calls sleep -> starts unwind, returns
|
||||
inst.exports.asyncify_stop_unwind();
|
||||
inst.exports.asyncify_start_rewind(BUF);
|
||||
rewinding = true;
|
||||
const r = inst.exports.vt(); // rewinds into the catch handler; sleep returns 50; vt completes
|
||||
console.log('vt result =', r);
|
||||
process.exit(r === 50 ? 0 : 1);
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
;; The DuplicateSymbol delegate-orphan shape, but the hoisted cpp arm SUSPENDS (calls an async import)
|
||||
;; before its __cxa_end_catch cleanup (try (do ..) (delegate $M)). Drives a real Asyncify unwind/rewind
|
||||
;; through the hoisted arm AND its retargeted delegate ($M -> caller), proving the fix doesn't break
|
||||
;; the suspend/rewind the hoist exists for. Same harness contract as the other -suspend tests: $vt
|
||||
;; yields 50.
|
||||
(module
|
||||
(import "env" "sleep" (func $sleep (param i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(tag $cpp (param i32))
|
||||
(func $vt (export "vt") (result i32)
|
||||
(local $r i32)
|
||||
(try $A
|
||||
(do
|
||||
(throw $cpp (i32.const 0)))
|
||||
(catch_all
|
||||
(try $M
|
||||
(do
|
||||
(try $inner
|
||||
(do
|
||||
(rethrow $A))
|
||||
(catch $cpp
|
||||
(drop (pop i32))
|
||||
(local.set $r (call $sleep (i32.const 50)))
|
||||
(try
|
||||
(do (nop))
|
||||
(delegate $M)))
|
||||
(catch_all
|
||||
(rethrow $A))))
|
||||
(catch_all
|
||||
(rethrow $A)))))
|
||||
(local.get $r))
|
||||
)
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
;; Regression repro of SYMBOL_EDIT_FRAME::DuplicateSymbol (KiCad eeschema). A cpp catch arm that is
|
||||
;; hoisted PAST an ancestor catch_all (the case-6 deferral) carries a nested __cxa_end_catch cleanup
|
||||
;; (try (do ..) (delegate $M)) whose delegate target $M is a mid try sitting INSIDE that ancestor
|
||||
;; catch_all. Hoisting the arm out (into the dispatch section, outside every try) orphaned the
|
||||
;; delegate -> wasm-validator "all delegate targets must be valid, on (delegate $M)". The fix
|
||||
;; retargets it to DELEGATE_CALLER_TARGET (a delegate can only target a try or the caller, not the
|
||||
;; $done block); re-throwing a cleanup exception to the caller is the C++ throw-during-cleanup
|
||||
;; (std::terminate) path, never taken in normal flow. cpp tag = single i32.
|
||||
(module
|
||||
(tag $cpp (param i32))
|
||||
|
||||
;; Exception path: $A throws cpp(1); its catch_all (re)throws into $inner whose cpp arm sets r:=42,
|
||||
;; its __cxa_end_catch cleanup delegates to the enclosing $M. expect 42.
|
||||
(func $caught (export "caught") (result i32)
|
||||
(local $r i32)
|
||||
(try $A
|
||||
(do
|
||||
(throw $cpp (i32.const 1)))
|
||||
(catch_all
|
||||
(try $M
|
||||
(do
|
||||
(try $inner
|
||||
(do
|
||||
(rethrow $A))
|
||||
(catch $cpp
|
||||
(drop (pop i32))
|
||||
(local.set $r (i32.const 42))
|
||||
(try
|
||||
(do (nop))
|
||||
(delegate $M)))
|
||||
(catch_all
|
||||
(rethrow $A))))
|
||||
(catch_all
|
||||
(rethrow $A)))))
|
||||
(local.get $r))
|
||||
|
||||
;; No-exception path: $A body falls through with r:=7. expect 7.
|
||||
(func $normal (export "normal") (result i32)
|
||||
(local $r i32)
|
||||
(try $A
|
||||
(do
|
||||
(local.set $r (i32.const 7)))
|
||||
(catch_all
|
||||
(try $M
|
||||
(do
|
||||
(try $inner
|
||||
(do
|
||||
(rethrow $A))
|
||||
(catch $cpp
|
||||
(drop (pop i32))
|
||||
(local.set $r (i32.const 42))
|
||||
(try
|
||||
(do (nop))
|
||||
(delegate $M)))
|
||||
(catch_all
|
||||
(rethrow $A))))
|
||||
(catch_all
|
||||
(rethrow $A)))))
|
||||
(local.get $r))
|
||||
)
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
;; Regression repro of PGM_BASE::HandleException (KiCad) — the case-6 shape (a cpp catch nested in an
|
||||
;; outer try's catch_all cleanup pad) WITH an intervening block in that cleanup that the cpp arm
|
||||
;; br's to. LLVM emits this for `try{} catch(A&) catch(B&) catch(...)`: the outer try has only a
|
||||
;; catch_all (destructor cleanup), inside which it (rethrow)s into a nested try whose cpp catch does
|
||||
;; the __cxa type dispatch and, when done, (br)s OUT to a block ($blk) sitting between the escape
|
||||
;; target and the arm. Hoisting that arm to the dispatch section orphaned the br — "all break targets
|
||||
;; must be valid, on (br $blk)". cpp tag = single i32.
|
||||
(module
|
||||
(tag $cpp (param i32))
|
||||
|
||||
;; Exception path: outer body throws cpp(1); the catch_all reclassifies via (rethrow $outer) inside
|
||||
;; (block $blk); the nested cpp arm handles it (r := 42) then (br $blk) to finish. expect 42.
|
||||
(func $caught (export "caught") (result i32)
|
||||
(local $r i32)
|
||||
(try $outer
|
||||
(do
|
||||
(throw $cpp (i32.const 1)))
|
||||
(catch_all
|
||||
(block $blk
|
||||
(try
|
||||
(do
|
||||
(rethrow $outer))
|
||||
(catch $cpp
|
||||
(drop (pop i32))
|
||||
(local.set $r (i32.const 42))
|
||||
(br $blk))
|
||||
(catch_all
|
||||
(rethrow $outer))))))
|
||||
(local.get $r))
|
||||
|
||||
;; No-exception path: body falls through with r := 7, neither catch runs. expect 7.
|
||||
(func $normal (export "normal") (result i32)
|
||||
(local $r i32)
|
||||
(try $outer
|
||||
(do
|
||||
(local.set $r (i32.const 7)))
|
||||
(catch_all
|
||||
(block $blk
|
||||
(try
|
||||
(do
|
||||
(rethrow $outer))
|
||||
(catch $cpp
|
||||
(drop (pop i32))
|
||||
(local.set $r (i32.const 42))
|
||||
(br $blk))
|
||||
(catch_all
|
||||
(rethrow $outer))))))
|
||||
(local.get $r))
|
||||
)
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
;; The HandleException nested-catchall-exit-block shape, but the nested cpp arm SUSPENDS (calls an
|
||||
;; async import) before it br's the intervening block. This drives a real Asyncify unwind/rewind
|
||||
;; through the hoisted arm AND its retargeted br ($blk -> $done), proving the fix doesn't break the
|
||||
;; suspend/rewind the hoist exists for. Same harness contract as value-typed-suspend.wat: $vt yields 50.
|
||||
(module
|
||||
(import "env" "sleep" (func $sleep (param i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(tag $cpp (param i32))
|
||||
(func $vt (export "vt") (result i32)
|
||||
(local $r i32)
|
||||
(try $outer
|
||||
(do
|
||||
(throw $cpp (i32.const 0)))
|
||||
(catch_all
|
||||
(block $blk
|
||||
(try
|
||||
(do
|
||||
(rethrow $outer))
|
||||
(catch $cpp
|
||||
(drop (pop i32))
|
||||
(local.set $r (call $sleep (i32.const 50)))
|
||||
(br $blk))
|
||||
(catch_all
|
||||
(rethrow $outer))))))
|
||||
(local.get $r))
|
||||
)
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Regression test for the value-typed (concrete-result) path of --hoist-cpp-catches.
|
||||
#
|
||||
# Value-typed cpp-catch tries do not arise from normal C++ EH lowering (LLVM keeps catch values in
|
||||
# locals → void/unreachable tries), so this case can't live in a C++ EH toy. These
|
||||
# hand-written modules exercise it directly:
|
||||
# (1) fuzz-exec — the pass must preserve the result value of value-typed cpp-catch tries
|
||||
# (exception path, no-exception path, and exception-payload routing).
|
||||
# (2) a real asyncify unwind/rewind through a value-typed catch that SUSPENDS (must yield 50).
|
||||
#
|
||||
# Requires wasm-opt built from the binaryen submodule (scripts/binaryen-hoist-pass/build-wasm-opt.sh);
|
||||
# that one binary (version_130 + our hoist pass) does both --hoist-cpp-catches and --asyncify. Run from anywhere.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
DIR="$ROOT/scripts/binaryen-hoist-pass/tests"
|
||||
WASMOPT="$ROOT/build-wasm/tools/binaryen-hoist-build/bin/wasm-opt"
|
||||
V130="$WASMOPT" # the submodule fork IS version_130 (asyncify unchanged), so it does --asyncify too
|
||||
[ -x "$WASMOPT" ] || { echo "build wasm-opt first: scripts/binaryen-hoist-pass/build-wasm-opt.sh"; exit 1; }
|
||||
|
||||
echo "== (1) value semantics preserved (fuzz-exec) =="
|
||||
"$WASMOPT" --hoist-cpp-catches -all -all --fuzz-exec "$DIR/value-typed-cpp-catch.wat" -o /dev/null 2>&1 \
|
||||
| grep -E 'comparing|=>'
|
||||
|
||||
echo "== (2) asyncify unwind/rewind through a value-typed suspending catch =="
|
||||
"$WASMOPT" --hoist-cpp-catches -all -all "$DIR/value-typed-suspend.wat" -o /tmp/vt_s.hoisted.wasm
|
||||
"$V130" --asyncify -all --pass-arg=asyncify-imports@env.sleep /tmp/vt_s.hoisted.wasm -o /tmp/vt_s.async.wasm
|
||||
node "$DIR/asyncify-harness.js" /tmp/vt_s.async.wasm
|
||||
echo "OK — value-typed path verified"
|
||||
|
||||
# Regression for the PGM_BASE::HandleException shape: a cpp catch nested in an outer try's catch_all
|
||||
# cleanup pad whose arm br's to an intervening block ($blk). Before the fix the hoisted arm's br was
|
||||
# orphaned -> "all break targets must be valid". (3) validates + checks value semantics; (4) drives a
|
||||
# real unwind/rewind through the hoisted arm with the retargeted br.
|
||||
echo "== (3) nested-catchall exit-block hoists + validates (fuzz-exec: caught=>42, normal=>7) =="
|
||||
"$WASMOPT" --hoist-cpp-catches -all -all --fuzz-exec "$DIR/nested-catchall-exit-block.wat" -o /dev/null 2>&1 \
|
||||
| grep -E 'comparing|=>'
|
||||
|
||||
echo "== (4) asyncify unwind/rewind through a suspending nested-catchall arm =="
|
||||
"$WASMOPT" --hoist-cpp-catches -all -all "$DIR/nested-catchall-suspend.wat" -o /tmp/ncb_s.hoisted.wasm
|
||||
"$V130" --asyncify -all --pass-arg=asyncify-imports@env.sleep /tmp/ncb_s.hoisted.wasm -o /tmp/ncb_s.async.wasm
|
||||
node "$DIR/asyncify-harness.js" /tmp/ncb_s.async.wasm
|
||||
echo "OK — nested-catchall path verified"
|
||||
|
||||
# Regression for the SYMBOL_EDIT_FRAME::DuplicateSymbol shape: a cpp catch arm hoisted PAST an ancestor
|
||||
# catch_all carries a nested __cxa_end_catch cleanup (try (do ..) (delegate $M)) whose delegate target
|
||||
# sits inside that catch_all -> orphaned by hoisting ("all delegate targets must be valid"). The fix
|
||||
# retargets it to DELEGATE_CALLER_TARGET. (5) validates + checks value semantics; (6) drives a real
|
||||
# unwind/rewind through the hoisted arm with the retargeted delegate.
|
||||
echo "== (5) delegate-orphan hoists + validates (fuzz-exec: caught=>42, normal=>7) =="
|
||||
"$WASMOPT" --hoist-cpp-catches -all -all --fuzz-exec "$DIR/delegate-orphan.wat" -o /dev/null 2>&1 \
|
||||
| grep -E 'comparing|=>'
|
||||
|
||||
echo "== (6) asyncify unwind/rewind through a suspending delegate-orphan arm =="
|
||||
"$WASMOPT" --hoist-cpp-catches -all -all "$DIR/delegate-orphan-suspend.wat" -o /tmp/dlg_s.hoisted.wasm
|
||||
"$V130" --asyncify -all --pass-arg=asyncify-imports@env.sleep /tmp/dlg_s.hoisted.wasm -o /tmp/dlg_s.async.wasm
|
||||
node "$DIR/asyncify-harness.js" /tmp/dlg_s.async.wasm
|
||||
echo "OK — delegate-orphan path verified"
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
;; Hand-written value-typed cpp-catch tries to exercise the $result routing in --hoist-cpp-catches.
|
||||
;; cpp tag = single i32 param. Each function returns an i32 via a (try (result i32) ...).
|
||||
(module
|
||||
(tag $cpp (param i32))
|
||||
;; exception path: body throws, catch yields 42 -> expect 42
|
||||
(func $vt_throw (export "vt_throw") (result i32)
|
||||
(try (result i32)
|
||||
(do (throw $cpp (i32.const 99)))
|
||||
(catch $cpp (drop (pop i32)) (i32.const 42))))
|
||||
;; normal path: body yields 7, catch never runs -> expect 7
|
||||
(func $vt_normal (export "vt_normal") (result i32)
|
||||
(try (result i32)
|
||||
(do (i32.const 7))
|
||||
(catch $cpp (drop (pop i32)) (i32.const 42))))
|
||||
;; payload routing: catch returns the exception payload it caught (123) -> expect 123
|
||||
(func $vt_payload (export "vt_payload") (result i32)
|
||||
(try (result i32)
|
||||
(do (throw $cpp (i32.const 123)))
|
||||
(catch $cpp (pop i32))))
|
||||
)
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
;; Value-typed try whose catch SUSPENDS: the catch calls an async import and yields its result.
|
||||
;; Drives the $result routing across a real asyncify unwind/rewind.
|
||||
(module
|
||||
(import "env" "sleep" (func $sleep (param i32) (result i32)))
|
||||
(memory (export "memory") 1)
|
||||
(tag $cpp (param i32))
|
||||
(func $vt (export "vt") (result i32)
|
||||
(try (result i32)
|
||||
(do (throw $cpp (i32.const 0)))
|
||||
(catch $cpp (drop (pop i32)) (call $sleep (i32.const 50))))))
|
||||
|
|
@ -96,36 +96,12 @@ if [ "$CLEAN_BUILD" = "1" ]; then
|
|||
make -f Makefile.wasm clean 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Native wasm-EH is the only build mode. The emsdk-bundled Binaryen v121 crashes asyncifying wasm-EH,
|
||||
# so we stub the in-link Asyncify and run --hoist-cpp-catches + --asyncify post-link on the Binaryen
|
||||
# submodule (version_130 + hoist pass) via apply-asyncify.sh.
|
||||
EMSDK_WASM_OPT="$PROJECT_ROOT/tools/emsdk/upstream/bin/wasm-opt"
|
||||
WASMOPT_STUB="$PROJECT_ROOT/wasm/stubs/wasm-opt-stub.sh"
|
||||
_eh_restore_wasmopt() { if [ -f "${EMSDK_WASM_OPT}.ehbak" ]; then mv -f "${EMSDK_WASM_OPT}.ehbak" "${EMSDK_WASM_OPT}"; fi; }
|
||||
EH_MARKER="$(mktemp)" # created before the build so 'find -newer' below selects freshly-linked apps
|
||||
if [ "${PCBJAM_ASYNC_BACKEND:-asyncify}" = "jspi" ]; then
|
||||
# JSPI: no binaryen instrumentation exists — no fork build, no stub dance,
|
||||
# no post-link pass, no dyncall/scheduler injection (the jspi-scheduler
|
||||
# ships as a --pre-js from Makefile.wasm). emcc's real wasm-opt runs
|
||||
# in-link like any normal build.
|
||||
echo ""
|
||||
echo "=== JSPI backend: in-link build, no post-link instrumentation ==="
|
||||
else
|
||||
echo ""
|
||||
echo "=== Building the Binaryen submodule (version_130 + hoist pass) ==="
|
||||
# One binaryen everywhere: the submodule fork is version_130 (asyncify unchanged) + our hoist
|
||||
# pass, so the same binary does --hoist-cpp-catches AND --asyncify/-O2. No separate v130 clone.
|
||||
export HOIST_WASMOPT="$("$SCRIPT_DIR/binaryen-hoist-pass/build-wasm-opt.sh")"
|
||||
export V130_WASMOPT="$HOIST_WASMOPT"
|
||||
echo " submodule wasm-opt: $HOIST_WASMOPT"
|
||||
echo "Stubbing in-link Asyncify (will run post-link instead)..."
|
||||
cp "$EMSDK_WASM_OPT" "${EMSDK_WASM_OPT}.ehbak"
|
||||
cp "$WASMOPT_STUB" "$EMSDK_WASM_OPT"; chmod +x "$EMSDK_WASM_OPT"
|
||||
trap _eh_restore_wasmopt EXIT
|
||||
fi
|
||||
# JSPI: no binaryen instrumentation exists — no fork build, no stub dance, no
|
||||
# post-link pass, no shim injection (the jspi-scheduler ships as a --pre-js
|
||||
# from Makefile.wasm). emcc's real wasm-opt runs in-link like any normal build.
|
||||
|
||||
# Build (pass DEBUG flag if requested). App links are independent, so honor
|
||||
# JOBS/PARALLEL_JOBS from env.sh (each emcc link is slow due to Asyncify).
|
||||
# JOBS/PARALLEL_JOBS from env.sh.
|
||||
if [ "$DEBUG_BUILD" = "1" ]; then
|
||||
make -j"${JOBS:-1}" -f Makefile.wasm DEBUG=1 "$MAKE_TARGET"
|
||||
else
|
||||
|
|
@ -133,54 +109,11 @@ else
|
|||
fi
|
||||
make_rc=$?
|
||||
if [ "$make_rc" -ne 0 ]; then
|
||||
# Fail loudly. Silently continuing to the post-link leaves the freshly-linked apps
|
||||
# asyncify-stubbed / un-injected, which looks like mass test failures rather than a build
|
||||
# error. (The EXIT trap restores the stubbed emsdk wasm-opt in the native-EH build.)
|
||||
echo "" >&2
|
||||
echo "ERROR: make failed (exit $make_rc); aborting before the post-link step." >&2
|
||||
echo "ERROR: make failed (exit $make_rc)." >&2
|
||||
exit "$make_rc"
|
||||
fi
|
||||
|
||||
# Inject the dyncall + asyncify-scheduler shims into every freshly-linked app. The
|
||||
# scheduler's currData save/restore (Emscripten #9153) is needed: without it a rewind that
|
||||
# resumes through a fresh wasm re-entry hits _asyncify_start_rewind(null) -> "memory access out
|
||||
# of bounds" — e.g. a context-menu pick while the main loop is parked. The Makefile only injects
|
||||
# it for the coroutine apps; inject-dyncall-shims.sh is idempotent (skips an already-shimmed glue),
|
||||
# so re-running it here is safe. The .wasm gets post-link hoist + asyncify first.
|
||||
_eh_restore_wasmopt; trap - EXIT
|
||||
if [ "${PCBJAM_ASYNC_BACKEND:-asyncify}" = "jspi" ]; then
|
||||
rm -f "$EH_MARKER"
|
||||
echo ""
|
||||
echo "=== Build complete (jspi backend) ==="
|
||||
exit 0
|
||||
fi
|
||||
echo ""
|
||||
echo "=== Post-link --hoist-cpp-catches + --asyncify (${JOBS:-1}-wide) ==="
|
||||
# The apps are independent here too (apply-asyncify rewrites each wasm in place; the shim
|
||||
# injector's temp file is per-js), so fan the post-link out across JOBS like the make phase.
|
||||
# This dominates the build: per-app wasm-opt can't feed many cores (small modules, serial
|
||||
# parse/write), so parallelism must come from running apps side by side — serial 3m52s even
|
||||
# with BINARYEN_CORES=16, vs 2m08s fanned out at ~11 GB peak RAM. HOIST_WASMOPT is resolved
|
||||
# once above, so workers skip the binaryen ninja check. xargs fails the build (exit 123) if
|
||||
# any app's post-link fails.
|
||||
export SCRIPT_DIR
|
||||
# Match EVERY freshly-linked app wasm, not just standalone/*/*_test.wasm: the main demo
|
||||
# (apps/minimal_test.wasm) is at the apps/ root, and the coroutine-pthread repros + wxpt app
|
||||
# are *_repro*.wasm / *_wxpt.wasm. The old '*_test.wasm under standalone' filter silently
|
||||
# skipped all of those, so under native wasm-EH they never got hoist+asyncify and crashed at
|
||||
# runtime with "asyncify_start_unwind not found".
|
||||
find "$WASM_APP_DIR" -name '*.wasm' -newer "$EH_MARKER" -print0 \
|
||||
| xargs -0 -n1 -P "${JOBS:-1}" bash -c '
|
||||
set -eo pipefail
|
||||
w="$1"
|
||||
"$SCRIPT_DIR/common/apply-asyncify.sh" --no-removelist "$w"
|
||||
js="${w%.wasm}.js"
|
||||
if [ -f "$js" ]; then
|
||||
( cd "$(dirname "$js")" && "$SCRIPT_DIR/common/inject-dyncall-shims.sh" "$(basename "$js")" )
|
||||
fi
|
||||
' _
|
||||
rm -f "$EH_MARKER"
|
||||
|
||||
echo ""
|
||||
echo "=== Build complete ==="
|
||||
|
||||
|
|
|
|||
|
|
@ -60,13 +60,14 @@ done
|
|||
BUILD_DIR="$PROJECT_ROOT/build-wasm/wxwidgets"
|
||||
WXLIB_PREFIX="libwx_wasmu"
|
||||
|
||||
# Async-backend stamp: PCBJAM_ASYNC_BACKEND changes COMPILE flags
|
||||
# (-DPCBJAM_JSPI selects whole other halves of the wasm port), which the
|
||||
# configure-cached incremental build cannot see — a knob flip against a stale
|
||||
# tree silently links the WRONG backend into every consumer. Force a clean
|
||||
# build whenever the stamp disagrees.
|
||||
# Async-backend stamp: the JSPI migration changed COMPILE flags in ways the
|
||||
# configure-cached incremental build cannot see — building over a stale
|
||||
# asyncify-era tree silently links a MIXED library. Force a clean build
|
||||
# whenever the stamp disagrees.
|
||||
BACKEND_STAMP="$BUILD_DIR/.pcbjam-async-backend"
|
||||
CURRENT_BACKEND="${PCBJAM_ASYNC_BACKEND:-asyncify}"
|
||||
# Single backend since the JSPI migration: the stamp still force-cleans any
|
||||
# pre-migration tree (asyncify objects would silently mix into the library).
|
||||
CURRENT_BACKEND="jspi"
|
||||
# ABSENT stamp = unknown provenance = same as a mismatch: an incremental build
|
||||
# over objects of unknown backend produced a MIXED library once (jspi evtloop
|
||||
# EM_JS in the glue next to live fiber dispatch — --allow-multiple-definition
|
||||
|
|
@ -193,15 +194,6 @@ if [ $NEEDS_CONFIGURE -eq 1 ]; then
|
|||
WX_EH_FLAGS="$DEPS_EH_FLAGS"
|
||||
echo "wx EH model flags: ${WX_EH_FLAGS}"
|
||||
|
||||
# Async backend (experiment/jspi): PCBJAM_ASYNC_BACKEND=jspi compiles the
|
||||
# wasm port's JSPI lanes (evtloop/app/window PCBJAM_JSPI blocks) instead of
|
||||
# the Asyncify/fiber lanes. ONE wx build output — flipping backends means
|
||||
# rebuilding (use --clean). Default stays asyncify until Phase 4.
|
||||
if [ "${PCBJAM_ASYNC_BACKEND:-asyncify}" = "jspi" ]; then
|
||||
WX_EH_FLAGS="$WX_EH_FLAGS -DPCBJAM_JSPI=1"
|
||||
echo "async backend: jspi (-DPCBJAM_JSPI)"
|
||||
fi
|
||||
|
||||
# Include emscripten cache sysroot for zlib headers
|
||||
export CFLAGS="-DZ_HAVE_UNISTD_H=1 -I$EM_CACHE_SYSROOT/include ${WX_DEBUG_FLAGS} ${WX_EH_FLAGS} -pthread -matomics -mbulk-memory"
|
||||
export CXXFLAGS="-DZ_HAVE_UNISTD_H=1 -I$EM_CACHE_SYSROOT/include -I$PCRE2_INCLUDE ${WX_DEBUG_FLAGS} ${WX_EH_FLAGS} -pthread -matomics -mbulk-memory"
|
||||
|
|
|
|||
|
|
@ -1,151 +0,0 @@
|
|||
#!/bin/bash
|
||||
# Unified post-link Asyncify pass for KiCad AND the wx test apps.
|
||||
#
|
||||
# Usage: apply-asyncify.sh [--no-removelist] <input.wasm> [output.wasm]
|
||||
#
|
||||
# Always: run our --hoist-cpp-catches fork pass FIRST (lets Asyncify suspend from inside C++ catch
|
||||
# blocks under native wasm-EH) with all wasm features enabled (-all, so binaryen parses the
|
||||
# EH instructions), then --asyncify + remove-list + -O2. Native wasm-EH is the only build mode.
|
||||
# --no-removelist skip the KiCad big-function remove-list (the small wx test apps don't contain
|
||||
# those symbols, and one bare entry — "match" — could collide).
|
||||
#
|
||||
# WHY post-link (not emcc's in-link Asyncify): the emsdk-bundled Binaryen crashes asyncifying
|
||||
# wasm-EH and a compiler/standalone version skew corrupts asyncify metadata. So the in-link pass is
|
||||
# stubbed (build-kicad-target.sh / build-wasm-test.sh) and the real transform runs here, on the host
|
||||
# (more RAM), with a pinned Binaryen. The cost: emcc's automatic asyncify-imports generation is
|
||||
# bypassed, so the BOUNDARY import list lives in asyncify-imports.txt (see that file).
|
||||
#
|
||||
# Binaryen selection (env overrides win, set by build-wasm-test.sh): one binaryen everywhere — the
|
||||
# submodule fork (version_130 + our --hoist-cpp-catches). Override the path via HOIST_WASMOPT / V130_WASMOPT.
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
|
||||
# --- flags ---
|
||||
# Native wasm-EH is the only build mode, so the catch-arm hoist pass always runs.
|
||||
DO_HOIST=1
|
||||
USE_REMOVELIST=1
|
||||
while [[ "${1:-}" == --* ]]; do
|
||||
case "$1" in
|
||||
--no-removelist) USE_REMOVELIST=0; shift ;;
|
||||
*) echo "apply-asyncify: unknown flag: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
INPUT_WASM="${1:-output/pcbnew.wasm}"
|
||||
OUTPUT_WASM="${2:-${INPUT_WASM}}"
|
||||
[ -f "${INPUT_WASM}" ] || { echo "ERROR: Input file not found: ${INPUT_WASM}" >&2; exit 1; }
|
||||
|
||||
# --- Binaryen tool: ONE binaryen everywhere (incl. docker/CI) — the submodule fork. It IS Binaryen
|
||||
# version_130 (its Asyncify.cpp is unmodified upstream) + our HoistCppCatches pass, so the SAME binary
|
||||
# does --hoist-cpp-catches AND --asyncify/-O2, for both native-EH and legacy JS-EH wasm. Built once via
|
||||
# build-wasm-opt.sh; no separate binaryen downloads (the emsdk-bundled v121 can't even asyncify wasm-EH).
|
||||
SUBMODULE_WASMOPT="${HOIST_WASMOPT:-$("${SCRIPT_DIR}/../binaryen-hoist-pass/build-wasm-opt.sh")}"
|
||||
WASM_OPT="${V130_WASMOPT:-$SUBMODULE_WASMOPT}"
|
||||
[ "$DO_HOIST" = 1 ] && HOIST_OPT="$SUBMODULE_WASMOPT"
|
||||
|
||||
# native wasm-EH needs -all so binaryen parses the EH instructions; HOIST_KEEP_NAMES keeps the
|
||||
# names section through -O2 for callstack debugging. KiCad/JS-EH uses neither (matches old behavior).
|
||||
FEAT=()
|
||||
[ "$DO_HOIST" = 1 ] && FEAT=(-all)
|
||||
G="${HOIST_KEEP_NAMES:+-g}"
|
||||
|
||||
# The asyncify removelist matches functions by NAME. wasm-opt strips the names section by default, so
|
||||
# the hoist pass (run before asyncify) would otherwise hand asyncify nameless functions — the
|
||||
# removelist then matches NOTHING and the giant try-dense functions it is meant to exclude
|
||||
# (BuildBitmapInfo: ~4986 native tries, etc.) get instrumented, which is the dominant driver of the
|
||||
# multi-GB asyncify RAM blowup on native-EH. So force the hoist pass to keep names whenever a
|
||||
# removelist is in play, so asyncify can see + exclude them. asyncify/-O2 keep their own G, so the
|
||||
# FINAL wasm is unchanged unless HOIST_KEEP_NAMES is set (asyncify matches on its INPUT names, which
|
||||
# the hoist output now carries).
|
||||
HOIST_G="${G}"
|
||||
[ "$USE_REMOVELIST" = 1 ] && HOIST_G="-g"
|
||||
|
||||
# --- the import boundary (shared) + the KiCad remove-list (opt-out), read from sibling files ---
|
||||
_join_list() { grep -vE '^[[:space:]]*#|^[[:space:]]*$' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | tr '\n' ',' | sed 's/,$//'; }
|
||||
ASYNCIFY_IMPORTS="${ASYNCIFY_IMPORTS_PASS:-$(_join_list "${SCRIPT_DIR}/asyncify-imports.txt")}"
|
||||
REMOVE_ARG=()
|
||||
if [ "$USE_REMOVELIST" = 1 ]; then
|
||||
REMOVE_ARG=("--pass-arg=asyncify-removelist@$(_join_list "${SCRIPT_DIR}/asyncify-removelist.txt")")
|
||||
fi
|
||||
|
||||
# --- memory machinery (identical to before; matters for the host-side KiCad pass) ---
|
||||
# Bound Binaryen's host thread pool: peak RAM scales with thread count.
|
||||
export BINARYEN_CORES="${BINARYEN_CORES:-8}"
|
||||
|
||||
# Preload a scalable allocator on Linux — glibc malloc collapses into futex lock-spin under
|
||||
# wasm-opt's many-thread allocation churn; jemalloc/mimalloc roughly halve wall-clock. macOS
|
||||
# already ships a scalable allocator. WASM_OPT_PRELOAD=none|0 forces a clean glibc baseline.
|
||||
if [[ "${WASM_OPT_PRELOAD:-}" == "none" || "${WASM_OPT_PRELOAD:-}" == "0" ]]; then
|
||||
WASM_OPT_PRELOAD=""; _PRELOAD_FORCED_OFF=1
|
||||
fi
|
||||
if [[ -z "${WASM_OPT_PRELOAD:-}" && -z "${_PRELOAD_FORCED_OFF:-}" && "$(uname -s)" == "Linux" ]]; then
|
||||
for _alloc in \
|
||||
"/usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2" \
|
||||
"/usr/lib/$(uname -m)-linux-gnu/libmimalloc.so.2" \
|
||||
/usr/lib/libjemalloc.so.2 /usr/lib/libmimalloc.so.2; do
|
||||
[[ -e "${_alloc}" ]] && { WASM_OPT_PRELOAD="${_alloc}"; break; }
|
||||
done
|
||||
fi
|
||||
if [[ -n "${WASM_OPT_PRELOAD:-}" ]]; then
|
||||
PRELOAD_CMD=(env "LD_PRELOAD=${WASM_OPT_PRELOAD}${LD_PRELOAD:+:${LD_PRELOAD}}")
|
||||
else
|
||||
PRELOAD_CMD=()
|
||||
fi
|
||||
# GNU `time -v` on Linux CI records peak RSS + wall-clock per pass; macOS `time` lacks -v.
|
||||
if /usr/bin/time -v true >/dev/null 2>&1; then TIME_CMD=(/usr/bin/time -v); else TIME_CMD=(); fi
|
||||
|
||||
echo "Applying Asyncify${DO_HOIST:+ (+hoist-cpp-catches)}..."
|
||||
echo " Input: ${INPUT_WASM}"
|
||||
echo " Output: ${OUTPUT_WASM}"
|
||||
echo " asyncify wasm-opt: ${WASM_OPT}"
|
||||
[ "$DO_HOIST" = 1 ] && echo " hoist wasm-opt: ${HOIST_OPT}"
|
||||
echo " BINARYEN_CORES=${BINARYEN_CORES} LD_PRELOAD=${WASM_OPT_PRELOAD:-<none>}"
|
||||
|
||||
SRC="${INPUT_WASM}"
|
||||
|
||||
# Refuse to double-instrument. A postprocess re-run on an artifact that a
|
||||
# previous (killed/partial) run already asyncified re-instruments the
|
||||
# instrumented module: the pass balloons to OOM/jetsam death, and the output
|
||||
# would be broken anyway. The asyncify export names only exist in a module
|
||||
# the pass already touched. Recover by re-copying the pristine post-link
|
||||
# artifact from the docker volume (docker/build.sh compile copy step).
|
||||
if LC_ALL=C grep -aq "asyncify_start_unwind" "${INPUT_WASM}"; then
|
||||
echo "ERROR: ${INPUT_WASM} already contains asyncify exports - refusing to" >&2
|
||||
echo "double-instrument. Restore the pristine post-link wasm first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1. (native wasm-EH only) hoist C++ catch arms so Asyncify can suspend from inside them.
|
||||
if [ "$DO_HOIST" = 1 ]; then
|
||||
echo "Running --hoist-cpp-catches${HOIST_G:+ (keeping names for removelist matching)}..."
|
||||
"${PRELOAD_CMD[@]}" "${TIME_CMD[@]}" "${HOIST_OPT}" --hoist-cpp-catches "${FEAT[@]}" ${HOIST_G} "${SRC}" -o "${OUTPUT_WASM}"
|
||||
SRC="${OUTPUT_WASM}"
|
||||
fi
|
||||
|
||||
# 2. The real Asyncify transform. ASYNCIFY_EXTRA_OPTS: optional extra wasm-opt flags (origin/main hook).
|
||||
echo "Running wasm-opt --asyncify (several minutes + significant RAM)..."
|
||||
"${PRELOAD_CMD[@]}" "${TIME_CMD[@]}" "${WASM_OPT}" --asyncify ${ASYNCIFY_EXTRA_OPTS:-} "${FEAT[@]}" ${G} \
|
||||
"--pass-arg=asyncify-imports@${ASYNCIFY_IMPORTS}" \
|
||||
"${REMOVE_ARG[@]}" \
|
||||
--pass-arg=asyncify-propagate-addlist \
|
||||
"${SRC}" -o "${OUTPUT_WASM}"
|
||||
|
||||
# ASYNCIFY_ONLY=1 stops before -O2 (benchmark harness in scripts/bench/ times just the transform).
|
||||
if [[ "${ASYNCIFY_ONLY:-0}" == "1" ]]; then
|
||||
echo "ASYNCIFY_ONLY=1 → skipping -O2 (benchmark mode)."; ls -lh "${OUTPUT_WASM}"; exit 0
|
||||
fi
|
||||
|
||||
# 3. Post-asyncify shrink. Asyncify spills every live local; without coalescing, large
|
||||
# coroutine-entry functions exceed V8's per-function locals limit and stall/crash the renderer.
|
||||
# -O1 runs CoalesceLocals (enough to keep instrumented functions under V8's local limit) and is the
|
||||
# level we ship EVERYWHERE — main CI, tag releases, and local builds all use it. NOT -Os/-Oz (they
|
||||
# break the asyncify runtime — Binaryen #4484). See docs/debugging/DEBUG.md §6-7.
|
||||
BINARYEN_OPT_LEVEL="${BINARYEN_OPT_LEVEL:--O1}"
|
||||
echo "Running wasm-opt ${BINARYEN_OPT_LEVEL} (shrink instrumented functions under V8's local limit)..."
|
||||
"${PRELOAD_CMD[@]}" "${TIME_CMD[@]}" "${WASM_OPT}" "${BINARYEN_OPT_LEVEL}" "${FEAT[@]}" ${G} "${OUTPUT_WASM}" -o "${OUTPUT_WASM}"
|
||||
|
||||
echo "Asyncify + ${BINARYEN_OPT_LEVEL} complete: ${OUTPUT_WASM}"
|
||||
ls -lh "${OUTPUT_WASM}"
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
#!/bin/bash
|
||||
# Apply wasm-emscripten-finalize transformation on host.
|
||||
#
|
||||
# Usage: ./scripts/common/apply-finalize.sh <input.wasm> <output.wasm>
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
|
||||
# wasm-emscripten-finalize comes from our Binaryen submodule (version_130 + --hoist-cpp-catches),
|
||||
# built alongside wasm-opt by build-wasm-opt.sh. Pinning finalize and the asyncify wasm-opt to ONE
|
||||
# Binaryen version removes the host's emsdk dependency from the post-process entirely (it is now
|
||||
# dyncall=node + finalize/asyncify=submodule), which is what was breaking on the ephemeral CI host.
|
||||
# HOIST_WASMOPT, when the build driver pre-warms the submodule build, points at the already-built
|
||||
# wasm-opt so we don't re-invoke the build per app; finalize sits next to it in the same bin/.
|
||||
WASM_OPT="${HOIST_WASMOPT:-$("${SCRIPT_DIR}/../binaryen-hoist-pass/build-wasm-opt.sh")}"
|
||||
FINALIZE="$(dirname "${WASM_OPT}")/wasm-emscripten-finalize"
|
||||
|
||||
INPUT_WASM="${1:-output/pcbnew.wasm}"
|
||||
OUTPUT_WASM="${2:-${INPUT_WASM}}"
|
||||
|
||||
if [ ! -f "${INPUT_WASM}" ]; then
|
||||
echo "ERROR: Input file not found: ${INPUT_WASM}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Applying wasm-emscripten-finalize..."
|
||||
echo " Input: ${INPUT_WASM}"
|
||||
echo " Output: ${OUTPUT_WASM}"
|
||||
echo " Tool: ${FINALIZE}"
|
||||
|
||||
# Run finalize with the same flags Emscripten would use
|
||||
# NOTE: --dwarf removed because debug info is in separate .debug.wasm file
|
||||
"${FINALIZE}" \
|
||||
-g \
|
||||
--bigint \
|
||||
--no-legalize-javascript-ffi \
|
||||
--detect-features \
|
||||
"${INPUT_WASM}" \
|
||||
-o "${OUTPUT_WASM}"
|
||||
|
||||
echo "Finalize complete: ${OUTPUT_WASM}"
|
||||
ls -lh "${OUTPUT_WASM}"
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
# Suspending imports for the post-link Asyncify pass (consumed by apply-asyncify.sh).
|
||||
# Binaryen's --asyncify instruments every function that can transitively REACH one of these
|
||||
# imports. This is the BOUNDARY list, not a function allowlist — the callers are auto-discovered.
|
||||
#
|
||||
# We run Asyncify post-link (outside emcc), so emcc's automatic import generation never runs;
|
||||
# this file replicates emcc's default async built-ins + our own suspending imports. A superset
|
||||
# is safe — binaryen ignores any import the wasm doesn't actually contain.
|
||||
#
|
||||
# --- Emscripten async built-ins (what emcc auto-adds for the in-link Asyncify) ---
|
||||
env.emscripten_sleep
|
||||
env.emscripten_scan_registers
|
||||
env.emscripten_lazy_load_code
|
||||
env.emscripten_wget
|
||||
env.emscripten_wget_data
|
||||
env.emscripten_idb_*
|
||||
#
|
||||
# --- Project suspending imports ---
|
||||
env.__asyncjs__*
|
||||
env.emscripten_fiber_swap
|
||||
env.js_*
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
# Functions EXCLUDED from Asyncify instrumentation (consumed by apply-asyncify.sh unless
|
||||
# --no-removelist). These are large, NON-suspending KiCad/OpenCASCADE functions (generated resource
|
||||
# tables, wxFormBuilder UI constructors, OCC geometry) that are dense with native wasm try/catch.
|
||||
# Asyncify's per-function cost is superlinear in try-count (it builds a CFG + liveness over every
|
||||
# try), so instrumenting these few giants is what drives the multi-GB RAM blowup of `wasm-opt
|
||||
# --asyncify` on the native-EH build. They never call a suspending import, so excluding them is safe.
|
||||
#
|
||||
# MATCHING: Binaryen escapes each entry, then treats entries containing `*` as wildcard patterns
|
||||
# (String::wildcardMatch) and entries without `*` as exact escaped function names. Full demangled
|
||||
# signatures don't exact-match (subtle spacing) and a `*` inside a pointer type (e.g. wxWindow*) turns
|
||||
# the whole entry into a pattern that still misses — so use a PREFIX wildcard per symbol, which matches
|
||||
# the function's escaped demangled name regardless of argument formatting. Patterns that match nothing
|
||||
# in a given app (e.g. the OCC entries when 3D is off) just emit a harmless "non-matching" warning.
|
||||
# Only the KiCad build passes the remove-list; the small wx test apps opt out via --no-removelist
|
||||
# (and the bare name "match" could collide there).
|
||||
COLOR_SETTINGS::COLOR_SETTINGS*
|
||||
BuildBitmapInfo*
|
||||
match
|
||||
DIALOG_PAD_PROPERTIES_BASE::DIALOG_PAD_PROPERTIES_BASE*
|
||||
buildKicadAboutBanner*
|
||||
IGESToBRep_CurveAndSurface::TransferGeometry*
|
||||
StepAP214_Protocol::StepAP214_Protocol*
|
||||
BRepCheck_ParallelAnalyzer::operator*
|
||||
ShapeFix_Wire::FixGap3d*
|
||||
ShapeFix_Wire::FixGap2d*
|
||||
PCB_EDIT_FRAME::setupUIConditions*
|
||||
# The ngspice model-parameter table initializers (sim_model_ngspice_data_*.cpp,
|
||||
# restored for the simulator split): thousands of straight-line emplace_backs,
|
||||
# nothing that can suspend. Uninstrumented they stay under the JS engines'
|
||||
# per-function locals limit and shave the post-asyncify module size
|
||||
# (bsim4/b3soi/b4soi/hsim alone are the four largest functions in eeschema).
|
||||
NGSPICE_MODEL_INFO_MAP::add*
|
||||
|
|
@ -1,176 +0,0 @@
|
|||
#!/bin/bash
|
||||
# Post-process the Emscripten-generated <app>.js for KiCad WASM (pcbnew, eeschema,
|
||||
# pl_editor, calculator, …).
|
||||
#
|
||||
# The actual JavaScript that gets injected lives in readable, standalone files in
|
||||
# scripts/common/shims/ (not inline heredocs):
|
||||
# - asyncify-scheduler.js the mailbox/scheduler (docs/features/async/17) —
|
||||
# the ONLY asyncify runtime (the legacy handlesleep.js
|
||||
# opt-out was deleted at doc 20 D-1)
|
||||
# - diagnostics.js optional logging-only instrumentation (see SHIM_DIAGNOSTICS)
|
||||
#
|
||||
# Native wasm-EH is the only build mode, so the .js has no invoke_* wrappers / dynCall_<sig> call
|
||||
# sites to bind. The build still links -sDYNCALLS=1, so asyncify-INSTRUMENTED dynCall_* trampolines
|
||||
# exist as wasm EXPORTS; the empty-callback fixes below route function-pointer stubs through
|
||||
# wasmExports["dynCall_<sig>"]. This MUST be the wasm trampoline, NOT getWasmTableEntry — the latter
|
||||
# bypasses the instrumentation and breaks unwind/rewind through indirect calls ("indirect call
|
||||
# signature mismatch" — caught every frame in Firefox; a hard renderer crash in Chrome/V8).
|
||||
#
|
||||
# Usage:
|
||||
# inject-dyncall-shims.sh <pcbnew.js>
|
||||
# SHIM_DIAGNOSTICS=1 inject-dyncall-shims.sh <pcbnew.js> # also inject diagnostics.js
|
||||
|
||||
set -e
|
||||
|
||||
JS_FILE="$1"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SHIM_DIR="$SCRIPT_DIR/shims"
|
||||
|
||||
# One-line toggle for the diagnostics module (default OFF).
|
||||
SHIM_DIAGNOSTICS="${SHIM_DIAGNOSTICS:-0}"
|
||||
|
||||
if [ -z "$JS_FILE" ] || [ ! -f "$JS_FILE" ]; then
|
||||
echo "Error: JS file not found: $JS_FILE"
|
||||
echo "Usage: $0 <path/to/pcbnew.js>"
|
||||
exit 1
|
||||
fi
|
||||
for f in asyncify-scheduler.js diagnostics.js; do
|
||||
if [ ! -f "$SHIM_DIR/$f" ]; then
|
||||
echo "Error: missing shim source $SHIM_DIR/$f"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# --- 1. Empty-callback fixes ---------------------------------------------------
|
||||
# Emscripten+pthreads emits some direct-call paths as no-op ((a1)=>{}) stubs that ARE used. Native
|
||||
# wasm-EH eliminates the invoke_* wrappers, so the .js has no dynCall_<sig> call sites to bind — but
|
||||
# the DYNCALLS=1 trampolines are still EXPORTED on the wasm, so route each function-pointer stub
|
||||
# through wasmExports["dynCall_<sig>"]. This MUST be the wasm trampoline, NOT getWasmTableEntry: the
|
||||
# fiber entry runs a coroutine that suspends+rewinds via Asyncify, and an Asyncify rewind cannot
|
||||
# resume through getWasmTableEntry's JS wrapper — the fiber would re-enter from the top and the tool's
|
||||
# Wait() re-runs (tool_manager ScheduleWait "!pendingWait" assert + busy-loop). The instrumented
|
||||
# dynCall_<sig> export rewinds correctly. (Without these fixes the libcontext fiber entry stays the
|
||||
# empty (a1=>{}) stub, so tool coroutines never start and every GAL app stalls at InvokeTool.)
|
||||
echo "Fixing empty callback arrow functions..."
|
||||
TOTAL_FIXED=0
|
||||
apply_fix() { # <grep/sed pattern> <sed replacement> <label>
|
||||
local before; before=$(grep -c "$1" "$JS_FILE" || true)
|
||||
if [ "$before" -gt 0 ]; then
|
||||
# Portable in-place edit (BSD `sed -i ''` and GNU `sed -i` differ; temp+mv works on both).
|
||||
sed "s/$1/$2/g" "$JS_FILE" > "${JS_FILE}.sedtmp" && mv "${JS_FILE}.sedtmp" "$JS_FILE"
|
||||
local after; after=$(grep -c "$1" "$JS_FILE" || true)
|
||||
echo " Fixed $((before - after)) $3"
|
||||
TOTAL_FIXED=$((TOTAL_FIXED + before - after))
|
||||
fi
|
||||
}
|
||||
apply_fix '((a1, a2, a3) => {})(eventTypeId,' '((a1, a2, a3) => wasmExports["dynCall_iiii"](callbackfunc, a1, a2, a3))(eventTypeId,' "HTML5 event callback(s) (wasmExports.dynCall_iiii)"
|
||||
apply_fix 'var result = (a1 => {})(arg);' 'var result = wasmExports["dynCall_ii"](ptr, arg);' "pthread entry callback(s) (wasmExports.dynCall_ii)"
|
||||
apply_fix 'return (a1 => {})(sig);' 'return wasmExports["dynCall_vi"](fp, sig);' "signal handler callback(s) (wasmExports.dynCall_vi)"
|
||||
apply_fix 'var wrapper = () => (a1 => {})(arg);' 'var wrapper = () => wasmExports["dynCall_vi"](func, arg);' "async timer callback(s) (wasmExports.dynCall_vi)"
|
||||
apply_fix 'var iterFunc = (() => {});' 'var iterFunc = () => wasmExports["dynCall_v"](func);' "main loop callback(s) (wasmExports.dynCall_v)"
|
||||
apply_fix '(a1 => {})(userData);' 'wasmExports["dynCall_vi"](entryPoint, userData);' "fiber entry callback(s) (wasmExports.dynCall_vi)"
|
||||
echo "Total: Fixed $TOTAL_FIXED empty callback(s)"
|
||||
|
||||
# --- 3. Asyncify scheduler shim ------------------------------------------------
|
||||
# Injected after Emscripten's fiber glue (the _emscripten_fiber_swap.isAsync marker),
|
||||
# or at EOF for non-fiber apps (a plain wx app still needs the currData machinery:
|
||||
# without it a rewind resuming through a fresh wasm re-entry hits
|
||||
# _asyncify_start_rewind(null) -> "memory access out of bounds").
|
||||
#
|
||||
# asyncify-scheduler.js (docs/features/async/17) is the ONLY asyncify runtime:
|
||||
# it owns the currData capture/restore, fiber guard, and trampoline heal, and
|
||||
# adds the deferred-wake drain + N1 single-writer tripwire + the mailbox/wait
|
||||
# lanes. The legacy handlesleep.js opt-out (WX_SCHEDULER=0) and the ablation
|
||||
# skip (SHIM_DISABLE_HANDLESLEEP) were deleted at doc 20 D-1 together with the
|
||||
# wx C++ paths they exercised.
|
||||
inject_shim_at_marker() { # <shim file> <label>
|
||||
local shim_file="$1" label="$2"
|
||||
local marker
|
||||
marker=$(grep -n '^_emscripten_fiber_swap\.isAsync = true;$' "$JS_FILE" | head -1 | cut -d: -f1)
|
||||
if [ -z "$marker" ]; then
|
||||
echo "" >> "$JS_FILE"
|
||||
cat "$SHIM_DIR/$shim_file" >> "$JS_FILE"
|
||||
echo "Injected $label at EOF (no fiber glue)"
|
||||
else
|
||||
head -n "$marker" "$JS_FILE" > "${JS_FILE}.tmp"
|
||||
echo "" >> "${JS_FILE}.tmp"
|
||||
cat "$SHIM_DIR/$shim_file" >> "${JS_FILE}.tmp"
|
||||
tail -n +$((marker + 1)) "$JS_FILE" >> "${JS_FILE}.tmp"
|
||||
mv "${JS_FILE}.tmp" "$JS_FILE"
|
||||
echo "Injected $label after line $marker"
|
||||
fi
|
||||
}
|
||||
|
||||
# NOTE: idempotence via the shim-source sentinel, not __wxSchedulerInstalled —
|
||||
# that string also appears in evtloop.cpp's EM_JS probe inside every glue.
|
||||
if grep -q '__WX_SCHEDULER_SHIM_SOURCE__' "$JS_FILE"; then
|
||||
echo "asyncify-scheduler already present - skipping"
|
||||
else
|
||||
inject_shim_at_marker asyncify-scheduler.js "asyncify-scheduler"
|
||||
fi
|
||||
|
||||
# --- 3b. embind dynCall fallback (dynCallLegacy -> wasmExports) ----------------
|
||||
# embind's generic caller (getDynCaller) routes through dynCallLegacy, which only
|
||||
# reads Module["dynCall_<sig>"]. But the DYNCALLS=1 trampolines are wasm EXPORTS,
|
||||
# not Module properties, so that lookup is undefined and an Asyncify unwind/rewind
|
||||
# through an embind call (e.g. kicadOpenFile -> OpenProjectFiles) dies with
|
||||
# "f is not a function" in Asyncify.doRewind. Add a wasmExports fallback so the
|
||||
# instrumented trampoline is found and rewind survives.
|
||||
if grep -q 'embind dynCall fallback installed' "$JS_FILE"; then
|
||||
echo "dynCallLegacy fallback already present - skipping"
|
||||
elif grep -qF ' var f = Module["dynCall_" + sig];' "$JS_FILE"; then
|
||||
perl -0pi -e 's/(\Q var f = Module["dynCall_" + sig];\E)/$1\n \/\/ embind dynCall fallback installed: DYNCALLS=1 trampolines live on wasmExports, not Module.\n if (!f && typeof wasmExports !== "undefined") f = wasmExports["dynCall_" + sig];/' "$JS_FILE"
|
||||
echo "Injected dynCallLegacy wasmExports fallback"
|
||||
else
|
||||
echo "Warning: dynCallLegacy pattern not found - skipping embind dynCall fallback"
|
||||
fi
|
||||
|
||||
# --- 3d. Embind invoker: don't Promise-wrap a SYNCHRONOUS call when the main loop is parked --------
|
||||
# Emscripten's embind invoker returns a Promise iff Asyncify.currData is set AFTER the wasm call. But
|
||||
# the native-EH per-frame-yield main loop parks via Asyncify (currData stays SET between frames), so a
|
||||
# JS-initiated embind call (e.g. kicadCollabSnapshot from a test or the UI) that does NOT itself
|
||||
# suspend is mis-detected as async and returns "[object Promise]" instead of the value -> the caller's
|
||||
# JSON.parse(...) gets "[object Promise]". Capture currData before the call and only treat it as async
|
||||
# if THIS call left a NEW currData. Harmless under legacy/JS-EH (currData is null when the app is idle).
|
||||
if grep -q 'Asyncify.currData !== __ehPrev' "$JS_FILE"; then
|
||||
echo "embind invoker currData re-entrancy fix already present - skipping"
|
||||
elif grep -q 'return Asyncify.currData ? Asyncify.whenDone' "$JS_FILE"; then
|
||||
perl -0pi -e 's/(invokerFnBody \+= \(returns \|\| isAsync \? "var rv = " : ""\))/invokerFnBody += "var __ehPrev = Asyncify.currData;\\n";\n $1/' "$JS_FILE"
|
||||
perl -0pi -e 's/return Asyncify\.currData \? Asyncify\.whenDone/return (Asyncify.currData && Asyncify.currData !== __ehPrev) ? Asyncify.whenDone/' "$JS_FILE"
|
||||
echo "Injected embind invoker currData re-entrancy fix"
|
||||
else
|
||||
echo "Warning: embind invoker currData pattern not found - skipping embind re-entrancy fix"
|
||||
fi
|
||||
|
||||
# --- 3c. Fiber trampoline self-heal -------------------------------------------
|
||||
# emscripten_set_main_loop(...,1) throws "unwind" during startup to establish the
|
||||
# main loop. KiCad establishes that loop from inside a tool coroutine, so the throw
|
||||
# propagates THROUGH Fibers.trampoline()'s do/while, skipping its
|
||||
# `trampolineRunning = false` reset. The flag then stays true forever and
|
||||
# Fibers.trampoline() becomes a permanent no-op (guard: `if (!trampolineRunning ...)`),
|
||||
# so every later fiber swap silently fails to switch — the schematic load and all
|
||||
# post-idle tool actions hang. Wrap the loop in try/finally so the flag is always
|
||||
# reset (self-healing).
|
||||
# (The SHIM_DISABLE_TRAMPOLINE_HEAL ablation skip was deleted at doc 20 D-1
|
||||
# with the races_test_noheal build that used it.)
|
||||
if grep -qF '} finally { Fibers.trampolineRunning = false; }' "$JS_FILE"; then
|
||||
echo "fiber trampoline self-heal already present - skipping"
|
||||
elif grep -qF 'Fibers.trampolineRunning = true;' "$JS_FILE"; then
|
||||
perl -0pi -e 's/(Fibers\.trampolineRunning = true;)(\s*)(do \{.*?\} while \(Fibers\.nextFiber\);)(\s*)(Fibers\.trampolineRunning = false;)/$1$2try {$3} finally { $5 }/s' "$JS_FILE"
|
||||
echo "Injected fiber trampoline self-heal (try/finally)"
|
||||
else
|
||||
echo "Warning: Fibers.trampoline pattern not found - skipping trampoline self-heal"
|
||||
fi
|
||||
|
||||
# --- 4. Optional diagnostics (logging only) -----------------------------------
|
||||
if [ "$SHIM_DIAGNOSTICS" = "1" ]; then
|
||||
if grep -q 'DIAG] Asyncify/fiber/modal diagnostics installed' "$JS_FILE"; then
|
||||
echo "diagnostics already present - skipping"
|
||||
else
|
||||
echo "" >> "$JS_FILE"
|
||||
cat "$SHIM_DIR/diagnostics.js" >> "$JS_FILE"
|
||||
echo "Appended diagnostics module (SHIM_DIAGNOSTICS=1)"
|
||||
fi
|
||||
else
|
||||
echo "diagnostics disabled (set SHIM_DIAGNOSTICS=1 to enable)"
|
||||
fi
|
||||
|
|
@ -7,6 +7,5 @@ wx_window_resize
|
|||
ProcessEvents
|
||||
wxWasmMailboxTick
|
||||
wxWasmTopLevelTick
|
||||
wxWasmMainLoopPump
|
||||
wxWasmJobTick
|
||||
pcbjam_libctx_entry
|
||||
|
|
|
|||
|
|
@ -1,801 +0,0 @@
|
|||
// === AsyncifyScheduler (S2 — scheduler core: registry, deferred wakes, single writer) ===
|
||||
// __WX_SCHEDULER_SHIM_SOURCE__ — injector idempotence sentinel. Must appear ONLY in
|
||||
// this file: the obvious marker (__wxSchedulerInstalled) also occurs in evtloop.cpp's
|
||||
// EM_JS probe text inside every glue, which made the injector skip real injections.
|
||||
// docs/features/async/17-mailbox-scheduler-plan.md · injected only on WX_SCHEDULER=1 builds.
|
||||
//
|
||||
// S2 state: this file REPLACES the legacy handlesleep.js on scheduler builds (the
|
||||
// injector's either-or flip). It carries:
|
||||
// S1 · the wx mailbox (timer/wheel messages, wx/wasm/private/mailbox.h) and the
|
||||
// embind mutator lane (doc 18 classification).
|
||||
// S2 · the scheduler core — every behavior the legacy shim provided (per-sleep
|
||||
// currData capture/restore, the stale-fiber consume-once/quarantine guard,
|
||||
// wake-window flags, flight recorder, trampoline heal) PLUS:
|
||||
// - DEFERRED WAKES: a sleep wake arriving while a transition is in flight
|
||||
// (state != Normal, or the fiber trampoline mid-loop) is queued and
|
||||
// delivered from a clean macrotask when the slot frees — the aliased-wake
|
||||
// class becomes unrepresentable instead of merely detected (doc 12 §law).
|
||||
// - N1 SINGLE-WRITER TRIPWIRE: Asyncify.currData is an accessor; a write
|
||||
// from pure JS (no wasm frames on the export stack) without scheduler
|
||||
// authorization is a STRAY — beaconed, counted, and (opt-in strict mode)
|
||||
// thrown. Wasm-driven writes (fiber_swap, handleSleep internals) are
|
||||
// runtime-legitimate and pass through.
|
||||
// Contract surfaces kept name-identical (external readers!): Asyncify.__wakingRoot
|
||||
// (libcontext EM_JS wasm_root_wake_in_flight), __inSleepWake, __wakingOwnerFiber,
|
||||
// __pendingSleepContexts, Fibers.__fcsTotal/__rootHotTotal/__rootFiber/
|
||||
// __validSuspensions/__internallyParked/__parkSleepBuf/__inFiberEntry,
|
||||
// the "[wx-asyncify] STATE"/"RECORDER" dump formats, window.__wxAsyncifyDump.
|
||||
if (typeof Asyncify !== "undefined" && !globalThis.__wxSchedulerInstalled) {
|
||||
globalThis.__wxSchedulerInstalled = true;
|
||||
Asyncify.__schedulerBuild = 1;
|
||||
|
||||
var AsyncifyScheduler = {
|
||||
// --- S1 wx mailbox ----------------------------------------------------
|
||||
mailbox: [],
|
||||
enqueued: 0,
|
||||
delivered: 0,
|
||||
_tickArmed: false,
|
||||
enqueueAfter: function (fn, arg, ms) {
|
||||
var self = this;
|
||||
setTimeout(function () {
|
||||
if (self.dead) return; // S6: never deliver into a torn-down app
|
||||
self.mailbox.push({ fn: fn, arg: arg });
|
||||
self.enqueued++;
|
||||
self._armDeliveryTick();
|
||||
}, ms);
|
||||
},
|
||||
pop: function () {
|
||||
var m = this.mailbox.shift();
|
||||
if (m) this.delivered++;
|
||||
return m || null;
|
||||
},
|
||||
// Deliver via a dedicated PLAIN export call (wxWasmMailboxTick), never
|
||||
// from inside a pump's awaited ProcessEvents ccall — a fiber swap there
|
||||
// sits on the JS-awaits-a-suspending-export boundary (#13302) and traps.
|
||||
// Resume ready contexts from a FRESH task. Separate from the mailbox tick
|
||||
// because it must run even when the mailbox is empty: a context wake is
|
||||
// work the pump owns, not a queued message.
|
||||
_armSchedPump: function () {
|
||||
if (this._pumpArmed) return;
|
||||
this._pumpArmed = true;
|
||||
var self = this;
|
||||
setTimeout(function () {
|
||||
self._pumpArmed = false;
|
||||
if (self.dead) return;
|
||||
try {
|
||||
if (Module["_wxWasmSchedPump"]) Module["_wxWasmSchedPump"]();
|
||||
} catch (e) {
|
||||
if (Module["_wx_dispatch_abandon"]) Module["_wx_dispatch_abandon"]();
|
||||
// The exception escaped a context through drain()'s fiber swap, so
|
||||
// the transition it started never completed: without this every
|
||||
// later pump refuses ("transition in flight") and all outstanding
|
||||
// waits stall forever (doc 22 Phase B).
|
||||
if (Module["_wxWasmSchedAbandon"]) Module["_wxWasmSchedAbandon"]();
|
||||
throw e;
|
||||
}
|
||||
}, 0);
|
||||
},
|
||||
_armDeliveryTick: function () {
|
||||
if (this._tickArmed) return;
|
||||
this._tickArmed = true;
|
||||
var self = this;
|
||||
setTimeout(function tick() {
|
||||
if (self.dead) { self._tickArmed = false; return; }
|
||||
try {
|
||||
if (Module["_wxWasmMailboxTick"]) Module["_wxWasmMailboxTick"]();
|
||||
} catch (e) {
|
||||
self._tickArmed = false;
|
||||
if (Module["_wx_dispatch_abandon"]) Module["_wx_dispatch_abandon"]();
|
||||
// Same containment as the top-level tick's error path (evtloop.cpp):
|
||||
// a throwing handler must not leave a parked quasi-modal unresolved.
|
||||
// No-ops when no such wait is open (5101 = wxID_CANCEL).
|
||||
self.resolveTopWait('nested', 0);
|
||||
self.resolveTopWait('modal', 5101);
|
||||
throw e;
|
||||
}
|
||||
if (self.mailbox.length > 0) {
|
||||
setTimeout(tick, 17);
|
||||
} else {
|
||||
self._tickArmed = false;
|
||||
}
|
||||
}, 0);
|
||||
},
|
||||
|
||||
// --- S1 embind lane ---------------------------------------------------
|
||||
MUTATOR_NAMES: [
|
||||
"kicadSetChrome", "kicadSetReadOnly",
|
||||
"kicadCollabApply", "kicadCollabApplyItems",
|
||||
"kicadCollabSnapshot", "kicadCollabSnapshotItems",
|
||||
"kicadCollabPresenceStart", "kicadCollabSetRemote",
|
||||
"kicadCollabSetPins", "kicadCollabSetStyle",
|
||||
"kicadCollabSetViewport", "kicadCollabFitViewport",
|
||||
"kicadCollabReleaseSelection", "kicadSetColorTheme",
|
||||
"kicadSaveBoard", "kicadSaveSchematic", "kicadSaveDrawingSheet",
|
||||
],
|
||||
mutatorQueue: [],
|
||||
mutatorsWrapped: 0,
|
||||
mutatorsDelivered: 0,
|
||||
_mutatorPumpArmed: false,
|
||||
_openBusy: function () {
|
||||
var probe = Module["kicadOpenFileBusy"];
|
||||
if (typeof probe !== "function") return false;
|
||||
try { return !!probe(); } catch (e) { return true; }
|
||||
},
|
||||
_wrapMutators: function () {
|
||||
var self = this;
|
||||
this.MUTATOR_NAMES.forEach(function (name) {
|
||||
var orig = Module[name];
|
||||
if (typeof orig !== "function") return;
|
||||
self.mutatorsWrapped++;
|
||||
Module[name] = function () {
|
||||
var args = arguments;
|
||||
var call = function () { return orig.apply(Module, args); };
|
||||
if (self.mutatorQueue.length === 0 && !self._openBusy()) {
|
||||
self.mutatorsDelivered++;
|
||||
return call();
|
||||
}
|
||||
return new Promise(function (resolve, reject) {
|
||||
self.mutatorQueue.push({ name: name, call: call, resolve: resolve, reject: reject });
|
||||
self._armMutatorPump();
|
||||
});
|
||||
};
|
||||
});
|
||||
if (this.mutatorsWrapped > 0)
|
||||
console.log("[wx-scheduler] embind lane: wrapped " + this.mutatorsWrapped + " mutator(s)");
|
||||
},
|
||||
// Phase F (doc 22 §10, the awaited-ccall entry class): when the binary
|
||||
// carries kicadOpenFileStart, the open body runs on a DISPATCH CONTEXT
|
||||
// and the await surface becomes a plain JS promise over the wait token —
|
||||
// the main stack never parks in place during a load. Conditional: older
|
||||
// binaries without the starter keep the legacy suspending export.
|
||||
_wrapOpenFile: function () {
|
||||
var self = this;
|
||||
var start = Module["kicadOpenFileStart"];
|
||||
if (typeof start !== "function" || typeof Module["kicadOpenFile"] !== "function") return;
|
||||
Module["kicadOpenFile"] = function (path) {
|
||||
// Mint the token HERE, in pure JS — the starter runs the load on a
|
||||
// dispatch context and Asyncify-suspends its own frame, so a token
|
||||
// RETURNED from it would arrive as a placeholder (0). We own the token
|
||||
// and await its promise; the job resolves it when the load finishes.
|
||||
var token = self.beginWait("open");
|
||||
start(token, path);
|
||||
// waitPromise consumes early-resolved entries (the fast-error path),
|
||||
// so a job that finished before this await still resolves correctly.
|
||||
return self.waitPromise(token).then(function (r) { return !!r; });
|
||||
};
|
||||
console.log("[wx-scheduler] open lane: kicadOpenFile routed through the dispatch context");
|
||||
},
|
||||
_armMutatorPump: function () {
|
||||
if (this._mutatorPumpArmed) return;
|
||||
this._mutatorPumpArmed = true;
|
||||
var self = this;
|
||||
var now = (typeof performance !== "undefined" && performance.now)
|
||||
? function () { return performance.now(); }
|
||||
: function () { return Date.now(); };
|
||||
setTimeout(function pump() {
|
||||
if (self.dead) { self._mutatorPumpArmed = false; return; }
|
||||
// Unkillable: an exception escaping this body would end the setTimeout
|
||||
// chain and wedge the queue forever (observed: 559 frozen messages).
|
||||
try {
|
||||
if (!self._openBusy()) {
|
||||
// Time-boxed drain: ~8 ms of work per 16 ms tick keeps the page
|
||||
// live while a long backlog drains in order.
|
||||
var t0 = now();
|
||||
while (self.mutatorQueue.length > 0 && now() - t0 < 8) {
|
||||
if (self._openBusy()) break;
|
||||
var m = self.mutatorQueue.shift();
|
||||
self.mutatorsDelivered++;
|
||||
try { m.resolve(m.call()); } catch (e) { m.reject(e); }
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
self._pumpErrors = (self._pumpErrors || 0) + 1;
|
||||
if (self._pumpErrors <= 5)
|
||||
console.warn("[wx-scheduler] mutator pump error (occurrence "
|
||||
+ self._pumpErrors + "): " + e);
|
||||
}
|
||||
if (self.mutatorQueue.length > 0) setTimeout(pump, 16);
|
||||
else self._mutatorPumpArmed = false;
|
||||
}, 16);
|
||||
},
|
||||
|
||||
// --- S4 wait registry ---------------------------------------------------
|
||||
// Token-based waits (doc 13 §2: wasm_begin_async_wait / wasm_yield_until /
|
||||
// wasm_resolve_wait). A wait is begun BEFORE the C++ side parks, so a
|
||||
// resolve that races ahead of the park (EndModal during Show()) simply
|
||||
// pre-resolves the promise — yieldUntil then returns immediately. Per-kind
|
||||
// LIFO stacks give wx modal/nested semantics ("innermost first") without
|
||||
// the legacy per-wait resolver stacks (_wxModalResolvers /
|
||||
// _wxNestedLoopExit — deleted at doc 20 D-1). Resolution flows
|
||||
// through the S2 deferred-wake law automatically: resolving a wait wakes
|
||||
// its parked sleep via the wrapped handleSleep path.
|
||||
waits: new Map(), // token → {kind, promise, resolve, resolved, result, awaited, contextParked}
|
||||
waitSeq: 0,
|
||||
waitStacks: {}, // kind → [unresolved tokens], LIFO
|
||||
waitsBegun: 0,
|
||||
waitsResolved: 0,
|
||||
earlyWaitResolves: 0, // resolves that landed before their waiter parked (Phase E)
|
||||
beginWait: function (kind) {
|
||||
var token = ++this.waitSeq;
|
||||
var entry = { kind: kind, resolved: false, resolve: null, promise: null };
|
||||
var self = this;
|
||||
entry.promise = new Promise(function (resolve) { entry.resolve = resolve; });
|
||||
this.waits.set(token, entry);
|
||||
(this.waitStacks[kind] = this.waitStacks[kind] || []).push(token);
|
||||
this.waitsBegun++;
|
||||
return token;
|
||||
},
|
||||
waitPromise: function (token) {
|
||||
var entry = this.waits.get(token);
|
||||
if (!entry) {
|
||||
console.warn("[wx-scheduler] waitPromise(" + token + "): unknown token");
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
if (entry.resolved) {
|
||||
// Resolved before the waiter parked (Phase E early-resolve window).
|
||||
// Consume the retained entry and hand the real result over — the old
|
||||
// path warned "unknown token" and returned 0, dropping it.
|
||||
this.waits.delete(token);
|
||||
return Promise.resolve(entry.result | 0);
|
||||
}
|
||||
entry.awaited = true;
|
||||
return entry.promise;
|
||||
},
|
||||
// Phase E: a wait resolved before its C++ waiter reached the park keeps its
|
||||
// entry (see resolveWait) so the result is not lost. wxWasmYieldUntil peeks
|
||||
// before parking a context and consumes the result instead of parking a
|
||||
// context nobody will ever resume.
|
||||
waitEarlyResolved: function (token) {
|
||||
var entry = this.waits.get(token);
|
||||
return entry && entry.resolved ? 1 : 0;
|
||||
},
|
||||
takeWaitResult: function (token) {
|
||||
var entry = this.waits.get(token);
|
||||
if (!entry || !entry.resolved) return 0;
|
||||
this.waits.delete(token);
|
||||
return entry.result | 0;
|
||||
},
|
||||
// doc 22 Phase C: this token's waiter parked a SCHEDULER CONTEXT instead of
|
||||
// suspending its stack in place, so there is no promise anyone awaits —
|
||||
// resolving one would strand the context forever. Marked from C++ at park
|
||||
// time; resolveWait routes such tokens to the registry instead.
|
||||
noteContextWait: function (token) {
|
||||
var entry = this.waits.get(token);
|
||||
if (entry) entry.contextParked = true;
|
||||
},
|
||||
resolveWait: function (token, result) {
|
||||
var entry = this.waits.get(token);
|
||||
if (!entry || entry.resolved) return false;
|
||||
entry.resolved = true;
|
||||
this.waitsResolved++;
|
||||
var stack = this.waitStacks[entry.kind];
|
||||
if (stack) {
|
||||
var idx = stack.indexOf(token);
|
||||
if (idx !== -1) stack.splice(idx, 1);
|
||||
}
|
||||
if (entry.contextParked) {
|
||||
// Mark ready only — never resume inline. The pump picks it up from a
|
||||
// fresh task, which is doc 13 §1.4's deferred-wake law applied to
|
||||
// contexts (a rewind inside this resolver's own turn is the whole
|
||||
// class of bug the scheduler exists to remove).
|
||||
this.waits.delete(token);
|
||||
try {
|
||||
Module["_wxWasmSchedResolveContextWait"](token, result | 0);
|
||||
} catch (e) {
|
||||
console.warn("[wx-scheduler] context wait " + token + " resolve failed: " + e);
|
||||
}
|
||||
this._armSchedPump();
|
||||
return true;
|
||||
}
|
||||
entry.result = result | 0;
|
||||
entry.resolve(result | 0);
|
||||
if (entry.awaited) {
|
||||
this.waits.delete(token);
|
||||
} else {
|
||||
// Nobody has parked on this token yet (Phase E early-resolve window:
|
||||
// a bridge whose request settled before the C++ frame reached the
|
||||
// park). Keep the entry, result attached — wxWasmYieldUntil or a late
|
||||
// waitPromise consumes it. Deleting here is what stranded the first
|
||||
// Phase E attempt: the later park waited on a wake nobody could send.
|
||||
this.earlyWaitResolves++;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// Resolve the INNERMOST unresolved wait of a kind (wx LIFO semantics).
|
||||
resolveTopWait: function (kind, result) {
|
||||
var stack = this.waitStacks[kind];
|
||||
if (!stack || stack.length === 0) return false;
|
||||
return this.resolveWait(stack[stack.length - 1], result);
|
||||
},
|
||||
pendingWaits: function (kind) {
|
||||
var stack = this.waitStacks[kind];
|
||||
return stack ? stack.length : 0;
|
||||
},
|
||||
|
||||
// --- S2 scheduler core state -------------------------------------------
|
||||
// Deferred sleep wakes: {deliver, result} queued because a transition was
|
||||
// in flight when the wake arrived. Delivered FIFO from a clean macrotask.
|
||||
readyWakes: [],
|
||||
deferredWakes: 0,
|
||||
drainedWakes: 0,
|
||||
_wakeDrainArmed: false,
|
||||
// N1: pure-JS currData writes seen without scheduler authorization.
|
||||
strayWrites: 0,
|
||||
// Phase E: fresh in-place Asyncify parks that began on a NON-main stack
|
||||
// (tool coroutine / scheduler context). Must be ZERO at the flip.
|
||||
inplaceParksOnFiberStack: 0,
|
||||
strictStrays: false, // tests set true → stray throws instead of beaconing
|
||||
_authorizedWrite: 0,
|
||||
authorize: function (fn) {
|
||||
this._authorizedWrite++;
|
||||
try { return fn(); } finally { this._authorizedWrite--; }
|
||||
},
|
||||
|
||||
// --- S6 lifetime --------------------------------------------------------
|
||||
// Called when the wx main loop exits (DoRun's top-level return path). The
|
||||
// app object is about to be destroyed: delivering anything after this
|
||||
// point runs callbacks into freed C++ state. Queued mutators reject,
|
||||
// queued messages and wakes drop — loudly, so a teardown that strands
|
||||
// work is visible in the console instead of surfacing as a later UAF.
|
||||
dead: false,
|
||||
shutdown: function (reason) {
|
||||
if (this.dead) return;
|
||||
this.dead = true;
|
||||
var stranded = {
|
||||
mailbox: this.mailbox.length,
|
||||
mutators: this.mutatorQueue.length,
|
||||
wakes: this.readyWakes.length,
|
||||
waits: this.waits.size,
|
||||
};
|
||||
this.mailbox.length = 0;
|
||||
for (var i = 0; i < this.mutatorQueue.length; i++) {
|
||||
try { this.mutatorQueue[i].reject(new Error("[wx-scheduler] shutdown: " + reason)); } catch (e) {}
|
||||
}
|
||||
this.mutatorQueue.length = 0;
|
||||
this.readyWakes.length = 0;
|
||||
if (stranded.mailbox || stranded.mutators || stranded.wakes || stranded.waits) {
|
||||
console.warn("[wx-scheduler] shutdown (" + reason + ") stranded:"
|
||||
+ " mailbox=" + stranded.mailbox
|
||||
+ " mutators=" + stranded.mutators
|
||||
+ " wakes=" + stranded.wakes
|
||||
+ " pendingWaits=" + stranded.waits);
|
||||
} else {
|
||||
console.log("[wx-scheduler] shutdown (" + reason + ") clean");
|
||||
}
|
||||
},
|
||||
|
||||
state: function () {
|
||||
return "[wx-scheduler] build=1 impl=S2-core"
|
||||
+ (this.dead ? " DEAD" : "")
|
||||
+ " mailbox=" + this.mailbox.length
|
||||
+ " enqueued=" + this.enqueued
|
||||
+ " delivered=" + this.delivered
|
||||
+ " mutQ=" + this.mutatorQueue.length
|
||||
+ " mutWrapped=" + this.mutatorsWrapped
|
||||
+ " mutDelivered=" + this.mutatorsDelivered
|
||||
+ " readyWakes=" + this.readyWakes.length
|
||||
+ " deferredWakes=" + this.deferredWakes
|
||||
+ " drainedWakes=" + this.drainedWakes
|
||||
+ " strayWrites=" + this.strayWrites
|
||||
+ " waits=" + this.waits.size
|
||||
+ " waitsBegun=" + this.waitsBegun
|
||||
+ " waitsResolved=" + this.waitsResolved
|
||||
+ " earlyWaitResolves=" + this.earlyWaitResolves
|
||||
+ " fiberStackParks=" + this.inplaceParksOnFiberStack;
|
||||
},
|
||||
};
|
||||
|
||||
globalThis.__wxScheduler = AsyncifyScheduler;
|
||||
|
||||
// ======================================================================
|
||||
// S2 core install. Skipped defensively if the legacy shim somehow got in
|
||||
// first — double-managing the wake path corrupts (the injector's either-or
|
||||
// flip should make this unreachable).
|
||||
// ======================================================================
|
||||
if (Asyncify.__nestedHandleSleepInstalled) {
|
||||
console.warn("[wx-scheduler] legacy handlesleep present - S2 core NOT installed (dual-management guard)");
|
||||
} else if (typeof Asyncify.handleSleep === "function"
|
||||
&& typeof Asyncify.allocateData === "function") {
|
||||
|
||||
// --- flight recorder + state dump (ported verbatim-in-spirit from
|
||||
// handlesleep.js; formats are parsed by guard-beacons.ts and
|
||||
// apps/tests/tools/repro-board-load.ts — do not change shapes) ---------
|
||||
Asyncify.__pendingSleepContexts = [];
|
||||
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
|
||||
+ " fcsTotal=" + (F.__fcsTotal || 0)
|
||||
+ " rootHotTotal=" + (F.__rootHotTotal || 0)
|
||||
+ " valid=[" + (F.__validSuspensions ? Array.from(F.__validSuspensions).join(",") : "") + "]"
|
||||
+ " parked=[" + (F.__internallyParked ? Array.from(F.__internallyParked).join(",") : "") + "]"
|
||||
+ " deferrals=" + (F.__rootDeferrals || 0))
|
||||
: " (no Fibers)")
|
||||
+ " | " + AsyncifyScheduler.state();
|
||||
return head + "\n[wx-asyncify] RECORDER (oldest first):\n " + Asyncify.__rec.join("\n ");
|
||||
};
|
||||
if (typeof window !== "undefined") {
|
||||
window.__wxAsyncifyDump = __dumpState;
|
||||
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) {
|
||||
var n = (counts[kind] = (counts[kind] || 0) + 1);
|
||||
if (n > 10 && n % 100 !== 0) return;
|
||||
var line = "[wx-asyncify] " + kind + ": " + msg + " (occurrence " + n + ")";
|
||||
if (withStack) {
|
||||
try { line += "\n" + String(new Error().stack).split("\n").slice(1, 8).join("\n"); } catch (e) {}
|
||||
}
|
||||
console.warn(line);
|
||||
};
|
||||
})();
|
||||
|
||||
// --- N1: single-writer accessor on Asyncify.currData ------------------
|
||||
// Writes made while compiled code is on the export stack are the wasm
|
||||
// runtime's own (fiber_swap, handleSleep's park/stop paths) — legitimate.
|
||||
// A pure-JS write (empty export stack) must come from a scheduler-
|
||||
// authorized span; anything else is a STRAY: the exact shape of every
|
||||
// historical corruption's bad write. Beacon + count; strict mode throws.
|
||||
(function () {
|
||||
var realCurrData = Asyncify.currData; // null at install time
|
||||
Object.defineProperty(Asyncify, "currData", {
|
||||
configurable: true,
|
||||
get: function () { return realCurrData; },
|
||||
set: function (v) {
|
||||
if ((!Asyncify.exportCallStack || Asyncify.exportCallStack.length === 0)
|
||||
&& AsyncifyScheduler._authorizedWrite === 0
|
||||
&& !(typeof Fibers !== "undefined" && Fibers.trampolineRunning)) {
|
||||
AsyncifyScheduler.strayWrites++;
|
||||
__wxAsyncifyReport("stray-currdata-write",
|
||||
"currData=" + (v || 0) + " written from pure JS without scheduler authorization", true);
|
||||
if (AsyncifyScheduler.strictStrays)
|
||||
throw new Error("[wx-scheduler] stray currData write (strict mode)");
|
||||
}
|
||||
realCurrData = v;
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
||||
// --- deferred-wake drain ----------------------------------------------
|
||||
var __transitionFree = function () {
|
||||
return Asyncify.state === 0
|
||||
&& !(typeof Fibers !== "undefined" && Fibers.trampolineRunning);
|
||||
};
|
||||
AsyncifyScheduler._scheduleWakeDrain = function () {
|
||||
if (this._wakeDrainArmed) return;
|
||||
this._wakeDrainArmed = true;
|
||||
var self = this;
|
||||
setTimeout(function () {
|
||||
self._wakeDrainArmed = false;
|
||||
if (self.dead) return; // S6: parked stacks are gone with the app
|
||||
// Deliver from a CLEAN macrotask (export stack empty by construction).
|
||||
while (self.readyWakes.length > 0 && __transitionFree()) {
|
||||
var w = self.readyWakes.shift();
|
||||
self.drainedWakes++;
|
||||
w.deliver(w.result);
|
||||
}
|
||||
if (self.readyWakes.length > 0) self._scheduleWakeDrain();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// --- handleSleep wrap: registry + capture/restore + deferral ----------
|
||||
var __originalAllocateData = Asyncify.allocateData.bind(Asyncify);
|
||||
Asyncify.allocateData = function () {
|
||||
var ptr = __originalAllocateData();
|
||||
for (var i = Asyncify.__pendingSleepContexts.length - 1; i >= 0; --i) {
|
||||
var ctx = Asyncify.__pendingSleepContexts[i];
|
||||
if (!ctx.capturedData) {
|
||||
ctx.capturedData = ptr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return ptr;
|
||||
};
|
||||
|
||||
var __originalHandleSleep = Asyncify.handleSleep.bind(Asyncify);
|
||||
Asyncify.handleSleep = function (startAsync) {
|
||||
__rec("sleep s=" + Asyncify.state + " cd=" + (Asyncify.currData || 0)
|
||||
+ " w=" + (Asyncify.__inSleepWake || 0));
|
||||
if (Asyncify.state === 0 && Asyncify.currData) {
|
||||
__wxAsyncifyReport("concurrent-park",
|
||||
"handleSleep entered while currData=" + Asyncify.currData, true);
|
||||
}
|
||||
if (Asyncify.state === 1) {
|
||||
__wxAsyncifyReport("reentrant-state",
|
||||
"handleSleep entered mid-unwind (state=1) currData=" + Asyncify.currData, true);
|
||||
}
|
||||
// Only a FRESH park (state 0) allocates data and needs tracking; the
|
||||
// state-2 resume re-entry returns synchronously through the rewind
|
||||
// branch (a context pushed for it leaks one per resume).
|
||||
if (Asyncify.state !== 0) {
|
||||
return __originalHandleSleep(startAsync);
|
||||
}
|
||||
// Phase F (doc 22 §10 F2/F3): report every fresh in-place park to the
|
||||
// REGISTRY. Begin() returns the owning context id (0 = main stack);
|
||||
// while recorded, fiber_enterable()/fiber_transfer refuse entering that
|
||||
// context — the registry-owned replacement for the deleted quarantine.
|
||||
// Also the Phase E telemetry: fiberStackParks must be 0 at the flip's
|
||||
// repro gate. Leaf probe into wasm; state is 0 here so no unwind is in
|
||||
// flight yet.
|
||||
var parkOwnerCtx = 0;
|
||||
try {
|
||||
if (Module["_wxWasmSchedInplaceParkBegin"]) {
|
||||
parkOwnerCtx = Module["_wxWasmSchedInplaceParkBegin"]() | 0;
|
||||
if (parkOwnerCtx) {
|
||||
AsyncifyScheduler.inplaceParksOnFiberStack++;
|
||||
__rec("inplace-park-on-fiber-stack ctx=" + parkOwnerCtx
|
||||
+ " n=" + AsyncifyScheduler.inplaceParksOnFiberStack);
|
||||
}
|
||||
}
|
||||
} catch (e) { /* probe must never break a park */ }
|
||||
var sleepCtx = {
|
||||
capturedData: null,
|
||||
cleanedUp: false,
|
||||
parkOwnerCtx: parkOwnerCtx,
|
||||
rootOwned: (typeof Fibers === "undefined")
|
||||
|| (!Fibers.__inFiberEntry
|
||||
&& !(Asyncify.__wakingOwnerFiber || false)),
|
||||
};
|
||||
Asyncify.__pendingSleepContexts.push(sleepCtx);
|
||||
|
||||
var cleanup = function () {
|
||||
if (sleepCtx.cleanedUp) return;
|
||||
sleepCtx.cleanedUp = true;
|
||||
if (sleepCtx.parkOwnerCtx) {
|
||||
try {
|
||||
if (Module["_wxWasmSchedInplaceParkEnd"]) {
|
||||
Module["_wxWasmSchedInplaceParkEnd"](sleepCtx.parkOwnerCtx);
|
||||
}
|
||||
} catch (e) { /* never break a wake */ }
|
||||
sleepCtx.parkOwnerCtx = 0;
|
||||
}
|
||||
var idx = Asyncify.__pendingSleepContexts.indexOf(sleepCtx);
|
||||
if (idx !== -1) Asyncify.__pendingSleepContexts.splice(idx, 1);
|
||||
};
|
||||
|
||||
try {
|
||||
return __originalHandleSleep(function (wakeUp) {
|
||||
// deliver(): the legacy shim's whole wake path — restore OUR buffer,
|
||||
// mark the wake window, swallow the "unwind" sentinel.
|
||||
var deliver = function (result) {
|
||||
__rec("wake buf=" + (sleepCtx.capturedData || 0) + " cdWas=" + (Asyncify.currData || 0)
|
||||
+ (sleepCtx.rootOwned ? " R" : " f"));
|
||||
if (sleepCtx.capturedData) {
|
||||
if (Asyncify.currData !== sleepCtx.capturedData) {
|
||||
__wxAsyncifyReport(
|
||||
Asyncify.currData ? "aliased-wake-live" : "overlapped-wake",
|
||||
"restoring currData=" + sleepCtx.capturedData +
|
||||
" over " + (Asyncify.currData || "null") +
|
||||
" state=" + Asyncify.state, !!Asyncify.currData);
|
||||
}
|
||||
AsyncifyScheduler.authorize(function () {
|
||||
Asyncify.currData = sleepCtx.capturedData;
|
||||
});
|
||||
}
|
||||
cleanup();
|
||||
Asyncify.__inSleepWake = (Asyncify.__inSleepWake || 0) + 1;
|
||||
var prevWakingOwnerFiber = Asyncify.__wakingOwnerFiber || false;
|
||||
Asyncify.__wakingOwnerFiber = !sleepCtx.rootOwned;
|
||||
var prevWakingRoot = Asyncify.__wakingRoot || 0;
|
||||
if (sleepCtx.rootOwned) Asyncify.__wakingRoot = (Asyncify.__wakingRoot || 0) + 1;
|
||||
try {
|
||||
return wakeUp(result);
|
||||
} catch (e) {
|
||||
if (e === "unwind") return;
|
||||
throw e;
|
||||
} finally {
|
||||
Asyncify.__inSleepWake -= 1;
|
||||
Asyncify.__wakingOwnerFiber = prevWakingOwnerFiber;
|
||||
if (sleepCtx.rootOwned) Asyncify.__wakingRoot = prevWakingRoot;
|
||||
}
|
||||
};
|
||||
return startAsync(function (result) {
|
||||
// THE S2 LAW (doc 12): a wake never starts a rewind while another
|
||||
// transition is in flight — it enqueues and the drain delivers
|
||||
// from a clean macrotask when the slot frees. The legacy shim
|
||||
// could only beacon this window (aliased-wake-live); the
|
||||
// scheduler removes it.
|
||||
if (!__transitionFree()) {
|
||||
AsyncifyScheduler.deferredWakes++;
|
||||
__rec("defer-wake buf=" + (sleepCtx.capturedData || 0)
|
||||
+ " s=" + Asyncify.state);
|
||||
AsyncifyScheduler.readyWakes.push({ deliver: deliver, result: result });
|
||||
AsyncifyScheduler._scheduleWakeDrain();
|
||||
return;
|
||||
}
|
||||
return deliver(result);
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
cleanup();
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
// Transition-completion signal: maybeStopUnwind is where an unwind
|
||||
// finishes (state → Normal) and the trampoline runs queued fiber
|
||||
// switches. After it settles, deferred wakes may proceed.
|
||||
var __originalMaybeStopUnwind = Asyncify.maybeStopUnwind.bind(Asyncify);
|
||||
Asyncify.maybeStopUnwind = function () {
|
||||
var ret = __originalMaybeStopUnwind();
|
||||
if (AsyncifyScheduler.readyWakes.length > 0 && __transitionFree())
|
||||
AsyncifyScheduler._scheduleWakeDrain();
|
||||
return ret;
|
||||
};
|
||||
|
||||
Asyncify.__nestedHandleSleepInstalled = true; // compat: tools probe this
|
||||
console.log("[wx-scheduler] S2 core installed (deferred wakes + N1 accessor)");
|
||||
}
|
||||
|
||||
// --- stale-fiber-rewind guard (ported from handlesleep.js; semantics
|
||||
// unchanged — these encode the consume-once/quarantine contracts of
|
||||
// docs/features/async/16) + trampoline heal ownership -------------------
|
||||
// Phase F F2/F3 (doc 22 §10, 2026-08-09): deletion was built, measured and
|
||||
// REVERTED. The registry now carries the in-place-park fact
|
||||
// (wxWasmSchedInplaceParkBegin/End) and refuses on the transfer lane, but
|
||||
// the quarantine's DROP is still the only correct recovery for a misrouted
|
||||
// yield-back under attribution rot (a C++-level refusal ghost-resumes the
|
||||
// yielding coroutine — measured as the lever's phase2 overshoot). This
|
||||
// block stays until attribution is registry-authoritative (gap 3).
|
||||
if (typeof Fibers !== "undefined"
|
||||
&& typeof Fibers.finishContextSwitch === "function"
|
||||
&& !Fibers.__staleRewindGuardInstalled) {
|
||||
Fibers.__validSuspensions = new Set();
|
||||
Fibers.__internallyParked = new Set();
|
||||
Fibers.__parkSleepBuf = new Map();
|
||||
|
||||
var __origFinishContextSwitch = Fibers.finishContextSwitch.bind(Fibers);
|
||||
var __fiberRefusals = 0;
|
||||
|
||||
var __fcsRec = (typeof Asyncify !== "undefined" && Asyncify.__recPush)
|
||||
? Asyncify.__recPush
|
||||
: function () {};
|
||||
|
||||
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
|
||||
+ " (occurrence " + __fiberRefusals + ")");
|
||||
}
|
||||
AsyncifyScheduler.authorize(function () {
|
||||
Asyncify.currData = null;
|
||||
});
|
||||
};
|
||||
|
||||
Fibers.finishContextSwitch = function (newFiber) {
|
||||
Fibers.__fcsTotal = (Fibers.__fcsTotal || 0) + 1;
|
||||
if (newFiber === Fibers.__rootFiber && (Asyncify.__inSleepWake || 0) > 0) {
|
||||
Fibers.__rootHotTotal = (Fibers.__rootHotTotal || 0) + 1;
|
||||
}
|
||||
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])
|
||||
+ " 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) + __remStr);
|
||||
if (Asyncify.currData) {
|
||||
var oldFiber = Asyncify.currData - 20;
|
||||
if (Fibers.__rootFiber === undefined) {
|
||||
Fibers.__rootFiber = oldFiber;
|
||||
}
|
||||
var parkBuf = Fibers.__parkSleepBuf.get(oldFiber);
|
||||
var stillParked = parkBuf !== undefined
|
||||
&& Array.isArray(Asyncify.__pendingSleepContexts)
|
||||
&& Asyncify.__pendingSleepContexts.some(function (c) { return c.capturedData === parkBuf; });
|
||||
if (!stillParked) {
|
||||
Fibers.__validSuspensions.add(oldFiber);
|
||||
Fibers.__internallyParked.delete(oldFiber);
|
||||
Fibers.__parkSleepBuf.delete(oldFiber);
|
||||
}
|
||||
}
|
||||
|
||||
var isRoot = newFiber === Fibers.__rootFiber;
|
||||
var HEAPU32v = (typeof GROWABLE_HEAP_U32 === "function") ? GROWABLE_HEAP_U32() : HEAPU32;
|
||||
var entryPoint = HEAPU32v[((newFiber + 12) >>> 2) >>> 0];
|
||||
if (!isRoot && Fibers.__internallyParked.has(newFiber)) {
|
||||
__refuseFiber(newFiber, "is asyncify-parked mid-body (sleep in flight)");
|
||||
return;
|
||||
}
|
||||
if (entryPoint === 0) {
|
||||
if (!Fibers.__validSuspensions.has(newFiber)) {
|
||||
__refuseFiber(newFiber, isRoot
|
||||
? "root suspension already consumed - a second rewind would replay stale frames"
|
||||
: "has no live suspension - rewinding would replay stale data");
|
||||
return;
|
||||
}
|
||||
Fibers.__validSuspensions.delete(newFiber);
|
||||
}
|
||||
|
||||
if (!isRoot) Fibers.__inFiberEntry = (Fibers.__inFiberEntry || 0) + 1;
|
||||
var ret;
|
||||
try {
|
||||
// The original writes currData (entry path nulls it, resume path sets
|
||||
// the fiber's buffer) from pure JS — scheduler-supervised here.
|
||||
ret = AsyncifyScheduler.authorize(function () {
|
||||
return __origFinishContextSwitch(newFiber);
|
||||
});
|
||||
} finally {
|
||||
if (!isRoot) Fibers.__inFiberEntry -= 1;
|
||||
}
|
||||
|
||||
if (!isRoot && !Fibers.nextFiber && Asyncify.currData) {
|
||||
Fibers.__internallyParked.add(newFiber);
|
||||
Fibers.__parkSleepBuf.set(newFiber, Asyncify.currData);
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
// Trampoline heal ownership (subsumes inject-dyncall-shims §3c): a throw
|
||||
// escaping the trampoline loop must not leave trampolineRunning wedged —
|
||||
// that guard being stuck turns every later fiber swap into a silent no-op.
|
||||
var __origTrampoline = Fibers.trampoline.bind(Fibers);
|
||||
Fibers.trampoline = function () {
|
||||
try {
|
||||
return __origTrampoline();
|
||||
} catch (e) {
|
||||
Fibers.trampolineRunning = false;
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
Fibers.__staleRewindGuardInstalled = true;
|
||||
}
|
||||
|
||||
// Wrap the embind mutators once the runtime has registered them.
|
||||
if (typeof Module !== "undefined") {
|
||||
if (Module["calledRun"]) {
|
||||
AsyncifyScheduler._wrapMutators();
|
||||
AsyncifyScheduler._wrapOpenFile();
|
||||
} else {
|
||||
var __wxSchedPrevInit = Module["onRuntimeInitialized"];
|
||||
Module["onRuntimeInitialized"] = function () {
|
||||
if (typeof __wxSchedPrevInit === "function") __wxSchedPrevInit();
|
||||
AsyncifyScheduler._wrapMutators();
|
||||
AsyncifyScheduler._wrapOpenFile();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[wx-scheduler] scaffolding installed (S2, core live)");
|
||||
}
|
||||
// === End AsyncifyScheduler ===
|
||||
|
|
@ -436,7 +436,6 @@
|
|||
entrySp: region.top,
|
||||
suspendedAt: 0
|
||||
};
|
||||
if (globalThis.__wxTrace) console.log('[TRACE] alloc region ' + region.base + ' -> act ' + rec.id + ':' + name);
|
||||
S._actStack.push(rec);
|
||||
S._setSp(region.top);
|
||||
var out;
|
||||
|
|
@ -466,7 +465,6 @@
|
|||
},
|
||||
|
||||
_endActivation: function (rec) {
|
||||
if (globalThis.__wxTrace) console.log('[TRACE] end ' + rec.id + ':' + rec.kind + ' live=' + (this._windowLive ? this._windowLive.id : '-'));
|
||||
this._suspended.delete(rec.id);
|
||||
if (this._windowLive === rec) {
|
||||
// completed from a RESUMED window: wasm's epilogue left SP at the
|
||||
|
|
@ -476,7 +474,6 @@
|
|||
if (rec.enclosingSp !== undefined) this._setSp(rec.enclosingSp);
|
||||
}
|
||||
if (rec.region) {
|
||||
if (globalThis.__wxTrace) console.log('[TRACE] free region ' + rec.region.base + ' <- act ' + rec.id);
|
||||
this._regionFree(rec.region);
|
||||
rec.region = null;
|
||||
}
|
||||
|
|
@ -485,7 +482,6 @@
|
|||
},
|
||||
|
||||
_pumpResume: function () {
|
||||
if (globalThis.__wxTrace) console.log('[TRACE] pump live=' + (this._windowLive ? this._windowLive.id + ':' + this._windowLive.kind : '-') + ' ready=' + this._resumeReady.map(function(e){return e.rec.id + ':' + e.rec.kind;}).join(','));
|
||||
if (this.dead) return;
|
||||
if (this._windowLive) {
|
||||
// Self-heal: an activation that suspended RAW (bypassing the shim)
|
||||
|
|
@ -580,7 +576,6 @@
|
|||
entrySp: S._sp(), suspendedAt: 0, anon: true
|
||||
};
|
||||
}
|
||||
if (globalThis.__wxTrace) console.log('[TRACE] suspend ' + rec.id + ':' + rec.kind + ' ' + kind + '/' + token + ' live=' + (S._windowLive ? S._windowLive.id : '-') + ' stack=' + S._actStack.map(function(r){return r.id;}).join(','));
|
||||
rec.sp = S._sp();
|
||||
rec.suspendedAt = Date.now();
|
||||
rec.waitKind = kind;
|
||||
|
|
@ -828,7 +823,7 @@
|
|||
S.installExportWraps([
|
||||
"wx_dom_event", "wx_dom_mouse", "wx_window_close", "wx_window_move",
|
||||
"wx_window_resize", "ProcessEvents", "wxWasmMailboxTick",
|
||||
"wxWasmTopLevelTick", "wxWasmMainLoopPump", "wxWasmJobTick"
|
||||
"wxWasmTopLevelTick", "wxWasmJobTick"
|
||||
]);
|
||||
// KiCad-only surfaces; both installers skip absent names, so the wx
|
||||
// test apps (no embind) pass through here untouched.
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
#!/usr/bin/env node
|
||||
// Computes the "sc" (source-content) hash for the KiCad-WASM output cache key
|
||||
// in .github/workflows/ci-ubicloud.yml.
|
||||
// in .github/workflows/wasm-build.yml.
|
||||
//
|
||||
// The cache key is:
|
||||
// kwasm-<os>-bin<binaryen-ver><opt-level>-k<kicad-sha>-wx<wx-sha>-sc<HASH>-e<epoch>
|
||||
// kwasm-<os>-k<kicad-sha>-wx<wx-sha>-sc<HASH>-3d<flag>-e<epoch>
|
||||
//
|
||||
// The kicad/wx submodule SHAs already capture the *sources*. This hash captures
|
||||
// the *build logic* that shapes the wasm bytes but lives outside those
|
||||
// submodules — the asyncify/finalize/dyncall/wasm-opt host steps, the per-tool
|
||||
// submodules — the host ENV-shim patch, the per-tool
|
||||
// compile scripts, the dependency builds, and the Docker toolchain. Those were
|
||||
// deliberately dropped from the key's hashFiles() (so routine script edits don't
|
||||
// trigger a 1-2h rebuild); folding the *output-determining* subset back in here
|
||||
|
|
@ -47,14 +47,12 @@ import { join, relative, sep } from "node:path";
|
|||
// { dir: "<repo-relative dir>", match: RE } files under the dir whose BASENAME matches RE, recursive
|
||||
// Paths are POSIX, relative to the repo root. This script always adds itself.
|
||||
const INPUTS = [
|
||||
// Host-side post-processing — these directly shape the final wasm bytes.
|
||||
{ file: "scripts/common/apply-asyncify.sh" },
|
||||
{ file: "scripts/common/apply-finalize.sh" },
|
||||
{ file: "scripts/common/inject-dyncall-shims.sh" },
|
||||
// The submodule build that provides BOTH host wasm-opt and wasm-emscripten-finalize.
|
||||
// (get-wasm-opt.sh is no longer on the build path — bench-only — so it no longer
|
||||
// belongs in the cache key.)
|
||||
{ file: "scripts/binaryen-hoist-pass/build-wasm-opt.sh" },
|
||||
// Host-side post-processing — shapes the shipped glue.
|
||||
{ file: "scripts/common/patch-env-shim.mjs" },
|
||||
// Link-time inputs baked into every editor app: the scheduler pre-js and
|
||||
// the promising-export census.
|
||||
{ file: "scripts/common/shims/jspi-scheduler.js" },
|
||||
{ file: "scripts/common/jspi-exports.txt" },
|
||||
|
||||
// Per-tool compile recipes (compile flags / emcc link options).
|
||||
{ dir: "scripts/kicad", match: /^build-.*\.sh$/ },
|
||||
|
|
|
|||
|
|
@ -296,29 +296,11 @@ else
|
|||
log_info "Building KiCad in RELEASE mode (skipping wasm-opt due to memory limits)"
|
||||
fi
|
||||
|
||||
# Async suspension backend (JSPI migration). Default asyncify; PCBJAM_ASYNC_BACKEND=jspi
|
||||
# links the browser apps with -sJSPI (native stack switching) instead of
|
||||
# -sASYNCIFY/-sDYNCALLS, defines PCBJAM_JSPI=1 for every TU (selects the JSPI
|
||||
# libcontext coroutine backend + the wx port's JSPI dispatch paths), and skips
|
||||
# the whole post-link asyncify pipeline (stub dance + host wasm-opt). The wx
|
||||
# build this app links against must be built with the SAME knob
|
||||
# (build-wx-wasm.sh reads it too). The headless CLIs (kicad_tools, occ_service)
|
||||
# stay asyncify-shaped regardless: their targets pin -sASYNCIFY=0 and run in
|
||||
# node/worker where no suspension backend is wanted.
|
||||
PCBJAM_ASYNC_BACKEND="${PCBJAM_ASYNC_BACKEND:-asyncify}"
|
||||
case "${APP_NAME}" in
|
||||
kicad_tools|occ_service) PCBJAM_ASYNC_BACKEND="asyncify" ;;
|
||||
esac
|
||||
if [ "${PCBJAM_ASYNC_BACKEND}" = "jspi" ]; then
|
||||
EXTRA_FLAGS="${EXTRA_FLAGS} -DPCBJAM_JSPI=1"
|
||||
# PCBJAM_JSPI is ABI-affecting (coroutine.h's callerStub grows a finish
|
||||
# hook — a template, so every instantiating TU must agree): route it into
|
||||
# KICAD_TU_ABI_FLAGS via EMBIND_CONFIG_DEFINES like the Debug -DDEBUG.
|
||||
EMBIND_CONFIG_DEFINES="${EMBIND_CONFIG_DEFINES} -DPCBJAM_JSPI=1"
|
||||
log_info "Async backend: JSPI (-sJSPI, -DPCBJAM_JSPI=1)"
|
||||
else
|
||||
log_info "Async backend: asyncify"
|
||||
fi
|
||||
# Suspension backend: JSPI (native stack switching) since the migration.
|
||||
# The headless CLIs (kicad_tools, occ_service) get NO suspension backend at
|
||||
# all — their targets pin -sASYNCIFY=0 and run in node/worker where nothing
|
||||
# may suspend. coroutine.h/libcontext key on __EMSCRIPTEN__ directly, so no
|
||||
# ABI define is threaded through the TU flags anymore.
|
||||
|
||||
# Step 6: Create build directory
|
||||
mkdir -p "${KICAD_BUILD}"
|
||||
|
|
@ -395,32 +377,11 @@ fi
|
|||
EMSDK_WASM_OPT="${EMSDK}/upstream/bin/wasm-opt"
|
||||
EMSDK_FINALIZE="${EMSDK}/upstream/bin/wasm-emscripten-finalize"
|
||||
|
||||
if [ "${APP_NAME}" = "kicad_tools" ] || [ "${APP_NAME}" = "occ_service" ] || [ "${PCBJAM_ASYNC_BACKEND}" = "jspi" ]; then
|
||||
# Use the real tools so the module is fully finalized inside the container
|
||||
# (no host post-processing / asyncify for these targets). JSPI mode has no
|
||||
# asyncify pass at all, so every app finalizes in-container.
|
||||
[ -f "${EMSDK_WASM_OPT}.real" ] && cp "${EMSDK_WASM_OPT}.real" "${EMSDK_WASM_OPT}"
|
||||
[ -f "${EMSDK_FINALIZE}.real" ] && cp "${EMSDK_FINALIZE}.real" "${EMSDK_FINALIZE}"
|
||||
log_info "Using real wasm-opt/finalize for ${APP_NAME} (finalize in-container)"
|
||||
else
|
||||
if [ -f "${EMSDK_WASM_OPT}" ] && [ ! -f "${EMSDK_WASM_OPT}.real" ]; then
|
||||
log_info "Backing up real wasm-opt..."
|
||||
mv "${EMSDK_WASM_OPT}" "${EMSDK_WASM_OPT}.real"
|
||||
fi
|
||||
# Always copy the latest stub (in case it was updated)
|
||||
cp "${STUBS_DIR}/wasm-opt-stub.sh" "${EMSDK_WASM_OPT}"
|
||||
chmod +x "${EMSDK_WASM_OPT}"
|
||||
log_info "wasm-opt stub installed (asyncify will run on host)"
|
||||
|
||||
if [ -f "${EMSDK_FINALIZE}" ] && [ ! -f "${EMSDK_FINALIZE}.real" ]; then
|
||||
log_info "Backing up real wasm-emscripten-finalize..."
|
||||
mv "${EMSDK_FINALIZE}" "${EMSDK_FINALIZE}.real"
|
||||
fi
|
||||
# Always copy the latest stub (in case it was updated)
|
||||
cp "${STUBS_DIR}/wasm-emscripten-finalize-stub.sh" "${EMSDK_FINALIZE}"
|
||||
chmod +x "${EMSDK_FINALIZE}"
|
||||
log_info "wasm-emscripten-finalize stub installed (finalize will run on host)"
|
||||
fi
|
||||
# JSPI has no post-link asyncify pass: every app finalizes in-container with
|
||||
# the REAL tools. The .real backups exist on containers that ran the retired
|
||||
# asyncify stub dance — restore them if present.
|
||||
[ -f "${EMSDK_WASM_OPT}.real" ] && cp "${EMSDK_WASM_OPT}.real" "${EMSDK_WASM_OPT}"
|
||||
[ -f "${EMSDK_FINALIZE}.real" ] && cp "${EMSDK_FINALIZE}.real" "${EMSDK_FINALIZE}"
|
||||
|
||||
# Step 6.5: Verify WASM support is in KiCad fork
|
||||
# The kicad submodule should already have WASM port detection and kiplatform support
|
||||
|
|
@ -527,37 +488,20 @@ if [ "${BUILD_3D_VIEWER}" = "ON" ]; then
|
|||
fi
|
||||
|
||||
# Multi-threaded CPU raytracer (mainline threading restored): link the main-thread
|
||||
# nanosleep->Asyncify-yield shim so the raw-thread raytracer joins (sleep_for busy-wait)
|
||||
# yield to the JS event loop instead of deadlocking on-demand pthread-Worker creation.
|
||||
# Mirrors the wasm/gl1 pattern (compile to .o, add to the link). Shim:
|
||||
# wasm/shims/nanosleep_yield.c; its EM_ASYNC_JS yield is covered by env.__asyncjs__* in
|
||||
# scripts/common/asyncify-imports.txt.
|
||||
# The synchronous node CLIs (ASYNCIFY=0) must NOT link it: their Asyncify JS
|
||||
# runtime doesn't exist, so the shim's yield throws "Asyncify is not defined"
|
||||
# on the first main-thread sleep (e.g. DRC copper-clearance's worker-poll).
|
||||
# A blocking CLI wants libc's real blocking nanosleep anyway — node permits
|
||||
# nanosleep-yield shim so the raw-thread raytracer joins (sleep_for busy-wait)
|
||||
# yield to the JS event loop instead of deadlocking on-demand pthread-Worker
|
||||
# creation (wasm/shims/nanosleep_yield.c, suspends via the jspi scheduler).
|
||||
# The synchronous node CLIs must NOT link it: nothing in them may suspend and
|
||||
# a blocking CLI wants libc's real blocking nanosleep anyway — node permits
|
||||
# Atomics.wait on its main thread.
|
||||
if [ "${APP_NAME}" = "kicad_tools" ] || [ "${APP_NAME}" = "occ_service" ]; then
|
||||
NANOSLEEP_YIELD_LINK=""
|
||||
else
|
||||
# Every main-thread sleep suspends in place (legal on a promising
|
||||
# activation; EM_ASYNC_JS auto-wraps as WebAssembly.Suspending under
|
||||
# -sJSPI) and routes through the jspi-scheduler turnstile.
|
||||
emcc -c -pthread "${PROJECT_ROOT}/wasm/shims/nanosleep_yield.c" -o "${STUBS_BUILD}/nanosleep_yield.o"
|
||||
if [ "${PCBJAM_ASYNC_BACKEND}" = "jspi" ]; then
|
||||
# JSPI: no scheduler-context lane exists — the weak
|
||||
# pcbjam_context_sleep_ms stays null and every main-thread sleep
|
||||
# suspends in place (legal on a promising activation; EM_ASYNC_JS
|
||||
# auto-wraps as WebAssembly.Suspending under -sJSPI).
|
||||
# context_sleep.cpp is fiber-only machinery and must not link.
|
||||
NANOSLEEP_YIELD_LINK="${STUBS_BUILD}/nanosleep_yield.o"
|
||||
else
|
||||
# Its scheduler-aware half (docs/features/async/22 Phase B): a main-thread
|
||||
# sleep on a scheduler context parks THAT CONTEXT instead of suspending the
|
||||
# stack in place. C++ because the registry is a header-only C++ layer in
|
||||
# wx's port, hence WX_CXXFLAGS for the include path (the same reason
|
||||
# thirdparty/libcontext needed it at Phase A).
|
||||
em++ -c -std=c++17 -pthread ${WX_CXXFLAGS} \
|
||||
"${PROJECT_ROOT}/wasm/shims/context_sleep.cpp" -o "${STUBS_BUILD}/context_sleep.o"
|
||||
NANOSLEEP_YIELD_LINK="${STUBS_BUILD}/nanosleep_yield.o ${STUBS_BUILD}/context_sleep.o"
|
||||
fi
|
||||
NANOSLEEP_YIELD_LINK="${STUBS_BUILD}/nanosleep_yield.o"
|
||||
fi
|
||||
|
||||
# mallinfo() stub for the mimalloc build: -sMALLOC=mimalloc doesn't export the
|
||||
|
|
@ -583,21 +527,22 @@ else
|
|||
PTHREAD_POOL_EXPR='navigator.hardwareConcurrency'
|
||||
fi
|
||||
|
||||
# Suspension-backend link surface. Asyncify: binaryen instrumentation +
|
||||
# DYNCALLS (+ the dynCall runtime export used by the post-link dyncall shims).
|
||||
# JSPI: native suspension — -sJSPI + the promising-export census
|
||||
# Suspension link surface. Browser apps: -sJSPI + the promising-export census
|
||||
# (scripts/common/jspi-exports.txt: the wx KEEPALIVE entries that can park +
|
||||
# pcbjam_libctx_entry; regenerate by grep, not memory), the jspi-scheduler
|
||||
# pre-js (successor of the post-link-injected asyncify-scheduler.js), and the
|
||||
# runtime methods its green-copy stack discipline needs (stackSave/
|
||||
# stackRestore/HEAPU8). -sDYNCALLS is a fatal link error under JSPI.
|
||||
if [ "${PCBJAM_ASYNC_BACKEND}" = "jspi" ]; then
|
||||
ASYNC_LINK_FLAGS="-sJSPI -sJSPI_EXPORTS=@${PROJECT_ROOT}/scripts/common/jspi-exports.txt --pre-js ${PROJECT_ROOT}/scripts/common/shims/jspi-scheduler.js"
|
||||
ASYNC_RUNTIME_METHODS="-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8','stackSave','stackRestore','HEAPU8','HEAP8','HEAP32']"
|
||||
else
|
||||
ASYNC_LINK_FLAGS="-sASYNCIFY=1 -sDYNCALLS=1 -sASYNCIFY_STACK_SIZE=65536"
|
||||
ASYNC_RUNTIME_METHODS="-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8','dynCall'] -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE=['\$dynCall']"
|
||||
fi
|
||||
# pre-js, and the runtime methods its spill-stack discipline needs
|
||||
# (stackSave/stackRestore/HEAPU8). Headless CLIs: nothing — their targets pin
|
||||
# -sASYNCIFY=0 and nothing in them may suspend.
|
||||
case "${APP_NAME}" in
|
||||
kicad_tools|occ_service)
|
||||
ASYNC_LINK_FLAGS=""
|
||||
ASYNC_RUNTIME_METHODS="-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8']"
|
||||
;;
|
||||
*)
|
||||
ASYNC_LINK_FLAGS="-sJSPI -sJSPI_EXPORTS=@${PROJECT_ROOT}/scripts/common/jspi-exports.txt --pre-js ${PROJECT_ROOT}/scripts/common/shims/jspi-scheduler.js"
|
||||
ASYNC_RUNTIME_METHODS="-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8','stackSave','stackRestore','HEAPU8','HEAP8','HEAP32']"
|
||||
;;
|
||||
esac
|
||||
|
||||
emcmake cmake "${KICAD_DIR}" \
|
||||
${CCACHE_OPTS} \
|
||||
|
|
|
|||
Loading…
Reference in a new issue