Skip to content
This repository was archived by the owner on Apr 28, 2025. It is now read-only.

Commit a298d92

Browse files
authored
Merge pull request #447 from tgross35/icount-benchmarks
Add benchmarks using iai-callgrind
2 parents 8ded576 + 8fc8d41 commit a298d92

File tree

10 files changed

+437
-18
lines changed

10 files changed

+437
-18
lines changed

.github/workflows/main.yaml

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ env:
1010
RUSTDOCFLAGS: -Dwarnings
1111
RUSTFLAGS: -Dwarnings
1212
RUST_BACKTRACE: full
13+
BENCHMARK_RUSTC: nightly-2025-01-16 # Pin the toolchain for reproducable results
1314

1415
jobs:
1516
test:
@@ -147,19 +148,70 @@ jobs:
147148
benchmarks:
148149
name: Benchmarks
149150
runs-on: ubuntu-24.04
151+
timeout-minutes: 20
150152
steps:
151153
- uses: actions/checkout@master
152-
- name: Install Rust
153-
run: rustup update nightly --no-self-update && rustup default nightly
154+
- uses: taiki-e/install-action@cargo-binstall
155+
156+
- name: Set up dependencies
157+
run: |
158+
rustup update "$BENCHMARK_RUSTC" --no-self-update
159+
rustup default "$BENCHMARK_RUSTC"
160+
# Install the version of iai-callgrind-runner that is specified in Cargo.toml
161+
iai_version="$(cargo metadata --format-version=1 --features icount |
162+
jq -r '.packages[] | select(.name == "iai-callgrind").version')"
163+
cargo binstall -y iai-callgrind-runner --version "$iai_version"
164+
sudo apt-get install valgrind
165+
154166
- uses: Swatinem/rust-cache@v2
155167
- name: Download musl source
156168
run: ./ci/download-musl.sh
157-
- run: |
169+
170+
- name: Run icount benchmarks
171+
env:
172+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
173+
run: |
174+
set -eux
175+
iai_home="iai-home"
176+
# Download the baseline from master
177+
./ci/ci-util.py locate-baseline --download --extract
178+
179+
# Run iai-callgrind benchmarks
180+
cargo bench --no-default-features \
181+
--features unstable,unstable-float,icount \
182+
--bench icount \
183+
-- \
184+
--save-baseline=default \
185+
--home "$(pwd)/$iai_home" \
186+
--regression='ir=5.0' \
187+
--save-summary
188+
# NB: iai-callgrind should exit on error but does not, so we inspect the sumary
189+
# for errors. See https://github.com/iai-callgrind/iai-callgrind/issues/337
190+
./ci/ci-util.py check-regressions "$iai_home"
191+
192+
# Name and tar the new baseline
193+
name="baseline-icount-$(date -u +'%Y%m%d%H%M')-${GITHUB_SHA:0:12}"
194+
echo "BASELINE_NAME=$name" >> "$GITHUB_ENV"
195+
tar cJf "$name.tar.xz" "$iai_home"
196+
197+
- name: Upload the benchmark baseline
198+
uses: actions/upload-artifact@v4
199+
with:
200+
name: ${{ env.BASELINE_NAME }}
201+
path: ${{ env.BASELINE_NAME }}.tar.xz
202+
203+
- name: Run wall time benchmarks
204+
run: |
158205
# Always use the same seed for benchmarks. Ideally we should switch to a
159206
# non-random generator.
160207
export LIBM_SEED=benchesbenchesbenchesbencheswoo!
161208
cargo bench --all --features libm-test/short-benchmarks,libm-test/build-musl
162209
210+
- name: Print test logs if available
211+
if: always()
212+
run: if [ -f "target/test-log.txt" ]; then cat target/test-log.txt; fi
213+
shell: bash
214+
163215
msrv:
164216
name: Check MSRV
165217
runs-on: ubuntu-24.04

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,7 @@ debug-assertions = true
7373
inherits = "release"
7474
lto = "fat"
7575
overflow-checks = true
76+
77+
[profile.bench]
78+
# Required for iai-callgrind
79+
debug = true

ci/ci-util.py

