Skip to content

Commit febdce6

Browse files
Add iai-callgrind benchmarks and CI workflow
Co-authored-by: samueltardieu <[email protected]>
1 parent 2b783da commit febdce6

File tree

8 files changed

+804
-0
lines changed

8 files changed

+804
-0
lines changed
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
name: iai-callgrind Benchmarks
2+
3+
on:
4+
pull_request:
5+
merge_group:
6+
7+
jobs:
8+
benchmarks:
9+
name: Run iai-callgrind benchmarks
10+
runs-on: ubuntu-latest
11+
permissions:
12+
pull-requests: write
13+
steps:
14+
- uses: actions/checkout@v6
15+
name: Checkout PR branch
16+
with:
17+
fetch-depth: 0
18+
19+
- name: Install Rust toolchain
20+
run: |
21+
rustup install --profile minimal stable
22+
rustup default stable
23+
24+
- name: Install valgrind
25+
run: sudo apt-get update && sudo apt-get install -y valgrind
26+
27+
- name: Install iai-callgrind-runner
28+
uses: baptiste0928/cargo-install@v3
29+
with:
30+
crate: iai-callgrind-runner
31+
32+
- uses: Swatinem/rust-cache@v2
33+
with:
34+
key: iai-callgrind
35+
36+
- name: Get base branch name
37+
id: base_branch
38+
run: |
39+
if [ "${{ github.event_name }}" = "pull_request" ]; then
40+
echo "name=${{ github.base_ref }}" >> "$GITHUB_OUTPUT"
41+
else
42+
echo "name=main" >> "$GITHUB_OUTPUT"
43+
fi
44+
45+
- name: Checkout base branch
46+
run: |
47+
git fetch origin ${{ steps.base_branch.outputs.name }}
48+
git checkout origin/${{ steps.base_branch.outputs.name }}
49+
50+
- name: Run benchmarks on base branch
51+
continue-on-error: true
52+
run: |
53+
echo "Running benchmarks on base branch: ${{ steps.base_branch.outputs.name }}"
54+
cargo bench --features iai --bench iai_algos --bench iai_edmondskarp --bench iai_kuhn_munkres --bench iai_separate_components 2>&1 | tee baseline-output.txt
55+
56+
- name: Checkout PR branch
57+
run: git checkout ${{ github.sha }}
58+
59+
- name: Clear target directory for PR build
60+
run: cargo clean
61+
62+
- name: Run benchmarks on PR branch
63+
run: |
64+
echo "Running benchmarks on PR branch"
65+
cargo bench --features iai --bench iai_algos --bench iai_edmondskarp --bench iai_kuhn_munkres --bench iai_separate_components 2>&1 | tee pr-output.txt
66+
67+
- name: Parse and compare results
68+
if: github.event_name == 'pull_request'
69+
id: parse_results
70+
run: |
71+
python3 << 'EOF'
72+
import re
73+
import os
74+
75+
def parse_benchmark_output(filename):
76+
"""Parse iai-callgrind output and extract benchmark results."""
77+
benchmarks = {}
78+
try:
79+
with open(filename, 'r') as f:
80+
content = f.read()
81+
82+
# Pattern to match benchmark names and their metrics
83+
benchmark_pattern = r'([^\n]+?)::[^\n]+?::([^\n]+?)\n\s+Instructions:\s+(\d+)'
84+
85+
for match in re.finditer(benchmark_pattern, content):
86+
bench_name = f"{match.group(1)}::{match.group(2)}"
87+
instructions = int(match.group(3))
88+
benchmarks[bench_name] = instructions
89+
except FileNotFoundError:
90+
pass
91+
92+
return benchmarks
93+
94+
baseline = parse_benchmark_output('baseline-output.txt')
95+
pr_results = parse_benchmark_output('pr-output.txt')
96+
97+
# Create markdown comment
98+
comment = "## 📊 iai-callgrind Benchmark Results\n\n"
99+
100+
if not baseline:
101+
comment += "⚠️ **No baseline benchmarks found.** This may be the first time these benchmarks are run on the base branch.\n\n"
102+
comment += "### PR Branch Results\n\n"
103+
comment += "| Benchmark | Instructions |\n"
104+
comment += "|-----------|-------------|\n"
105+
for name, instr in sorted(pr_results.items()):
106+
comment += f"| `{name}` | {instr:,} |\n"
107+
else:
108+
# Compare results
109+
improvements = []
110+
regressions = []
111+
unchanged = []
112+
new_benchmarks = []
113+
114+
for name, pr_instr in sorted(pr_results.items()):
115+
if name in baseline:
116+
base_instr = baseline[name]
117+
diff = pr_instr - base_instr
118+
pct_change = (diff / base_instr) * 100 if base_instr > 0 else 0
119+
120+
result = {
121+
'name': name,
122+
'base': base_instr,
123+
'pr': pr_instr,
124+
'diff': diff,
125+
'pct': pct_change
126+
}
127+
128+
if abs(pct_change) < 0.1: # Less than 0.1% change
129+
unchanged.append(result)
130+
elif diff < 0:
131+
improvements.append(result)
132+
else:
133+
regressions.append(result)
134+
else:
135+
new_benchmarks.append({'name': name, 'pr': pr_instr})
136+
137+
# Summary
138+
if regressions:
139+
comment += f"### ⚠️ {len(regressions)} Regression(s) Detected\n\n"
140+
comment += "| Benchmark | Base | PR | Change | % |\n"
141+
comment += "|-----------|------|----|---------|\n"
142+
for r in sorted(regressions, key=lambda x: abs(x['pct']), reverse=True):
143+
comment += f"| `{r['name']}` | {r['base']:,} | {r['pr']:,} | +{r['diff']:,} | +{r['pct']:.2f}% |\n"
144+
comment += "\n"
145+
146+
if improvements:
147+
comment += f"### ✅ {len(improvements)} Improvement(s)\n\n"
148+
comment += "| Benchmark | Base | PR | Change | % |\n"
149+
comment += "|-----------|------|----|---------|\n"
150+
for r in sorted(improvements, key=lambda x: abs(x['pct']), reverse=True):
151+
comment += f"| `{r['name']}` | {r['base']:,} | {r['pr']:,} | {r['diff']:,} | {r['pct']:.2f}% |\n"
152+
comment += "\n"
153+
154+
if unchanged:
155+
comment += f"### ➡️ {len(unchanged)} Unchanged (within ±0.1%)\n\n"
156+
comment += "<details><summary>Click to expand</summary>\n\n"
157+
comment += "| Benchmark | Instructions |\n"
158+
comment += "|-----------|-------------|\n"
159+
for r in unchanged:
160+
comment += f"| `{r['name']}` | {r['pr']:,} |\n"
161+
comment += "\n</details>\n\n"
162+
163+
if new_benchmarks:
164+
comment += f"### 🆕 {len(new_benchmarks)} New Benchmark(s)\n\n"
165+
comment += "| Benchmark | Instructions |\n"
166+
comment += "|-----------|-------------|\n"
167+
for nb in new_benchmarks:
168+
comment += f"| `{nb['name']}` | {nb['pr']:,} |\n"
169+
comment += "\n"
170+
171+
if not regressions and not improvements and not new_benchmarks:
172+
comment += "### ✅ All benchmarks unchanged\n\n"
173+
174+
comment += "\n---\n"
175+
comment += "*iai-callgrind measures instructions executed, which is deterministic and not affected by system load.*\n"
176+
177+
# Write to file
178+
with open('comment.txt', 'w') as f:
179+
f.write(comment)
180+
181+
print("Comment generated successfully")
182+
EOF
183+
184+
- name: Post comment to PR
185+
if: github.event_name == 'pull_request'
186+
uses: actions/github-script@v7
187+
with:
188+
script: |
189+
const fs = require('fs');
190+
const comment = fs.readFileSync('comment.txt', 'utf8');
191+
192+
// Find existing comment
193+
const { data: comments } = await github.rest.issues.listComments({
194+
owner: context.repo.owner,
195+
repo: context.repo.repo,
196+
issue_number: context.issue.number,
197+
});
198+
199+
const botComment = comments.find(comment =>
200+
comment.user.type === 'Bot' &&
201+
comment.body.includes('iai-callgrind Benchmark Results')
202+
);
203+
204+
if (botComment) {
205+
// Update existing comment
206+
await github.rest.issues.updateComment({
207+
owner: context.repo.owner,
208+
repo: context.repo.repo,
209+
comment_id: botComment.id,
210+
body: comment
211+
});
212+
} else {
213+
// Create new comment
214+
await github.rest.issues.createComment({
215+
owner: context.repo.owner,
216+
repo: context.repo.repo,
217+
issue_number: context.issue.number,
218+
body: comment
219+
});
220+
}
221+
222+
- name: Add summary
223+
if: always()
224+
run: |
225+
if [ -f comment.txt ]; then
226+
cat comment.txt >> $GITHUB_STEP_SUMMARY
227+
else
228+
echo "## Benchmark Results" >> $GITHUB_STEP_SUMMARY
229+
echo "Benchmark comparison was not generated." >> $GITHUB_STEP_SUMMARY
230+
fi

Cargo.lock

Lines changed: 98 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)