jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose

The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.

Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
  could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
  (WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
  WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.

Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
  workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
  of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
  scaffolding (existed to parallelize the deleted wasm-opt phase; the
  postprocess is a seconds-long node script and now runs inline),
  build-monitor's dead asyncify rows, sched-context orphan build
  output, dead .gitignore entries, the .jspi-assets spike dir (the two
  wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
  called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
  main_stack_runner.h + 5 includes, the always-null context-sleep weak
  hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
  noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
  WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
  (gerber-demo keeps it: it loads the deployed CDN release, which
  predates emscripten 6 — noted inline).

Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).

Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).

Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).

Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).

Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
This commit is contained in:
Viktor Vaczi 2026-08-14 09:25:32 +02:00
commit 9c475a804e
120 changed files with 1522 additions and 2974 deletions

View file

@ -1,110 +0,0 @@
> **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
pass (`scripts/common/apply-asyncify.sh`) is slow on the glibc Linux CI runner
because glibc `malloc` collapses into per-arena lock (`futex`) contention under
many threads. We preload **jemalloc** to fix it. This harness measures the effect
locally instead of paying for a full ~160-min Hetzner CI run per experiment.
**Key idea:** `wasm-opt`/asyncify is a standalone pass over an *already-compiled*
`.wasm` (`docker/build.sh:194`). So we build the eeschema `.wasm` **once** on the
Mac, copy it into a Linux VM, and replay only the optimizer across a matrix of
`{glibc, jemalloc} × core counts`. No KiCad compile happens in the VM.
## Caveats (read before trusting numbers)
- **This Mac is 10 cores / 32 GB.** The local core sweep tops out at ~10 threads.
The CI runner is 32 vCPU, so the *full* 32-thread contention is **not**
reproducible here — the local run shows the **direction** (jemalloc vs glibc,
and how wall-clock scales with cores), not CI's absolute worst case. The final
32-thread pick must still be confirmed on the real runner.
- **aarch64 ≠ CI's x86_64.** The VM is aarch64 (HVF, near-native speed). The
glibc arena-lock pathology is arch-independent, so the **ratios** transfer;
**absolute seconds do not** match the x86_64 AMD runner.
- asyncify peaks ~1015 GB RAM → the VM gets 20 GB; close other heavy apps.
## 1. Build the fixture (once, on the Mac)
```bash
mkdir -p bench
./docker/build.sh eeschema --build-deps # long: full cold build
# Pull the Docker-side raw (pre-finalize) wasm while the builder container is up:
docker compose -f docker/docker-compose.yml cp \
kicad-wasm-builder:/workspace/build-wasm/kicad-eeschema/eeschema/eeschema.wasm \
bench/eeschema.raw.wasm
# Finalize it to match what asyncify actually consumes in the pipeline:
./scripts/common/apply-finalize.sh bench/eeschema.raw.wasm bench/eeschema.finalized.wasm
```
Fallback if `compose cp` fails:
`docker compose -f docker/docker-compose.yml exec kicad-wasm-builder cat <path> > bench/eeschema.raw.wasm`
## 2. Provision and boot the VM (Mac)
```bash
brew install qemu # one-time
./scripts/bench/setup-vm.sh prepare
./scripts/bench/setup-vm.sh run # serial console; quit with Ctrl-a x
```
Wait ~12 min for cloud-init (installs git/curl/time/libjemalloc2/strace).
## 3. Load repo + fixture into the VM
```bash
# from the Mac, in another terminal:
scp -P 2222 -o StrictHostKeyChecking=no bench/eeschema.finalized.wasm bench@localhost:~/
./scripts/bench/setup-vm.sh ssh
# inside the VM:
git clone <this-repo-url> repo && cd repo
git checkout istvanmatejcsok/feat/ci-hetzner-allcores
mkdir -p bench && mv ~/eeschema.finalized.wasm bench/
# get-wasm-opt.sh auto-downloads Binaryen v121 aarch64-linux on first use
```
## 4. Run the benchmark (in the VM)
```bash
STRACE=1 ./scripts/bench/wasm-opt-bench.sh
```
Sweeps `CORES="1 4 8 10"` × `{glibc, jemalloc}`, writing `bench/results.csv`
(wall-clock + peak RSS per cell) and per-cell logs under `bench/results/`. With
`STRACE=1` it also records `futex` syscall share per allocator (expect ~99% on
glibc, far lower with jemalloc). Override the sweep with e.g. `CORES="8 10"`.
## Interpreting
- jemalloc rows should show **lower wall-clock** than glibc, widening as cores rise.
- glibc wall-clock that *stops improving* (or worsens) with more cores = the
arena-lock storm; jemalloc should keep scaling.
- These ratios justify the `LD_PRELOAD` fix; use them (plus one real-runner
confirmation) to choose CI's `BINARYEN_CORES`.
Artifacts (`bench/*.wasm`, `bench/results*`, `scripts/bench/vm/`) are gitignored.
## 5. Full Docker build in the VM (CI dry-run) — vm-build.sh
Verifies CI orchestration changes (docker/build.sh, compose limits, pipelining)
on Linux+Docker without burning a Hetzner slot. The guest is aarch64/HVF:
a *functional* CI proxy, not an x86 performance proxy.
```bash
# one-time: bigger disk + Docker-enabled cloud-init, then boot
VM_DISK=80G ./scripts/bench/setup-vm.sh prepare
./scripts/bench/setup-vm.sh run # leave running in its own terminal
# from the Mac: cold calculator build inside the guest (deps + docker image)
./scripts/bench/vm-build.sh # = calculator --build-deps
# pipeline smoke test (deps already in the guest volume from the previous run)
KICAD_PIPELINE=1 ./scripts/bench/vm-build.sh calculator,pl_editor
```
Prints the guest build wall time at the end; build logs stream through ssh.

View file

@ -1,2 +0,0 @@
instance-id: kicad-wasmopt-bench
local-hostname: kicad-bench

View file

@ -1,42 +0,0 @@
#cloud-config
# NoCloud seed for the wasm-opt benchmark VM. setup-vm.sh substitutes
# __SSH_PUBKEY__ with your ~/.ssh/id_ed25519.pub before building the seed ISO.
hostname: kicad-bench
users:
- name: bench
sudo: "ALL=(ALL) NOPASSWD:ALL"
shell: /bin/bash
# Key auth only: setup-vm.sh requires an SSH pubkey and substitutes it below,
# so no password login is needed. Lock the password and disable SSH password
# auth (below); recover via the substituted key, not a shared password.
lock_passwd: true
ssh_authorized_keys:
- __SSH_PUBKEY__
# Disable SSH password authentication — the seed carries a pubkey.
ssh_pwauth: false
package_update: true
# Docker CE from the official repo (mirrors the CI workflow's install step) so
# scripts/bench/vm-build.sh can run the full docker/build.sh pipeline in-guest.
apt:
sources:
docker.list:
source: "deb [signed-by=$KEY_FILE] https://download.docker.com/linux/ubuntu noble stable"
keyid: 9DC858229FC7DD38854AE2D88D81803C0EBFCD88
packages:
- git
- curl
- ca-certificates
- time
- libjemalloc2
- strace
- rsync
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
runcmd:
# The docker group only exists after the package installs, so the user's
# `groups:` stanza can't grant it — add membership here (applies to new
# ssh sessions, which is all vm-build.sh uses).
- usermod -aG docker bench

View file