Lines changed: 152 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import subprocess as sp
1010
import sys
1111
from dataclasses import dataclass
12+
from glob import glob, iglob
1213
from inspect import cleandoc
1314
from os import getenv
1415
from pathlib import Path
@@ -18,16 +19,33 @@
1819
"""
1920
usage:
2021
21-
./ci/ci-util.py <SUBCOMMAND>
22+
./ci/ci-util.py <COMMAND> [flags]
2223
23-
SUBCOMMAND:
24-
generate-matrix Calculate a matrix of which functions had source change,
25-
print that as JSON object.
24+
COMMAND:
25+
generate-matrix
26+
Calculate a matrix of which functions had source change, print that as
27+
a JSON object.
28+
29+
locate-baseline [--download] [--extract]
30+
Locate the most recent benchmark baseline available in CI and, if flags
31+
specify, download and extract it. Never exits with nonzero status if
32+
downloading fails.
33+
34+
Note that `--extract` will overwrite files in `iai-home`.
35+
36+
check-regressions [iai-home]
37+
Check `iai-home` (or `iai-home` if unspecified) for `summary.json`
38+
files and see if there are any regressions. This is used as a workaround
39+
for `iai-callgrind` not exiting with error status; see
40+
<https://github.com/iai-callgrind/iai-callgrind/issues/337>.
2641
"""
2742
)
2843

2944
REPO_ROOT = Path(__file__).parent.parent
3045
GIT = ["git", "-C", REPO_ROOT]
46+
DEFAULT_BRANCH = "master"
47+
WORKFLOW_NAME = "CI" # Workflow that generates the benchmark artifacts
48+
ARTIFACT_GLOB = "baseline-icount*"
3149

3250
# Don't run exhaustive tests if these files change, even if they contaiin a function
3351
# definition.
@@ -40,6 +58,11 @@
4058
TYPES = ["f16", "f32", "f64", "f128"]
4159

4260

61+
def eprint(*args, **kwargs):
62+
"""Print to stderr."""
63+
print(*args, file=sys.stderr, **kwargs)
64+
65+
4366
class FunctionDef(TypedDict):
4467
"""Type for an entry in `function-definitions.json`"""
4568

@@ -145,9 +168,125 @@ def make_workflow_output(self) -> str:
145168
return output
146169

147170

