Skip to content

Commit 0785aeb

Browse files
authored
feat(perf): capture run metadata and regression signals
Land performance metadata, confidence, and historical regression reporting from Codex task triage.
1 parent a7ef0b8 commit 0785aeb

6 files changed

Lines changed: 273 additions & 14 deletions

File tree

docs/trackers/performance-benchmark-runs.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,3 +461,12 @@ Outputs:
461461
- Bulk read: `results_dev_perf_dashboard/bulk_read_multi/perf/README.md`
462462
- Bulk write: `results_dev_perf_dashboard/bulk_write_multi/perf/README.md`
463463
- Per-cell fast: `results_dev_perf_dashboard/per_cell_fast/perf/README.md`
464+
465+
466+
## Canonical run-comparison procedure
467+
468+
1. Run benchmark with stable knobs (`--warmup`, `--iters`, fixed adapter set) and commit code before each run.
469+
2. Review `perf/results.json` metadata and confirm same profile, CPU, core count, memory, Python, and adapter build info.
470+
3. Compare `perf/matrix.csv` columns `read_tail_ratio`/`write_tail_ratio` and `regression_status`. Treat `confidence_note=high` as noisy and rerun.
471+
4. For regression gates, compare against `results/perf/history.jsonl` median of recent samples (last 5, min 3).
472+
5. Investigate any `regressed:<pct>%` row above the configured threshold, such as `regressed:12.3%`; require two consecutive confirmations before escalating.

src/excelbench/perf/renderer.py

Lines changed: 124 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ def render_perf_markdown(results: PerfResults, path: Path) -> None:
7474
lines.append(f"*Profile: {data['metadata']['profile']}*")
7575
lines.append(f"*Platform: {data['metadata']['platform']}*")
7676
lines.append(f"*Python: {data['metadata']['python']}*")
77+
run_env = data["metadata"].get("run_environment") or {}
78+
if run_env:
79+
lines.append(f"*CPU: {run_env.get('cpu_model') or 'unknown'}*")
80+
lines.append(
81+
f"*Cores: {run_env.get('core_count') or 'unknown'} | "
82+
f"Memory MB: {run_env.get('memory_total_mb') or 'unknown'}*"
83+
)
7784
if data["metadata"].get("commit"):
7885
lines.append(f"*Commit: {data['metadata']['commit']}*")
7986
cfg = data["metadata"].get("config", {})
@@ -93,6 +100,10 @@ def render_perf_markdown(results: PerfResults, path: Path) -> None:
93100
"These numbers measure only the library under test. "
94101
"Write timings do NOT include oracle verification."
95102
)
103+
lines.append(
104+
"Confidence note: treat deltas under ~5% as noise unless "
105+
"stable across multiple runs."
106+
)
96107
lines.append("")
97108

98109
workload_features = _collect_workload_features(libs, features, lookup)
@@ -107,10 +118,10 @@ def render_perf_markdown(results: PerfResults, path: Path) -> None:
107118
for lib in libs:
108119
caps = set(data["libraries"][lib].get("capabilities", []))
109120
if "read" in caps:
110-
header += f" {lib} (R p50 ms) |"
121+
header += f" {lib} (R p50/p95 ms) |"
111122
sep += "--------------|"
112123
if "write" in caps:
113-
header += f" {lib} (W p50 ms) |"
124+
header += f" {lib} (W p50/p95 ms) |"
114125
sep += "--------------|"
115126

116127
tier_map: dict[int, list[str]] = {0: [], 1: [], 2: []}
@@ -133,9 +144,9 @@ def render_perf_markdown(results: PerfResults, path: Path) -> None:
133144
entry = lookup.get((feat, lib))
134145
perf = entry.get("perf") if entry else None
135146
if "read" in caps:
136-
row += f" {_fmt_p50_ms(perf, 'read')} |"
147+
row += f" {_fmt_p50_p95_ms(perf, 'read')} |"
137148
if "write" in caps:
138-
row += f" {_fmt_p50_ms(perf, 'write')} |"
149+
row += f" {_fmt_p50_p95_ms(perf, 'write')} |"
139150
lines.append(row)
140151
lines.append("")
141152

@@ -303,7 +314,7 @@ def _fmt_rate(rate: float) -> str:
303314
return f"{rate:.2f}"
304315

305316