@ -1,166 +0,0 @@
#!/bin/bash
# o2-config-sweep.sh — replay wasm-opt over a PREBUILT asyncified fixture under a
# matrix of {Binaryen version, optimization passes, thread count, allocator/THP}.
# RUNS ON THE LINUX CI BOX.
#
# WHY: the on-box diagnostic (docs/ci-build-slowness-findings.md) proved the
# ~80-min `-O2` cost is ~90% FUTEX LOCK CONTENTION inside wasm-opt (Binaryen's
# global type mutex), NOT the allocator and NOT memory management (madvise=0,
# THP compaction=0, identical under glibc and mimalloc). So the levers that can
# move wall-clock are: a NEWER Binaryen (the devs cut this contention after our
# pinned v121) and FEWER threads (less lock contention) — plus reducing the -O2
# work itself (lighter passes). Allocator/THP are dead ends, kept only as controls.
#
# CONFIG NAME GRAMMAR: <preset>[@<cores>]
# preset sets Binaryen version + passes + allocator/THP (see config_env).
# optional @<cores> overrides BINARYEN_CORES for that one cell (sweep threads).
# e.g. "v130-O2@8" = Binaryen 130, -O2, 8 threads.
#
# TWO MODES:
# CAP_SECONDS=0 (default): run to completion → true wall-clock + output size.
# CAP_SECONDS>0: WINDOWED sample — the lock storm is steady-state, so measure
# it over a [60s, CAP-30s] window and kill the pass. Reports, per cell:
# win_sysfrac = system CPU fraction in the window (lock contention; LOWER better)
# win_usercores = REAL-work cores in the window (progress rate; HIGHER better
# → predicts shorter wall for the same pass set)
# win_tlb = TLB-shootdown interrupts in the window
# ~CAP/60 min per cell, so the whole matrix fits one CI run.
#
# Usage: CONFIGS="baseline v130-O2 baseline@8" CAP_SECONDS=600 \
# ./scripts/bench/o2-config-sweep.sh <asyncified.wasm>
# Env: CONFIGS, CORES (default nproc), CAP_SECONDS (default 0), DIAGNOSTIC (1 =
# perf kernel-symbol sample, full mode). Output under /bench/o2-results/.
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "${SCRIPT_DIR}/../.." && pwd)"
FIXTURE="${1:?usage: o2-config-sweep.sh <asyncified-fixture.wasm>}"
CONFIGS="${CONFIGS:-baseline}"
CORES_DEFAULT="${CORES:-$(nproc)}"
CAP_SECONDS="${CAP_SECONDS:-0}"
DIAGNOSTIC="${DIAGNOSTIC:-0}"
[[ "$(uname -s)" == "Linux" ]] || { echo "ERROR: run on the Linux CI box." >&2; exit 1; }
[[ -f "${FIXTURE}" ]] || { echo "ERROR: fixture not found: ${FIXTURE}" >&2; exit 1; }
command -v /usr/bin/time >/dev/null || { echo "ERROR: apt-get install -y time" >&2; exit 1; }
ARCH="$(uname -m)"
WASM_OPT_121="$("${REPO}/scripts/common/get-wasm-opt.sh" 2>/dev/null)" # pinned v121
JEMALLOC="$(ls /usr/lib/${ARCH}-linux-gnu/libjemalloc.so.2 2>/dev/null | head -1 || true)"
MIMALLOC="$(ls /usr/lib/${ARCH}-linux-gnu/libmimalloc.so* 2>/dev/null | sort | tail -1 || true)"
OUTDIR="${REPO}/bench/o2-results"; mkdir -p "${OUTDIR}"
CSV="${OUTDIR}/results.csv"
echo "config,cores,ver,flags,thp,mode,wall,wall_s,user_s,sys_s,vol_ctxsw,win_sysfrac,win_usercores,win_tlb,madvise,out_bytes,preload" > "${CSV}"
sudo sysctl -w kernel.perf_event_paranoid=-1 >/dev/null 2>&1 || true
PERF=""; command -v perf >/dev/null 2>&1 && perf stat -e syscalls:sys_enter_madvise -- true >/dev/null 2>&1 && PERF="yes"
echo "perf syscall counting: ${PERF:-NO}"
CLK="$(getconf CLK_TCK 2>/dev/null || echo 100)"
# Resolve (download+cache) a standalone wasm-opt for a Binaryen version. 121 = the
# pinned one from get-wasm-opt.sh; anything else is fetched from GitHub releases.
get_wasm_opt_ver() {
local ver="$1"
if [[ -z "$ver" || "$ver" == "121" ]]; then echo "${WASM_OPT_121}"; return 0; fi
local dir="${REPO}/build-wasm/tools/binaryen-${ver}" bin="${REPO}/build-wasm/tools/binaryen-${ver}/bin/wasm-opt"
if [[ ! -x "$bin" ]]; then
local url="https://github.com/WebAssembly/binaryen/releases/download/version_${ver}/binaryen-version_${ver}-${ARCH}-linux.tar.gz"
mkdir -p "${REPO}/build-wasm/tools"
echo " downloading Binaryen v${ver}..." >&2
curl -fsSL -o "/tmp/binaryen-${ver}.tgz" "$url" || { echo " ERR: download v${ver} failed ($url)" >&2; return 1; }
tar -xzf "/tmp/binaryen-${ver}.tgz" -C "${REPO}/build-wasm/tools" || return 1
mv "${REPO}/build-wasm/tools/binaryen-version_${ver}" "$dir" 2>/dev/null || true
fi
[[ -x "$bin" ]] && echo "$bin" || return 1
}
snap_cpu() { awk '/^cpu /{u=$2;s=$4;t=0;for(i=2;i<=NF;i++)t+=$i;print u,s,t}' /proc/stat; }
snap_tlb() { awk '/TLB/{for(i=2;i<=NF;i++)if($i~/^[0-9]+$/)s+=$i}END{print s+0}' /proc/interrupts; }
snap_vm() { awk '/^compact_stall /{c=$2}/^thp_fault_alloc /{t=$2}END{print c+0,t+0}' /proc/vmstat; }
thp_state(){ grep -oP '\[\K[^]]+' /sys/kernel/mm/transparent_hugepage/enabled 2>/dev/null || echo "?"; }
set_thp() { [[ "$1" == "asis" ]] && return 0
echo "$1"|sudo tee /sys/kernel/mm/transparent_hugepage/enabled >/dev/null 2>&1 || true
echo "$1"|sudo tee /sys/kernel/mm/transparent_hugepage/defrag >/dev/null 2>&1 || true; }
# preset -> BIN_VER, OPTARGS (pass list), PRELOAD, THP_WANT, EXTRA env
BIN_VER=""; OPTARGS=(); PRELOAD=""; THP_WANT="asis"; EXTRA=()
LIGHT=(--flatten --simplify-locals --coalesce-locals --reorder-locals --vacuum)
config_env() {
BIN_VER="121"; OPTARGS=(-O2); PRELOAD=""; THP_WANT="asis"; EXTRA=()
case "$1" in
baseline) ;; # v121 -O2 (current CI = control)
v130-O2) BIN_VER="130" ;; # newer Binaryen, same passes — lock fixed?
v130-O1) BIN_VER="130"; OPTARGS=(-O1) ;;
v121-O1) OPTARGS=(-O1) ;; # lighter passes (less work)
v130-light) BIN_VER="130"; OPTARGS=("${LIGHT[@]}") ;;
v121-light) OPTARGS=("${LIGHT[@]}") ;;
mimalloc-retain) PRELOAD="${MIMALLOC}"; EXTRA=(MIMALLOC_PURGE_DELAY=-1 MIMALLOC_ALLOW_THP=0) ;; # control (proven dead)
thp-off) THP_WANT="never" ;; # control (proven dead)
*) echo "ERROR: unknown preset '$1'" >&2; return 1 ;;
esac
if [[ -n "${PRELOAD}" && ! -e "${PRELOAD}" ]]; then echo "WARN: preload for '$1' missing — SKIP" >&2; return 2; fi
}
field() { grep -F "$1" "$2" 2>/dev/null | tail -1 | sed 's/.*: //' | tr -d ' '; }
run_config() {
local cfg="$1" base cores
base="${cfg%@*}"; if [[ "$cfg" == *"@"* ]]; then cores="${cfg##*@}"; else cores="${CORES_DEFAULT}"; fi
if ! config_env "${base}"; then echo "${cfg},${cores},,,skip,skip,SKIPPED,,,,,,,,,," >> "${CSV}"; return 0; fi
local wopt; wopt="$(get_wasm_opt_ver "${BIN_VER}")" || {
echo " cannot resolve Binaryen v${BIN_VER} — SKIP"; echo "${cfg},${cores},${BIN_VER},,skip,skip,SKIP-NOBIN,,,,,,,,,," >> "${CSV}"; return 0; }
set_thp "${THP_WANT}"; local thp_now; thp_now="$(thp_state)"
local flags="${OPTARGS[*]}"
local timef="${OUTDIR}/${cfg//[@ ]/_}.time" statf="${OUTDIR}/${cfg//[@ ]/_}.perfstat" logf="${OUTDIR}/${cfg//[@ ]/_}.log"
local outw="/tmp/o2-out.wasm"
cp "${FIXTURE}" /tmp/o2-in.wasm
local -a runenv=(BINARYEN_CORES="${cores}" "${EXTRA[@]}"); [[ -n "${PRELOAD}" ]] && runenv+=("LD_PRELOAD=${PRELOAD}")
echo ""; echo "=== ${cfg} (binaryen=$("${wopt}" --version 2>&1 | grep -oE '[0-9]+' | head -1), flags='${flags}', cores=${cores}, THP=${thp_now}, preload=${PRELOAD:-none}, extra=${EXTRA[*]:-none}) ==="
local mode="full" wall wall_s user sys volcsw sysf usercores tlbd madv outsz
sysf=NA; usercores=NA; tlbd=NA; madv=NA
if [[ "${CAP_SECONDS}" -gt 0 ]]; then
mode="cap${CAP_SECONDS}"
( /usr/bin/time -v -o "${timef}" timeout "${CAP_SECONDS}" env "${runenv[@]}" "${wopt}" "${OPTARGS[@]}" /tmp/o2-in.wasm -o "${outw}" > "${logf}" 2>&1 ) &
local rp=$!
sleep 60
local cu0 cs0 ct0 tlb0; read -r cu0 cs0 ct0 <<<"$(snap_cpu)"; tlb0="$(snap_tlb)"
local win=$(( CAP_SECONDS>120 ? CAP_SECONDS-90 : 30 )); sleep "${win}"
local cu1 cs1 ct1 tlb1; read -r cu1 cs1 ct1 <<<"$(snap_cpu)"; tlb1="$(snap_tlb)"
wait "${rp}" 2>/dev/null || true
local dtot=$((ct1-ct0)) dsys=$((cs1-cs0)) duser=$((cu1-cu0))
sysf="$(awk -v s=${dsys} -v t=${dtot} 'BEGIN{print (t>0)?sprintf("%.2f",s/t):"NA"}')"
usercores="$(awk -v u=${duser} -v c=${CLK} -v w=${win} 'BEGIN{print (w>0)?sprintf("%.2f",(u/c)/w):"NA"}')"
tlbd=$((tlb1-tlb0)); wall="cap@${CAP_SECONDS}"; wall_s="${CAP_SECONDS}"
else
if [[ -n "${PERF}" ]]; then
/usr/bin/time -v -o "${timef}" perf stat -o "${statf}" -e syscalls:sys_enter_madvise,syscalls:sys_enter_munmap \
env "${runenv[@]}" "${wopt}" "${OPTARGS[@]}" /tmp/o2-in.wasm -o "${outw}" > "${logf}" 2>&1
madv="$(grep -E 'sys_enter_madvise' "${statf}" 2>/dev/null | awk '{gsub(/,/,"",$1);print $1}' | head -1)"
else
/usr/bin/time -v -o "${timef}" env "${runenv[@]}" "${wopt}" "${OPTARGS[@]}" /tmp/o2-in.wasm -o "${outw}" > "${logf}" 2>&1
fi
if [[ $? -ne 0 ]]; then echo " FAILED"; tail -4 "${logf}"; echo "${cfg},${cores},${BIN_VER},${flags// /+},${thp_now},full,FAILED,,,,,,,,,," >> "${CSV}"; return 0; fi
wall="$(field 'Elapsed (wall clock) time' "${timef}")"
wall_s="$(awk -F: '{if(NF==3)print $1*3600+$2*60+$3;else if(NF==2)print $1*60+$2;else print $1}' <<<"${wall}")"
fi
user="$(field 'User time (seconds)' "${timef}")"; sys="$(field 'System time (seconds)' "${timef}")"
volcsw="$(field 'Voluntary context switches' "${timef}")"; outsz="$(stat -c %s "${outw}" 2>/dev/null || echo NA)"
printf '%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n' \
"${cfg}" "${cores}" "${BIN_VER}" "${flags// /+}" "${thp_now}" "${mode}" "${wall}" "${wall_s}" \
"${user}" "${sys}" "${volcsw}" "${sysf}" "${usercores}" "${tlbd}" "${madv}" "${outsz}" "${PRELOAD:-none}" >> "${CSV}"
echo " mode=${mode} wall=${wall} user=${user}s sys=${sys}s vol_ctxsw=${volcsw} out=${outsz}B"
echo " WINDOW sys_frac=${sysf} (lock contention) user_cores=${usercores} (real-work rate) tlb=${tlbd} madvise=${madv}"
rm -f "${outw}" /tmp/o2-in.wasm
}
echo "Fixture: ${FIXTURE} ($(stat -c %s "${FIXTURE}" 2>/dev/null) bytes)"
echo "Configs: ${CONFIGS} CORES_DEFAULT=${CORES_DEFAULT} CAP_SECONDS=${CAP_SECONDS} THP(init)=$(thp_state)"
for cfg in ${CONFIGS}; do run_config "${cfg}"; done
echo ""; echo "=== results (${CSV}) ==="; column -t -s, "${CSV}" || cat "${CSV}"

