Skip to content

Commit 2bcebb1

Browse files
Kikobeatsclaude
andauthored
ci(benchmark): compare base and head on the same runner (#26)
* ci(benchmark): compare base and head on the same runner The stored baseline came from whatever runner and lts/* node the last master push got, so PR comments flagged untouched cases as 1.7-2.4x slower. Base and head now run alternately in the same job (5 rounds each, median), and the action compares head against that base. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci(benchmark): keep head results when base cannot run A failed base install, or a base run that exits without writing its output, now warns and compares head alone instead of failing the job. The summary shows each case's own unit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 37ae603 commit 2bcebb1

1 file changed

Lines changed: 82 additions & 22 deletions

File tree

‎.github/workflows/benchmark.yml‎

Lines changed: 82 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,113 @@
11
name: benchmark
22

33
on:
4-
push:
5-
branches:
6-
- master
74
pull_request:
85

96
permissions:
10-
contents: write
7+
contents: read
118
pull-requests: write
129

1310
jobs:
1411
benchmark:
1512
name: Benchmark
1613
runs-on: ubuntu-latest
1714
steps:
18-
- name: Checkout
15+
- name: Checkout head
1916
uses: actions/checkout@v7
2017
with:
18+
path: head
19+
persist-credentials: false
20+
- name: Checkout base
21+
uses: actions/checkout@v7
22+
with:
23+
ref: ${{ github.event.pull_request.base.sha }}
24+
path: base
2125
persist-credentials: false
2226
- name: Setup Node.js
2327
uses: actions/setup-node@v7
2428
with:
2529
node-version: lts/*
26-
- name: Restore previous benchmark
27-
uses: actions/cache/restore@v6
28-
with:
29-
path: cache/benchmark-data.json
30-
key: benchmark-${{ github.run_id }}-${{ github.run_attempt }}
31-
restore-keys: benchmark-
32-
- name: Run benchmark
33-
run: node benchmark/index.js
34-
- name: Continuous Benchmark
35-
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
30+
- name: Run base and head alternately
31+
env:
32+
ROUNDS: 5
33+
run: |
34+
set -euo pipefail
35+
mkdir -p results
36+
run_side () {
37+
if [ "$1" = base ] && [ ! -f base/benchmark/index.js ]; then return 0; fi
38+
if (cd "$1" && node benchmark/index.js) && [ -f "$1/benchmark-output.json" ]; then
39+
mv "$1/benchmark-output.json" "results/$1-$2.json"
40+
elif [ "$1" = base ]; then
41+
echo "::warning::base benchmark failed in round $2, comparing without it"
42+
else
43+
exit 1
44+
fi
45+
}
46+
for round in $(seq "$ROUNDS"); do
47+
if [ $((round % 2)) -eq 1 ]; then run_side base "$round"; run_side head "$round"
48+
else run_side head "$round"; run_side base "$round"; fi
49+
done
50+
- name: Compare
51+
env:
52+
NAME: https-tls
53+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
54+
run: |
55+
node - <<'EOF'
56+
const { readdirSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } = require('fs')
57+
const median = values => {
58+
const sorted = [...values].sort((a, b) => a - b)
59+
const middle = sorted.length >> 1
60+
return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2
61+
}
62+
const round = value => Number(value.toFixed(2))
63+
const readSide = side =>
64+
readdirSync('results')
65+
.filter(file => file.startsWith(`${side}-`))
66+
.map(file => JSON.parse(readFileSync(`results/${file}`, 'utf8')))
67+
const merge = runs => {
68+
const names = [...new Set(runs.flatMap(run => run.map(result => result.name)))]
69+
return names.map(name => {
70+
const matches = runs.flatMap(run => run.filter(result => result.name === name))
71+
const values = matches.map(result => result.value)
72+
return {
73+
name,
74+
unit: matches[0].unit,
75+
value: round(median(values)),
76+
range: `${round(Math.min(...values))}-${round(Math.max(...values))}`,
77+
extra: `median of ${values.length} rounds: ${values.join(', ')}`
78+
}
79+
})
80+
}
81+
const head = merge(readSide('head'))
82+
const baseRuns = readSide('base')
83+
const base = baseRuns.length ? merge(baseRuns) : []
84+
writeFileSync('head-output.json', JSON.stringify(head, null, 2))
85+
mkdirSync('cache', { recursive: true })
86+
const seed = base.length
87+
? { [process.env.NAME]: [{ commit: { id: process.env.BASE_SHA }, date: Date.now(), tool: 'customSmallerIsBetter', benches: base }] }
88+
: {}
89+
writeFileSync('cache/benchmark-data.json', JSON.stringify({ lastUpdate: Date.now(), repoUrl: '', entries: seed }))
90+
const rows = head.map(current => {
91+
const previous = base.find(result => result.name === current.name)
92+
const ratio = previous ? (current.value / previous.value).toFixed(2) : ''
93+
const format = result => (result ? `${result.value} ${result.unit}` : '')
94+
return `| \`${current.name}\` | ${format(previous)} | ${format(current)} | ${ratio} |`
95+
})
96+
const summary = ['| case | base | head | head/base |', '|-|-|-|-|', ...rows].join('\n')
97+
console.log(summary)
98+
appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary + '\n')
99+
EOF
100+
- name: Comment
101+
if: github.event.pull_request.head.repo.full_name == github.repository
36102
uses: benchmark-action/github-action-benchmark@v1.22.2
37103
with:
38104
name: https-tls
39105
tool: customSmallerIsBetter
40-
output-file-path: benchmark-output.json
106+
output-file-path: head-output.json
41107
external-data-json-path: cache/benchmark-data.json
42108
github-token: ${{ secrets.GITHUB_TOKEN }}
43109
auto-push: false
44110
comment-always: true
45111
comment-on-alert: false
46112
fail-on-alert: false
47113
summary-always: false
48-
- name: Save benchmark
49-
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
50-
uses: actions/cache/save@v6
51-
with:
52-
path: cache/benchmark-data.json
53-
key: benchmark-${{ github.run_id }}-${{ github.run_attempt }}

0 commit comments

Comments
 (0)