306-
def _fmt_p50_ms(perf: dict[str, Any] | None, op: str) -> str:
317+
def _fmt_p50_p95_ms(perf: dict[str, Any] | None, op: str) -> str:
307318
if not perf or not isinstance(perf, dict):
308319
return "—"
309320
op_data = perf.get(op)
@@ -316,16 +327,37 @@ def _fmt_p50_ms(perf: dict[str, Any] | None, op: str) -> str:
316327
if p50 is None:
317328
return "—"
318329
try:
319-
return f"{float(p50):.2f}"
330+
p95 = wall.get("p95")
331+
p95_txt = f"/{float(p95):.2f}" if p95 is not None else ""
332+
return f"{float(p50):.2f}{p95_txt}"
320333
except (TypeError, ValueError):
321334
return "—"
322335

323336

324337
def render_perf_csv(results: PerfResults, path: Path) -> None:
325338
data = perf_results_to_json_dict(results)
339+
history_path = path.parent / "history.jsonl"
340+
history_entries = _load_matching_history_entries(data, history_path)
341+
header_columns = [
342+
"library",
343+
"feature",
344+
"read_p50_wall_ms",
345+
"read_p95_wall_ms",
346+
"read_op_count",
347+
"read_op_unit",
348+
"read_p50_units_per_sec",
349+
"write_p50_wall_ms",
350+
"write_p95_wall_ms",
351+
"write_op_count",
352+
"write_op_unit",
353+
"write_p50_units_per_sec",
354+
"read_tail_ratio",
355+
"write_tail_ratio",
356+
"confidence_note",
357+
"regression_status",
358+
]
326359
lines = [
327-
"library,feature,read_p50_wall_ms,read_p95_wall_ms,read_op_count,read_op_unit,read_p50_units_per_sec,"
328-
"write_p50_wall_ms,write_p95_wall_ms,write_op_count,write_op_unit,write_p50_units_per_sec",
360+
",".join(header_columns),
329361
]
330362
for r in data["results"]:
331363
perf = r.get("perf") or {}
@@ -350,6 +382,9 @@ def _rate(count: Any, p50_ms: Any) -> str:
350382
def _f(v: Any) -> str:
351383
return "" if v is None else str(v)
352384

385+
read_tail_ratio = _tail_ratio(read_wall)
386+
write_tail_ratio = _tail_ratio(write_wall)
387+
reg_status = _regression_status(history_entries, r)
353388
lines.append(
354389
",".join(
355390
[
@@ -365,6 +400,17 @@ def _f(v: Any) -> str:
365400
_f(write_count),
366401
_f(write_unit),
367402
_rate(write_count, write_wall.get("p50")),
403+
_f(read_tail_ratio),
404+
_f(write_tail_ratio),
405+
_f(
406+
"high"
407+
if (
408+
(read_tail_ratio or 0) > 0.20
409+
or (write_tail_ratio or 0) > 0.20
410+
)
411+
else "ok"
412+
),
413+
reg_status,
368414
]
369415
)
370416
)
@@ -400,3 +446,73 @@ def append_perf_history(results: PerfResults, history_path: Path) -> None:
400446