View file

@ -1,92 +0,0 @@
#!/bin/bash
# Provision a local QEMU aarch64 + HVF Ubuntu 24.04 VM for the wasm-opt benchmark.
# RUNS ON THE macOS HOST (Apple Silicon). See scripts/bench/README.md.
#
# Subcommands:
# prepare download cloud image, build NoCloud seed ISO, copy UEFI vars
# run boot the VM (foreground, serial console; ssh on :2222)
# ssh ssh into the running VM
# (none) = prepare (if needed) then run
#
# Env overrides: VM_CORES (default 10), VM_MEM (default 20G), SSH_PORT (2222),
# VM_DISK (default +30G grow; use VM_DISK=80G for full Docker builds via
# scripts/bench/vm-build.sh — emsdk image + deps + build tree need ~50 GB).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VMDIR="${SCRIPT_DIR}/vm"
CLOUD_INIT="${SCRIPT_DIR}/cloud-init"
VM_CORES="${VM_CORES:-10}" # Mac has 10 cores; this caps the local sweep
VM_MEM="${VM_MEM:-20G}" # asyncify peaks ~10-15 GB; leave headroom for macOS
VM_DISK="${VM_DISK:-30G}" # disk grow beyond the ~3.5 GB cloud image
SSH_PORT="${SSH_PORT:-2222}"
IMG_URL="https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img"
QEMU_SHARE="$(brew --prefix qemu)/share/qemu"
FW_CODE="${QEMU_SHARE}/edk2-aarch64-code.fd"
FW_VARS_SRC="${QEMU_SHARE}/edk2-arm-vars.fd"
BASE_IMG="${VMDIR}/ubuntu-24.04-arm64.img"
DISK="${VMDIR}/disk.qcow2"
SEED="${VMDIR}/seed.iso"
FW_VARS="${VMDIR}/edk2-vars.fd"
PUBKEY="${HOME}/.ssh/id_ed25519.pub"
prepare() {
mkdir -p "${VMDIR}"
command -v qemu-system-aarch64 >/dev/null || { echo "Install qemu: brew install qemu" >&2; exit 1; }
[[ -f "${PUBKEY}" ]] || { echo "No SSH pubkey at ${PUBKEY}" >&2; exit 1; }
if [[ ! -f "${BASE_IMG}" ]]; then
echo "Downloading Ubuntu 24.04 arm64 cloud image..."
curl -fL -o "${BASE_IMG}" "${IMG_URL}"
fi
# Fresh working disk each prepare: copy the cloud image and grow it (the
# cloud image is ~3.5 GB; binaryen + fixture + scratch need more headroom).
echo "Creating working disk (${DISK}, +${VM_DISK})..."
cp "${BASE_IMG}" "${DISK}"
qemu-img resize "${DISK}" "+${VM_DISK}"
# Writable UEFI vars store (copy of the template).
cp "${FW_VARS_SRC}" "${FW_VARS}"
# Build the NoCloud seed ISO (label must be CIDATA), injecting the pubkey.
echo "Building cloud-init seed ISO..."
local tmp; tmp="$(mktemp -d)"
sed "s|__SSH_PUBKEY__|$(cat "${PUBKEY}")|" "${CLOUD_INIT}/user-data" > "${tmp}/user-data"
cp "${CLOUD_INIT}/meta-data" "${tmp}/meta-data"
rm -f "${SEED}"
hdiutil makehybrid -iso -joliet -default-volume-name CIDATA -o "${SEED}" "${tmp}" >/dev/null
rm -rf "${tmp}"
echo "Prepared. Boot with: $0 run"
}
run() {
[[ -f "${DISK}" && -f "${SEED}" && -f "${FW_VARS}" ]] || { echo "Run '$0 prepare' first." >&2; exit 1; }
echo "Booting VM: ${VM_CORES} vCPU, ${VM_MEM} RAM, ssh -> localhost:${SSH_PORT} (user: bench)"
echo "First boot runs cloud-init (installs packages); wait ~1-2 min before ssh."
echo "Quit the serial console with: Ctrl-a x"
exec qemu-system-aarch64 \
-machine virt -accel hvf -cpu host \
-smp "${VM_CORES}" -m "${VM_MEM}" \
-drive "if=pflash,format=raw,readonly=on,file=${FW_CODE}" \
-drive "if=pflash,format=raw,file=${FW_VARS}" \
-drive "if=virtio,format=qcow2,file=${DISK}" \
-drive "if=virtio,format=raw,file=${SEED}" \
-netdev "user,id=n0,hostfwd=tcp::${SSH_PORT}-:22" \
-device virtio-net,netdev=n0 \
-nographic
}
do_ssh() { exec ssh -p "${SSH_PORT}" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null bench@localhost "$@"; }
case "${1:-}" in
prepare) prepare ;;
run) run ;;
ssh) shift; do_ssh "$@" ;;
"") [[ -f "${DISK}" && -f "${SEED}" ]] || prepare; run ;;
*) echo "Usage: $0 {prepare|run|ssh}" >&2; exit 1 ;;
esac

