# 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 }}