Skip to content

Commit dfd3a4a

Browse files
docs(benchmarks): document per-profile benchmark commands
1 parent 5fdca18 commit dfd3a4a

3 files changed

Lines changed: 160 additions & 26 deletions

File tree

BENCHMARK.md

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,6 @@ cmake --build build-review --target demo_benchmark_matrix
1818
matrix, appends `benchmarks/demo_benchmark_history.json`, and refreshes
1919
this file with the latest snapshot.
2020

21-
## Profile meanings
22-
23-
| Profile | Meaning |
24-
| --- | --- |
25-
| Standard timer, default sink | Baseline ScopeTimer cost with the normal synchronous sink and wall-clock timestamps enabled. |
26-
| Standard timer, wall time disabled | Standard timer with `SCOPE_TIMER_WALLTIME=0` so the report shows the cost of dropping `start=` and `end=` timestamp formatting. |
27-
| Standard timer, buffered sink | Single-thread run with the thread-buffered sink enabled to show how much caller-thread write overhead falls when flushes are batched. |
28-
| Standard timer, buffered sink (threaded stress) | Multi-threaded buffered run that stresses contention and cross-thread flush behavior under the standard timer format. |
29-
| Standard timer, async sink | Multi-threaded run with the async sink so flush work moves to the background writer instead of the calling thread. |
30-
| Hot-path timer, async sink | Lowest-overhead profile: hot-path timer format plus the async sink, measured under the threaded stress workload. |
31-
3221
## Current benchmark snapshot
3322

3423
- Recorded at: `2026-04-16T10:45:23+00:00`
@@ -51,3 +40,60 @@ this file with the latest snapshot.
5140

5241
Full historical results remain in
5342
`benchmarks/demo_benchmark_history.json`.
43+
44+
## Profile reference
45+
46+
The commands below rerun one profile at a time through
47+
`scripts/benchmark_demo.py`. The harness handles the alternating
48+
`SCOPE_TIMER=0` and `SCOPE_TIMER=1` passes for you; the `--env` flags
49+
shown here are only the profile-specific knobs.
50+
51+
These examples use `binary=./build-bench/Benchmark`, `scenario=hotpath-bench`, `iterations=5`, `runs=8`, `threads=4`, and `sink_bytes=4096`.
52+
53+
### Standard timer, default sink
54+
55+
Baseline ScopeTimer cost with the normal synchronous sink and wall-clock timestamps enabled.
56+
57+
```bash
58+
python3 scripts/benchmark_demo.py --binary ./build-bench/Benchmark --scenario hotpath-bench --iterations 5 --runs 8
59+
```
60+
61+
### Standard timer, wall time disabled
62+
63+
Standard timer with `SCOPE_TIMER_WALLTIME=0` so the report shows the cost of dropping `start=` and `end=` timestamp formatting.
64+
65+
```bash
66+
python3 scripts/benchmark_demo.py --binary ./build-bench/Benchmark --scenario hotpath-bench --iterations 5 --runs 8 --env SCOPE_TIMER_WALLTIME=0
67+
```
68+
69+
### Standard timer, buffered sink
70+
71+
Single-thread run with the thread-buffered sink enabled to show how much caller-thread write overhead falls when flushes are batched.
72+
73+
```bash
74+
python3 scripts/benchmark_demo.py --binary ./build-bench/Benchmark --scenario hotpath-bench --iterations 5 --runs 8 --env SCOPE_TIMER_BENCH_SINK=BUFFERED --env SCOPE_TIMER_WALLTIME=0
75+
```
76+
77+
### Standard timer, buffered sink (threaded stress)
78+
79+
Multi-threaded buffered run that stresses contention and cross-thread flush behavior under the standard timer format.
80+
81+
```bash
82+
python3 scripts/benchmark_demo.py --binary ./build-bench/Benchmark --scenario hotpath-bench --iterations 5 --runs 8 --env SCOPE_TIMER_BENCH_SINK=BUFFERED --env SCOPE_TIMER_BENCH_SINK_BYTES=4096 --env SCOPE_TIMER_BENCH_THREADS=4 --env SCOPE_TIMER_WALLTIME=0
83+
```
84+
85+
### Standard timer, async sink
86+
87+
Multi-threaded run with the async sink so flush work moves to the background writer instead of the calling thread.
88+
89+
```bash
90+
python3 scripts/benchmark_demo.py --binary ./build-bench/Benchmark --scenario hotpath-bench --iterations 5 --runs 8 --env SCOPE_TIMER_BENCH_SINK=ASYNC --env SCOPE_TIMER_BENCH_SINK_BYTES=4096 --env SCOPE_TIMER_BENCH_THREADS=4 --env SCOPE_TIMER_WALLTIME=0
91+
```
92+
93+
### Hot-path timer, async sink
94+
95+
Lowest-overhead profile: hot-path timer format plus the async sink, measured under the threaded stress workload.
96+
97+
```bash
98+
python3 scripts/benchmark_demo.py --binary ./build-bench/Benchmark --scenario hotpath-bench --iterations 5 --runs 8 --env SCOPE_TIMER_BENCH_SINK=ASYNC --env SCOPE_TIMER_BENCH_SINK_BYTES=4096 --env SCOPE_TIMER_BENCH_THREADS=4 --env SCOPE_TIMER_BENCH_TIMER=HOTPATH --env SCOPE_TIMER_WALLTIME=0
99+
```

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,11 @@ Build, coverage, Sonar, and benchmark-target usage now live in
222222
Elapsed-format examples and the log-summary pipeline now live in
223223
[TESTS.md](TESTS.md).
224224
225+
## Benchmarks ##
226+
227+
Current benchmark results, profile guidance, and reproducible per-profile
228+
commands now live in [BENCHMARK.md](BENCHMARK.md).
229+
225230
---
226231
227232
<!-- markdownlint-disable MD034 -->

