Skip to content

Commit 441b38f

Browse files
committed
feat: add codspeed benchmarks for valgrind
1 parent c9593ee commit 441b38f

6 files changed

Lines changed: 314 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: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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+
valgrind:
19+
- /usr/bin/valgrind
20+
- ../vg-in-place
21+
steps:
22+
- uses: actions/checkout@v4
23+
with:
24+
lfs: true
25+
26+
# Skip installing package docs to avoid wasting time when installing build dependencies
27+
# See: https://github.com/actions/runner-images/issues/10977#issuecomment-2810713336
28+
- name: Skip installing package docs
29+
if: runner.os == 'Linux'
30+
run: |
31+
sudo tee /etc/dpkg/dpkg.cfg.d/01_nodoc > /dev/null << 'EOF'
32+
path-exclude /usr/share/doc/*
33+
path-exclude /usr/share/man/*
34+
path-exclude /usr/share/info/*
35+
EOF
36+
37+
# Build and install Valgrind
38+
- name: Update apt-get cache
39+
run: sudo apt-get update
40+
41+
- name: Install build dependencies
42+
run: |
43+
sudo apt-get install -y \
44+
build-essential \
45+
automake \
46+
autoconf \
47+
libc6-dev \
48+
gdb \
49+
docbook \
50+
docbook-xsl \
51+
docbook-xml \
52+
xsltproc
53+
54+
- name: Run autogen
55+
run: ./autogen.sh
56+
57+
- name: Configure
58+
run: ./configure
59+
60+
- name: Build Valgrind
61+
run: make -j$(nproc)
62+
63+
- name: Verify Valgrind build
64+
run: |
65+
# Verify that vg-in-place script exists
66+
test -f ./vg-in-place || { echo "vg-in-place not found!"; exit 1; }
67+
# Test valgrind works with vg-in-place
68+
./vg-in-place --version
69+
./vg-in-place --tool=callgrind --help > /dev/null || { echo "callgrind tool not accessible!"; exit 1; }
70+
echo "Valgrind build successful and callgrind tool is accessible"
71+
72+
# Install upstream Valgrind for comparison
73+
- name: Install upstream Valgrind
74+
run: sudo apt-get install -y valgrind
75+
76+
# Setup benchmarks and run them
77+
- name: Install uv
78+
uses: astral-sh/setup-uv@v5
79+
80+
# Ensure that the runner doesn't install it's own valgrind by linking vg-in-place to /usr/local/bin/valgrind,
81+
# which makes the version check work correctly.
82+
- name: Link vg-in-place to /usr/local/bin/valgrind
83+
run: sudo ln -sf "$PWD/vg-in-place" /usr/local/bin/valgrind
84+
85+
- name: Run the benchmarks
86+
uses: CodSpeedHQ/action@main
87+
env:
88+
# We currently don't support sub-processes in benchmarks, since we cannot find the
89+
# benchmark root frame when using the process with most samples.
90+
CODSPEED_PERF_ENABLED: false
91+
with:
92+
working-directory: bench
93+
mode: walltime
94+
run: uv run bench.py --binary-path ${{ matrix.benchmark }} --valgrind-path ${{ matrix.valgrind }}

bench/bench.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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+
def pytest_generate_tests(metafunc):
86+
"""Parametrize tests with benchmark_id."""
87+
if "benchmark_context" in metafunc.fixturenames:
88+
runner = getattr(metafunc.config, "_valgrind_runner", None)
89+
if not runner:
90+
metafunc.parametrize("benchmark_context", ["unknown"])
91+
return
92+
93+
benchmark_id = f"{runner.binary_path.stem}, {runner.valgrind_version}"
94+
metafunc.parametrize("benchmark_context", [benchmark_id], ids=[benchmark_id])
95+
96+
97+
@pytest.mark.benchmark
98+
def test_baseline(runner, benchmark_context):
99+
if runner:
100+
runner.run_valgrind("--read-inline-info=no")
101+
102+
103+
@pytest.mark.benchmark
104+
def test_with_inline_info(runner, benchmark_context):
105+
if runner:
106+
runner.run_valgrind("--read-inline-info=yes")
107+
108+
109+
@pytest.mark.benchmark
110+
def test_full_inline(runner, benchmark_context):
111+
if runner:
112+
args = [
113+
"--trace-children=yes",
114+
"--cache-sim=yes",
115+
"--I1=32768,8,64",
116+
"--D1=32768,8,64",
117+
"--LL=8388608,16,64",
118+
"--collect-systime=nsec",
119+
"--compress-strings=no",
120+
"--combine-dumps=yes",
121+
"--dump-line=no",
122+
"--read-inline-info=yes",
123+
]
124+
runner.run_valgrind(*args)
125+
126+
127+
@pytest.mark.benchmark
128+
def test_full(runner, benchmark_context):
129+
if runner:
130+
args = [
131+
"--trace-children=yes",
132+
"--cache-sim=yes",
133+
"--I1=32768,8,64",
134+
"--D1=32768,8,64",
135+
"--LL=8388608,16,64",
136+
"--collect-systime=nsec",
137+
"--compress-strings=no",
138+
"--combine-dumps=yes",
139+
"--dump-line=no",
140+
]
141+
runner.run_valgrind(*args)
142+
143+
144+
def main():
145+
parser = argparse.ArgumentParser(
146+
description="Benchmark Valgrind with pytest-codspeed",
147+
formatter_class=argparse.RawDescriptionHelpFormatter,
148+
epilog="""
149+
Examples:
150+
# Run with default binary
151+
uv run bench.py --binary-path /path/to/binary
152+
153+
# Run with custom valgrind installation
154+
uv run bench.py --binary-path /path/to/binary --valgrind-path /usr/local/bin/valgrind
155+
""",
156+
)
157+
158+
parser.add_argument(
159+
"--binary-path",
160+
type=str,
161+
required=True,
162+
help="Path to the binary to profile",
163+
)
164+
parser.add_argument(
165+
"--valgrind-path",
166+
type=str,
167+
default="valgrind",
168+
help="Path to valgrind executable (default: valgrind)",
169+
)
170+
parser.add_argument(
171+
"--output-dir",
172+
type=str,
173+
default="/tmp",
174+
help="Directory for callgrind files (default: /tmp)",
175+
)
176+
177+
args = parser.parse_args()
178+
179+
# Create runner instance
180+
runner = ValgrindRunner(
181+
binary_path=args.binary_path,
182+
valgrind_path=args.valgrind_path,
183+
output_dir=args.output_dir,
184+
)
185+
print(f"Valgrind version: {runner.valgrind_version}")
186+
print(f"Binary: {args.binary_path}")
187+
188+
# Plugin to pass runner to tests
189+
class RunnerPlugin:
190+
def pytest_configure(self, config):
191+
config._valgrind_runner = runner
192+
193+
exit_code = pytest.main(
194+
[__file__, "-v", "--codspeed"],
195+
plugins=[RunnerPlugin()],
196+
)
197+
if exit_code != 0 and exit_code != 5:
198+
print(f"Benchmark execution returned exit code: {exit_code}")
199+
200+
201+
if __name__ == "__main__":
202+
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)