benchmarks.yml

  1name: Benchmarks
  2
  3on:
  4  pull_request:
  5    branches: [master]
  6  push:
  7    branches: [master]
  8
  9permissions:
 10  contents: read
 11
 12jobs:
 13  benchmark:
 14    runs-on: ubuntu-latest
 15    timeout-minutes: 30
 16    steps:
 17      - name: Checkout PR
 18        uses: actions/checkout@v7
 19        with:
 20          fetch-depth: 0
 21
 22      - name: Set up Go
 23        uses: actions/setup-go@v6
 24        with:
 25          go-version: "1.26.4"
 26
 27      - name: Install system dependencies
 28        run: sudo apt-get update && sudo apt-get install -y libpcsclite-dev
 29
 30      - name: Install benchstat
 31        run: go install golang.org/x/perf/cmd/benchstat@latest
 32
 33      - name: Resolve base ref
 34        id: base
 35        run: |
 36          if [ "${{ github.event_name }}" = "pull_request" ]; then
 37            echo "ref=${{ github.event.pull_request.base.sha }}" >> "$GITHUB_OUTPUT"
 38          else
 39            echo "ref=${{ github.event.before }}" >> "$GITHUB_OUTPUT"
 40          fi
 41
 42      - name: Benchmark PR
 43        run: |
 44          go test -run=^$ -bench=. -benchmem -benchtime=3x -count=6 ./backend/ ./tui/ \
 45            | tee new.txt
 46
 47      - name: Checkout base
 48        run: git checkout ${{ steps.base.outputs.ref }}
 49
 50      - name: Benchmark base
 51        run: |
 52          go test -run=^$ -bench=. -benchmem -benchtime=3x -count=6 ./backend/ ./tui/ \
 53            | tee old.txt || echo "base benchmarks failed" > old.txt
 54
 55      - name: Restore PR checkout
 56        run: git checkout ${{ github.sha }}
 57
 58      - name: Compare with benchstat
 59        run: |
 60          set +e
 61          benchstat old.txt new.txt | tee benchstat.txt
 62
 63      - name: Classify result
 64        run: |
 65          python3 - <<'PY' > verdict.txt
 66          import re
 67          worse, better = 0, 0
 68          with open("benchstat.txt") as f:
 69              for line in f:
 70                  m = re.search(r"([-+]?\d+\.\d+)%", line)
 71                  if not m:
 72                      continue
 73                  delta = float(m.group(1))
 74                  if "ns/op" in line or "B/op" in line or "allocs/op" in line:
 75                      if delta > 3:
 76                          worse += 1
 77                      elif delta < -3:
 78                          better += 1
 79          status = "neutral"
 80          if worse > 0 and worse >= better:
 81              status = "regression"
 82          elif better > 0:
 83              status = "improvement"
 84          print(f"status={status}")
 85          print(f"worse={worse}")
 86          print(f"better={better}")
 87          PY
 88          cat verdict.txt
 89
 90      - name: Record PR metadata
 91        if: github.event_name == 'pull_request'
 92        run: |
 93          echo "${{ github.event.pull_request.number }}" > pr-number.txt
 94
 95      - name: Upload artifacts
 96        if: always()
 97        uses: actions/upload-artifact@v7
 98        with:
 99          name: benchmarks
100          path: |
101            old.txt
102            new.txt
103            benchstat.txt
104            verdict.txt
105            pr-number.txt
106          if-no-files-found: ignore