perf(ci): cache the per-run toolchain + test-app work (binaryen, emsdk, apps, browsers)

Four new caches in wasm-build.yml, keyed on their real inputs:

- Binaryen post-process tools (bin/ only, keyed on the submodule SHA): on a
  hit BINARYEN_TRUST_PREBUILT=1 tells build-wasm-opt.sh to skip cmake+ninja
  and trust the restored binaries (~46s/run). The guard is env-gated so local
  pass iteration (uncommitted sources, same SHA) is unaffected.
- Built wx test apps (tests/apps minus kicad/ staging + gal-webgl/): skips
  the whole build step on a hit (~3 min — the post-link hoist+asyncify over
  ~74 apps dominates). Key covers wx SHA + kicad SHA (some apps compile real
  KiCad sources: thread_pool.cpp, libcontext), binaryen SHA, tracked app
  sources, and the build/post-link scripts + shims. Gated on a wx cache HIT:
  the app build creates the libwx_*.a symlinks the GAL link needs, so on a
  wx rebuild the apps must rebuild too.
- Host emsdk (tools/emsdk, keyed on the pinned EMSCRIPTEN_VERSION): mostly
  availability insurance — a fresh install is only ~23s but pulls ~340 MB
  from github.com + storage.googleapis.com on every run. downloads/ tarballs
  are pruned before the post-job save.
- Playwright browsers (~/.cache/ms-playwright keyed on tests/package-lock).

Warm-cache runs should drop from ~12 min to ~8-8.5 min (e2e suites are the
floor). Measured baselines from runs 28577824366 / 28520845242.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeiSRRScdaox5jBueJNcyG
This commit is contained in:
Viktor Vaczi 2026-07-02 12:40:22 +02:00
commit b4cb10b1c6
2 changed files with 91 additions and 1 deletions

View file

