pcbjam/.github/workflows/wasm-opt-bench.yml
Istvan Matejcsok 6953fbd894 ci: ephemeral Hetzner CI — full build + e2e on main push, all else dispatch-only
Per run, a fresh ccx53 VM is created (Cyclenerd/hcloud-github-runner),
builds + tests run on it, and it is always torn down — no persistent
infra, no lingering cost. Secrets: HCLOUD_TOKEN, HETZNER_RUNNER_PAT.

- ci-full-build.yml: the main-branch CI and the only push-triggered
  workflow. Full build of all 6 tools (pipelined wasm-opt, self-built
  Binaryen 130, ~1h15m) + KiCad e2e; docs-only pushes excluded via
  paths-ignore.
- ci.yml (eeschema-only smoke) and wasm-opt-bench.yml (config sweeps):
  workflow_dispatch-only, since every push-run costs a paid VM.
- Shared one-Hetzner-slot concurrency group across the workflows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 07:50:47 +02:00

197 lines
9 KiB
YAML

# Dedicated benchmark of the host-side `wasm-opt -O2` pass (the ~80-min CI
# bottleneck). Same ephemeral Hetzner ccx53 as the main CI, but instead of the
# full build+e2e it builds the asyncified `-O2` INPUT once, caches it as an
# artifact, and replays scripts/bench/o2-config-sweep.sh over it under a matrix
# of allocator/THP/thread configs. Goal: find the env that kills the kernel
# page-management storm (see docs/ci-build-slowness-findings.md).
#
# Manual-only (workflow_dispatch): every run costs a paid ephemeral Hetzner VM,
# so nothing here triggers on push. Dispatch from the Actions tab (the workflow
# must exist on the default branch for that).
#
# Parameters live in scripts/bench/sweep.conf (committed) or the dispatch
# inputs below. To reuse a prior run's fixture (skip the ~40-min build), set
# fixture_run_id to that run's id.
#
# Reuses the main CI's secrets: HCLOUD_TOKEN, HETZNER_RUNNER_PAT.
name: wasm-opt bench
on:
workflow_dispatch:
inputs:
configs: { description: "space-separated config presets (override conf)", required: false, default: "" }
cores: { description: "BINARYEN_CORES (override conf)", required: false, default: "" }
fixture_run_id: { description: "run id to pull the cached fixture from (blank = build it)", required: false, default: "" }
diagnostic: { description: "1 = capture perf/vmstat/irq around first config", required: false, default: "" }
cap_seconds: { description: "windowed-sample seconds per config (0 = run to completion)", required: false, default: "" }
# One Hetzner bench VM at a time. (The main CI uses a different runner; still,
# don't push the ci-hetzner branches while a bench runs — we only have one slot.)
concurrency:
group: wasm-opt-bench
cancel-in-progress: false
jobs:
create-runner:
name: Create Hetzner runner
runs-on: ubuntu-latest
outputs:
label: ${{ steps.create.outputs.label }}
server_id: ${{ steps.create.outputs.server_id }}
steps:
- name: Create ephemeral ccx53 runner
id: create
uses: Cyclenerd/hcloud-github-runner@v1
with:
mode: create
github_token: ${{ secrets.HETZNER_RUNNER_PAT }}
hcloud_token: ${{ secrets.HCLOUD_TOKEN }}
server_type: ccx53
location: nbg1
image: ubuntu-24.04
bench:
name: wasm-opt -O2 config sweep (Hetzner ccx53)
needs: create-runner
runs-on: ${{ needs.create-runner.outputs.label }}
timeout-minutes: 210
env:
KICAD_LOG_NESTED: "1" # live build logs straight to the Actions console
steps:
- name: Checkout (with submodules)
uses: actions/checkout@v4
with:
submodules: recursive
- name: Resolve sweep parameters
id: params
# Pass workflow_dispatch inputs via env (never interpolate ${{ }} into a
# shell body) so a value can't be parsed as shell. They override the
# committed conf when non-empty; the run id is validated numeric before
# it reaches download-artifact.
env:
IN_CONFIGS: ${{ inputs.configs }}
IN_CORES: ${{ inputs.cores }}
IN_FIXTURE_RUN_ID: ${{ inputs.fixture_run_id }}
IN_DIAGNOSTIC: ${{ inputs.diagnostic }}
IN_CAP: ${{ inputs.cap_seconds }}
run: |
set -e
CONF="scripts/bench/sweep.conf"
[ -f "$CONF" ] && . "$CONF" || true
CONFIGS="${IN_CONFIGS:-${CONFIGS_CONF:-baseline}}"
CORES="${IN_CORES:-${CORES_CONF:-}}"
FIXTURE_RUN_ID="${IN_FIXTURE_RUN_ID:-${FIXTURE_RUN_ID_CONF:-}}"
DIAGNOSTIC="${IN_DIAGNOSTIC:-${DIAGNOSTIC_CONF:-0}}"
CAP_SECONDS="${IN_CAP:-${CAP_SECONDS_CONF:-0}}"
if [ -n "$FIXTURE_RUN_ID" ] && ! printf '%s' "$FIXTURE_RUN_ID" | grep -qE '^[0-9]+$'; then
echo "::error::FIXTURE_RUN_ID must be numeric (got '$FIXTURE_RUN_ID')"; exit 1
fi
BUILD_FIXTURE=true; [ -n "$FIXTURE_RUN_ID" ] && BUILD_FIXTURE=false
{
echo "CONFIGS=$CONFIGS"
echo "CORES=$CORES"
echo "FIXTURE_RUN_ID=$FIXTURE_RUN_ID"
echo "DIAGNOSTIC=$DIAGNOSTIC"
echo "CAP_SECONDS=$CAP_SECONDS"
echo "BUILD_FIXTURE=$BUILD_FIXTURE"
} >> "$GITHUB_ENV"
echo "build_fixture=$BUILD_FIXTURE" >> "$GITHUB_OUTPUT"
echo "::notice::configs='$CONFIGS' cores='${CORES:-nproc}' cap=${CAP_SECONDS}s build_fixture=$BUILD_FIXTURE fixture_run_id='${FIXTURE_RUN_ID:-none}' diagnostic=$DIAGNOSTIC"
- name: Show machine
run: |
echo "nproc=$(nproc)"; free -h; df -h /
echo "THP: $(cat /sys/kernel/mm/transparent_hugepage/enabled)"; uname -a
# Allocators + measurement tools. mimalloc is the key data point (Binaryen
# #5561): prefer the distro package, source-build it if not packaged.
- name: Install bench deps (allocators, time, perf, strace)
run: |
export DEBIAN_FRONTEND=noninteractive
sudo apt-get update
sudo apt-get install -y time strace libjemalloc2 cmake g++ git curl ca-certificates \
linux-tools-common linux-tools-generic "linux-tools-$(uname -r)" || \
sudo apt-get install -y time strace libjemalloc2 cmake g++ git curl ca-certificates linux-tools-common linux-tools-generic
sudo apt-get install -y libmimalloc2.0 || sudo apt-get install -y libmimalloc-dev || true
if ! ls /usr/lib/$(uname -m)-linux-gnu/libmimalloc.so* >/dev/null 2>&1; then
echo "mimalloc not packaged — building from source"
git clone --depth 1 https://github.com/microsoft/mimalloc /tmp/mimalloc
cmake -S /tmp/mimalloc -B /tmp/mimalloc/out -DCMAKE_BUILD_TYPE=Release >/dev/null
cmake --build /tmp/mimalloc/out -j"$(nproc)" >/dev/null
sudo cp -av /tmp/mimalloc/out/libmimalloc.so* /usr/lib/$(uname -m)-linux-gnu/
fi
echo "allocators present:"; ls -l /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so* /usr/lib/$(uname -m)-linux-gnu/libmimalloc.so* 2>/dev/null || true
# ---- Fixture: build once (asyncify, no -O2) and cache, or download it ----
- name: Install Docker (fixture build only)
if: steps.params.outputs.build_fixture == 'true'
run: |
export DEBIAN_FRONTEND=noninteractive
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" \
| sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
- name: Build asyncified fixture (ASYNCIFY_ONLY, no -O2)
if: steps.params.outputs.build_fixture == 'true'
run: |
# Stop after the asyncify pass so the artifact is exactly the -O2 INPUT.
ASYNCIFY_ONLY=1 BINARYEN_CORES="$(nproc)" ./docker/build.sh eeschema --build-deps -j "$(nproc)"
ls -lh output/eeschema.wasm
cp output/eeschema.wasm output/eeschema.asyncified.wasm
- name: Upload fixture artifact
if: steps.params.outputs.build_fixture == 'true'
uses: actions/upload-artifact@v4
with:
name: o2-fixture
path: output/eeschema.asyncified.wasm
retention-days: 7
- name: Download cached fixture
if: steps.params.outputs.build_fixture == 'false'
uses: actions/download-artifact@v4
with:
name: o2-fixture
path: output
run-id: ${{ env.FIXTURE_RUN_ID }}
github-token: ${{ secrets.GITHUB_TOKEN }}
# ---- The sweep ----
- name: Run -O2 config sweep
run: |
FIX="output/eeschema.asyncified.wasm"
[ -f "$FIX" ] || FIX="output/eeschema.wasm"
ls -lh "$FIX"
CONFIGS="${CONFIGS}" CORES="${CORES}" DIAGNOSTIC="${DIAGNOSTIC}" CAP_SECONDS="${CAP_SECONDS}" \
./scripts/bench/o2-config-sweep.sh "$FIX"
- name: Upload bench results
if: always()
uses: actions/upload-artifact@v4
with:
name: o2-bench-results-${{ github.run_id }}
path: bench/o2-results/**
if-no-files-found: warn
delete-runner:
name: Delete Hetzner runner
needs: [create-runner, bench]
runs-on: ubuntu-latest
if: always()
steps:
- name: Delete ephemeral runner
uses: Cyclenerd/hcloud-github-runner@v1
with:
mode: delete
github_token: ${{ secrets.HETZNER_RUNNER_PAT }}
hcloud_token: ${{ secrets.HCLOUD_TOKEN }}
name: ${{ needs.create-runner.outputs.label }}
server_id: ${{ needs.create-runner.outputs.server_id }}