scripts/record_demo_benchmarks.py

Lines changed: 98 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,27 @@
1111

1212
import argparse
1313
import json
14+
import shlex
1415
import subprocess
1516
from datetime import datetime, timezone
1617
from pathlib import Path
1718
from typing import Any
1819

1920
import benchmark_demo
2021

22+
REPO_ROOT = Path(__file__).resolve().parent.parent
2123
SCHEMA_VERSION = 1
2224
NOISE_TOLERANCE_PCT = 2.0
2325
SINK_BYTES_PLACEHOLDER = "{sink_bytes}"
2426
THREADS_PLACEHOLDER = "{threads}"
27+
DEFAULT_REPORT_CONFIG: dict[str, Any] = {
28+
"binary": "./build-bench/Benchmark",
29+
"scenario": "hotpath-bench",
30+
"iterations": 5,
31+
"runs": 8,
32+
"threads": 4,
33+
"sink_bytes": 4096,
34+
}
2535

2636
PROFILE_DEFS: list[dict[str, Any]] = [
2737
{
@@ -171,6 +181,88 @@ def format_profile_env(template_env: dict[str, str], threads: int, sink_bytes: i
171181
}
172182

173183

184+
def display_path(path_value: str) -> str:
185+
if not path_value:
186+
return path_value
187+
188+
path = Path(path_value).expanduser()
189+
try:
190+
return f"./{path.resolve().relative_to(REPO_ROOT)}"
191+
except (OSError, ValueError):
192+
return path_value
193+
194+
195+
def profile_doc_config(entry: dict[str, Any] | None) -> dict[str, Any]:
196+
config = dict(DEFAULT_REPORT_CONFIG)
197+
if entry is None:
198+
return config
199+
200+
config.update(entry.get("benchmark_config", {}))
201+
config["binary"] = display_path(str(config.get("binary", DEFAULT_REPORT_CONFIG["binary"])))
202+
return config
203+
204+
205+
def profile_benchmark_command(profile_def: dict[str, Any], config: dict[str, Any]) -> str:
206+
env = format_profile_env(
207+
profile_def["env"],
208+
int(config.get("threads", DEFAULT_REPORT_CONFIG["threads"])),
209+
int(config.get("sink_bytes", DEFAULT_REPORT_CONFIG["sink_bytes"])),
210+
)
211+
command = [
212+
"python3",
213+
"scripts/benchmark_demo.py",
214+
"--binary",
215+
str(config.get("binary", DEFAULT_REPORT_CONFIG["binary"])),
216+
"--scenario",
217+
str(config.get("scenario", DEFAULT_REPORT_CONFIG["scenario"])),
218+
"--iterations",
219+
str(config.get("iterations", DEFAULT_REPORT_CONFIG["iterations"])),
220+
"--runs",
221+
str(config.get("runs", DEFAULT_REPORT_CONFIG["runs"])),
222+
]
223+
for key, value in env.items():
224+
command.extend(["--env", f"{key}={value}"])
225+
return shlex.join(command)
226+
227+
228+
def profile_reference_lines(config: dict[str, Any]) -> list[str]:
229+
lines = [
230+
"## Profile reference",
231+
"",
232+
"The commands below rerun one profile at a time through",
233+
"`scripts/benchmark_demo.py`. The harness handles the alternating",
234+
"`SCOPE_TIMER=0` and `SCOPE_TIMER=1` passes for you; the `--env` flags",
235+
"shown here are only the profile-specific knobs.",
236+
"",
237+
(
238+
"These examples use "
239+
f"`binary={config.get('binary', DEFAULT_REPORT_CONFIG['binary'])}`, "
240+
f"`scenario={config.get('scenario', DEFAULT_REPORT_CONFIG['scenario'])}`, "
241+
f"`iterations={config.get('iterations', DEFAULT_REPORT_CONFIG['iterations'])}`, "
242+
f"`runs={config.get('runs', DEFAULT_REPORT_CONFIG['runs'])}`, "
243+
f"`threads={config.get('threads', DEFAULT_REPORT_CONFIG['threads'])}`, "
244+
f"and `sink_bytes={config.get('sink_bytes', DEFAULT_REPORT_CONFIG['sink_bytes'])}`."
245+
),
246+
"",
247+
]
248+
249+
for profile_def in PROFILE_DEFS:
250+
lines.extend(
251+
[
252+
f"### {profile_def['label']}",
253+
"",
254+
profile_def["description"],
255+
"",
256+
"```bash",
257+
profile_benchmark_command(profile_def, config),
258+
"```",
259+
"",
260+
]
261+
)
262+
263+
return lines
264+
265+
174266
def load_history(path: Path) -> dict[str, Any]:
175267
if not path.exists():
176268
return {"schema_version": SCHEMA_VERSION, "history": []}
@@ -247,6 +339,8 @@ def comparison_delta_text(comparison: dict[str, Any]) -> str:
247339

248340
def render_report(history: dict[str, Any]) -> str:
249341
entries = history.get("history", [])
342+
latest = entries[-1] if entries else None
343+
doc_config = profile_doc_config(latest)
250344
lines = [
251345
"<!-- markdownlint-disable MD013 -->",
252346
"",
@@ -267,25 +361,12 @@ def render_report(history: dict[str, Any]) -> str:
267361
"`example/Benchmark.cpp`. `demo_benchmark_matrix` runs the full profile",
268362
"matrix, appends `benchmarks/demo_benchmark_history.json`, and refreshes",
269363
"this file with the latest snapshot.",
270-
"",
271-
"## Profile meanings",
272-
"",
273-
"| Profile | Meaning |",
274-
"| --- | --- |",
275364
]
276365

277-
for profile_def in PROFILE_DEFS:
278-
lines.append(
279-
"| "
280-
f"{profile_def['label']} | "
281-
f"{profile_def['description']} |"
282-
)
283-
284-
lines.append("")
285-
286366
if not entries:
287367
lines.extend(
288368
[
369+
"",
289370
"## Current benchmark snapshot",
290371
"",
291372
"No benchmark history has been recorded yet.",
@@ -299,15 +380,16 @@ def render_report(history: dict[str, Any]) -> str:
299380
"`demo_benchmark_matrix` run.",
300381
]
301382
)
383+
lines.extend(["", *profile_reference_lines(doc_config)])
302384
return "\n".join(lines)
303385

304-
latest = entries[-1]
305386
previous = entries[-2] if len(entries) > 1 else None
306387
git = latest.get("git", {})
307388
config = latest.get("benchmark_config", {})
308389

309390
lines.extend(
310391
[
392+
"",
311393
"## Current benchmark snapshot",
312394
"",
313395
f"- Recorded at: `{latest.get('recorded_at_utc', 'unknown')}`",
@@ -373,6 +455,7 @@ def render_report(history: dict[str, Any]) -> str:
373455
"`benchmarks/demo_benchmark_history.json`.",
374456
]
375457
)
458+
lines.extend(["", *profile_reference_lines(doc_config)])
376459

377460
return "\n".join(lines)
378461

0 commit comments

Comments
 (0)