@ -95,11 +95,13 @@ jobs:
BIN=$(git -C binaryen rev-parse --short HEAD) BIN=$(git -C binaryen rev-parse --short HEAD)
SC=$(node scripts/deploy/wasm-cache-hash.mjs) SC=$(node scripts/deploy/wasm-cache-hash.mjs)
EPOCH=$(cat .ci-cache-epoch 2>/dev/null || echo 0) EPOCH=$(cat .ci-cache-epoch 2>/dev/null || echo 0)
EMV=$(. scripts/common/versions.sh && echo "$EMSCRIPTEN_VERSION")
THREED='${{ inputs.build_3d_viewer }}' THREED='${{ inputs.build_3d_viewer }}'
BASE="kbase-${{ runner.os }}-k${KICAD}-wx${WX}-sc${SC}-3d${THREED}-e${EPOCH}" BASE="kbase-${{ runner.os }}-k${KICAD}-wx${WX}-sc${SC}-3d${THREED}-e${EPOCH}"
FINAL="kwasm-${{ runner.os }}-bin${BIN}${{ inputs.opt_level }}-k${KICAD}-wx${WX}-sc${SC}-3d${THREED}-e${EPOCH}" FINAL="kwasm-${{ runner.os }}-bin${BIN}${{ inputs.opt_level }}-k${KICAD}-wx${WX}-sc${SC}-3d${THREED}-e${EPOCH}"
{ {
echo "kicad=$KICAD"; echo "wx=$WX"; echo "sc=$SC"; echo "epoch=$EPOCH" echo "kicad=$KICAD"; echo "wx=$WX"; echo "sc=$SC"; echo "epoch=$EPOCH"
echo "bin=$BIN"; echo "emv=$EMV"
echo "base_key=$BASE"; echo "final_key=$FINAL" echo "base_key=$BASE"; echo "final_key=$FINAL"
} >> "$GITHUB_OUTPUT" } >> "$GITHUB_OUTPUT"
@ -116,6 +118,23 @@ jobs:
echo "skip=$SKIP" >> "$GITHUB_OUTPUT" echo "skip=$SKIP" >> "$GITHUB_OUTPUT"
echo "WASM output-cache restore skip=$SKIP" echo "WASM output-cache restore skip=$SKIP"
# Binaryen post-process tools (submodule fork): otherwise built from source on
# every fresh VM (~46s on 30 cores, measured run 28577824366). Cache just bin/
# keyed on the exact submodule SHA; on a hit BINARYEN_TRUST_PREBUILT tells
# build-wasm-opt.sh to skip cmake+ninja and trust the restored binaries.
# Needed by BOTH the host post-process (final-miss path) and the test-app
# build, hence not gated on run_tests.
- name: Cache Binaryen post-process tools
id: binopt-cache
uses: actions/cache@v4
with:
path: build-wasm/tools/binaryen-hoist-build/bin
key: binopt-${{ runner.os }}-${{ steps.keys.outputs.bin }}
- name: Trust prebuilt Binaryen tools (cache hit)
if: steps.binopt-cache.outputs.cache-hit == 'true'
run: echo "BINARYEN_TRUST_PREBUILT=1" >> "$GITHUB_ENV"
# The cached paths the e2e tests need: final wasms (or base, mid-build) + # The cached paths the e2e tests need: final wasms (or base, mid-build) +
# the sysroot headers the host GAL build compiles against. Same glob set for # the sysroot headers the host GAL build compiles against. Same glob set for
# both tiers — only the bytes (base vs final) and the key differ. # both tiers — only the bytes (base vs final) and the key differ.
@ -255,6 +274,20 @@ jobs:
!output/*.wasm.debug.wasm !output/*.wasm.debug.wasm
# --- e2e tests (gated on run_tests) ------------------------------------ # --- e2e tests (gated on run_tests) ------------------------------------
# Host emsdk toolchain (tools/emsdk): env.sh auto-installs it on first use
# (~23s: emsdk repo clone + ~340 MB from storage.googleapis.com, measured run
# 28577824366). Caching it is speed-neutral-to-slightly-positive; the real
# value is availability — without it a github.com/storage.googleapis.com
# hiccup fails every run. Keyed on the pinned EMSCRIPTEN_VERSION (versions.sh);
# the emscripten ports cache (zlib) rides along. The spent downloads/ tarballs
# are pruned below before the post-job save.
- name: Cache emsdk toolchain
if: inputs.run_tests
uses: actions/cache@v4
with:
path: tools/emsdk
key: emsdk-${{ runner.os }}-${{ steps.keys.outputs.emv }}
- name: Restore wx build cache - name: Restore wx build cache
id: wx-cache id: wx-cache
if: inputs.run_tests if: inputs.run_tests
@ -276,14 +309,50 @@ jobs:
if: inputs.run_tests if: inputs.run_tests
run: ./scripts/build-wx-wasm.sh run: ./scripts/build-wx-wasm.sh
# Built wx test apps (tests/apps): without this every run recompiles, relinks
# and — the expensive part — re-runs the hoist+asyncify post-link on all ~74
# apps (~2m20s even 30-wide). Key = every build input: the wx lib identity
# (submodule SHA + the same script hashes as the wx cache key; the wx SHA also
# covers the Makefile's JS_FILES from wxwidgets/build/wasm), the KICAD
# submodule SHA (some apps compile real KiCad sources — thread_pool.cpp,
# libcontext, headers), the binaryen SHA (post-link wasm-opt), the app
# sources (tracked cpp/h/html + Makefile), and the build/post-link scripts +
# JS shims (wasm/** = shims + the wasm-opt stub). hashFiles runs at restore
# time, on a fresh checkout, so it sees only tracked sources — never build
# outputs. On a hit the build step is skipped entirely. Excluded:
# tests/apps/kicad (setup:kicad staging from output/) and gal-webgl (its own
# step below rebuilds it every run anyway).
# Gated on a wx cache HIT: the test-app build is what creates the
# libwx_*.a -> libwx_*-emscripten.a symlinks the GAL link (wx-config --libs)
# needs. A restored wx cache contains them (saved at job end, after they
# exist), but a freshly rebuilt wx tree does not — so on a wx miss the apps
# must rebuild too, or the GAL step breaks.
- name: Cache built wx test apps
id: testapps-cache
if: inputs.run_tests && steps.wx-cache.outputs.cache-hit == 'true'
uses: actions/cache@v4
with:
path: |
tests/apps
!tests/apps/kicad
!tests/apps/gal-webgl
key: testapps-${{ runner.os }}-wx${{ steps.keys.outputs.wx }}-k${{ steps.keys.outputs.kicad }}-bin${{ steps.keys.outputs.bin }}-${{ hashFiles('tests/apps/**/*.cpp', 'tests/apps/**/*.h', 'tests/apps/**/*.html', 'tests/apps/Makefile.wasm', 'scripts/build-wx-wasm.sh', 'scripts/build-wasm-test.sh', 'scripts/common/versions.sh', 'scripts/common/env.sh', 'scripts/common/functions.sh', 'scripts/common/apply-asyncify.sh', 'scripts/common/asyncify-imports.txt', 'scripts/common/inject-dyncall-shims.sh', 'scripts/common/shims/**', 'wasm/**') }}
- name: Build wxWidgets test apps - name: Build wxWidgets test apps
if: inputs.run_tests if: inputs.run_tests && steps.testapps-cache.outputs.cache-hit != 'true'
run: ./scripts/build-wasm-test.sh run: ./scripts/build-wasm-test.sh
- name: Build GAL WebGL test app - name: Build GAL WebGL test app
if: inputs.run_tests if: inputs.run_tests
run: ./scripts/build-gal-webgl-test.sh run: ./scripts/build-gal-webgl-test.sh
# The emsdk cache saves in the post-job phase; the downloads/ tarballs
# (~340 MB) are spent after install — drop them so they never ride in the
# cache. No-op on cache-hit runs (already pruned before the save).
- name: Prune emsdk download tarballs (cache hygiene)
if: inputs.run_tests
run: rm -rf tools/emsdk/downloads
- name: Install test deps - name: Install test deps
if: inputs.run_tests if: inputs.run_tests
working-directory: tests working-directory: tests
@ -296,6 +365,16 @@ jobs:
corepack enable corepack enable
pnpm install --frozen-lockfile pnpm install --frozen-lockfile
# Browser binaries keyed on the lockfile (which pins the playwright version).
# On a hit `playwright install` skips the downloads; --with-deps still
# apt-installs its small OS dep set either way.
- name: Cache Playwright browsers
if: inputs.run_tests
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: pw-${{ runner.os }}-${{ hashFiles('tests/package-lock.json') }}
- name: Install Playwright browsers - name: Install Playwright browsers
if: inputs.run_tests if: inputs.run_tests
working-directory: tests working-directory: tests

View file

@ -22,6 +22,17 @@ if [ ! -f "${SRC}/src/passes/HoistCppCatches.cpp" ]; then
exit 1 exit 1
fi fi
# CI fast-path: the workflow cache-restores bin/ keyed on the exact submodule SHA and
# sets this var on a hit, so the restored binaries are authoritative — skip cmake+ninja.
# Never set it locally when iterating on the pass: uncommitted source edits would be
# silently ignored (the SHA key can't see them).
if [ "${BINARYEN_TRUST_PREBUILT:-0}" = "1" ] \
&& [ -x "${BUILD}/bin/wasm-opt" ] && [ -x "${BUILD}/bin/wasm-emscripten-finalize" ]; then
echo "Using prebuilt Binaryen tools (BINARYEN_TRUST_PREBUILT=1): ${BUILD}/bin" >&2
echo "${BUILD}/bin/wasm-opt"
exit 0
fi
# Configure once (mirrors scripts/common/get-wasm-opt.sh's from-source flags). # Configure once (mirrors scripts/common/get-wasm-opt.sh's from-source flags).
if [ ! -f "${BUILD}/build.ninja" ]; then if [ ! -f "${BUILD}/build.ninja" ]; then
echo "Configuring Binaryen submodule build (one-time, ~5 min to build)..." >&2 echo "Configuring Binaryen submodule build (one-time, ~5 min to build)..." >&2