Skip to content

Commit 628c926

Browse files
ci: replace the flaky absolute perf gate with a same-run A/B gate
The nightly perf-regression gate failed about every other day (#162): it timed a 0.39s workload with median-of-3 and no warmup, then compared the result against a baseline recorded on a different machine and restored from an actions/cache entry that the update_baseline input could never overwrite. benchmarks/cleanbench/ab.py measures a base and a head checkout on the same runner. Each measurement runs in a fresh worker subprocess with that side's src first on PYTHONPATH (the worker refuses to run if freshdata came from elsewhere). Sides alternate order across pairs; runtime compares the fastest run per side, memory the median cold-clean peak RSS delta. A breach only fails when a full confirmation re-run breaches the same metric. perf-regression.yml now runs it for PRs touching src/ or the harness (base = PR base commit), daily on main (base = main ~26h earlier, alert issue on failure) and weekly against the latest release tag (report only), with a manual base_ref / accept_regression dispatch.
1 parent 9ed7993 commit 628c926

3 files changed

Lines changed: 574 additions & 21 deletions

File tree

‎.github/workflows/perf-regression.yml‎

Lines changed: 91 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,128 @@
11
name: Performance regression
22

3-
# Nightly (and release-candidate) T5 perf gate: runtime slowdown <= 20% and
4-
# memory overhead <= 15% versus the cached v1.0-equivalent baseline.
3+
# Same-run A/B perf gate on the CleanBench T5 workload: HEAD is measured against
4+
# a base commit on the same runner, interleaved, and fails only when it is >20%
5+
# slower or uses >15% more peak memory than base AND a confirmation re-run
6+
# reproduces the breach. No absolute numbers from other machines are compared.
7+
#
8+
# pull_request PR head vs the PR base commit (blocking)
9+
# daily main vs main as of ~26 hours earlier (blocking, alert issue)
10+
# weekly main vs the latest release tag (report only)
11+
# manual any base_ref; accept_regression=true reports without failing
512
on:
613
schedule:
714
- cron: "0 5 * * *"
15+
- cron: "30 5 * * 1"
16+
pull_request:
17+
paths:
18+
- "src/freshdata/**"
19+
- "benchmarks/cleanbench/**"
20+
- ".github/workflows/perf-regression.yml"
821
workflow_dispatch:
922
inputs:
10-
update_baseline:
11-
description: "Re-pin the perf baseline to this run"
23+
base_ref:
24+
description: "Base ref to compare against (default: main as of 26h ago)"
25+
type: string
26+
default: ""
27+
accept_regression:
28+
description: "Report only; do not fail on a regression"
1229
type: boolean
1330
default: false
1431

32+
concurrency:
33+
group: perf-regression-${{ github.ref }}
34+
cancel-in-progress: true
35+
36+
permissions:
37+
contents: read
38+
1539
jobs:
1640
perf:
1741
runs-on: ubuntu-latest
18-
timeout-minutes: 20
42+
timeout-minutes: 45
1943
permissions:
20-
issues: write
2144
contents: read
45+
issues: write
2246
steps:
2347
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
48+
with:
49+
fetch-depth: 0 # the base commit and release tags must be resolvable
2450
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
2551
with:
2652
python-version: "3.12"
53+
cache: pip
2754
- name: Install
2855
run: |
2956
python -m pip install -U pip
3057
pip install -e ".[bench]"
31-
- name: Restore perf baseline
32-
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
33-
with:
34-
path: benchmarks/cleanbench/results/baseline_v1.json
35-
key: cleanbench-baseline-${{ runner.os }}-v1
36-
- name: T5 perf gate
58+
- name: Resolve base commit
59+
id: base
60+
env:
61+
EVENT: ${{ github.event_name }}
62+
SCHEDULE: ${{ github.event.schedule }}
63+
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
64+
INPUT_BASE_REF: ${{ inputs.base_ref }}
65+
INPUT_ACCEPT: ${{ inputs.accept_regression }}
66+
run: |
67+
set -euo pipefail
68+
mode=gate
69+
if [ "$EVENT" = "pull_request" ]; then
70+
ref="$PR_BASE_SHA"
71+
elif [ "$EVENT" = "workflow_dispatch" ] && [ -n "$INPUT_BASE_REF" ]; then
72+
ref="$INPUT_BASE_REF"
73+
elif [ "$SCHEDULE" = "30 5 * * 1" ]; then
74+
ref="$(git describe --tags --abbrev=0 --match 'v*' origin/main)"
75+
mode=report
76+
else
77+
ref="$(git rev-list -1 --before='26 hours ago' origin/main)"
78+
fi
79+
if [ "$INPUT_ACCEPT" = "true" ]; then mode=report; fi
80+
sha="$(git rev-parse --verify "${ref}^{commit}")"
81+
{
82+
echo "ref=$ref"
83+
echo "sha=$sha"
84+
echo "mode=$mode"
85+
} >> "$GITHUB_OUTPUT"
86+
if [ "$sha" = "$(git rev-parse HEAD)" ]; then
87+
echo "skip=true" >> "$GITHUB_OUTPUT"
88+
echo "Base \`$ref\` is HEAD; nothing to compare." >> "$GITHUB_STEP_SUMMARY"
89+
else
90+
echo "skip=false" >> "$GITHUB_OUTPUT"
91+
git worktree add --detach "$RUNNER_TEMP/perf-base" "$sha"
92+
fi
93+
- name: Same-run A/B perf gate
94+
if: steps.base.outputs.skip != 'true'
95+
env:
96+
BASE_REF: ${{ steps.base.outputs.ref }}
97+
BASE_SHA: ${{ steps.base.outputs.sha }}
98+
MODE: ${{ steps.base.outputs.mode }}
3799
run: |
38-
EXTRA=""
39-
if [ "${{ inputs.update_baseline }}" = "true" ]; then EXTRA="--update-baseline"; fi
40-
python -m benchmarks.cleanbench --tracks T5 --check-gates $EXTRA
100+
GATE=""
101+
if [ "$MODE" = "gate" ]; then GATE="--check-gates"; fi
102+
python -m benchmarks.cleanbench.ab \
103+
--base-src "$RUNNER_TEMP/perf-base/src" \
104+
--head-src src \
105+
--base-label "$BASE_REF (${BASE_SHA:0:7})" \
106+
--head-label "HEAD (${GITHUB_SHA:0:7})" \
107+
--output benchmarks/cleanbench/results/latest.ab.json \
108+
--summary "$GITHUB_STEP_SUMMARY" \
109+
$GATE
41110
- name: Upload results
42-
if: always()
111+
if: always() && steps.base.outputs.skip == 'false'
43112
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
44113
with:
45-
name: perf-results
46-
path: benchmarks/cleanbench/results/latest.*
114+
name: perf-ab-results
115+
path: benchmarks/cleanbench/results/latest.ab.json
47116
retention-days: 30
48117
- name: Open/refresh alert issue on failure
49-
if: failure()
118+
if: failure() && github.event_name == 'schedule'
50119
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
51120
with:
52121
script: |
53122
const title = "nightly perf-regression gate failing";
54-
const body = `The scheduled T5 performance-regression gate failed ` +
55-
`(runtime > 120% or memory > 115% of baseline).\n\n` +
123+
const body = `The scheduled same-run A/B performance gate failed: main ran ` +
124+
`>20% slower or used >15% more peak memory than main from ~26 hours ` +
125+
`earlier, reproduced on a confirmation run (or the job errored).\n\n` +
56126
`Run: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}` +
57127
`/actions/runs/${context.runId}`;
58128
const open = await github.rest.issues.listForRepo({

0 commit comments

Comments
 (0)