name: Bench Gate # Compares hot-path benchmarks between the PR head and its merge # base, so performance regressions in the sync engine and DB write # paths fail the PR instead of shipping. The gated regression classes # have all shipped before: # # - discovery/skip work scaling with archive size instead of new # data (#912, the providerSourceUnchangedInDB gap) # - O(session history) work per incremental append (#954) # - bulk ingest throughput (#411) # - per-row query-shape regressions in usage aggregation (#309) # # Both sides run `make bench-gate` — the Makefile is the single # source of truth for the gated package list, sample count, and # iteration count — and cmd/benchgate compares the outputs. It gates # on allocs/op and B/op (deterministic on a given machine, tight # thresholds) and on ns/op with a loose 2x threshold that only # catches algorithmic blowups; both sides run on the same runner # within one job, so the comparison is apples to apples. # # Benchmarks that only exist on one side are reported but never fail # the gate: a benchmark added by a PR has no baseline and is reported # without gating, then gates automatically once merged. Because each # side benchmarks its own Makefile's package list, a PR that adds a # package to the gate cannot break the base run. A partially failing # base run degrades to a partial baseline (whatever benchmarks it # produced still gate) rather than silently disabling the whole gate. on: pull_request: # Docs/frontend-only PRs cannot change the gated Go paths. If # this check is ever made required on branch protection, pair it # with a no-op sibling workflow on the inverse paths. paths: - "**.go" - "go.mod" - "go.sum" - "Makefile" - ".github/workflows/bench.yml" concurrency: group: bench-${{ github.head_ref || github.ref }} cancel-in-progress: true permissions: contents: read jobs: bench-gate: name: Benchmark Gate runs-on: ubuntu-latest steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false fetch-depth: 0 - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version-file: go.mod - name: Run benchmarks (PR head) run: | set -euo pipefail make -s bench-gate | tee /tmp/bench-new.txt - name: Run benchmarks (merge base) env: BASE_REF: ${{ github.base_ref }} # A failing merge-base run keeps whatever benchmark output it # produced: go test emits results per package, so one broken # package (or a base predating the bench-gate target) leaves # a partial or empty baseline and benchgate gates only what # exists on both sides. The warning makes the degraded run # visible instead of a silently green vacuous pass. # # The sample count and fixed iteration count are evaluated # from the PR head's Makefile and passed into the base run: # two benchmarks grow their fixture per iteration, so a PR # that changes BENCH_GATE_COUNT/TIME must not compare against # a baseline measured with the old values. The package list # intentionally stays per-side, so growing the gate cannot # break the base run. run: | set -euo pipefail eval "$(make -s bench-gate-config)" base=$(git merge-base HEAD "origin/$BASE_REF") git worktree add /tmp/bench-base "$base" if ! make -s -C /tmp/bench-base bench-gate \ BENCH_GATE_COUNT="$BENCH_GATE_COUNT" \ BENCH_GATE_TIME="$BENCH_GATE_TIME" > /tmp/bench-old.txt; then echo "::warning title=Bench Gate::merge-base benchmark run exited non-zero; gating against its partial output" fi - name: Compare against merge base run: go run ./cmd/benchgate -old /tmp/bench-old.txt -new /tmp/bench-new.txt