View file

@ -1,33 +0,0 @@
# Parameters for the wasm-opt config sweep (.github/workflows/wasm-opt-bench.yml).
# Edit, commit, push to a bench/** branch to launch a run.
#
# Config grammar: <preset>[@<cores>] (see scripts/bench/o2-config-sweep.sh)
# presets: baseline(v121 -O2) v130-O2 v130-O1 v121-O1 v130-light v121-light
# mimalloc-retain thp-off (last two are proven-dead controls)
# @<cores> overrides BINARYEN_CORES for that cell (thread sweep).
# CAP_SECONDS_CONF: >0 = windowed storm sample (~CAP/60 min/cell, whole matrix in
# one run); 0 = run each to completion (true wall-clock). FIXTURE_RUN_ID_CONF:
# reuse a prior run's cached fixture (blank = build it ~40 min).
#
# VERDICT from run #1 (27197360957): the -O2 cost is ~90% FUTEX LOCK CONTENTION
# in Binaryen (type mutex), NOT allocator/THP (madvise=0, compaction=0, identical
# under glibc & mimalloc; mimalloc-retain saved only 8%). So allocator/THP are
# dead; the levers are NEWER Binaryen (devs fixed this after v121) and FEWER
# threads, plus lighter passes. Run #2 triages those windowed (fast):
# win_sysfrac = lock contention (LOWER better)
# win_usercores = real-work rate (HIGHER better → shorter wall for same passes)
# ---- Run #2: windowed triage (10 min/cell) on the cached run-#1 fixture ----
# baseline = v121 -O2 @32c (control: expect sys_frac~0.88, user_cores~2.6)
# v130-O2 = newer Binaryen @32c (does the version fix the lock?)
# baseline@8 = v121 -O2 @8c (do fewer threads cut contention?)
# v130-O2@8 = newer + fewer threads
# v121-O1 = lighter passes @32c (does less work progress faster?)
CONFIGS_CONF="baseline v130-O2 baseline@8 v130-O2@8 v121-O1"
CORES_CONF=""
FIXTURE_RUN_ID_CONF="27197360957"
DIAGNOSTIC_CONF="0"
CAP_SECONDS_CONF="600"
# ---- Run #3 (planned): full run (CAP_SECONDS_CONF="0") of the winner(s) for true
# wall-clock + output validity, then Chrome e2e. e.g. CONFIGS="v130-O2 v130-O2@8"

View file

