c2af5ad3da
Closes the architect's tidy follow-up item on bench-number methodology. The harness now accepts --runs N (default 1, byte- identical output for back-compat); with N>=2 it reports median + min..max per cell across runs, with N>=4 it drops the slowest run before aggregating to match bench/run.sh's drop- slowest throughput convention. bench/run.sh now invokes the harness with --runs 5 for each of the three latency arms, so a single bench/run.sh run produces both a regression-stable throughput table and a regression-stable latency table. The qualitative claims from JOURNAL 2026-05-08 (RC tail latency 23x better than Boehm; RC RSS lower than Boehm) hold at the new stat-of-5 confidence level — a 5-run smoke on the explicit-rc arm shows p99=296µs (range 289-311), p99/median=1.31x (range 1.28-1.37). Variance is well below the signal.
246 lines
9.2 KiB
Python
Executable File
246 lines
9.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Per-operation latency harness for the bench_latency_{implicit,explicit}
|
|
fixtures.
|
|
|
|
The fixtures emit a stdout marker line every PRINT_K bench operations.
|
|
We spawn the binary with its stdout connected to a PTY so libc's
|
|
printf path is line-buffered (rather than block-buffered as it would
|
|
be on a pipe), record `time.monotonic_ns()` at every line read, and
|
|
report the inter-arrival distribution.
|
|
|
|
The headline numbers are median, p99, p99.9, max of the inter-arrival
|
|
gaps (in microseconds). Tail latency is the question; total wall time
|
|
is not.
|
|
|
|
We do NOT count the gap between READY (8888) and the first sample —
|
|
that gap includes the build_tree(19) cost and is not a steady-state
|
|
operation. We also drop the very last gap (DONE 9999) because the
|
|
program exit path can include final teardown that is not part of the
|
|
loop budget.
|
|
|
|
Usage:
|
|
python3 bench/latency_harness.py <binary> [--label LABEL]
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import pty
|
|
import resource
|
|
import select
|
|
import sys
|
|
import time
|
|
|
|
|
|
def run_pty(binary: str, timeout_s: float = 600.0) -> tuple[list[int], int, int, int, float]:
|
|
"""Run `binary` with stdout on a PTY, return (timestamps_ns, lines, exit_status, max_rss_kb, wall_s)."""
|
|
pid, fd = pty.fork()
|
|
if pid == 0:
|
|
# Child: replace argv[0] with the binary and exec.
|
|
try:
|
|
os.execvp(binary, [binary])
|
|
except FileNotFoundError as e:
|
|
sys.stderr.write(f"exec failed: {e}\n")
|
|
os._exit(127)
|
|
|
|
# Parent: read from the PTY master, timestamp every newline.
|
|
timestamps: list[int] = []
|
|
line_count = 0
|
|
buf = bytearray()
|
|
deadline = time.monotonic() + timeout_s
|
|
t_start = time.monotonic()
|
|
|
|
try:
|
|
while True:
|
|
remaining = deadline - time.monotonic()
|
|
if remaining <= 0:
|
|
os.kill(pid, 9)
|
|
raise TimeoutError(f"binary exceeded {timeout_s:.0f}s")
|
|
r, _, _ = select.select([fd], [], [], min(remaining, 1.0))
|
|
if not r:
|
|
continue
|
|
try:
|
|
chunk = os.read(fd, 4096)
|
|
except OSError:
|
|
# PTY master read after child exit -> EIO on Linux.
|
|
break
|
|
if not chunk:
|
|
break
|
|
now = time.monotonic_ns()
|
|
buf.extend(chunk)
|
|
# Walk the buffer; record `now` for each newline encountered.
|
|
# Multiple newlines in one read share the same monotonic_ns
|
|
# — that's a known quantization artefact of bursty stdio
|
|
# delivery, NOT a per-op timing artefact. We surface it in
|
|
# the report.
|
|
nl = buf.find(b"\n")
|
|
while nl != -1:
|
|
timestamps.append(now)
|
|
line_count += 1
|
|
del buf[: nl + 1]
|
|
nl = buf.find(b"\n")
|
|
finally:
|
|
try:
|
|
os.close(fd)
|
|
except OSError:
|
|
pass
|
|
|
|
# Wait for child and capture rusage.
|
|
_, status = os.waitpid(pid, 0)
|
|
ru = resource.getrusage(resource.RUSAGE_CHILDREN)
|
|
t_end = time.monotonic()
|
|
return timestamps, line_count, status, ru.ru_maxrss, t_end - t_start
|
|
|
|
|
|
def percentile(xs: list[float], p: float) -> float:
|
|
"""Nearest-rank percentile (xs assumed sorted)."""
|
|
if not xs:
|
|
return float("nan")
|
|
k = max(0, min(len(xs) - 1, int(round((p / 100.0) * (len(xs) - 1)))))
|
|
return xs[k]
|
|
|
|
|
|
def summarise(timestamps_ns: list[int], lines: int) -> dict | None:
|
|
"""Compute the per-cell summary stats for one run. Returns None on
|
|
failure (too few lines for a meaningful distribution)."""
|
|
if lines < 5:
|
|
return None
|
|
gaps_ns: list[int] = []
|
|
for i in range(1, len(timestamps_ns)):
|
|
gaps_ns.append(timestamps_ns[i] - timestamps_ns[i - 1])
|
|
# Trim the first gap (READY -> first chunk; includes the loop's
|
|
# first chunk's full work) and the last gap (last chunk -> DONE;
|
|
# includes program teardown). Everything between is steady-state.
|
|
trimmed = gaps_ns[1:-1] if len(gaps_ns) >= 3 else gaps_ns
|
|
gaps_us = sorted(g / 1000.0 for g in trimmed)
|
|
if not gaps_us:
|
|
return None
|
|
return {
|
|
"n": len(trimmed),
|
|
"min": min(gaps_us),
|
|
"median": percentile(gaps_us, 50),
|
|
"mean": sum(gaps_us) / len(gaps_us),
|
|
"p90": percentile(gaps_us, 90),
|
|
"p99": percentile(gaps_us, 99),
|
|
"p999": percentile(gaps_us, 99.9),
|
|
"max": max(gaps_us),
|
|
"zero_gaps": sum(1 for g in trimmed if g == 0),
|
|
}
|
|
|
|
|
|
def cell_median(values: list[float]) -> tuple[float, float, float]:
|
|
"""Iter 18g.tidy.fu: median + min + max of a list of per-run values
|
|
for one cell (e.g. p99 across N runs). Drops slowest-by-magnitude
|
|
if N >= 4 to match `bench/run.sh`'s drop-slowest convention.
|
|
Returns (median, min, max) — the orchestrator can read both the
|
|
central tendency and the spread from one tuple.
|
|
"""
|
|
if not values:
|
|
return (float("nan"), float("nan"), float("nan"))
|
|
s = sorted(values)
|
|
if len(s) >= 4:
|
|
# Drop the largest (slowest = highest latency) to absorb a
|
|
# one-off STW pause or scheduler hiccup. Mirrors run.sh.
|
|
s = s[:-1]
|
|
n = len(s)
|
|
if n % 2 == 1:
|
|
med = s[n // 2]
|
|
else:
|
|
med = (s[n // 2 - 1] + s[n // 2]) / 2
|
|
return (med, s[0], s[-1])
|
|
|
|
|
|
def report(
|
|
label: str,
|
|
runs: list[tuple[list[int], int, int, int, float]],
|
|
) -> None:
|
|
"""Iter 18g.tidy.fu: report one or many runs. With N=1 the output
|
|
is byte-identical to the pre-stat-of-N format. With N>1 each cell
|
|
shows median(min..max) across runs after dropping the slowest.
|
|
"""
|
|
summaries: list[dict] = []
|
|
last_status = 0
|
|
last_rss = 0
|
|
last_wall = 0.0
|
|
last_lines = 0
|
|
for timestamps, lines, status, rss_kb, wall_s in runs:
|
|
last_status, last_rss, last_wall, last_lines = status, rss_kb, wall_s, lines
|
|
s = summarise(timestamps, lines)
|
|
if s is not None:
|
|
summaries.append(s)
|
|
|
|
if not summaries:
|
|
print(f"[{label}] FAIL: no successful runs (lines={last_lines}, status={last_status})")
|
|
return
|
|
|
|
print(f"=== {label} ===")
|
|
if len(summaries) == 1:
|
|
# Single-run path: keep the pre-stat-of-N output exactly.
|
|
s = summaries[0]
|
|
print(f" lines: {last_lines} exit_status: {last_status} wall: {last_wall:.3f}s max_rss: {last_rss} KB")
|
|
print(f" inter-arrival gaps (steady-state, n={s['n']}, trimmed first+last):")
|
|
print(f" min: {s['min']:10.1f} us")
|
|
print(f" median: {s['median']:10.1f} us")
|
|
print(f" mean: {s['mean']:10.1f} us")
|
|
print(f" p90: {s['p90']:10.1f} us")
|
|
print(f" p99: {s['p99']:10.1f} us")
|
|
print(f" p99.9: {s['p999']:10.1f} us")
|
|
print(f" max: {s['max']:10.1f} us")
|
|
print(f" p99/median: {s['p99'] / s['median'] if s['median'] else float('nan'):.2f}x")
|
|
print(f" max/median: {s['max'] / s['median'] if s['median'] else float('nan'):.2f}x")
|
|
print(f" zero-ns gaps (read coalescing): {s['zero_gaps']} / {s['n']}")
|
|
return
|
|
|
|
# Multi-run path: median + min..max per cell across runs (drop
|
|
# slowest if N >= 4, matching bench/run.sh).
|
|
n_kept = len(summaries) - 1 if len(summaries) >= 4 else len(summaries)
|
|
print(f" runs: {len(summaries)} kept: {n_kept} (drop slowest if N>=4) last exit_status: {last_status}")
|
|
print(f" per-run distribution medians (steady-state, trimmed first+last):")
|
|
|
|
def fmt_cell(key: str) -> str:
|
|
med, lo, hi = cell_median([s[key] for s in summaries])
|
|
return f"median={med:7.1f} range=[{lo:7.1f}, {hi:7.1f}]"
|
|
|
|
for key in ("min", "median", "mean", "p90", "p99", "p999", "max"):
|
|
label_key = "p99.9" if key == "p999" else key
|
|
print(f" {label_key:7s} {fmt_cell(key)} us")
|
|
|
|
# Derived ratios computed per-run, then aggregated.
|
|
p99_med_ratios = [s["p99"] / s["median"] if s["median"] else float("nan") for s in summaries]
|
|
max_med_ratios = [s["max"] / s["median"] if s["median"] else float("nan") for s in summaries]
|
|
pmm_med, pmm_lo, pmm_hi = cell_median(p99_med_ratios)
|
|
mmm_med, mmm_lo, mmm_hi = cell_median(max_med_ratios)
|
|
print(f" p99/median median={pmm_med:.2f}x range=[{pmm_lo:.2f}x, {pmm_hi:.2f}x]")
|
|
print(f" max/median median={mmm_med:.2f}x range=[{mmm_lo:.2f}x, {mmm_hi:.2f}x]")
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("binary", help="path to the bench binary")
|
|
ap.add_argument("--label", default=None, help="label for the report")
|
|
ap.add_argument("--timeout", type=float, default=600.0)
|
|
ap.add_argument(
|
|
"--runs",
|
|
type=int,
|
|
default=1,
|
|
help="number of timed runs (default 1). With N>=2 the report shows median + range per cell across runs; with N>=4 the slowest run is dropped before aggregation, matching bench/run.sh's drop-slowest convention.",
|
|
)
|
|
args = ap.parse_args()
|
|
|
|
if args.runs < 1:
|
|
print(f"--runs must be >= 1 (got {args.runs})", file=sys.stderr)
|
|
return 2
|
|
|
|
label = args.label or os.path.basename(args.binary)
|
|
runs: list[tuple[list[int], int, int, int, float]] = []
|
|
for _ in range(args.runs):
|
|
runs.append(run_pty(args.binary, args.timeout))
|
|
report(label, runs)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|