Skip to content

Commit e2da712

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

File tree

6 files changed

+270
-0
lines changed

6 files changed

+270
-0
lines changed

.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 won't 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: 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(executable=self.valgrind_path, args=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+
@pytest.fixture
78+
def runner(request):
79+
"""Fixture to provide runner instance to tests."""
80+
return request.config._valgrind_runner
81+
82+
83+
@pytest.mark.benchmark
84+
def test_baseline(runner):
85+
if runner:
86+
runner.run_valgrind("--read-inline-info=no")
87+
88+
89+
@pytest.mark.benchmark
90+
def test_with_inline_info(runner):
91+
if runner:
92+
runner.run_valgrind("--read-inline-info=yes")
93+
94+
95+
@pytest.mark.benchmark
96+
def test_full_inline(runner):
97+
if runner:
98+
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")
99+
100+
@pytest.mark.benchmark
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()
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)