Skip to content

Commit e16a7f8

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

File tree

9 files changed

+367
-0
lines changed

9 files changed

+367
-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: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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+
timeout-minutes: 20
14+
strategy:
15+
matrix:
16+
# IMPORTANT: The binary has to match the architecture of the runner!
17+
cmd:
18+
- testdata/take_strings-aarch64 varbinview_non_null
19+
- echo Hello, World!
20+
- ls bench.py
21+
- python3 testdata/test.py
22+
- stress-ng --cpu 1 --timeout 1s
23+
- stress-ng --cpu 4 --timeout 1s
24+
valgrind:
25+
- "3.26.0"
26+
- "3.25.1"
27+
- "local"
28+
steps:
29+
- uses: actions/checkout@v4
30+
with:
31+
lfs: true
32+
- uses: extractions/setup-just@v3
33+
34+
# Skip installing package docs to avoid wasting time when installing build dependencies
35+
# See: https://github.com/actions/runner-images/issues/10977#issuecomment-2810713336
36+
- name: Skip installing package docs
37+
if: runner.os == 'Linux'
38+
run: |
39+
sudo tee /etc/dpkg/dpkg.cfg.d/01_nodoc > /dev/null << 'EOF'
40+
path-exclude /usr/share/doc/*
41+
path-exclude /usr/share/man/*
42+
path-exclude /usr/share/info/*
43+
EOF
44+
45+
- name: Cache Valgrind build
46+
uses: actions/cache@v4
47+
id: valgrind-cache
48+
with:
49+
path: /tmp/valgrind-build
50+
key: valgrind-${{ matrix.valgrind }}-${{ runner.os }}-${{ matrix.valgrind == 'local' && hashFiles('coregrind/**', 'include/**', 'VEX/**', 'cachegrind/**', 'callgrind/**', 'dhat/**', 'drd/**', 'helgrind/**', 'lackey/**', 'massif/**', 'memcheck/**', 'none/**', 'exp-bbv/**', 'auxprogs/**', '*.ac', '*.am', '*.in', 'autogen.sh', 'configure*') || 'build' }}
51+
52+
# Build and install Valgrind
53+
- name: Update apt-get cache
54+
if: steps.valgrind-cache.outputs.cache-hit != 'true'
55+
run: |
56+
sudo apt-get update
57+
58+
# Remove existing Valgrind installation
59+
sudo apt-get remove -y valgrind || true
60+
61+
- name: Install build dependencies
62+
if: steps.valgrind-cache.outputs.cache-hit != 'true'
63+
run: |
64+
sudo apt-get install -y \
65+
build-essential \
66+
automake \
67+
autoconf \
68+
gdb \
69+
docbook \
70+
docbook-xsl \
71+
docbook-xml \
72+
xsltproc
73+
74+
- name: Build Valgrind (${{ matrix.valgrind }})
75+
if: steps.valgrind-cache.outputs.cache-hit != 'true'
76+
run: just build ${{ matrix.valgrind }}
77+
78+
- name: Install Valgrind (${{ matrix.valgrind }})
79+
run: |
80+
just install ${{ matrix.valgrind }}
81+
82+
# Ensure libc6-dev is installed for Valgrind to work properly
83+
sudo apt-get update
84+
sudo apt-get install -y libc6-dev stress-ng
85+
86+
- name: Verify Valgrind build
87+
run: /usr/local/bin/valgrind --version
88+
89+
# Setup benchmarks and run them
90+
- name: Install uv
91+
uses: astral-sh/setup-uv@v5
92+
93+
- name: Run the benchmarks
94+
uses: CodSpeedHQ/action@main
95+
env:
96+
CODSPEED_PERF_ENABLED: false
97+
with:
98+
working-directory: bench
99+
mode: walltime
100+
run: ./bench.py --cmd "${{ matrix.cmd }}" --valgrind-path /usr/local/bin/valgrind

Justfile

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Builds a specific valgrind version
2+
# Usage:
3+
# - just build 3.24.0: Downloads the specified version from sourceware.org, builds and installs it
4+
# - just build local: Builds the local source tree
5+
build version:
6+
#!/usr/bin/env bash
7+
set -euo pipefail
8+
9+
mkdir -p /tmp/valgrind-build
10+
rm -rf /tmp/valgrind-build/valgrind-{{ version }}*
11+
12+
if [ "{{ version }}" = "local" ]; then
13+
cp -r . /tmp/valgrind-build/valgrind-local
14+
else
15+
wget -q -O /tmp/valgrind-build/valgrind-{{ version }}.tar.bz2 \
16+
https://sourceware.org/pub/valgrind/valgrind-{{ version }}.tar.bz2
17+
tar -xjf /tmp/valgrind-build/valgrind-{{ version }}.tar.bz2 \
18+
-C /tmp/valgrind-build
19+
fi
20+
21+
just build-in "/tmp/valgrind-build/valgrind-{{ version }}"
22+
23+
build-in dir:
24+
#!/usr/bin/env bash
25+
set -euo pipefail
26+
cd "{{ dir }}"
27+
28+
# Check if we need to run autogen.sh (for git checkouts)
29+
if [ -f "autogen.sh" ] && [ ! -f "configure" ]; then
30+
./autogen.sh
31+
fi
32+
33+
./configure
34+
make include/vgversion.h
35+
make -j$(nproc) -C VEX
36+
make -j$(nproc) -C coregrind
37+
make -j$(nproc) -C callgrind
38+
39+
40+
install version:
41+
#!/usr/bin/env bash
42+
set -euo pipefail
43+
44+
cd "/tmp/valgrind-build/valgrind-{{ version }}"
45+
sudo make install
46+

bench/bench.py

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
#!/usr/bin/env -S uv run --script
2+
# /// script
3+
# requires-python = ">=3.9"
4+
# dependencies = [
5+
# "pytest>=8.4.2",
6+
# "pytest-codspeed>=4.2.0",
7+
# ]
8+
# ///
9+
10+
import argparse
11+
import os
12+
import shlex
13+
import subprocess
14+
import sys
15+
from pathlib import Path
16+
import time
17+
18+
import pytest
19+
20+
21+
class ValgrindRunner:
22+
"""Run Valgrind with different configurations."""
23+
24+
def __init__(
25+
self,
26+
cmd: str,
27+
valgrind_path: str = "valgrind",
28+
):
29+
"""Initialize valgrind runner.
30+
31+
Args:
32+
cmd: Command to profile (can be a path or arbitrary shell command)
33+
valgrind_path: Path to valgrind executable
34+
output_dir: Directory for callgrind output files
35+
"""
36+
self.cmd = cmd
37+
self.valgrind_path = valgrind_path
38+
39+
# Verify valgrind is available
40+
result = subprocess.run(
41+
[self.valgrind_path, "--version"],
42+
capture_output=True,
43+
text=True,
44+
)
45+
if result.returncode != 0:
46+
raise RuntimeError(f"Valgrind not found at: {self.valgrind_path}")
47+
self.valgrind_version = result.stdout.strip()
48+
49+
def run_valgrind(self, *args: str) -> None:
50+
"""Execute valgrind with given arguments.
51+
52+
Args:
53+
*args: Valgrind arguments
54+
"""
55+
56+
cmd = [
57+
self.valgrind_path,
58+
"--tool=callgrind",
59+
"--log-file=/dev/null",
60+
*args,
61+
*shlex.split(self.cmd),
62+
]
63+
64+
result = subprocess.run(
65+
cmd,
66+
capture_output=True,
67+
text=True,
68+
)
69+
if result.returncode != 0:
70+
raise RuntimeError(
71+
f"Valgrind execution failed with code {result.returncode}\n"
72+
f"Stdout:\n{result.stdout}\n"
73+
f"Stderr:\n{result.stderr}"
74+
)
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+
def pytest_generate_tests(metafunc):
84+
"""Parametrize tests with valgrind configurations."""
85+
if "valgrind_args" in metafunc.fixturenames:
86+
runner = getattr(metafunc.config, "_valgrind_runner", None)
87+
if not runner:
88+
return
89+
90+
# Define valgrind configurations
91+
configs = [
92+
(["--read-inline-info=no"], "no-inline"),
93+
(["--read-inline-info=yes"], "inline"),
94+
(
95+
[
96+
"--trace-children=yes",
97+
"--cache-sim=yes",
98+
"--I1=32768,8,64",
99+
"--D1=32768,8,64",
100+
"--LL=8388608,16,64",
101+
"--collect-systime=nsec",
102+
"--compress-strings=no",
103+
"--combine-dumps=yes",
104+
"--dump-line=no",
105+
"--read-inline-info=yes",
106+
],
107+
"full-with-inline",
108+
),
109+
(
110+
[
111+
"--trace-children=yes",
112+
"--cache-sim=yes",
113+
"--I1=32768,8,64",
114+
"--D1=32768,8,64",
115+
"--LL=8388608,16,64",
116+
"--collect-systime=nsec",
117+
"--compress-strings=no",
118+
"--combine-dumps=yes",
119+
"--dump-line=no",
120+
],
121+
"full-no-inline",
122+
),
123+
]
124+
125+
# Create test IDs with format: valgrind-version, command, config-name
126+
test_ids = [
127+
f"{runner.valgrind_version}, {runner.cmd}, {config_name}"
128+
for _, config_name in configs
129+
]
130+
131+
# Parametrize with just the args
132+
metafunc.parametrize(
133+
"valgrind_args",
134+
[args for args, _ in configs],
135+
ids=test_ids,
136+
)
137+
138+
139+
@pytest.mark.benchmark
140+
def test_valgrind(runner, valgrind_args):
141+
if runner:
142+
runner.run_valgrind(*valgrind_args)
143+
144+
145+
def main():
146+
parser = argparse.ArgumentParser(
147+
description="Benchmark Valgrind with pytest-codspeed",
148+
formatter_class=argparse.RawDescriptionHelpFormatter,
149+
epilog="""
150+
Examples:
151+
# Run with a binary path
152+
uv run bench.py --cmd /path/to/binary
153+
154+
# Run with an arbitrary command
155+
uv run bench.py --cmd 'echo "hello world"'
156+
157+
# Run with custom valgrind installation
158+
uv run bench.py --cmd /usr/bin/ls --valgrind-path /usr/local/bin/valgrind
159+
""",
160+
)
161+
162+
parser.add_argument(
163+
"--cmd",
164+
type=str,
165+
required=True,
166+
help="Command to profile (can be a path to a binary or any arbitrary command)",
167+
)
168+
parser.add_argument(
169+
"--valgrind-path",
170+
type=str,
171+
default="valgrind",
172+
help="Path to valgrind executable (default: valgrind)",
173+
)
174+
args = parser.parse_args()
175+
176+
# Create runner instance
177+
runner = ValgrindRunner(
178+
cmd=args.cmd,
179+
valgrind_path=args.valgrind_path,
180+
)
181+
print(f"Valgrind version: {runner.valgrind_version}")
182+
print(f"Command: {args.cmd}")
183+
184+
# Plugin to pass runner to tests
185+
class RunnerPlugin:
186+
def pytest_configure(self, config):
187+
config._valgrind_runner = runner
188+
189+
exit_code = pytest.main(
190+
[__file__, "-v", "--codspeed", "--codspeed-warmup-time=0", "--codspeed-max-time=5"],
191+
plugins=[RunnerPlugin()],
192+
)
193+
if exit_code != 0 and exit_code != 5:
194+
print(f"Benchmark execution returned exit code: {exit_code}")
195+
196+
197+
if __name__ == "__main__":
198+
main()

bench/pytest.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[pytest]
2+
norecursedirs = testdata __pycache__ .pytest_cache *.egg-info
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

bench/testdata/test.py

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:cf603e7740f7f7cbf211c7b240f8426c0bf602353290cdb3c9a52adbb0dfaec1
3+
size 22

0 commit comments

Comments
 (0)