|
| 1 | +name: Benchmark |
| 2 | +on: |
| 3 | + issue_comment: |
| 4 | + types: [created] |
| 5 | + pull_request: |
| 6 | + types: [labeled] |
| 7 | + |
| 8 | +jobs: |
| 9 | + setup: |
| 10 | + name: Setup |
| 11 | + if: | |
| 12 | + (github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '/benchmark')) || |
| 13 | + (github.event_name == 'pull_request' && github.event.label.name == 'benchmark') |
| 14 | + runs-on: macos-26 |
| 15 | + permissions: |
| 16 | + contents: read |
| 17 | + pull-requests: write |
| 18 | + outputs: |
| 19 | + matrix: ${{ steps.matrix.outputs.matrix }} |
| 20 | + pr_number: ${{ steps.pr-number.outputs.number }} |
| 21 | + head_sha: ${{ steps.pr.outputs.head_sha }} |
| 22 | + merge_base: ${{ steps.commits.outputs.merge_base }} |
| 23 | + steps: |
| 24 | + - name: Get PR number |
| 25 | + id: pr-number |
| 26 | + run: | |
| 27 | + if [ "${{ github.event_name }}" = "issue_comment" ]; then |
| 28 | + echo "number=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT" |
| 29 | + else |
| 30 | + echo "number=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT" |
| 31 | + fi |
| 32 | +
|
| 33 | + - name: Get PR details |
| 34 | + id: pr |
| 35 | + env: |
| 36 | + GH_TOKEN: ${{ github.token }} |
| 37 | + run: | |
| 38 | + PR_DATA=$(gh api repos/${{ github.repository }}/pulls/${{ steps.pr-number.outputs.number }}) |
| 39 | + HEAD_SHA=$(echo "$PR_DATA" | jq -r '.head.sha') |
| 40 | + HEAD_REF=$(echo "$PR_DATA" | jq -r '.head.ref') |
| 41 | + BASE_REF=$(echo "$PR_DATA" | jq -r '.base.ref') |
| 42 | + echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT" |
| 43 | + echo "head_ref=$HEAD_REF" >> "$GITHUB_OUTPUT" |
| 44 | + echo "base_ref=$BASE_REF" >> "$GITHUB_OUTPUT" |
| 45 | +
|
| 46 | + - name: Add reaction to comment |
| 47 | + if: github.event_name == 'issue_comment' |
| 48 | + env: |
| 49 | + GH_TOKEN: ${{ github.token }} |
| 50 | + run: | |
| 51 | + gh api repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions \ |
| 52 | + -f content='+1' |
| 53 | +
|
| 54 | + - uses: actions/checkout@master |
| 55 | + with: |
| 56 | + ref: ${{ steps.pr.outputs.head_sha }} |
| 57 | + fetch-depth: 0 |
| 58 | + |
| 59 | + - name: Get merge-base |
| 60 | + id: commits |
| 61 | + run: | |
| 62 | + git fetch origin ${{ steps.pr.outputs.base_ref }} |
| 63 | + MERGE_BASE=$(git merge-base HEAD origin/${{ steps.pr.outputs.base_ref }}) |
| 64 | + echo "merge_base=$MERGE_BASE" >> "$GITHUB_OUTPUT" |
| 65 | +
|
| 66 | + - name: Generate matrix |
| 67 | + id: matrix |
| 68 | + run: | |
| 69 | + # Prepend hardcoded periphery (self) project to the list from config |
| 70 | + PERIPHERY='{"name": "periphery", "self": true}' |
| 71 | + MATRIX=$(jq -c --argjson periphery "$PERIPHERY" '{project: ([$periphery] + .projects)}' .github/benchmark-projects.json) |
| 72 | + echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT" |
| 73 | +
|
| 74 | + - name: Select Xcode version |
| 75 | + run: sudo xcode-select -s /Applications/Xcode_26.2.0.app |
| 76 | + |
| 77 | + - name: Build HEAD |
| 78 | + run: swift build -c release --product periphery |
| 79 | + |
| 80 | + - name: Upload HEAD build |
| 81 | + uses: actions/upload-artifact@v4 |
| 82 | + with: |
| 83 | + name: periphery-head |
| 84 | + path: .build/release/periphery |
| 85 | + |
| 86 | + - name: Build merge-base |
| 87 | + run: | |
| 88 | + git checkout ${{ steps.commits.outputs.merge_base }} |
| 89 | + swift build -c release --product periphery |
| 90 | +
|
| 91 | + - name: Upload merge-base build |
| 92 | + uses: actions/upload-artifact@v4 |
| 93 | + with: |
| 94 | + name: periphery-base |
| 95 | + path: .build/release/periphery |
| 96 | + |
| 97 | + benchmark: |
| 98 | + name: Benchmark (${{ matrix.project.name }}) |
| 99 | + needs: setup |
| 100 | + runs-on: macos-26 |
| 101 | + permissions: |
| 102 | + contents: read |
| 103 | + strategy: |
| 104 | + fail-fast: false |
| 105 | + matrix: ${{ fromJson(needs.setup.outputs.matrix) }} |
| 106 | + steps: |
| 107 | + - uses: actions/checkout@master |
| 108 | + if: ${{ matrix.project.self }} |
| 109 | + with: |
| 110 | + ref: ${{ needs.setup.outputs.head_sha }} |
| 111 | + fetch-depth: 0 |
| 112 | + |
| 113 | + - name: Select Xcode version |
| 114 | + run: sudo xcode-select -s /Applications/Xcode_26.2.0.app |
| 115 | + |
| 116 | + - name: Download HEAD build |
| 117 | + uses: actions/download-artifact@v4 |
| 118 | + with: |
| 119 | + name: periphery-head |
| 120 | + path: .build/release |
| 121 | + |
| 122 | + - name: Make binary executable |
| 123 | + run: chmod +x .build/release/periphery |
| 124 | + |
| 125 | + - name: Install hyperfine |
| 126 | + run: brew install hyperfine |
| 127 | + |
| 128 | + - name: Clone target project |
| 129 | + if: ${{ !matrix.project.self }} |
| 130 | + run: | |
| 131 | + git clone --depth 1 --branch ${{ matrix.project.ref }} ${{ matrix.project.url }} /tmp/target-project |
| 132 | +
|
| 133 | + - name: Build project |
| 134 | + working-directory: ${{ matrix.project.self && '.' || '/tmp/target-project' }} |
| 135 | + run: swift build |
| 136 | + |
| 137 | + - name: Benchmark HEAD (self) |
| 138 | + if: ${{ matrix.project.self }} |
| 139 | + run: | |
| 140 | + hyperfine --show-output --warmup 3 --export-json head-benchmark.json \ |
| 141 | + './.build/release/periphery scan --quiet --skip-build' |
| 142 | +
|
| 143 | + - name: Benchmark HEAD (external) |
| 144 | + if: ${{ !matrix.project.self }} |
| 145 | + working-directory: /tmp/target-project |
| 146 | + run: | |
| 147 | + hyperfine --show-output --warmup 3 --export-json ${{ github.workspace }}/head-benchmark.json \ |
| 148 | + '${{ github.workspace }}/.build/release/periphery scan --quiet --skip-build' |
| 149 | +
|
| 150 | + - name: Download merge-base build |
| 151 | + uses: actions/download-artifact@v4 |
| 152 | + with: |
| 153 | + name: periphery-base |
| 154 | + path: .build/release |
| 155 | + |
| 156 | + - name: Make binary executable |
| 157 | + run: chmod +x .build/release/periphery |
| 158 | + |
| 159 | + - name: Benchmark merge-base (self) |
| 160 | + if: ${{ matrix.project.self }} |
| 161 | + run: | |
| 162 | + hyperfine --show-output --warmup 3 --export-json base-benchmark.json \ |
| 163 | + './.build/release/periphery scan --quiet --skip-build' |
| 164 | +
|
| 165 | + - name: Benchmark merge-base (external) |
| 166 | + if: ${{ !matrix.project.self }} |
| 167 | + working-directory: /tmp/target-project |
| 168 | + run: | |
| 169 | + hyperfine --show-output --warmup 3 --export-json ${{ github.workspace }}/base-benchmark.json \ |
| 170 | + '${{ github.workspace }}/.build/release/periphery scan --quiet --skip-build' |
| 171 | +
|
| 172 | + - name: Generate result summary |
| 173 | + run: | |
| 174 | + HEAD_MEAN=$(jq '.results[0].mean' head-benchmark.json) |
| 175 | + BASE_MEAN=$(jq '.results[0].mean' base-benchmark.json) |
| 176 | + HEAD_STDDEV=$(jq '.results[0].stddev' head-benchmark.json) |
| 177 | + BASE_STDDEV=$(jq '.results[0].stddev' base-benchmark.json) |
| 178 | + CHANGE=$(echo "scale=2; (($HEAD_MEAN - $BASE_MEAN) / $BASE_MEAN) * 100" | bc) |
| 179 | +
|
| 180 | + jq -n \ |
| 181 | + --arg name "${{ matrix.project.name }}" \ |
| 182 | + --argjson head_mean "$HEAD_MEAN" \ |
| 183 | + --argjson base_mean "$BASE_MEAN" \ |
| 184 | + --argjson head_stddev "$HEAD_STDDEV" \ |
| 185 | + --argjson base_stddev "$BASE_STDDEV" \ |
| 186 | + --argjson change "$CHANGE" \ |
| 187 | + '{name: $name, head_mean: $head_mean, base_mean: $base_mean, head_stddev: $head_stddev, base_stddev: $base_stddev, change: $change}' \ |
| 188 | + > result-${{ matrix.project.name }}.json |
| 189 | +
|
| 190 | + - name: Upload benchmark results |
| 191 | + uses: actions/upload-artifact@v4 |
| 192 | + with: |
| 193 | + name: benchmark-${{ matrix.project.name }} |
| 194 | + path: | |
| 195 | + head-benchmark.json |
| 196 | + base-benchmark.json |
| 197 | + result-${{ matrix.project.name }}.json |
| 198 | +
|
| 199 | + summary: |
| 200 | + name: Post Results |
| 201 | + needs: [setup, benchmark] |
| 202 | + runs-on: ubuntu-latest |
| 203 | + permissions: |
| 204 | + pull-requests: write |
| 205 | + steps: |
| 206 | + - name: Download all artifacts |
| 207 | + uses: actions/download-artifact@v4 |
| 208 | + with: |
| 209 | + pattern: benchmark-* |
| 210 | + merge-multiple: true |
| 211 | + |
| 212 | + - name: Generate comment |
| 213 | + id: comment |
| 214 | + run: | |
| 215 | + echo "## Benchmark Results" > comment.md |
| 216 | + echo "" >> comment.md |
| 217 | + echo "Comparing \`${{ needs.setup.outputs.merge_base }}\` (merge-base) → \`${{ needs.setup.outputs.head_sha }}\` (HEAD)" >> comment.md |
| 218 | + echo "" >> comment.md |
| 219 | + echo "| Project | Merge-base (s) | HEAD (s) | Change |" >> comment.md |
| 220 | + echo "|---------|----------------|----------|--------|" >> comment.md |
| 221 | +
|
| 222 | + for f in result-*.json; do |
| 223 | + NAME=$(jq -r '.name' "$f") |
| 224 | + BASE_MEAN=$(jq -r '.base_mean' "$f") |
| 225 | + HEAD_MEAN=$(jq -r '.head_mean' "$f") |
| 226 | + BASE_STDDEV=$(jq -r '.base_stddev' "$f") |
| 227 | + HEAD_STDDEV=$(jq -r '.head_stddev' "$f") |
| 228 | + CHANGE=$(jq -r '.change' "$f") |
| 229 | +
|
| 230 | + printf "| %s | %.3f ±%.3f | %.3f ±%.3f | %+.1f%% |\n" \ |
| 231 | + "$NAME" "$BASE_MEAN" "$BASE_STDDEV" "$HEAD_MEAN" "$HEAD_STDDEV" "$CHANGE" >> comment.md |
| 232 | + done |
| 233 | +
|
| 234 | + - name: Post comment |
| 235 | + env: |
| 236 | + GH_TOKEN: ${{ github.token }} |
| 237 | + run: | |
| 238 | + gh pr comment ${{ needs.setup.outputs.pr_number }} \ |
| 239 | + --repo ${{ github.repository }} \ |
| 240 | + --body-file comment.md |
| 241 | +
|
| 242 | + - name: Remove benchmark label |
| 243 | + if: github.event_name == 'pull_request' |
| 244 | + env: |
| 245 | + GH_TOKEN: ${{ github.token }} |
| 246 | + run: | |
| 247 | + gh pr edit ${{ needs.setup.outputs.pr_number }} \ |
| 248 | + --repo ${{ github.repository }} \ |
| 249 | + --remove-label benchmark |
0 commit comments