148-
def eprint(*args, **kwargs):
149-
"""Print to stderr."""
150-
print(*args, file=sys.stderr, **kwargs)
171+
def locate_baseline(flags: list[str]) -> None:
172+
"""Find the most recent baseline from CI, download it if specified.
173+
174+
This returns rather than erroring, even if the `gh` commands fail. This is to avoid
175+
erroring in CI if the baseline is unavailable (artifact time limit exceeded, first
176+
run on the branch, etc).
177+
"""
178+
179+
download = False
180+
extract = False
181+
182+
while len(flags) > 0:
183+
match flags[0]:
184+
case "--download":
185+
download = True
186+
case "--extract":
187+
extract = True
188+
case _:
189+
eprint(USAGE)
190+
exit(1)
191+
flags = flags[1:]
192+
193+
if extract and not download:
194+
eprint("cannot extract without downloading")
195+
exit(1)
196+
197+
try:
198+
# Locate the most recent job to complete with success on our branch
199+
latest_job = sp.check_output(
200+
[
201+
"gh",
202+
"run",
203+
"list",
204+
"--limit=1",
205+
"--status=success",
206+
f"--branch={DEFAULT_BRANCH}",
207+
"--json=databaseId,url,headSha,conclusion,createdAt,"
208+
"status,workflowDatabaseId,workflowName",
209+
f'--jq=select(.[].workflowName == "{WORKFLOW_NAME}")',
210+
],
211+
text=True,
212+
)
213+
eprint(f"latest: '{latest_job}'")
214+
except sp.CalledProcessError as e:
215+
eprint(f"failed to run github command: {e}")
216+
return
217+
218+
try:
219+
latest = json.loads(latest_job)[0]
220+
eprint("latest job: ", json.dumps(latest, indent=4))
221+
except json.JSONDecodeError as e:
222+
eprint(f"failed to decode json '{latest_job}', {e}")
223+
return
224+
225+
if not download:
226+
eprint("--download not specified, returning")
227+
return
228+
229+
job_id = latest.get("databaseId")
230+
if job_id is None:
231+
eprint("skipping download step")
232+
return
233+
234+
sp.run(
235+
["gh", "run", "download", str(job_id), f"--pattern={ARTIFACT_GLOB}"],
236+
check=False,
237+
)
238+
239+
if not extract:
240+
eprint("skipping extraction step")
241+
return
242+
243+
# Find the baseline with the most recent timestamp. GH downloads the files to e.g.
244+
# `some-dirname/some-dirname.tar.xz`, so just glob the whole thing together.
245+
candidate_baselines = glob(f"{ARTIFACT_GLOB}/{ARTIFACT_GLOB}")
246+
if len(candidate_baselines) == 0:
247+
eprint("no possible baseline directories found")
248+
return
249+
250+
candidate_baselines.sort(reverse=True)
251+
baseline_archive = candidate_baselines[0]
252+
eprint(f"extracting {baseline_archive}")
253+
sp.run(["tar", "xJvf", baseline_archive], check=True)
254+
eprint("baseline extracted successfully")
255+
256+
257+
def check_iai_regressions(iai_home: str | None | Path):
258+
"""Find regressions in iai summary.json files, exit with failure if any are
259+
found.
260+
"""
261+
if iai_home is None:
262+
iai_home = "iai-home"
263+
iai_home = Path(iai_home)
264+
265+
found_summaries = False
266+
regressions = []
267+
for summary_path in iglob("**/summary.json", root_dir=iai_home, recursive=True):
268+
found_summaries = True
269+
with open(iai_home / summary_path, "r") as f:
270+
summary = json.load(f)
271+
272+
summary_regs = []
273+
run = summary["callgrind_summary"]["callgrind_run"]
274+
name_entry = {"name": f"{summary["function_name"]}.{summary["id"]}"}
275+
276+
for segment in run["segments"]:
277+
summary_regs.extend(segment["regressions"])
278+
279+
summary_regs.extend(run["total"]["regressions"])
280+
281+
regressions.extend(name_entry | reg for reg in summary_regs)
282+
283+
if not found_summaries:
284+
eprint(f"did not find any summary.json files within {iai_home}")
285+
exit(1)
286+
287+
if len(regressions) > 0:
288+
eprint("Found regressions:", json.dumps(regressions, indent=4))
289+
exit(1)
151290

152291

153292
def main():
@@ -156,6 +295,12 @@ def main():
156295
ctx = Context()
157296
output = ctx.make_workflow_output()
158297
print(f"matrix={output}")
298+
case ["locate-baseline", *flags]:
299+
locate_baseline(flags)
300+
case ["check-regressions"]:
301+
check_iai_regressions(None)
302+
case ["check-regressions", iai_home]:
303+
check_iai_regressions(iai_home)
159304
case ["--help" | "-h"]:
160305
print(USAGE)
161306
exit()

crates/libm-test/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,17 @@ build-musl = ["dep:musl-math-sys"]
2020
# Enable report generation without bringing in more dependencies by default
2121
benchmarking-reports = ["criterion/plotters", "criterion/html_reports"]
2222

23+
# Enable icount benchmarks (requires iai-callgrind and valgrind)
24+
icount = ["dep:iai-callgrind"]
25+
2326
# Run with a reduced set of benchmarks, such as for CI
2427
short-benchmarks = []
2528

2629
[dependencies]
2730
anyhow = "1.0.90"
2831
az = { version = "1.2.1", optional = true }
2932
gmp-mpfr-sys = { version = "1.6.4", optional = true, default-features = false, features = ["mpfr"] }
33+
iai-callgrind = { version = "0.14.0", optional = true }
3034
indicatif = { version = "0.17.9", default-features = false }
3135
libm = { path = "../..", features = ["unstable-public-internals"] }
3236
libm-macros = { path = "../libm-macros" }
@@ -48,6 +52,11 @@ rand = { version = "0.8.5", optional = true }
4852
criterion = { version = "0.5.1", default-features = false, features = ["cargo_bench_support"] }
4953
libtest-mimic = "0.8.1"
5054

55+
[[bench]]
56+
name = "icount"
57+
harness = false
58+
required-features = ["icount"]
59+
5160
[[bench]]
5261
name = "random"
5362
harness = false

0 commit comments

Comments
 (0)