|
1 | 1 | name: benchmark |
2 | 2 |
|
3 | 3 | on: |
4 | | - push: |
5 | | - branches: |
6 | | - - master |
7 | 4 | pull_request: |
8 | 5 |
|
9 | 6 | permissions: |
10 | | - contents: write |
| 7 | + contents: read |
11 | 8 | pull-requests: write |
12 | 9 |
|
13 | 10 | jobs: |
14 | 11 | benchmark: |
15 | 12 | name: Benchmark |
16 | 13 | runs-on: ubuntu-latest |
17 | 14 | steps: |
18 | | - - name: Checkout |
| 15 | + - name: Checkout head |
19 | 16 | uses: actions/checkout@v7 |
20 | 17 | 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 |
21 | 25 | persist-credentials: false |
22 | 26 | - name: Setup Node.js |
23 | 27 | uses: actions/setup-node@v7 |
24 | 28 | with: |
25 | 29 | 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 |
36 | 102 | uses: benchmark-action/github-action-benchmark@v1.22.2 |
37 | 103 | with: |
38 | 104 | name: https-tls |
39 | 105 | tool: customSmallerIsBetter |
40 | | - output-file-path: benchmark-output.json |
| 106 | + output-file-path: head-output.json |
41 | 107 | external-data-json-path: cache/benchmark-data.json |
42 | 108 | github-token: ${{ secrets.GITHUB_TOKEN }} |
43 | 109 | auto-push: false |
44 | 110 | comment-always: true |
45 | 111 | comment-on-alert: false |
46 | 112 | fail-on-alert: false |
47 | 113 | 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