Skip to content

Commit 001c5f0

Browse files
fix: exclude noisy micro-benchmarks from regression gate
Drop test_parse_session_small and test_search_full_corpus from gated baselines; clarify ubuntu CI provenance in _note; detect duplicate benchmark names; expand reduce_baselines tests.
1 parent 5d816d5 commit 001c5f0

6 files changed

Lines changed: 81 additions & 17 deletions

File tree

benchmarks/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ The memory test (`test_parse_memory.py`) is intentionally **not** skipped by `--
3636

3737
## CI gate
3838

39-
The `benchmarks` job on **ubuntu-latest** runs pytest-benchmark, then `scripts/check_benchmark_regression.py`. CI fails when any gated benchmark mean exceeds its baseline by more than **20%**. Benchmarks without a baseline entry (e.g. new `cache` group) print a warning and do not fail the gate.
39+
The `benchmarks` job on **ubuntu-latest** runs pytest-benchmark (`--benchmark-json=benchmark-results.json`), then `scripts/check_benchmark_regression.py benchmark-results.json benchmarks/baselines.json`. CI fails when any **gated** benchmark mean exceeds its baseline by more than **20%**.
40+
41+
**Gated:** parse medium/large, export 10/50/100 sessions.
42+
43+
**Not gated (informational only):** `test_parse_session_small`, `test_search_full_corpus` (sub-ms CI noise), and the `cache` group. Benchmarks without a baseline entry print a warning and do not fail the gate.
4044

4145
## Refresh baselines
4246

benchmarks/baselines.json

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
{
2-
"_note": "Means captured from ubuntu-latest CI post-cache (PR #90). Refresh via make update-baselines on ubuntu.",
2+
"_note": "Gated means from ubuntu-latest CI benchmark-results.json (post-cache PR #90). Excluded from gate: test_parse_session_small, test_search_full_corpus (sub-ms CI noise). Refresh via make update-baselines on ubuntu.",
33
"updated": "2026-06-17T21:00:00Z",
44
"machine": "Linux",
55
"groups": {
66
"parse": {
7-
"test_parse_session_small": 0.000105,
87
"test_parse_session_medium": 0.002956,
98
"test_parse_session_large": 0.029678
109
},
@@ -13,8 +12,6 @@
1312
"test_bulk_export_session_count[sessions-50]": 0.021144,
1413
"test_bulk_export_session_count[sessions-100]": 0.042003
1514
},
16-
"search": {
17-
"test_search_full_corpus": 0.001092
18-
}
15+
"search": {}
1916
}
2017
}

scripts/check_benchmark_regression.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@
99

1010
THRESHOLD = 1.20
1111

12+
# Sub-ms timings are too noisy for a fixed 20% gate on ubuntu CI.
13+
EXCLUDED_FROM_GATE = frozenset(
14+
{
15+
"test_parse_session_small",
16+
"test_search_full_corpus",
17+
}
18+
)
19+
1220

1321
class BenchmarkDataError(ValueError):
1422
"""Raised when benchmark JSON input is malformed or missing required fields."""
@@ -38,7 +46,10 @@ def load_results(results_path: str | Path) -> dict[str, float]:
3846
raise BenchmarkDataError(
3947
f"{path} benchmarks[{index}] missing 'name' or 'stats.mean'"
4048
) from exc
41-
results[str(name)] = mean
49+
name = str(name)
50+
if name in results:
51+
raise BenchmarkDataError(f"{path} duplicate benchmark name {name!r}")
52+
results[name] = mean
4253
return results
4354

4455

@@ -62,8 +73,13 @@ def load_baseline_means(baselines_path: str | Path) -> dict[str, float]:
6273
if not isinstance(value, dict):
6374
continue
6475
for name, mean in value.items():
76+
name = str(name)
77+
if name in means:
78+
raise BenchmarkDataError(
79+
f"{path} duplicate benchmark name {name!r} across groups"
80+
)
6581
try:
66-
means[str(name)] = float(mean)
82+
means[name] = float(mean)
6783
except (TypeError, ValueError) as exc:
6884
raise BenchmarkDataError(
6985
f"{path} groups[{group_name!r}][{name!r}] is not a numeric mean"

scripts/reduce_baselines.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
from pathlib import Path
1010

1111
try:
12-
from scripts.check_benchmark_regression import BenchmarkDataError
12+
from scripts.check_benchmark_regression import EXCLUDED_FROM_GATE, BenchmarkDataError
1313
except ModuleNotFoundError:
14-
from check_benchmark_regression import BenchmarkDataError
14+
from check_benchmark_regression import EXCLUDED_FROM_GATE, BenchmarkDataError
1515

1616
GATED_GROUPS = ("parse", "export", "search")
1717

@@ -58,12 +58,17 @@ def reduce_baselines(
5858
group = entry.get("group")
5959
if group not in GATED_GROUPS:
6060
continue
61+
if str(name) in EXCLUDED_FROM_GATE:
62+
continue
6163
groups[group][str(name)] = mean * slack
6264

6365
machine_info = raw.get("machine_info")
6466
machine = machine_info.get("system") if isinstance(machine_info, dict) else None
6567
output: dict[str, object] = {
66-
"_note": "CI gates the ubuntu benchmarks job when mean exceeds baseline by >20%.",
68+
"_note": (
69+
"Gated means from ubuntu-latest CI (post-cache). "
70+
"Excluded from gate: test_parse_session_small, test_search_full_corpus (CI noise)."
71+
),
6772
"updated": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"),
6873
"machine": machine,
6974
"groups": groups,

tests/test_check_benchmark_regression.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66

77
import pytest
88

9-
from scripts.check_benchmark_regression import BenchmarkDataError, check_regression, load_results
9+
from scripts.check_benchmark_regression import (
10+
BenchmarkDataError,
11+
check_regression,
12+
load_baseline_means,
13+
load_results,
14+
)
1015

1116

1217
def _write_results(path, benchmarks: list[dict]) -> None:
@@ -147,3 +152,17 @@ def test_main_reports_benchmark_data_error(tmp_path, capsys: pytest.CaptureFixtu
147152

148153
assert main([str(bad), str(baselines)]) == 2
149154
assert "ERROR:" in capsys.readouterr().err
155+
156+
157+
def test_duplicate_baseline_name_raises(tmp_path) -> None:
158+
baselines = tmp_path / "baselines.json"
159+
_write_baselines(
160+
baselines,
161+
{
162+
"parse": {"test_parse_session_medium": 0.002},
163+
"export": {"test_parse_session_medium": 0.003},
164+
},
165+
)
166+
167+
with pytest.raises(BenchmarkDataError, match="duplicate benchmark name"):
168+
load_baseline_means(baselines)

tests/test_reduce_baselines.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def test_reduce_baselines_writes_gated_groups_only(tmp_path) -> None:
2929
_write_raw(
3030
raw,
3131
[
32+
{"group": "parse", "name": "test_parse_session_medium", "stats": {"mean": 0.002}},
3233
{"group": "parse", "name": "test_parse_session_small", "stats": {"mean": 0.0001}},
3334
{"group": "cache", "name": "test_cache_warm_hit", "stats": {"mean": 1e-05}},
3435
],
@@ -37,24 +38,23 @@ def test_reduce_baselines_writes_gated_groups_only(tmp_path) -> None:
3738
output = reduce_baselines(raw, out)
3839

3940
assert output["machine"] == "Linux"
40-
assert "test_parse_session_small" in output["groups"]["parse"]
41+
assert "test_parse_session_medium" in output["groups"]["parse"]
42+
assert "test_parse_session_small" not in output["groups"]["parse"]
4143
assert "cache" not in output["groups"]
42-
written = json.loads(out.read_text(encoding="utf-8"))
43-
assert "test_cache_warm_hit" not in written["groups"]["parse"]
4444

4545

4646
def test_reduce_baselines_applies_slack(tmp_path) -> None:
4747
raw = tmp_path / "raw.json"
4848
out = tmp_path / "baselines.json"
4949
_write_raw(
5050
raw,
51-
[{"group": "search", "name": "test_search_full_corpus", "stats": {"mean": 0.002}}],
51+
[{"group": "parse", "name": "test_parse_session_medium", "stats": {"mean": 0.002}}],
5252
)
5353

5454
reduce_baselines(raw, out, slack=1.5)
5555
data = json.loads(out.read_text(encoding="utf-8"))
5656

57-
assert data["groups"]["search"]["test_search_full_corpus"] == pytest.approx(0.003)
57+
assert data["groups"]["parse"]["test_parse_session_medium"] == pytest.approx(0.003)
5858

5959

6060
def test_reduce_baselines_rejects_missing_benchmarks_key(tmp_path) -> None:
@@ -77,3 +77,26 @@ def test_reduce_baselines_cli_rejects_non_positive_slack(tmp_path) -> None:
7777
with pytest.raises(SystemExit) as exc_info:
7878
main([str(raw), str(tmp_path / "out.json"), "--slack", "0"])
7979
assert exc_info.value.code == 2
80+
81+
82+
def test_reduce_baselines_machine_info_non_dict(tmp_path) -> None:
83+
raw = tmp_path / "raw.json"
84+
raw.write_text(
85+
json.dumps(
86+
{
87+
"machine_info": "not-a-dict",
88+
"benchmarks": [
89+
{
90+
"group": "parse",
91+
"name": "test_parse_session_medium",
92+
"stats": {"mean": 0.002},
93+
}
94+
],
95+
}
96+
),
97+
encoding="utf-8",
98+
)
99+
100+
output = reduce_baselines(raw, tmp_path / "out.json")
101+
102+
assert output["machine"] is None

0 commit comments

Comments
 (0)