@ -1,100 +0,0 @@
#!/bin/bash
# Run a real docker/build.sh build INSIDE the local QEMU Linux VM, as a cheap
# stand-in for the Hetzner CI runner (Linux + Docker + the same scripts), so CI
# orchestration changes can be verified without burning a Hetzner slot.
# RUNS ON THE macOS HOST. The guest is aarch64 under HVF — near-native speed,
# a correct *functional* proxy for CI but NOT an x86 performance proxy.
#
# Usage:
# ./scripts/bench/vm-build.sh [build.sh args...]
# default args: calculator --build-deps
#
# Examples:
# ./scripts/bench/vm-build.sh # cold calculator build
# KICAD_PIPELINE=1 ./scripts/bench/vm-build.sh calculator,pl_editor # pipeline smoke test
#
# Prereqs (one-time):
# VM_DISK=80G ./scripts/bench/setup-vm.sh prepare # 80G: emsdk image + deps + build tree
# ./scripts/bench/setup-vm.sh run # boot in another terminal, wait for cloud-init
#
# Env: SSH_PORT (2222), KICAD_PIPELINE/KICAD_PIPELINE_JOBS (forwarded to the guest),
# VM_BUILD_JOBS (compile -j inside the guest; default = guest nproc).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
SSH_PORT="${SSH_PORT:-2222}"
GUEST="bench@localhost"
GUEST_DIR="kicad-wasm"
SSH_OPTS=(-p "${SSH_PORT}" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null)
vssh() { ssh "${SSH_OPTS[@]}" "${GUEST}" "$@"; }
if ! vssh true 2>/dev/null; then
echo "ERROR: VM not reachable on localhost:${SSH_PORT}." >&2
echo "Boot it first: ./scripts/bench/setup-vm.sh run (wait ~2 min for cloud-init)" >&2
exit 1
fi
# Wait until cloud-init finished installing Docker (first boot takes a few min).
echo "Waiting for cloud-init to finish (installs Docker on first boot)..."
vssh "cloud-init status --wait >/dev/null 2>&1 || true"
vssh "docker info >/dev/null 2>&1" || {
echo "ERROR: Docker not usable in the guest. Re-run setup-vm.sh prepare with the" >&2
echo "updated cloud-init (installs docker-ce) and boot a fresh VM." >&2
exit 1
}
# Sync the working tree (incl. submodule content, no .git) into the guest.
# Same exclude set as build.sh's container-sync, plus the VM images themselves.
echo "Syncing repo to ${GUEST}:~/${GUEST_DIR} ..."
rsync -az --delete \
-e "ssh ${SSH_OPTS[*]}" \
--exclude="build-wasm" \
--exclude="output" \
--exclude=".git" \
--exclude="logs" \
--exclude=".idea" \
--exclude="node_modules" \
--exclude="tools/emsdk" \
--exclude="scripts/bench/vm" \
--exclude="tests/test-results" \
--exclude="tests/playwright-report" \
"${PROJECT_ROOT}/" "${GUEST}:${GUEST_DIR}/"
BUILD_ARGS=("$@")
[ ${#BUILD_ARGS[@]} -eq 0 ] && BUILD_ARGS=(calculator --build-deps)
# Append a guest-sized -j unless the caller already passed one.
JOBS_FLAG='-j "${JOBS}"'
[[ " ${BUILD_ARGS[*]} " == *" -j "* ]] && JOBS_FLAG=""
# Guest-side env:
# - COMPOSE_PROJECT_NAME: no .git in the guest tree, so build.sh can't derive a
# branch name — pin the project name explicitly.
# - KICAD_DOCKER_CPUS/MEM: the compose dev-Mac caps (10 CPU / 32G) point at a
# guest that may have fewer cores and definitely has less RAM; size to guest.
# - KICAD_NO_MONITOR/KICAD_LOG_NESTED: no TTY dashboard over ssh, stream output.
REMOTE_CMD=$(cat <<EOF
set -e
cd ${GUEST_DIR}
GUEST_CORES=\$(nproc)
GUEST_MEM_G=\$(awk '/MemTotal/{printf "%d", \$2/1024/1024 - 3}' /proc/meminfo)
export COMPOSE_PROJECT_NAME=kicad-wasm-vm
export KICAD_DOCKER_CPUS=\${GUEST_CORES}
export KICAD_DOCKER_MEM=\${GUEST_MEM_G}G
export KICAD_NO_MONITOR=1 KICAD_LOG_NESTED=1
export KICAD_PIPELINE=${KICAD_PIPELINE:-0} KICAD_PIPELINE_JOBS=${KICAD_PIPELINE_JOBS:-2}
JOBS=${VM_BUILD_JOBS:-\$GUEST_CORES}
echo "=== VM build: \$(uname -m), \${GUEST_CORES} cores, docker mem \${KICAD_DOCKER_MEM}, -j \${JOBS}, args: ${BUILD_ARGS[*]} ==="
time ./docker/build.sh ${BUILD_ARGS[*]} ${JOBS_FLAG}
ls -lh output/
EOF
)
START=$(date +%s)
vssh "${REMOTE_CMD}"
END=$(date +%s)
echo ""
echo "=== VM build wall time: $(( (END - START) / 60 ))m $(( (END - START) % 60 ))s (args: ${BUILD_ARGS[*]}) ==="

View file

@ -91,6 +91,7 @@ BEGIN {
# Canonical per-app stage rows, in order. Sub-stages (wxwidgets-configure /
# -compile) fold into the wxwidgets row via parent[].
ROWN = 0
ord[++ROWN] = "container-sync"; lab["container-sync"] = "Sync source to container"
ord[++ROWN] = "deps"; lab["deps"] = "Dependencies"
ord[++ROWN] = "wxwidgets"; lab["wxwidgets"] = "wxWidgets"
ord[++ROWN] = "kicad-stubs"; lab["kicad-stubs"] = "Stub libraries"
@ -98,10 +99,7 @@ BEGIN {
ord[++ROWN] = "kicad-compile"; lab["kicad-compile"] = "KiCad compile"
ord[++ROWN] = "kicad-bitmaps"; lab["kicad-bitmaps"] = "Bitmap resources"
ord[++ROWN] = "copy-output"; lab["copy-output"] = "Copy output"
ord[++ROWN] = "binaryen"; lab["binaryen"] = "Build Binaryen"
ord[++ROWN] = "dyncall-shims"; lab["dyncall-shims"] = "dynCall shims"
ord[++ROWN] = "finalize"; lab["finalize"] = "Finalize WASM"
ord[++ROWN] = "asyncify"; lab["asyncify"] = "Asyncify"
ord[++ROWN] = "env-shim"; lab["env-shim"] = "ENV merge shim"
for (i = 1; i <= ROWN; i++) ridx[ord[i]] = i
parent["wxwidgets-configure"] = "wxwidgets"
parent["wxwidgets-compile"] = "wxwidgets"
@ -152,12 +150,11 @@ curRawKey == "kicad-compile" {
if ($0 ~ /Configuring KiCad with CMake/) h_phase = "kicad-configure"
if ($0 ~ /\(CMake target:/) h_phase = "kicad-compile"
if ($0 ~ /Building bitmap resources/) h_phase = "kicad-bitmaps"
if ($0 ~ /Applying asyncify transformation/) h_phase = "asyncify"
if (h_phase == "kicad-compile" && match($0, /[0-9]+%\]/)) {
p = substr($0, RSTART, RLENGTH); gsub(/[^0-9]/, "", p); h_pct = p + 0
}
if (h_phase == "kicad-compile" && $0 ~ /Building (C|CXX) object/) h_cc++
if ($0 ~ /Build complete\. Output files/ || $0 ~ /Asyncify complete/) h_done = 1
if ($0 ~ /Build complete\. Output files/) h_done = 1
}
END {
if (markerCount > 0) { render_markers(); }
@ -173,8 +170,6 @@ function render_markers( state, totalEl, pk, curIdx, i, k, st, det, subdet, en
printf "H|-|0|0|%d|%s\n", totalEl, state
if (curRawKey == "container-sync") {
printf "R|active|Sync source to container|%s\n", fmt(termTs - rowStartTs["container-sync"])
} else if (curRawKey == "binaryen") {
printf "R|active|Build Binaryen|%s\n", fmt(termTs - rowStartTs["binaryen"])
} else {
printf "N|waiting for build to start...\n"
}
@ -234,13 +229,12 @@ function render_heuristic( state, order, ph, i, k, st, names) {
}
printf "N|(no progress markers in this log - inferred from log text)\n"
# Reduced ordered phase set we can detect heuristically.
split("wxwidgets kicad-configure kicad-compile kicad-bitmaps asyncify", order, " ")
split("wxwidgets kicad-configure kicad-compile kicad-bitmaps", order, " ")
names["wxwidgets"]="wxWidgets"; names["kicad-configure"]="KiCad configure (CMake)"
names["kicad-compile"]="KiCad compile"; names["kicad-bitmaps"]="Bitmap resources"
names["asyncify"]="Asyncify"
ph = 0
for (i = 1; i <= 5; i++) if (order[i] == h_phase) ph = i
for (i = 1; i <= 5; i++) {
for (i = 1; i <= 4; i++) if (order[i] == h_phase) ph = i
for (i = 1; i <= 4; i++) {
k = order[i]
if (h_done) st = "done"
else if (i < ph) st = "done"

View file

@ -189,8 +189,8 @@ if [ $NEEDS_CONFIGURE -eq 1 ]; then
fi
# Exception model: native WebAssembly exceptions (legacy binary encoding) + wasm setjmp/longjmp,
# single-sourced from scripts/common/env.sh. The catch-arm-hoisting pass (run post-link, see
# build-wasm-test.sh) lets Asyncify suspend from inside C++ catch blocks. See docs/features/wasm-exceptions/.
# single-sourced from scripts/common/env.sh. -sWASM_LEGACY_EXCEPTIONS=1 pins the EH binary
# encoding (exnref is not adopted across our pinned emsdk/browsers). See docs/features/wasm-exceptions/.
WX_EH_FLAGS="$DEPS_EH_FLAGS"
echo "wx EH model flags: ${WX_EH_FLAGS}"

View file

@ -41,8 +41,8 @@ export EMSDK_QUIET=1
# WebAssembly exceptions (legacy binary encoding) are the only build mode. -sSUPPORT_LONGJMP=wasm is
# required because the deps that use setjmp/longjmp (freetype, cairo, OpenCASCADE) must use wasm
# setjmp — emscripten's JS-longjmp implementation cannot coexist with -fwasm-exceptions (it would
# leave emscripten_longjmp undefined). -sWASM_LEGACY_EXCEPTIONS=1 selects the EH binary encoding our
# post-link Asyncify + catch-arm-hoisting pass can consume (Asyncify can't handle exnref).
# leave emscripten_longjmp undefined). -sWASM_LEGACY_EXCEPTIONS=1 pins the legacy EH binary encoding:
# the exnref encoding is not adopted across our pinned emsdk toolchain and target browsers.
export DEPS_EH_FLAGS="-fwasm-exceptions -sSUPPORT_LONGJMP=wasm -sWASM_LEGACY_EXCEPTIONS=1"
# Emscripten SDK setup
@ -123,23 +123,6 @@ elif [ -z "$JOBS" ]; then
export JOBS=1
fi
# Common linker flags for WASM
export WASM_LDFLAGS="\
-sALLOW_MEMORY_GROWTH=1 \
-sINITIAL_MEMORY=256MB \
-sSTACK_SIZE=5MB \
-sASYNCIFY=1 \
-sASYNCIFY_STACK_SIZE=16384 \
-sLEGACY_GL_EMULATION \
-sMAX_WEBGL_VERSION=2"
# Threading flags (when enabled)
export PTHREAD_LDFLAGS="\
-pthread \
-sPROXY_TO_PTHREAD=1 \
-sPTHREAD_POOL_SIZE=navigator.hardwareConcurrency \
-sOFFSCREENCANVAS_SUPPORT=1"
# Create output directories
mkdir -p "$BUILD_ROOT" "$DEPS_ROOT" "$SYSROOT"/{lib,include,share} "$STAMPS_DIR"

View file

@ -1,141 +0,0 @@
#!/bin/bash
# Get path to Binaryen wasm-opt
#
# Usage: ./scripts/common/get-wasm-opt.sh
# Output: Prints path to wasm-opt executable
#
# Prefers the emsdk-bundled Binaryen (tools/emsdk/upstream/bin/) so that the
# wasm-opt and wasm-emscripten-finalize versions match the Emscripten that
# generated the JS glue. A version mismatch between the compiler's Binaryen
# (e.g. v121+72 dev) and a standalone release (v121) corrupts asyncify
# metadata, causing "func is not a function" errors at runtime.
#
# Falls back to downloading standalone Binaryen v130 if emsdk is not installed
# locally (e.g. CI environments that only use Docker).
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
# --- Prefer emsdk-bundled Binaryen (matches the Emscripten that compiled the WASM) ---
# Override: set BINARYEN_VERSION in the env to FORCE a specific standalone release
# (skips the emsdk preference). Used to A/B Binaryen versions — e.g. v121 carries a
# wasm::Type lock-contention bug that makes the host-side -O2 pass ~9x slower than
# v130 (see docs/ci-build-slowness-findings.md). Validate any bump with the e2e
# suite: a Binaryen/emsdk skew can corrupt asyncify metadata ("func is not a function").
EMSDK_WASM_OPT="${PROJECT_ROOT}/tools/emsdk/upstream/bin/wasm-opt"
# build-kicad-target.sh replaces emsdk's wasm-opt with a no-op stub (moving the
# real binary to wasm-opt.real) so emcc skips the in-link asyncify. That's meant
# for the container emsdk, but a host-mode run leaves the HOST emsdk stubbed —
# and the stub fakes --version and exits 0, so the host-side asyncify/-O2
# "succeed" while doing NOTHING (broken wasm: "asyncify_stop_unwind is not a
# function"). Always prefer the preserved real binary when it exists.
if [ -x "${EMSDK_WASM_OPT}.real" ]; then
EMSDK_WASM_OPT="${EMSDK_WASM_OPT}.real"
fi
if [ -z "${BINARYEN_VERSION:-}" ] && [ -x "${EMSDK_WASM_OPT}" ]; then
EMSDK_VERSION=$("${EMSDK_WASM_OPT}" --version 2>&1 || true)
echo "Using emsdk-bundled Binaryen: ${EMSDK_VERSION}" >&2
echo "${EMSDK_WASM_OPT}"
exit 0
fi
# --- Fallback: download standalone Binaryen (for CI or environments without local emsdk) ---
echo "emsdk Binaryen not found at ${EMSDK_WASM_OPT}, falling back to standalone download..." >&2
# Default v130: v121 has a wasm::Type lock convoy that makes -O2 ~9x slower on
# many-core Linux (docs/ci-build-slowness-findings.md). v130 output validated by
# the full e2e suite locally (31/31) and Chromium-green on CI run 27226030304.
BINARYEN_VERSION="${BINARYEN_VERSION:-130}"
# BINARYEN_BUILD_FROM_SOURCE=1: compile wasm-opt ourselves instead of using the
# official Linux release tarballs, which are badly built — measured on the
# calculator fixture, identical output sha256: asyncify 4x faster on x86
# (CI run 27276830256: 3:50 -> 0:58) and 13x on aarch64; -O2 equal. The macOS
# tarball is well-built (self-build is ~12% SLOWER there), so this is only
# worth enabling on Linux CI. Needs cmake, ninja, g++, git. ~5 min on 32 cores,
# cached in build-wasm/tools after the first call.
if [[ "${BINARYEN_BUILD_FROM_SOURCE:-0}" == "1" ]]; then
SELF_DIR="${PROJECT_ROOT}/build-wasm/tools/binaryen-${BINARYEN_VERSION}-selfbuilt"
SELF_WASM_OPT="${SELF_DIR}/bin/wasm-opt"
if [ ! -x "${SELF_WASM_OPT}" ]; then
SRC_DIR="${PROJECT_ROOT}/build-wasm/tools/binaryen-src-${BINARYEN_VERSION}"
BUILD_DIR="${PROJECT_ROOT}/build-wasm/tools/binaryen-build-${BINARYEN_VERSION}"
echo "Building Binaryen v${BINARYEN_VERSION} from source (one-time, ~5 min)..." >&2
if [ ! -d "${SRC_DIR}" ]; then
git clone -q --depth 1 --branch "version_${BINARYEN_VERSION}" \
--recurse-submodules --shallow-submodules \
https://github.com/WebAssembly/binaryen.git "${SRC_DIR}" >&2
fi
cmake -S "${SRC_DIR}" -B "${BUILD_DIR}" -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS="-Wno-maybe-uninitialized" \
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON -DBUILD_TESTS=OFF >&2
ninja -C "${BUILD_DIR}" wasm-opt wasm-emscripten-finalize >&2
# wasm-opt links lib/libbinaryen.so via rpath $ORIGIN/../lib.
mkdir -p "${SELF_DIR}/bin" "${SELF_DIR}/lib"
cp "${BUILD_DIR}/bin/wasm-opt" "${BUILD_DIR}/bin/wasm-emscripten-finalize" "${SELF_DIR}/bin/"
cp "${BUILD_DIR}"/lib/libbinaryen.* "${SELF_DIR}/lib/" 2>/dev/null || true
echo "Self-built Binaryen installed to ${SELF_DIR}" >&2
fi
SELF_VERSION=$("${SELF_WASM_OPT}" --version 2>&1 | grep -o '[0-9]\+' | head -1)
if [ "${SELF_VERSION}" != "${BINARYEN_VERSION}" ]; then
echo "ERROR: self-built wasm-opt version mismatch (got ${SELF_VERSION}, expected ${BINARYEN_VERSION})" >&2
exit 1
fi
echo "Using self-built Binaryen v${BINARYEN_VERSION} (${SELF_WASM_OPT})" >&2
echo "${SELF_WASM_OPT}"
exit 0
fi
BINARYEN_DIR="${PROJECT_ROOT}/build-wasm/tools/binaryen-${BINARYEN_VERSION}"
WASM_OPT="${BINARYEN_DIR}/bin/wasm-opt"
download_binaryen() {
# Detect platform
local os=$(uname -s)
local arch=$(uname -m)
local platform=""
case "${os}-${arch}" in
Darwin-arm64) platform="arm64-macos" ;;
Darwin-x86_64) platform="x86_64-macos" ;;
Linux-aarch64) platform="aarch64-linux" ;;
Linux-x86_64) platform="x86_64-linux" ;;
*)
echo "ERROR: Unsupported platform: ${os}-${arch}" >&2
exit 1
;;
esac
local url="https://github.com/WebAssembly/binaryen/releases/download/version_${BINARYEN_VERSION}/binaryen-version_${BINARYEN_VERSION}-${platform}.tar.gz"
local tarball="${PROJECT_ROOT}/build-wasm/tools/binaryen-${BINARYEN_VERSION}.tar.gz"
echo "Downloading Binaryen v${BINARYEN_VERSION} for ${platform}..." >&2
mkdir -p "${PROJECT_ROOT}/build-wasm/tools"
curl -L -o "${tarball}" "${url}"
echo "Extracting..." >&2
tar -xzf "${tarball}" -C "${PROJECT_ROOT}/build-wasm/tools"
mv "${PROJECT_ROOT}/build-wasm/tools/binaryen-version_${BINARYEN_VERSION}" "${BINARYEN_DIR}"
rm "${tarball}"
echo "Binaryen v${BINARYEN_VERSION} installed to ${BINARYEN_DIR}" >&2
}
# Download Binaryen if not cached
if [ ! -x "${WASM_OPT}" ]; then
download_binaryen
fi
# Verify version
INSTALLED_VERSION=$("${WASM_OPT}" --version 2>&1 | grep -o '[0-9]\+' | head -1)
if [ "${INSTALLED_VERSION}" != "${BINARYEN_VERSION}" ]; then
echo "WARNING: wasm-opt version mismatch (got ${INSTALLED_VERSION}, expected ${BINARYEN_VERSION})" >&2
echo "Re-downloading..." >&2
rm -rf "${BINARYEN_DIR}"
download_binaryen
fi
# Output path to wasm-opt (this is the only stdout output)
echo "${WASM_OPT}"

View file

@ -1,3 +1,15 @@
# Promising-export census for the KiCad browser apps: every wasm entry export
# whose body can reach a suspension (park) under JSPI. Consumed as
# -sJSPI_EXPORTS=@thisfile by scripts/kicad/build-kicad-target.sh; emscripten's
# symbol-list parser strips lines starting with '#'. Do NOT add blank lines
# (they would parse as empty symbol entries).
# Regenerate by grep, not memory: the wx entries are the EMSCRIPTEN_KEEPALIVE
# exports in wxwidgets/src/wasm/ that can park; pcbjam_libctx_entry is
# libcontext's coroutine entry (kicad/thirdparty/libcontext).
# Kept in sync BY HAND with two siblings:
# - scripts/common/shims/jspi-scheduler.js installExportWraps list (same set
# minus main — the runtime calls main before the wraps exist)
# - tests/apps/Makefile.wasm WX_JSPI_EXPORTS (comma-joined copy)
main
wx_dom_event
wx_dom_mouse

View file

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

View file

@ -1,9 +1,9 @@
// jspi-scheduler.js — the JSPI-era successor of asyncify-scheduler.js.
// jspi-scheduler.js — the wx scheduler shim for the JSPI runtime.
//
// Ships as a --pre-js. Keeps the S4 token-wait registry contract byte-for-byte
// Ships as a --pre-js. Provides the S4 token-wait registry
// (beginWait/waitPromise/resolveWait/resolveTopWait/waitEarlyResolved/
// takeWaitResult/pendingWaits/noteContextWait/shutdown) so every C++ bridge
// and web caller keeps working, and adds the two things JSPI needs:
// takeWaitResult/pendingWaits/shutdown) that every C++ bridge and web
// caller relies on, plus the two things JSPI needs:
//
// 1. ACTIVATION TRACKING. Every promising export the app declares is wrapped
// so the shim always knows which activation is executing synchronously
@ -20,30 +20,21 @@
// into parked frames' locals (stack-allocated wxDialog members mutated
// by a cross-tick EndModal), resurrecting dead state at resume.
//
// Observability: an event ring + live activation table via __wxWaitDump()
// (alias __wxAsyncifyDump kept one release for crash-report consumers).
//
// Asyncify-era machinery that has NO successor here, by design: currData
// single-writer tripwire, deferred-wake queue, stale-fiber quarantine,
// trampoline heal, dyncall shims — the states they policed are
// unrepresentable under JSPI.
// Observability: an event ring + live activation table via __wxWaitDump().
(function () {
"use strict";
if (globalThis.__wxScheduler && globalThis.__wxScheduler.backend === "jspi") {
if (globalThis.__wxSchedulerInstalled) {
return; // idempotent under double injection
}
var RING_CAP = 256;
var S = {
backend: "jspi",
// --- mailbox lane (timers/wheel; ordering machinery, mechanism-free) ----
// Same contract as the asyncify shim: enqueueAfter queues a C callback,
// delivery happens through the dedicated _wxWasmMailboxTick export from a
// fresh task, in order. Under JSPI the tick is a promising export — a
// enqueueAfter queues a C callback; delivery happens through the dedicated
// _wxWasmMailboxTick export from a fresh task, in order. The tick is a
// suspension inside a delivered handler parks the tick's own activation,
// and the rejection path carries the same containment (a throwing handler
// must not leave a parked quasi-modal unresolved).
@ -94,17 +85,12 @@
}, 0);
},
// --- S1 embind lane (contract-identical port from asyncify-scheduler) --
// --- S1 embind lane --
// Mutators (doc 18 classification) must not enter wasm while a load is in
// flight: the open activation is suspended mid-load and a collab-apply /
// save / theme flip entering between its parks would mutate the board
// under it. Semantic exclusion — nothing asyncify-specific about it.
// The FIFO drains, in order, once kicadOpenFileBusy clears.
//
// Retired here, by design: _wrapOpenFile / kicadOpenFileStart (the
// asyncify Phase F starter route). Under JSPI kicadOpenFile is an embind
// async() export — its own promising activation parks legally and the
// call returns a real Promise; no dispatch-context detour exists.
// under it. The exclusion is semantic, independent of the suspension
// mechanism. The FIFO drains, in order, once kicadOpenFileBusy clears.
MUTATOR_NAMES: [
"kicadSetChrome", "kicadSetReadOnly",
"kicadCollabApply", "kicadCollabApplyItems",
@ -181,16 +167,11 @@
},
// The embind PARKERs (kicadOpenFile / kicadOpenFiles / kicadLibsReload,
// registered emscripten::async() under PCBJAM_JSPI): wrap them with the
// registered emscripten::async()): wrap them with the
// same activation tracking as the raw promising exports, so their parks
// (wxWasmYieldUntil inside the load) find a tracked record and get the
// green-copy spill-stack discipline. Embind names live on Module WITHOUT
// green-region spill-stack discipline. Embind names live on Module WITHOUT
// the underscore prefix, hence the separate installer.
// NOT here: the kicadTestFiberPark* levers. They are emscripten::async()
// (a plain embind call into a suspending body throws on strict-JSPI
// Firefox), but the parker wrap's turnstile queueing would DEFER a
// mid-park poke until the park drains — the exact race the levers exist
// to stage. Their suspensions ride the untracked-anon-record path.
PARKER_NAMES: ["kicadOpenFile", "kicadOpenFiles", "kicadLibsReload"],
_wrapParkers: function () {
var wrapped = 0;
@ -252,12 +233,6 @@
return entry.result | 0;
},
// JSPI: context waits do not exist; kept as a loud no-op for transition
// callers (nothing registers them — wxWasmYieldUntil suspends in place).
noteContextWait: function (token) {
console.warn("[wx-scheduler] noteContextWait(" + token + ") under jspi backend");
},
resolveWait: function (token, result) {
var entry = this.waits.get(token);
if (!entry || entry.resolved) return false;
@ -294,7 +269,7 @@
dead: false,
shutdown: function (why) {
this.dead = true;
// S6 teardown contract (same as the asyncify shim): queued-but-
// S6 teardown contract: queued-but-
// undelivered mutators FAIL LOUDLY instead of hanging their callers,
// and undelivered mailbox messages drop — the pumps stop themselves on
// the dead flag.
@ -307,8 +282,8 @@
if (stranded) {
console.warn("[wx-scheduler] shutdown (" + why + ") stranded:" + stranded);
} else {
// teardown-gate contract (e2e/app-quit.spec.ts, ported from the
// asyncify shim): a clean exit must SAY so on the console
// teardown-gate contract (e2e/app-quit.spec.ts): a clean exit must
// SAY so on the console
console.log("[wx-scheduler] shutdown (" + why + ") clean");
}
this._note("shutdown", why, stranded);
@ -751,7 +726,7 @@
return wrapped;
},
// --- observability skeleton (finalized in Phase 7) ---------------------
// --- observability ------------------------------------------------------
_ring: [],
_note: function (ev, a, b) {
this._ring.push([Date.now(), ev, String(a), b | 0]);
@ -768,7 +743,6 @@
});
});
return {
backend: "jspi",
dead: this.dead,
waitsBegun: this.waitsBegun,
waitsResolved: this.waitsResolved,
@ -787,7 +761,7 @@
globalThis.__wxScheduler = S;
globalThis.__wxSchedulerInstalled = true; // wxWasmSchedulerAssertInstalled probe
// --- Phase 7 signals ------------------------------------------------------
// --- diagnostic signals ---------------------------------------------------
// SuspendError attributor: a SuspendError means a PLAIN (non-promising)
// wasm entry tried to park — a missed -sJSPI_EXPORTS/installExportWraps
// entry. The engine cannot say WHICH export, but the live dump (what was
@ -825,8 +799,6 @@
});
}, 10000);
globalThis.__wxWaitDump = function () { return S.dump(); };
// transition alias: crash-report consumers read __wxAsyncifyDump
globalThis.__wxAsyncifyDump = globalThis.__wxWaitDump;
// Self-install the activation wraps once the runtime is up (this file ships
// as a --pre-js, so Module exists here). The name set mirrors the

