test(bench): inject fake CPU regression to validate detection pipeline #26
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: PR checks | |
| # Per-package parallel pipeline: each codec is built, tested, and benchmarked | |
| # independently. The matrix mirrors what CircleCI was doing — every entry | |
| # runs concurrently, any failure fails the workflow. | |
| # | |
| # CodSpeed's first-class GHA integration replaces the manual codspeed-bench | |
| # job from CircleCI: `CodSpeedHQ/action@v3` installs valgrind, sets up | |
| # instrumentation, and uploads results in one step. | |
| on: | |
| pull_request: | |
| push: | |
| branches-ignore: | |
| - main | |
| # workflow_dispatch lets CodSpeed trigger a backtest run from the | |
| # dashboard (to seed initial perf data after the repo is connected). | |
| workflow_dispatch: | |
| # Cancel in-flight runs when a new push lands on the same PR / branch. | |
| concurrency: | |
| group: pr-checks-${{ github.workflow }}-${{ github.head_ref || github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| pull-requests: write # CodSpeed action posts a sticky PR comment | |
| id-token: write # OIDC token used by CodSpeedHQ/action for auth | |
| jobs: | |
| detect-changes: | |
| # Build the matrix dynamically from the set of packages that actually | |
| # changed since main. Mirrors CircleCI's --since main skip logic. On a | |
| # PR where every README was touched (this branch) the list is the full | |
| # 8 packages; on a docs-only PR the build matrix is empty. | |
| runs-on: ubuntu-latest | |
| outputs: | |
| packages: ${{ steps.list.outputs.packages }} | |
| any: ${{ steps.list.outputs.any }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - id: list | |
| name: List changed packages | |
| run: | | |
| set -e | |
| git fetch --no-tags --depth=50 origin main || true | |
| BASE=$(git merge-base origin/main HEAD || echo "origin/main") | |
| ALL=(charls libjpeg-turbo-8bit libjpeg-turbo-12bit openjpeg openjphjs little-endian big-endian dicom-codec) | |
| changed=() | |
| for pkg in "${ALL[@]}"; do | |
| if ! git diff --quiet "$BASE"..HEAD -- "packages/$pkg/"; then | |
| changed+=("$pkg") | |
| fi | |
| done | |
| if [ ${#changed[@]} -eq 0 ]; then | |
| echo "No packages changed since $BASE." | |
| echo 'packages=[]' >> "$GITHUB_OUTPUT" | |
| echo "any=false" >> "$GITHUB_OUTPUT" | |
| else | |
| json=$(printf '%s\n' "${changed[@]}" | jq -R . | jq -s -c .) | |
| echo "Changed packages: $json" | |
| echo "packages=$json" >> "$GITHUB_OUTPUT" | |
| echo "any=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| build: | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.any == 'true' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| package: ${{ fromJson(needs.detect-changes.outputs.packages) }} | |
| runs-on: ubuntu-latest | |
| container: | |
| image: emscripten/emsdk:3.1.74 | |
| steps: | |
| - name: Install yarn + cmake + C++ build deps | |
| run: | | |
| npm install --global yarn@1.22.22 | |
| apt-get update | |
| apt-get -y install build-essential git | |
| wget -qO- "https://cmake.org/files/v3.17/cmake-3.17.4-Linux-x86_64.tar.gz" \ | |
| | tar --strip-components=1 -xz -C /usr/local | |
| apt-get autoremove -y | |
| apt-get clean -y | |
| rm -rf /var/lib/apt/lists/* | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Allow git to operate on the workspace | |
| # The container runs as root but the workspace is owned by the | |
| # checkout action's user, which makes git complain about | |
| # `dubious ownership`. Mark it safe. | |
| run: git config --global --add safe.directory "$GITHUB_WORKSPACE" | |
| - name: Init submodules for this package | |
| run: | | |
| if [ -d "packages/${{ matrix.package }}/extern" ]; then | |
| git submodule update --init --recursive "packages/${{ matrix.package }}/extern" | |
| else | |
| echo "No extern/ submodule for ${{ matrix.package }}; skipping." | |
| fi | |
| - name: Restore yarn cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cache/yarn | |
| key: yarn-${{ hashFiles('yarn.lock') }} | |
| restore-keys: yarn- | |
| - name: Install dependencies | |
| run: yarn install --frozen-lockfile | |
| - name: Build | |
| run: cd "packages/${{ matrix.package }}" && yarn run build:ci | |
| - name: Ensure dist exists (no-op packages still need a placeholder) | |
| run: mkdir -p "packages/${{ matrix.package }}/dist" | |
| - name: Upload dist | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: dist-${{ matrix.package }} | |
| path: packages/${{ matrix.package }}/dist | |
| if-no-files-found: ignore | |
| retention-days: 7 | |
| test: | |
| needs: [detect-changes, build] | |
| if: needs.detect-changes.outputs.any == 'true' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| package: ${{ fromJson(needs.detect-changes.outputs.packages) }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Download all built dists | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: dist-* | |
| path: tmp/ | |
| - name: Replay dists into packages/<pkg>/dist | |
| # actions/download-artifact lands each artifact in tmp/<name>/. | |
| # Move each into its proper packages/<pkg>/dist location so vitest | |
| # finds them, mirroring how CircleCI workspace persist worked. | |
| run: | | |
| set -e | |
| for d in tmp/dist-*; do | |
| [ -d "$d" ] || continue | |
| pkg=$(basename "$d" | sed 's/^dist-//') | |
| mkdir -p "packages/$pkg/dist" | |
| shopt -s dotglob nullglob | |
| cp -r "$d"/* "packages/$pkg/dist/" 2>/dev/null || true | |
| done | |
| ls packages/*/dist 2>/dev/null | head | |
| - name: Restore yarn cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cache/yarn | |
| key: yarn-${{ hashFiles('yarn.lock') }} | |
| restore-keys: yarn- | |
| - name: Install dependencies | |
| run: yarn install --frozen-lockfile | |
| - name: Test | |
| run: cd "packages/${{ matrix.package }}" && yarn run test:ci | |
| codspeed-bench: | |
| needs: [detect-changes, build] | |
| if: needs.detect-changes.outputs.any == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Download all built dists | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: dist-* | |
| path: tmp/ | |
| - name: Replay dists into packages/<pkg>/dist | |
| run: | | |
| set -e | |
| for d in tmp/dist-*; do | |
| [ -d "$d" ] || continue | |
| pkg=$(basename "$d" | sed 's/^dist-//') | |
| mkdir -p "packages/$pkg/dist" | |
| shopt -s dotglob nullglob | |
| cp -r "$d"/* "packages/$pkg/dist/" 2>/dev/null || true | |
| done | |
| - name: Restore yarn cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cache/yarn | |
| key: yarn-${{ hashFiles('yarn.lock') }} | |
| restore-keys: yarn- | |
| - name: Install dependencies | |
| run: yarn install --frozen-lockfile | |
| - name: Run CodSpeed benchmarks | |
| # CodSpeedHQ/action@v4 sets up CPU simulation (Cachegrind-based | |
| # instruction counting on a modeled CPU + cache hierarchy), runs | |
| # the inner command under valgrind, uploads to codspeed.io, and | |
| # posts/updates a sticky PR comment with the per-bench deltas. | |
| # Authenticates via GitHub OIDC (id-token: write above) so no | |
| # CODSPEED_TOKEN secret is needed. | |
| # | |
| # mode: simulation (vs walltime) — we chose simulation because: | |
| # - deterministic: <1% run-to-run drift (verified across 3 | |
| # runs of identical source) | |
| # - free CI minutes (walltime needs CodSpeed macro-runners) | |
| # - regression-detection signal is strong even though the | |
| # headline numbers are MODELED instruction-time, not real | |
| # wall-clock (JS-loop benches inflate 30-100x vs production | |
| # V8 due to no JIT under Cachegrind; wasm decode kernels | |
| # inflate ~5-15x; pure native ~1x) | |
| # See BENCHMARKING.md at the repo root for the full measurement | |
| # model, how to read the cold/warm bench split, and what the | |
| # CodSpeed dashboard warnings mean. | |
| uses: CodSpeedHQ/action@v4 | |
| with: | |
| mode: simulation | |
| run: yarn run bench |