Skip to content

Commit 9282f77

Browse files
committed
feat: add codspeed benchmarks for valgrind
1 parent f03d9fa commit 9282f77

4 files changed

Lines changed: 238 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/codspeed.yml

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
benchmark:
16+
- testdata/take_strings
17+
steps:
18+
- uses: actions/checkout@v4
19+
with:
20+
lfs: true
21+
22+
# Build and install Valgrind
23+
#
24+
- name: Update apt-get cache
25+
run: sudo apt-get update
26+
27+
- name: Install build dependencies
28+
run: |
29+
sudo apt-get install -y \
30+
build-essential \
31+
automake \
32+
autoconf \
33+
libc6-dev \
34+
gcc-multilib \
35+
libc6-dev-i386 \
36+
gdb \
37+
docbook \
38+
docbook-xsl \
39+
docbook-xml \
40+
xsltproc
41+
42+
- name: Run autogen
43+
run: ./autogen.sh
44+
45+
- name: Configure
46+
run: ./configure
47+
48+
- name: Build Valgrind
49+
run: make -j$(nproc)
50+
51+
- name: Install valgrind
52+
run: sudo make install
53+
54+
# Setup benchmarks and run them
55+
#
56+
- name: Install uv
57+
uses: astral-sh/setup-uv@v5
58+
59+
- name: Run benchmark
60+
working-directory: bench
61+
run: uv run bench.py --binary-path ${{ matrix.benchmark }}
62+
63+
- name: Run the benchmarks
64+
uses: CodSpeedHQ/action@main
65+
with:
66+
mode: walltime

bench/bench.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
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(cmd)
69+
if result.returncode != 0:
70+
raise RuntimeError(f"Valgrind execution failed with code {result.returncode}")
71+
72+
# Clean up
73+
if callgrind_output.exists():
74+
callgrind_output.unlink()
75+
76+
77+
# Store runner in pytest config
78+
_runner = None
79+
80+
81+
@pytest.fixture
82+
def runner(request):
83+
"""Fixture to provide runner instance to tests."""
84+
return request.config._valgrind_runner
85+
86+
87+
def test_baseline(runner):
88+
if runner:
89+
runner.run_valgrind("--read-inline-info=no")
90+
91+
92+
def test_with_inline_info(runner):
93+
if runner:
94+
runner.run_valgrind("--read-inline-info=yes")
95+
96+
97+
def test_full_inline(runner):
98+
if runner:
99+
runner.run_valgrind("--trace-children=yes", "--cache-sim=yes", "--I1=32768,8,64", "--D1=32768,8,64", "--LL=8388608,16,64", "--collect-systime=nsec", "--compress-strings=no", "--combine-dumps=yes", "--dump-line=no", "--read-inline-info=yes")
100+
101+
def test_full(runner):
102+
if runner:
103+
runner.run_valgrind("--trace-children=yes", "--cache-sim=yes", "--I1=32768,8,64", "--D1=32768,8,64", "--LL=8388608,16,64", "--collect-systime=nsec", "--compress-strings=no", "--combine-dumps=yes", "--dump-line=no")
104+
105+
106+
def main():
107+
parser = argparse.ArgumentParser(
108+
description="Benchmark Valgrind with pytest-codspeed",
109+
formatter_class=argparse.RawDescriptionHelpFormatter,
110+
epilog="""
111+
Examples:
112+
# Run with default binary
113+
uv run bench.py --binary-path /path/to/binary
114+
115+
# Run with custom valgrind installation
116+
uv run bench.py --binary-path /path/to/binary --valgrind-path /usr/local/bin/valgrind
117+
""",
118+
)
119+
120+
parser.add_argument(
121+
"--binary-path",
122+
type=str,
123+
required=True,
124+
help="Path to the binary to profile",
125+
)
126+
parser.add_argument(
127+
"--valgrind-path",
128+
type=str,
129+
default="valgrind",
130+
help="Path to valgrind executable (default: valgrind)",
131+
)
132+
parser.add_argument(
133+
"--output-dir",
134+
type=str,
135+
default="/tmp",
136+
help="Directory for callgrind files (default: /tmp)",
137+
)
138+
139+
args = parser.parse_args()
140+
141+
# Create runner instance
142+
runner = ValgrindRunner(
143+
binary_path=args.binary_path,
144+
valgrind_path=args.valgrind_path,
145+
output_dir=args.output_dir,
146+
)
147+
print(f"Valgrind version: {runner.valgrind_version}")
148+
print(f"Binary: {args.binary_path}")
149+
150+
# Plugin to pass runner to tests
151+
class RunnerPlugin:
152+
def pytest_configure(self, config):
153+
config._valgrind_runner = runner
154+
155+
exit_code = pytest.main(
156+
[
157+
__file__,
158+
"-v",
159+
"--codspeed"
160+
],
161+
plugins=[RunnerPlugin()],
162+
)
163+
if exit_code != 0 and exit_code != 5:
164+
print(f"Benchmark execution returned exit code: {exit_code}")
165+
166+
167+
if __name__ == "__main__":
168+
main()

bench/testdata/take_strings

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)