View file

@ -53,6 +53,8 @@ const INPUTS = [
// the promising-export census.
{ file: "scripts/common/shims/jspi-scheduler.js" },
{ file: "scripts/common/jspi-exports.txt" },
// Single-sources DEPS_EH_FLAGS (the exception-model link flags).
{ file: "scripts/common/env.sh" },
// Per-tool compile recipes (compile flags / emcc link options).
{ dir: "scripts/kicad", match: /^build-.*\.sh$/ },

View file

@ -1,10 +1,9 @@
#!/bin/bash
# Sourced library. Single source of truth for the 5-repo layout:
# Sourced library. Single source of truth for the 4-repo layout:
#
# root = pcbjam main
# ├── kicad (kicad/) wasm-port
# ├── wxwidgets (wxwidgets/) wasm-port
# ├── binaryen (binaryen/) wasm-port
# └── pcbjam-shared (web/pcbjam-shared/) main [MIT contract]
#
# Bash variable names can't contain '-', so pcbjam-shared's KEY is
@ -13,18 +12,16 @@
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
REPOS=(root kicad wxwidgets binaryen pcbjam_shared)
REPOS=(root kicad wxwidgets pcbjam_shared)
PATH_root="$ROOT_DIR"
PATH_kicad="$ROOT_DIR/kicad"
PATH_wxwidgets="$ROOT_DIR/wxwidgets"
PATH_binaryen="$ROOT_DIR/binaryen"
PATH_pcbjam_shared="$ROOT_DIR/web/pcbjam-shared"
MAIN_root="main"
MAIN_kicad="wasm-port"
MAIN_wxwidgets="wasm-port"
MAIN_binaryen="wasm-port"
MAIN_pcbjam_shared="main"
repo_path() {

View file

@ -296,11 +296,10 @@ else
log_info "Building KiCad in RELEASE mode (skipping wasm-opt due to memory limits)"
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.
# Suspension backend: JSPI (native stack switching). The headless CLIs
# (kicad_tools, occ_service) get no suspension backend — they run in
# node/workers where nothing may suspend. coroutine.h/libcontext key on
# __EMSCRIPTEN__ directly, so no ABI define is threaded through the TU flags.
# Step 6: Create build directory
mkdir -p "${KICAD_BUILD}"
@ -365,23 +364,11 @@ fi
log_info "Stub libraries built"
# Step 6.2/6.3: wasm-opt + wasm-emscripten-finalize handling.
# For the editor apps these tools OOM on the huge debug wasm, so we stub them in
# the container and run them on the host (docker/build.sh phase 2). The small,
# debug-stripped (-g0) CLI finalizes fine in-container, so for kicad_tools
# we restore/keep the real tools and skip host post-processing entirely.
# EMSDK is needed below (emscripten sysroot paths in the CMake invocation).
if [ -z "${EMSDK}" ]; then
log_error "EMSDK environment variable is not set."
exit 1
fi
EMSDK_WASM_OPT="${EMSDK}/upstream/bin/wasm-opt"
EMSDK_FINALIZE="${EMSDK}/upstream/bin/wasm-emscripten-finalize"
# 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
@ -531,16 +518,16 @@ fi
# (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, 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.
# (stackSave/stackRestore/HEAPU8). Headless CLIs: nothing — no suspension
# backend; 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']"
JSPI_LINK_FLAGS=""
JSPI_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']"
JSPI_LINK_FLAGS="-sJSPI -sJSPI_EXPORTS=@${PROJECT_ROOT}/scripts/common/jspi-exports.txt --pre-js ${PROJECT_ROOT}/scripts/common/shims/jspi-scheduler.js"
JSPI_RUNTIME_METHODS="-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','UTF8ToString','stringToUTF8','lengthBytesUTF8','stackSave','stackRestore','HEAPU8','HEAP8','HEAP32']"
;;
esac
@ -556,7 +543,7 @@ emcmake cmake "${KICAD_DIR}" \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DCMAKE_CXX_FLAGS="${EXTRA_FLAGS} -Xclang -fno-pch-timestamp -pthread --use-port=zlib -DKICAD_USE_PLATFORM_WASM=1${DIAG_DEFINES} -I${SYSROOT}/include -I${STUBS_DIR} -include ${STUBS_DIR}/char_traits_uint16_workaround.h" \
-DCMAKE_C_FLAGS="${EXTRA_FLAGS} -pthread --use-port=zlib -I${SYSROOT}/include -I${STUBS_DIR}" \
-DCMAKE_EXE_LINKER_FLAGS="${LINKER_DEBUG_FLAGS} -pthread ${ASYNC_LINK_FLAGS} -sUSE_PTHREADS=1 -sMALLOC=mimalloc -sPTHREAD_POOL_SIZE='${PTHREAD_POOL_EXPR}' -sPTHREAD_POOL_SIZE_STRICT=0 -sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=256MB -sMAXIMUM_MEMORY=4GB -sMAX_WEBGL_VERSION=2 ${GL3D_LINK_FLAGS} ${NANOSLEEP_YIELD_LINK} ${MALLINFO_STUB_LINK} ${ASYNC_RUNTIME_METHODS} ${EMBIND_LINK_FLAG} -L${SYSROOT}/lib -L${KICAD_BUILD}/common -L${KICAD_BUILD}/common/gal ${STUBS_BUILD}/libgit2_stub.a ${STUBS_BUILD}/libcurl_stub.a${APP_STUB_LINK} ${STUBS_BUILD}/libnng_stub.a ${EMBIND_OBJ}" \
-DCMAKE_EXE_LINKER_FLAGS="${LINKER_DEBUG_FLAGS} -pthread ${JSPI_LINK_FLAGS} -sUSE_PTHREADS=1 -sMALLOC=mimalloc -sPTHREAD_POOL_SIZE='${PTHREAD_POOL_EXPR}' -sPTHREAD_POOL_SIZE_STRICT=0 -sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=256MB -sMAXIMUM_MEMORY=4GB -sMAX_WEBGL_VERSION=2 ${GL3D_LINK_FLAGS} ${NANOSLEEP_YIELD_LINK} ${MALLINFO_STUB_LINK} ${JSPI_RUNTIME_METHODS} ${EMBIND_LINK_FLAG} -L${SYSROOT}/lib -L${KICAD_BUILD}/common -L${KICAD_BUILD}/common/gal ${STUBS_BUILD}/libgit2_stub.a ${STUBS_BUILD}/libcurl_stub.a${APP_STUB_LINK} ${STUBS_BUILD}/libnng_stub.a ${EMBIND_OBJ}" \
-DCMAKE_SHARED_LINKER_FLAGS="-Wl,--allow-multiple-definition" \
-DCMAKE_MODULE_LINKER_FLAGS="-Wl,--allow-multiple-definition" \
-DZLIB_LIBRARY="${EMSDK}/upstream/emscripten/cache/sysroot/lib/wasm32-emscripten/pic/libz.a" \