2026-06-01 17:31:03 +02:00
|
|
|
#!/bin/bash
|
|
|
|
|
# Live terminal dashboard for KiCad WASM builds.
|
|
|
|
|
#
|
|
|
|
|
# Builds redirect all output to logs/<script>/<timestamp>.log (see
|
|
|
|
|
# scripts/common/logging.sh). This monitor tails the newest log and renders a
|
|
|
|
|
# single, self-refreshing screen of named stages + progress, so you never have to
|
|
|
|
|
# open the log to see "where are we, X of N".
|
|
|
|
|
#
|
|
|
|
|
# It reads the @KW@ progress markers emitted by scripts/common/stages.sh, and the
|
|
|
|
|
# CMake "[ N%]" lines for within-compile progress. If a log has no @KW@ markers
|
|
|
|
|
# (old logs, or a build started before this tooling existed), it falls back to
|
|
|
|
|
# inferring stages from the existing "=== ... ===" / "[INFO] ..." log lines.
|
|
|
|
|
#
|
|
|
|
|
# Usage:
|
|
|
|
|
# ./scripts/build-monitor.sh # follow newest logs/build/*.log
|
|
|
|
|
# ./scripts/build-monitor.sh --dir build-calculator # watch a different subdir
|
|
|
|
|
# ./scripts/build-monitor.sh --once [logfile] # one-shot snapshot (non-TTY ok)
|
|
|
|
|
# ./scripts/build-monitor.sh <logfile> # follow a specific log file
|
|
|
|
|
#
|
|
|
|
|
# --managed: don't self-exit on the done/failed marker; run until killed. Used when
|
|
|
|
|
# a parent process (logging.sh's auto-launch) owns the monitor's lifecycle.
|
|
|
|
|
|
|
|
|
|
set -u
|
|
|
|
|
|
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
|
|
|
LOGS_ROOT="${PROJECT_ROOT}/logs"
|
|
|
|
|
|
|
|
|
|
ONCE=0
|
|
|
|
|
MANAGED=0
|
|
|
|
|
DIR="build"
|
|
|
|
|
LOGFILE=""
|
|
|
|
|
|
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
|
|
|
case "$1" in
|
|
|
|
|
--once) ONCE=1; shift ;;
|
|
|
|
|
--managed) MANAGED=1; shift ;;
|
|
|
|
|
--dir) DIR="$2"; shift 2 ;;
|
|
|
|
|
--dir=*) DIR="${1#--dir=}"; shift ;;
|
|
|
|
|
-h|--help)
|
|
|
|
|
grep '^#' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//; 1d'
|
|
|
|
|
exit 0 ;;
|
|
|
|
|
*) LOGFILE="$1"; shift ;;
|
|
|
|
|
esac
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
# Newest *.log under logs/<dir>, or empty string if none yet.
|
|
|
|
|
find_newest_log() {
|
|
|
|
|
ls -t "${LOGS_ROOT}/${DIR}"/*.log 2>/dev/null | head -1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Colors + cursor/erase escapes (only when stdout is a terminal). Captured once so
|
|
|
|
|
# the redraw doesn't fork tput per line. C_HOME/C_EL/C_ED drive the flicker-free
|
|
|
|
|
# in-place repaint (move home + erase each line + erase to end of screen) instead of
|
|
|
|
|
# blanking the screen with `clear`, which would flash.
|
|
|
|
|
if [[ -t 1 ]]; then
|
|
|
|
|
C_RESET=$(tput sgr0); C_BOLD=$(tput bold); C_DIM=$(tput dim)
|
|
|
|
|
C_GREEN=$(tput setaf 2); C_CYAN=$(tput setaf 6); C_RED=$(tput setaf 1)
|
|
|
|
|
C_YELLOW=$(tput setaf 3)
|
|
|
|
|
C_HOME=$(tput cup 0 0); C_EL=$(tput el); C_ED=$(tput ed)
|
|
|
|
|
else
|
|
|
|
|
C_RESET=""; C_BOLD=""; C_DIM=""; C_GREEN=""; C_CYAN=""; C_RED=""; C_YELLOW=""
|
|
|
|
|
C_HOME=""; C_EL=""; C_ED=""
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
SPINNER='|/-\'
|
|
|
|
|
|
|
|
|
|
# All parsing + state computation lives in awk: portable across macOS bash 3.2 /
|
|
|
|
|
# BSD awk, and avoids associative-array bashisms. It reads the log and emits simple
|
|
|
|
|
# pipe-delimited records that the bash render() function formats.
|
|
|
|
|
#
|
|
|
|
|
# Records:
|
|
|
|
|
# H|<app>|<index>|<total>|<elapsedSec>|<state> state = running|done|failed
|
|
|
|
|
# R|<status>|<label>|<detail> status = done|active|pending|skipped|failed
|
|
|
|
|
# B|<pct> active-compile bar pct, or -1
|
|
|
|
|
# N|<note> optional note line
|
|
|
|
|
read -r -d '' AWK_PROG <<'AWK' || true
|
|
|
|
|
function fmt(sec, m,s) {
|
|
|
|
|
if (sec < 0) sec = 0
|
|
|
|
|
m = int(sec / 60); s = sec % 60
|
|
|
|
|
if (m > 0) return m "m" s "s"
|
|
|
|
|
return s "s"
|
|
|
|
|
}
|
|
|
|
|
# Start ts of the next stage row that actually ran after index i (else terminal ts).
|
|
|
|
|
function next_seen_start(i, j) {
|
|
|
|
|
for (j = i + 1; j <= ROWN; j++)
|
|
|
|
|
if (ord[j] in rowSeen) return rowStartTs[ord[j]]
|
|
|
|
|
return termTs
|
|
|
|
|
}
|
|
|
|
|
BEGIN {
|
|
|
|
|
# Canonical per-app stage rows, in order. Sub-stages (wxwidgets-configure /
|
|
|
|
|
# -compile) fold into the wxwidgets row via parent[].
|
|
|
|
|
ROWN = 0
|
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
2026-08-14 09:25:32 +02:00
|
|
|
ord[++ROWN] = "container-sync"; lab["container-sync"] = "Sync source to container"
|
2026-06-01 17:31:03 +02:00
|
|
|
ord[++ROWN] = "deps"; lab["deps"] = "Dependencies"
|
|
|
|
|
ord[++ROWN] = "wxwidgets"; lab["wxwidgets"] = "wxWidgets"
|
|
|
|
|
ord[++ROWN] = "kicad-stubs"; lab["kicad-stubs"] = "Stub libraries"
|
|
|
|
|
ord[++ROWN] = "kicad-configure"; lab["kicad-configure"] = "KiCad configure (CMake)"
|
|
|
|
|
ord[++ROWN] = "kicad-compile"; lab["kicad-compile"] = "KiCad compile"
|
|
|
|
|
ord[++ROWN] = "kicad-bitmaps"; lab["kicad-bitmaps"] = "Bitmap resources"
|
build+perf: wasm-opt the shipped wasm, and measure real frames in CI
emcc only runs Binaryen at link -O2+ (link.py: should_run_binaryen_optimizer
returns OPT_LEVEL >= 2) and we link at -O1, so the shipped module had never seen
wasm-opt at all — it kept its entire 19.56 MB name section, ~20% of the editor
(-sJSPI sets ASYNCIFY=2, which suppresses wasm-ld's --strip-debug, leaving
wasm-opt as the only thing that would drop it). Step 8.2 runs it post-link and
in-container, so CI's cached compile phase covers it and the host post-process
stays pure-host.
Default -O2, picked by measuring every level on the same module: -O0 already
captures 27% of the raw win (it is mostly the name section), -O2 costs 23 s and
gives the best frame rate, and -O3/-O4/-Os/-Oz cost 48-132 s for at most 1.5%
more brotli — -O4 is not even smaller than -O3. Targets that already link -O2/-Oz
(occ_service, kicad_tools) are skipped by testing for the target_features
section, which emcc strips whenever it ran the optimizer itself, so there is no
hard-coded target list to drift. Feature flags come from the module's own
target_features section and so cannot diverge from the link.
The perf specs reported requestAnimationFrame ticks as "FPS". That is not a frame
rate: rAF fires on the compositor's schedule whether or not the GAL redrew, and
it read 120/s on a board where the renderer completed zero frames in six seconds.
measureInteractionFps now counts completed GAL frames — runs of draws to the
default framebuffer, exactly one per frame in every AA mode — and drives a pure
middle-drag pan after a zoom-to-fit. Mixing wheel zoom into the drive made the
result depend on where the wheel left the view: +-20% across identical repeats,
against +-2% for pan alone. The report gains a GAL fps column with a regression
flag on the 1x number; rAF is kept so historical runs stay comparable.
CI has no GPU, so its number is a software-rasteriser redraw rate — a regression
signal, not a user-facing frame rate. Method and measurements in the bench report.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-22 12:53:00 +02:00
|
|
|
ord[++ROWN] = "wasm-opt"; lab["wasm-opt"] = "wasm-opt (Binaryen)"
|
2026-06-01 17:31:03 +02:00
|
|
|
ord[++ROWN] = "copy-output"; lab["copy-output"] = "Copy output"
|
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
2026-08-14 09:25:32 +02:00
|
|
|
ord[++ROWN] = "env-shim"; lab["env-shim"] = "ENV merge shim"
|
2026-06-01 17:31:03 +02:00
|
|
|
for (i = 1; i <= ROWN; i++) ridx[ord[i]] = i
|
|
|
|
|
parent["wxwidgets-configure"] = "wxwidgets"
|
|
|
|
|
parent["wxwidgets-compile"] = "wxwidgets"
|
|
|
|
|
|
|
|
|
|
markerCount = 0; firstTs = 0
|
|
|
|
|
appName = ""; appIndex = 0; appTotal = 0
|
|
|
|
|
curRawKey = ""; doneFlag = 0; failFlag = 0; failCode = 0
|
|
|
|
|
lastpct = -1; cc = 0
|
|
|
|
|
# heuristic-mode flags
|
|
|
|
|
h_app = ""; h_phase = ""; h_pct = -1; h_cc = 0; h_done = 0
|
|
|
|
|
}
|
|
|
|
|
$1 == "@KW@" {
|
|
|
|
|
markerCount++
|
|
|
|
|
ts = $2 + 0
|
|
|
|
|
if (firstTs == 0) firstTs = ts
|
|
|
|
|
kind = $3
|
|
|
|
|
if (kind == "app") {
|
|
|
|
|
appName = $4; appIndex = $5 + 0; appTotal = $6 + 0
|
|
|
|
|
# New app segment: reset per-segment stage tracking.
|
|
|
|
|
delete rowSeen; delete rowStartTs
|
|
|
|
|
curRawKey = ""; lastpct = -1; cc = 0
|
|
|
|
|
} else if (kind == "stage") {
|
|
|
|
|
key = $4
|
|
|
|
|
pk = (key in parent) ? parent[key] : key
|
|
|
|
|
if (!(pk in rowSeen)) { rowSeen[pk] = 1; rowStartTs[pk] = ts }
|
|
|
|
|
curRawKey = key; curTs = ts
|
|
|
|
|
if (key == "kicad-compile") { lastpct = -1; cc = 0 }
|
|
|
|
|
} else if (kind == "done") {
|
|
|
|
|
doneFlag = 1; termRaw = ts
|
|
|
|
|
} else if (kind == "failed") {
|
|
|
|
|
failFlag = 1; failCode = $4 + 0; termRaw = ts
|
|
|
|
|
}
|
|
|
|
|
next
|
|
|
|
|
}
|
|
|
|
|
# Within an active KiCad compile, track CMake percent and compiled-object count.
|
|
|
|
|
curRawKey == "kicad-compile" {
|
|
|
|
|
if (match($0, /[0-9]+%\]/)) {
|
|
|
|
|
p = substr($0, RSTART, RLENGTH); gsub(/[^0-9]/, "", p); lastpct = p + 0
|
|
|
|
|
}
|
|
|
|
|
if ($0 ~ /Building (C|CXX) object/) cc++
|
|
|
|
|
}
|
|
|
|
|
# ---- Heuristic fallback signal collection (only used if no @KW@ markers) ----
|
|
|
|
|
{
|
|
|
|
|
if ($0 ~ /=== Building (pcbnew|eeschema|calculator)/) {
|
|
|
|
|
s = $0; sub(/.*=== Building /, "", s); sub(/ .*/, "", s); h_app = s
|
|
|
|
|
}
|
|
|
|
|
if ($0 ~ /=== Building wxWidgets/) h_phase = "wxwidgets"
|
|
|
|
|
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 (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++
|
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
2026-08-14 09:25:32 +02:00
|
|
|
if ($0 ~ /Build complete\. Output files/) h_done = 1
|
2026-06-01 17:31:03 +02:00
|
|
|
}
|
|
|
|
|
END {
|
|
|
|
|
if (markerCount > 0) { render_markers(); }
|
|
|
|
|
else { render_heuristic(); }
|
|
|
|
|
}
|
|
|
|
|
function render_markers( state, totalEl, pk, curIdx, i, k, st, det, subdet, endts) {
|
|
|
|
|
state = doneFlag ? "done" : (failFlag ? "failed" : "running")
|
|
|
|
|
termTs = (doneFlag || failFlag) ? termRaw : NOW
|
|
|
|
|
totalEl = (firstTs > 0) ? termTs - firstTs : 0
|
|
|
|
|
|
|
|
|
|
# Pre-app phase (container sync runs before the first app marker).
|
|
|
|
|
if (appName == "") {
|
|
|
|
|
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 {
|
|
|
|
|
printf "N|waiting for build to start...\n"
|
|
|
|
|
}
|
|
|
|
|
printf "B|-1\n"
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
printf "H|%s|%d|%d|%d|%s\n", appName, appIndex, appTotal, totalEl, state
|
|
|
|
|
pk = (curRawKey in parent) ? parent[curRawKey] : curRawKey
|
|
|
|
|
curIdx = (pk in ridx) ? ridx[pk] : 0
|
|
|
|
|
|
|
|
|
|
for (i = 1; i <= ROWN; i++) {
|
|
|
|
|
k = ord[i]
|
|
|
|
|
det = ""
|
|
|
|
|
if (doneFlag) {
|
|
|
|
|
st = (k in rowSeen) ? "done" : "skipped"
|
|
|
|
|
} else if (failFlag) {
|
|
|
|
|
if (i == curIdx) st = "failed"
|
|
|
|
|
else if (i < curIdx) st = (k in rowSeen) ? "done" : "skipped"
|
|
|
|
|
else st = "pending"
|
|
|
|
|
} else {
|
|
|
|
|
if (i < curIdx) st = (k in rowSeen) ? "done" : "skipped"
|
|
|
|
|
else if (i == curIdx) st = "active"
|
|
|
|
|
else st = "pending"
|
|
|
|
|
}
|
|
|
|
|
if (st == "done") {
|
|
|
|
|
endts = next_seen_start(i)
|
|
|
|
|
det = fmt(endts - rowStartTs[k])
|
|
|
|
|
} else if (st == "active") {
|
|
|
|
|
if (k == "kicad-compile") {
|
|
|
|
|
if (lastpct >= 0) det = sprintf("[%3d%%] %d files", lastpct, cc)
|
|
|
|
|
else det = sprintf("%d files", cc)
|
|
|
|
|
} else {
|
|
|
|
|
subdet = ""
|
|
|
|
|
if (k == "wxwidgets") {
|
|
|
|
|
if (curRawKey == "wxwidgets-configure") subdet = "configure "
|
|
|
|
|
else if (curRawKey == "wxwidgets-compile") subdet = "compile "
|
|
|
|
|
}
|
|
|
|
|
det = subdet fmt(termTs - rowStartTs[k])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
printf "R|%s|%s|%s\n", st, lab[k], det
|
|
|
|
|
}
|
|
|
|
|
if (curIdx > 0 && ord[curIdx] == "kicad-compile" && !doneFlag && !failFlag && lastpct >= 0)
|
|
|
|
|
printf "B|%d\n", lastpct
|
|
|
|
|
else
|
|
|
|
|
printf "B|-1\n"
|
|
|
|
|
}
|
|
|
|
|
function render_heuristic( state, order, ph, i, k, st, names) {
|
|
|
|
|
state = h_done ? "done" : "running"
|
|
|
|
|
printf "H|%s|0|0|-1|%s\n", (h_app == "" ? "-" : h_app), state
|
|
|
|
|
# Freshly-created/empty log at startup: nothing to infer yet.
|
|
|
|
|
if (h_app == "" && h_phase == "" && !h_done) {
|
|
|
|
|
printf "N|starting build...\n"
|
|
|
|
|
printf "B|-1\n"
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
printf "N|(no progress markers in this log - inferred from log text)\n"
|
|
|
|
|
# Reduced ordered phase set we can detect heuristically.
|
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
2026-08-14 09:25:32 +02:00
|
|
|
split("wxwidgets kicad-configure kicad-compile kicad-bitmaps", order, " ")
|
2026-06-01 17:31:03 +02:00
|
|
|
names["wxwidgets"]="wxWidgets"; names["kicad-configure"]="KiCad configure (CMake)"
|
|
|
|
|
names["kicad-compile"]="KiCad compile"; names["kicad-bitmaps"]="Bitmap resources"
|
|
|
|
|
ph = 0
|
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
2026-08-14 09:25:32 +02:00
|
|
|
for (i = 1; i <= 4; i++) if (order[i] == h_phase) ph = i
|
|
|
|
|
for (i = 1; i <= 4; i++) {
|
2026-06-01 17:31:03 +02:00
|
|
|
k = order[i]
|
|
|
|
|
if (h_done) st = "done"
|
|
|
|
|
else if (i < ph) st = "done"
|
|
|
|
|
else if (i == ph) st = "active"
|
|
|
|
|
else st = "pending"
|
|
|
|
|
det = ""
|
|
|
|
|
if (st == "active" && k == "kicad-compile")
|
|
|
|
|
det = (h_pct >= 0) ? sprintf("[%3d%%] %d files", h_pct, h_cc) : sprintf("%d files", h_cc)
|
|
|
|
|
printf "R|%s|%s|%s\n", st, names[k], det
|
|
|
|
|
}
|
|
|
|
|
printf "B|%s\n", (h_phase == "kicad-compile" && h_pct >= 0) ? h_pct : "-1"
|
|
|
|
|
}
|
|
|
|
|
AWK
|
|
|
|
|
|
|
|
|
|
# Draw a width-w progress bar for percent p.
|
|
|
|
|
bar() {
|
|
|
|
|
local p="$1" w=24 filled i out=""
|
|
|
|
|
filled=$(( p * w / 100 ))
|
|
|
|
|
for ((i = 0; i < w; i++)); do
|
|
|
|
|
if (( i < filled )); then out+="#"; else out+="."; fi
|
|
|
|
|
done
|
|
|
|
|
printf '%s' "$out"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Render one snapshot from awk output ($1) to the screen.
|
|
|
|
|
render() {
|
|
|
|
|
local data="$1" tick="$2"
|
|
|
|
|
local spin="${SPINNER:$((tick % 4)):1}"
|
|
|
|
|
local app idx total elapsed state note="" barpct=-1
|
|
|
|
|
local -a rows=()
|
|
|
|
|
|
|
|
|
|
while IFS='|' read -r kind a b c d e; do
|
|
|
|
|
case "$kind" in
|
|
|
|
|
H) app="$a"; idx="$b"; total="$c"; elapsed="$d"; state="$e" ;;
|
|
|
|
|
R) rows+=("$a|$b|$c") ;;
|
|
|
|
|
B) barpct="$a" ;;
|
|
|
|
|
N) note="$a" ;;
|
|
|
|
|
esac
|
|
|
|
|
done <<< "$data"
|
|
|
|
|
|
|
|
|
|
local title="KiCad WASM build"
|
|
|
|
|
[[ "$app" != "-" && -n "$app" ]] && title+=" ${C_BOLD}${app}${C_RESET}"
|
|
|
|
|
[[ "${total:-0}" -gt 0 ]] && title+=" (${idx}/${total})"
|
|
|
|
|
case "$state" in
|
|
|
|
|
done) title+=" ${C_GREEN}[done]${C_RESET}" ;;
|
|
|
|
|
failed) title+=" ${C_RED}[FAILED]${C_RESET}" ;;
|
|
|
|
|
*) title+=" ${C_CYAN}[building ${spin}]${C_RESET}" ;;
|
|
|
|
|
esac
|
|
|
|
|
|
|
|
|
|
printf '%s== %s ==%s\n' "$C_BOLD" "$title" "$C_RESET"
|
|
|
|
|
printf '%s\n' "------------------------------------------------------------"
|
|
|
|
|
[[ -n "$note" ]] && printf ' %s%s%s\n' "$C_DIM" "$note" "$C_RESET"
|
|
|
|
|
|
|
|
|
|
local r st label detail glyph color
|
|
|
|
|
for r in ${rows[@]+"${rows[@]}"}; do
|
|
|
|
|
IFS='|' read -r st label detail <<< "$r"
|
|
|
|
|
case "$st" in
|
|
|
|
|
done) glyph="[x]"; color="$C_GREEN" ;;
|
|
|
|
|
active) glyph="[${spin}]"; color="$C_CYAN" ;;
|
|
|
|
|
pending) glyph="[ ]"; color="$C_DIM" ;;
|
|
|
|
|
skipped) glyph="[-]"; color="$C_DIM" ;;
|
|
|
|
|
failed) glyph="[!]"; color="$C_RED" ;;
|
|
|
|
|
*) glyph="[ ]"; color="$C_RESET" ;;
|
|
|
|
|
esac
|
|
|
|
|
printf ' %s%s %-24s%s %s\n' "$color" "$glyph" "$label" "$C_RESET" "$detail"
|
|
|
|
|
if [[ "$st" == "active" && "$barpct" -ge 0 ]]; then
|
|
|
|
|
printf ' %s %s%%\n' "$(bar "$barpct")" "$barpct"
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
printf '%s\n' "------------------------------------------------------------"
|
|
|
|
|
local elapsed_str="--"
|
|
|
|
|
[[ "${elapsed:--1}" -ge 0 ]] 2>/dev/null && elapsed_str="$(fmt_sec "$elapsed")"
|
|
|
|
|
printf ' elapsed %s · log %s\n' "$elapsed_str" "$(basename "${CUR_LOG:-?}")"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Seconds -> "Xm YYs" (bash side, for the footer).
|
|
|
|
|
fmt_sec() {
|
|
|
|
|
local s="$1" m
|
|
|
|
|
(( s < 0 )) && s=0
|
|
|
|
|
m=$(( s / 60 )); s=$(( s % 60 ))
|
|
|
|
|
if (( m > 0 )); then printf '%dm%02ds' "$m" "$s"; else printf '%ds' "$s"; fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
snapshot() {
|
|
|
|
|
local now data
|
|
|
|
|
now=$(date +%s)
|
|
|
|
|
data=$(awk -v NOW="$now" "$AWK_PROG" "$CUR_LOG")
|
|
|
|
|
render "$data" "${1:-0}"
|
|
|
|
|
# Surface the build state to the caller via global.
|
|
|
|
|
LAST_STATE=$(printf '%s\n' "$data" | awk -F'|' '$1=="H"{print $6}')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Write a pre-built frame in place, without clearing the screen first (no flash).
|
|
|
|
|
# Home the cursor, erase each line to its end (handles a shorter new line), then
|
|
|
|
|
# erase to end of screen (handles a shorter new frame, e.g. the bar/note lines
|
|
|
|
|
# disappearing). On a non-TTY C_HOME/C_EL/C_ED are empty, so this is plain printing.
|
|
|
|
|
paint() {
|
|
|
|
|
local frame="$1" line
|
|
|
|
|
printf '%s' "$C_HOME"
|
|
|
|
|
while IFS= read -r line; do
|
|
|
|
|
printf '%s%s\n' "$line" "$C_EL"
|
|
|
|
|
done <<< "$frame"
|
|
|
|
|
printf '%s' "$C_ED"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# --- one-shot mode -----------------------------------------------------------
|
|
|
|
|
if [[ "$ONCE" -eq 1 ]]; then
|
|
|
|
|
CUR_LOG="${LOGFILE:-$(find_newest_log)}"
|
|
|
|
|
if [[ -z "$CUR_LOG" || ! -f "$CUR_LOG" ]]; then
|
|
|
|
|
echo "No log found in ${LOGS_ROOT}/${DIR}/ (and none given)." >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
snapshot 0
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# --- live follow mode --------------------------------------------------------
|
|
|
|
|
tick=0
|
|
|
|
|
trap 'tput cnorm 2>/dev/null; printf "%s" "$C_RESET"; echo; exit 0' INT TERM
|
|
|
|
|
[[ -t 1 ]] && tput civis 2>/dev/null
|
|
|
|
|
# Clear once up front; every later frame overwrites in place via paint() so the
|
|
|
|
|
# screen is never blanked between refreshes (no flash).
|
|
|
|
|
[[ -t 1 ]] && tput clear 2>/dev/null
|
|
|
|
|
|
|
|
|
|
while true; do
|
|
|
|
|
# Re-resolve newest log each tick (unless a specific file was given) so the
|
|
|
|
|
# monitor latches onto a build started after it.
|
|
|
|
|
if [[ -z "$LOGFILE" ]]; then CUR_LOG="$(find_newest_log)"; else CUR_LOG="$LOGFILE"; fi
|
|
|
|
|
|
|
|
|
|
if [[ -z "$CUR_LOG" || ! -f "$CUR_LOG" ]]; then
|
|
|
|
|
paint "$(printf '%sWaiting for a build log in %s/%s/ ...%s\n%s(spinner %s)%s' \
|
|
|
|
|
"$C_DIM" "$LOGS_ROOT" "$DIR" "$C_RESET" \
|
|
|
|
|
"$C_DIM" "${SPINNER:$((tick % 4)):1}" "$C_RESET")"
|
|
|
|
|
sleep 1; tick=$((tick + 1)); continue
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Fetch state once, derive build state in this scope (a $(...) capture of
|
|
|
|
|
# render() can't propagate a global), then render to a string and paint it.
|
|
|
|
|
now=$(date +%s)
|
|
|
|
|
data=$(awk -v NOW="$now" "$AWK_PROG" "$CUR_LOG")
|
|
|
|
|
state=$(printf '%s\n' "$data" | awk -F'|' '$1=="H"{print $6}')
|
|
|
|
|
paint "$(render "$data" "$tick")"
|
|
|
|
|
|
|
|
|
|
# In managed mode the parent owns our lifecycle and will kill us; keep painting
|
|
|
|
|
# the final frame until then. Otherwise self-exit once the build is finished.
|
|
|
|
|
if [[ "$MANAGED" -eq 0 && ( "${state:-running}" == "done" || "${state:-running}" == "failed" ) ]]; then
|
|
|
|
|
tput cnorm 2>/dev/null
|
|
|
|
|
break
|
|
|
|
|
fi
|
|
|
|
|
sleep 1
|
|
|
|
|
tick=$((tick + 1))
|
|
|
|
|
done
|