401447
with open(history_path, "a") as f:
402448
f.write(json.dumps(entry) + "\n")
449+
450+
451+
def _tail_ratio(wall: dict[str, Any]) -> float | None:
452+
p50 = wall.get("p50")
453+
p95 = wall.get("p95")
454+
try:
455+
if p50 is None or p95 is None:
456+
return None
457+
p50_float = float(p50)
458+
if p50_float == 0:
459+
return None
460+
return round(max(float(p95) - p50_float, 0.0) / p50_float, 4)
461+
except (TypeError, ValueError, ZeroDivisionError):
462+
return None
463+
464+
465+
def _load_matching_history_entries(
466+
data: dict[str, Any],
467+
history_path: Path,
468+
) -> list[dict[str, Any]]:
469+
if not history_path.exists():
470+
return []
471+
metadata = data.get("metadata", {})
472+
current_profile = metadata.get("profile")
473+
current_config = metadata.get("config")
474+
entries: list[dict[str, Any]] = []
475+
for line in history_path.read_text().splitlines():
476+
try:
477+
entry = json.loads(line)
478+
if entry.get("profile") != current_profile or entry.get("config") != current_config:
479+
continue
480+
entries.append(entry)
481+
except json.JSONDecodeError:
482+
continue
483+
return entries
484+
485+
486+
def _regression_status(
487+
history_entries: list[dict[str, Any]],
488+
row: dict[str, Any],
489+
*,
490+
threshold_pct: float = 10.0,
491+
) -> str:
492+
if not history_entries:
493+
return "no_history"
494+
vals: list[float] = []
495+
for entry in history_entries:
496+
try:
497+
sample = entry.get("p50_wall_ms", {}).get(row["library"], {}).get(row["feature"], {})
498+
rv = sample.get("read_p50")
499+
if rv is not None:
500+
vals.append(float(rv))
501+
except (TypeError, ValueError, KeyError):
502+
continue
503+
if len(vals) < 3:
504+
return "insufficient_history"
505+
baseline = sorted(vals[-5:])[len(vals[-5:]) // 2]
506+
cur = ((row.get("perf") or {}).get("read") or {}).get("wall_ms", {}).get("p50")
507+
try:
508+
curf = float(cur)
509+
except (TypeError, ValueError):
510+
return "n/a"
511+
if baseline <= 0:
512+
return "n/a"
513+
delta = ((curf - baseline) / baseline) * 100.0
514+
if delta > threshold_pct:
515+
return f"regressed:{delta:.1f}%"
516+
if delta < -threshold_pct:
517+
return f"improved:{delta:.1f}%"
518+
return f"stable:{delta:.1f}%"

src/excelbench/perf/runner.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
from __future__ import annotations
1010

11+
import os
12+
import platform
1113
from collections.abc import Callable
1214
from dataclasses import asdict, dataclass
1315
from datetime import UTC, date, datetime, timedelta
@@ -67,6 +69,13 @@ class PerfFeatureResult:
6769
notes: str | None = None
6870

6971

72+
@dataclass(frozen=True)
73+
class PerfRunEnvironment:
74+
cpu_model: str | None
75+
core_count: int | None
76+
memory_total_mb: float | None
77+
78+
7079
@dataclass(frozen=True)
7180
class PerfMetadata:
7281
benchmark_version: str
@@ -77,6 +86,7 @@ class PerfMetadata:
7786
python: str
7887
commit: str | None
7988
config: PerfConfig
89+
run_environment: PerfRunEnvironment
8090

8191

8292
@dataclass(frozen=True)
@@ -101,7 +111,6 @@ def run_perf(
101111
breakdown: bool = False,
102112
memory_mode: MemoryMode = "getrusage",
103113
) -> PerfResults:
104-
import platform as _platform
105114

106115
from excelbench.generator.generate import load_manifest
107116
from excelbench.harness.adapters import get_all_adapters
@@ -137,16 +146,17 @@ def run_perf(
137146
benchmark_version=BENCHMARK_VERSION,
138147
run_date=datetime.now(UTC),
139148
excel_version=manifest.excel_version,
140-
platform=f"{_platform.system()}-{_platform.machine()}",
149+
platform=f"{platform.system()}-{platform.machine()}",
141150
profile=profile,
142-
python=_platform.python_version(),
151+
python=platform.python_version(),
143152
commit=_get_git_commit(),
144153
config=PerfConfig(
145154
warmup=warmup,
146155
iters=iters,
147156
iteration_policy=iteration_policy_normalized,
148157
breakdown=breakdown,
149158
),
159+
run_environment=_collect_run_environment(),
150160
)
151161

152162
libraries = {a.name: _library_info_dict(a.info) for a in adapters}
@@ -238,6 +248,7 @@ def perf_results_to_json_dict(results: PerfResults) -> dict[str, Any]:
238248
"python": results.metadata.python,
239249
"commit": results.metadata.commit,
240250
"config": asdict(results.metadata.config),
251+
"run_environment": asdict(results.metadata.run_environment),
241252
},
242253
"libraries": results.libraries,
243254
"results": [_feature_result_to_dict(r) for r in results.results],
@@ -1704,3 +1715,22 @@ def run_one_iteration(
17041715
)
17051716

17061717
raise ValueError(f"kind must be 'read' or 'write'; got {kind!r}")
1718+
1719+
1720+
def _collect_run_environment() -> PerfRunEnvironment:
1721+
cpu_model = platform.processor() or None
1722+
if not cpu_model:
1723+
cpu_model = os.environ.get("PROCESSOR_IDENTIFIER")
1724+
core_count = os.cpu_count()
1725+
mem_mb = None
1726+
try:
1727+
pages = os.sysconf("SC_PHYS_PAGES")
1728+
page_size = os.sysconf("SC_PAGE_SIZE")
1729+
mem_mb = float(pages * page_size) / (1024.0 * 1024.0)
1730+
except (AttributeError, OSError, ValueError):
1731+
mem_mb = None
1732+
return PerfRunEnvironment(
1733+
cpu_model=cpu_model,
1734+
core_count=core_count,
1735+
memory_total_mb=round(mem_mb, 2) if mem_mb is not None else None,
1736+
)

