Skip to content

Commit 84dd35d

Browse files
committed
feat: add codspeed benchmarks for valgrind
1 parent f03d9fa commit 84dd35d

6 files changed

Lines changed: 292 additions & 0 deletions

File tree

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
bench/testdata/* filter=lfs diff=lfs merge=lfs -text

.github/workflows/ci.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ jobs:
2222
steps:
2323
- uses: actions/checkout@v4
2424

25+
# Skip installing package docs to avoid wasting time when installing valgrind
26+
# See: https://github.com/actions/runner-images/issues/10977#issuecomment-2810713336
27+
- name: Skip installing package docs
28+
if: runner.os == 'Linux'
29+
run: |
30+
sudo tee /etc/dpkg/dpkg.cfg.d/01_nodoc > /dev/null << 'EOF'
31+
path-exclude /usr/share/doc/*
32+
path-exclude /usr/share/man/*
33+
path-exclude /usr/share/info/*
34+
EOF
35+
2536
- name: Update apt-get cache
2637
run: sudo apt-get update
2738

.github/workflows/codspeed.yml

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
name: CodSpeed Benchmarks
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
pull_request:
8+
workflow_dispatch:
9+
10+
jobs:
11+
benchmarks:
12+
runs-on: codspeed-macro
13+
strategy:
14+
matrix:
15+
# IMPORTANT: The binary has to match the architecture of the runner!
16+
benchmark:
17+
- testdata/take_strings-aarch64
18+
steps:
19+
- uses: actions/checkout@v4
20+
with:
21+
lfs: true
22+
23+
# Skip installing package docs to avoid wasting time when installing valgrind
24+
# See: https://github.com/actions/runner-images/issues/10977#issuecomment-2810713336
25+
- name: Skip installing package docs
26+
if: runner.os == 'Linux'
27+
run: |
28+
sudo tee /etc/dpkg/dpkg.cfg.d/01_nodoc > /dev/null << 'EOF'
29+
path-exclude /usr/share/doc/*
30+
path-exclude /usr/share/man/*
31+
path-exclude /usr/share/info/*
32+
EOF
33+
34+
# Build and install Valgrind
35+
#
36+
- name: Update apt-get cache
37+
run: sudo apt-get update
38+
39+
- name: Install build dependencies
40+
run: |
41+
sudo apt-get install -y \
42+
build-essential \
43+
automake \
44+
autoconf \
45+
libc6-dev \
46+
gdb \
47+
docbook \
48+
docbook-xsl \
49+
docbook-xml \
50+
xsltproc
51+
52+
- name: Run autogen
53+
run: ./autogen.sh
54+
55+
- name: Configure
56+
run: ./configure
57+
58+
- name: Build Valgrind
59+
run: make -j$(nproc)
60+
61+
- name: Verify Valgrind build
62+
run: |
63+
# Verify that vg-in-place script exists
64+
test -f ./vg-in-place || { echo "vg-in-place not found!"; exit 1; }
65+
# Test valgrind works with vg-in-place
66+
./vg-in-place --version
67+
./vg-in-place --tool=callgrind --help > /dev/null || { echo "callgrind tool not accessible!"; exit 1; }
68+
echo "Valgrind build successful and callgrind tool is accessible"
69+
70+
# Setup benchmarks and run them
71+
#
72+
- name: Install uv
73+
uses: astral-sh/setup-uv@v5
74+
75+
- name: Run the benchmarks
76+
uses: CodSpeedHQ/action@main
77+
env:
78+
# We currently don't support sub-processes in benchmarks, since we cannot find the
79+
# benchmark root frame when using the process with most samples.
80+
CODSPEED_PERF_ENABLED: false
81+
with:
82+
working-directory: bench
83+
mode: walltime
84+
run: uv run bench.py --binary-path ${{ matrix.benchmark }} --valgrind-path ${{ github.workspace }}/vg-in-place

bench/bench.py

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
#!/usr/bin/env python3
2+
# /// script
3+
# requires-python = ">=3.9"
4+
# dependencies = [
5+
# "pytest>=8.0",
6+
# "pytest-codspeed>=4.2.0",
7+
# ]
8+
# ///
9+
10+
import argparse
11+
import os
12+
import subprocess
13+
from pathlib import Path
14+
15+
import pytest
16+
17+
18+
class ValgrindRunner:
19+
"""Run Valgrind with different configurations."""
20+
21+
def __init__(
22+
self,
23+
binary_path: str,
24+
valgrind_path: str = "valgrind",
25+
output_dir: str = "/tmp",
26+
):
27+
"""Initialize valgrind runner.
28+
29+
Args:
30+
binary_path: Path to the binary to profile
31+
valgrind_path: Path to valgrind executable
32+
output_dir: Directory for callgrind output files
33+
"""
34+
self.binary_path = Path(binary_path)
35+
self.valgrind_path = valgrind_path
36+
self.output_dir = Path(output_dir)
37+
self.output_dir.mkdir(parents=True, exist_ok=True)
38+
39+
if not self.binary_path.exists():
40+
raise FileNotFoundError(f"Binary not found: {self.binary_path}")
41+
42+
# Verify valgrind is available
43+
result = subprocess.run(
44+
[self.valgrind_path, "--version"],
45+
capture_output=True,
46+
text=True,
47+
)
48+
if result.returncode != 0:
49+
raise RuntimeError(f"Valgrind not found at: {self.valgrind_path}")
50+
self.valgrind_version = result.stdout.strip()
51+
52+
def run_valgrind(self, *args: str) -> None:
53+
"""Execute valgrind with given arguments.
54+
55+
Args:
56+
*args: Valgrind arguments
57+
"""
58+
callgrind_output = self.output_dir / f"callgrind.{os.getpid()}"
59+
60+
cmd = [
61+
self.valgrind_path,
62+
"--tool=callgrind",
63+
f"--callgrind-out-file={callgrind_output}",
64+
*args,
65+
str(self.binary_path),
66+
]
67+
68+
result = subprocess.run(executable=self.valgrind_path, args=cmd)
69+
if result.returncode != 0:
70+
raise RuntimeError(
71+
f"Valgrind execution failed with code {result.returncode}"
72+
)
73+
74+
# Clean up
75+
if callgrind_output.exists():
76+
callgrind_output.unlink()
77+
78+
79+
@pytest.fixture
80+
def runner(request):
81+
"""Fixture to provide runner instance to tests."""
82+
return request.config._valgrind_runner
83+
84+
85+
@pytest.mark.benchmark
86+
def test_baseline(runner):
87+
if runner:
88+
runner.run_valgrind("--read-inline-info=no")
89+
90+
91+
@pytest.mark.benchmark
92+
def test_with_inline_info(runner):
93+
if runner:
94+
runner.run_valgrind("--read-inline-info=yes")
95+
96+
97+
@pytest.mark.benchmark
98+
def test_full_inline(runner):
99+
if runner:
100+
args = [
101+
"--trace-children=yes",
102+
"--cache-sim=yes",
103+
"--I1=32768,8,64",
104+
"--D1=32768,8,64",
105+
"--LL=8388608,16,64",
106+
"--collect-systime=nsec",
107+
"--compress-strings=no",
108+
"--combine-dumps=yes",
109+
"--dump-line=no",
110+
"--read-inline-info=yes",
111+
]
112+
runner.run_valgrind(*args)
113+
114+
115+
@pytest.mark.benchmark
116+
def test_full(runner):
117+
if runner:
118+
args = [
119+
"--trace-children=yes",
120+
"--cache-sim=yes",
121+
"--I1=32768,8,64",
122+
"--D1=32768,8,64",
123+
"--LL=8388608,16,64",
124+
"--collect-systime=nsec",
125+
"--compress-strings=no",
126+
"--combine-dumps=yes",
127+
"--dump-line=no",
128+
]
129+
runner.run_valgrind(*args)
130+
131+
132+
def main():
133+
parser = argparse.ArgumentParser(
134+
description="Benchmark Valgrind with pytest-codspeed",
135+
formatter_class=argparse.RawDescriptionHelpFormatter,
136+
epilog="""
137+
Examples:
138+
# Run with default binary
139+
uv run bench.py --binary-path /path/to/binary
140+
141+
# Run with custom valgrind installation
142+
uv run bench.py --binary-path /path/to/binary --valgrind-path /usr/local/bin/valgrind
143+
""",
144+
)
145+
146+
parser.add_argument(
147+
"--binary-path",
148+
type=str,
149+
required=True,
150+
help="Path to the binary to profile",
151+
)
152+
parser.add_argument(
153+
"--valgrind-path",
154+
type=str,
155+
default="valgrind",
156+
help="Path to valgrind executable (default: valgrind)",
157+
)
158+
parser.add_argument(
159+
"--output-dir",
160+
type=str,
161+
default="/tmp",
162+
help="Directory for callgrind files (default: /tmp)",
163+
)
164+
165+
args = parser.parse_args()
166+
167+
# Create runner instance
168+
runner = ValgrindRunner(
169+
binary_path=args.binary_path,
170+
valgrind_path=args.valgrind_path,
171+
output_dir=args.output_dir,
172+
)
173+
print(f"Valgrind version: {runner.valgrind_version}")
174+
print(f"Binary: {args.binary_path}")
175+
176+
# Plugin to pass runner to tests
177+
class RunnerPlugin:
178+
def pytest_configure(self, config):
179+
config._valgrind_runner = runner
180+
181+
exit_code = pytest.main(
182+
[__file__, "-v", "--codspeed"],
183+
plugins=[RunnerPlugin()],
184+
)
185+
if exit_code != 0 and exit_code != 5:
186+
print(f"Benchmark execution returned exit code: {exit_code}")
187+
188+
189+
if __name__ == "__main__":
190+
main()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:d241a1c2932e11d4b5226d193ecf7c120bb881f5f0108884071048dcd5bd6696
3+
size 282407216

bench/testdata/take_strings-x86_64

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:c184f81f7046a8a78cb272ac4a1c7ad616b5e3dd20dcc40638f1db485abc5b22
3+
size 272199232

0 commit comments

Comments
 (0)