tests/test_perf_cli.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
PerfMetadata,
1717
PerfOpResult,
1818
PerfResults,
19+
PerfRunEnvironment,
1920
PerfStats,
2021
)
2122

@@ -91,6 +92,8 @@ def test_perf_command_writes_outputs(tmp_path: Path) -> None:
9192
assert data["metadata"]["config"]["iters"] == 1
9293
assert data["metadata"]["config"]["iteration_policy"] == "fixed"
9394
assert "openpyxl" in data["libraries"]
95+
assert "run_environment" in data["metadata"]
96+
assert "cpu_model" in data["metadata"]["run_environment"]
9497

9598

9699
def test_perf_markdown_header_matches_all_rendered_cells(tmp_path: Path) -> None:
@@ -110,6 +113,11 @@ def test_perf_markdown_header_matches_all_rendered_cells(tmp_path: Path) -> None
110113
iteration_policy="fixed",
111114
breakdown=False,
112115
),
116+
run_environment=PerfRunEnvironment(
117+
cpu_model=None,
118+
core_count=None,
119+
memory_total_mb=None,
120+
),
113121
),
114122
libraries={
115123
"openpyxl": {
@@ -158,8 +166,10 @@ def test_perf_markdown_header_matches_all_rendered_cells(tmp_path: Path) -> None
158166

159167
markdown = readme.read_text()
160168
assert (
161-
"| Feature | openpyxl (R p50 ms) | openpyxl (W p50 ms) | "
162-
"python-calamine (R p50 ms) |"
169+
"| Feature | openpyxl (R p50/p95 ms) | openpyxl (W p50/p95 ms) | "
170+
"python-calamine (R p50/p95 ms) |"
163171
) in markdown
164-
assert "| cell_values | 1.00 | 2.00 | 0.50 |" in markdown
172+
assert "| cell_values | 1.00/1.00 | 2.00/2.00 | 0.50/0.50 |" in markdown
165173
assert markdown.count("**Tier 0") == 1
174+
assert "Confidence note:" in markdown
175+
assert "p50/p95" in markdown

tests/test_perf_data_shape.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,3 +622,45 @@ def test_shape_fixtures_stale_corrupt_manifest_with_needs_1m(tmp_path: Path) ->
622622

623623
# Corrupt manifest with needs_1m=True triggers regeneration.
624624
assert _shape_fixtures_stale(manifest, generator, needs_1m=True) is True
625+
626+
627+
def test_perf_csv_includes_regression_status(tmp_path: Path) -> None:
628+
from excelbench.perf.renderer import render_perf_csv
629+
from excelbench.perf.runner import (
630+
PerfConfig,
631+
PerfFeatureResult,
632+
PerfMetadata,
633+
PerfOpResult,
634+
PerfResults,
635+
PerfRunEnvironment,
636+
PerfStats,
637+
)
638+
639+
stats = PerfStats(min=1, p50=1, p95=2)
640+
res = PerfResults(
641+
metadata=PerfMetadata(
642+
benchmark_version="x",
643+
run_date=datetime.now(UTC),
644+
excel_version="x",
645+
platform="x",
646+
profile="xlsx",
647+
python="3",
648+
commit=None,
649+
config=PerfConfig(warmup=0, iters=1, iteration_policy="fixed", breakdown=False),
650+
run_environment=PerfRunEnvironment(cpu_model=None, core_count=1, memory_total_mb=None),
651+
),
652+
libraries={"openpyxl": {"capabilities": ["read"]}},
653+
results=[
654+
PerfFeatureResult(
655+
feature="f",
656+
library="openpyxl",
657+
workload_size="tiny",
658+
perf={"read": PerfOpResult(wall_ms=stats, cpu_ms=stats), "write": None},
659+
)
660+
],
661+
)
662+
out = tmp_path / "m.csv"
663+
render_perf_csv(res, out)
664+
txt = out.read_text()
665+
assert "regression_status" in txt
666+
assert "confidence_note" in txt

0 commit comments

Comments
 (0)