Skip to content

Commit 84ed410

Browse files
ci: fix benchmark gate flakiness on ubuntu-latest
1 parent d652299 commit 84ed410

4 files changed

Lines changed: 104 additions & 23 deletions

File tree

benchmarks/baselines.json

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,32 @@
11
{
2-
"_note": "Gated means from max of two ubuntu-latest CI runs (slow/1.19 margin). Sub-100us cache lookup benches excluded from gate due to runner variance.",
3-
"updated": "2026-07-15T16:14:31Z",
2+
"_note": "Gated means from max of three ubuntu-latest CI runs, divided by 1.19 (targets ~1.19x on slowest runner). Sub-100us cache lookup benches excluded from gate.",
3+
"updated": "2026-07-15T16:40:00Z",
44
"machine": "Linux",
55
"groups": {
6-
"parse": {
7-
"test_list_workspace_projects_nocache[composers-10]": 0.009873695869187243,
8-
"test_list_workspace_projects_nocache[composers-200]": 0.1458800642857019,
9-
"test_list_workspace_projects_nocache[composers-50]": 0.04375626806723154
10-
},
116
"export": {
12-
"test_post_export_zip[composers-10]": 0.006389603808617876,
13-
"test_post_export_zip[composers-50]": 0.02472151171218505
7+
"test_post_export_zip[composers-10]": 0.006890030582232384,
8+
"test_post_export_zip[composers-50]": 0.02651060126050394
9+
},
10+
"parse": {
11+
"test_list_workspace_projects_nocache[composers-10]": 0.010318600237016761,
12+
"test_list_workspace_projects_nocache[composers-200]": 0.15541806190476232,
13+
"test_list_workspace_projects_nocache[composers-50]": 0.04608288053221206
1414
},
1515
"search": {
16-
"test_search_full_corpus_indexed": 0.03357773425985082,
17-
"test_search_full_corpus_live_scan": 0.02699565326798046
16+
"test_search_full_corpus_indexed": 0.035584541499678296,
17+
"test_search_full_corpus_live_scan": 0.02765475991102243
1818
},
1919
"summary-cache": {
20-
"test_composer_map_cache_lookup[hit]": 9.159433254539677e-05,
21-
"test_composer_map_cache_lookup[miss]": 9.052273017297571e-05,
22-
"test_fingerprint_workspace_entries[10]": 0.0014558767614049227,
23-
"test_fingerprint_workspace_entries[200]": 0.01772436102941095,
24-
"test_fingerprint_workspace_entries[50]": 0.006177960517756895,
25-
"test_summary_cache_lookup[hit]": 9.256659141385228e-05,
26-
"test_summary_cache_lookup[miss]": 9.249739842055407e-05,
27-
"test_summary_cache_round_trip": 0.00051773609280467,
28-
"test_tab_summary_cache_lookup[hit]": 0.00010446294267543434,
29-
"test_tab_summary_cache_lookup[miss]": 0.00010424118202523503
20+
"test_composer_map_cache_lookup[hit]": 5.162541185094655e-05,
21+
"test_composer_map_cache_lookup[miss]": 5.0766855798028954e-05,
22+
"test_fingerprint_workspace_entries[10]": 0.0015340259698651347,
23+
"test_fingerprint_workspace_entries[200]": 0.019140506097323155,
24+
"test_fingerprint_workspace_entries[50]": 0.006636160535039493,
25+
"test_summary_cache_lookup[hit]": 5.213728423690053e-05,
26+
"test_summary_cache_lookup[miss]": 5.181927082383982e-05,
27+
"test_summary_cache_round_trip": 0.0002906505793927613,
28+
"test_tab_summary_cache_lookup[hit]": 5.963107194614513e-05,
29+
"test_tab_summary_cache_lookup[miss]": 5.868233534912213e-05
3030
}
3131
}
3232
}

scripts/check_benchmark_regression.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@
1717
EXCLUDED_FROM_GATE: frozenset[str] = frozenset(
1818
{
1919
# round_trip calls set_cached_projects (file write) + get_cached_projects (file read)
20-
# each round. OS page-cache state on shared runners causes 35x variation between
20+
# each round. OS page-cache state on shared runners causes 3-5x variation between
2121
# consecutive CI runs, making this ungatable with any reasonable slack.
2222
"test_summary_cache_round_trip",
2323
# Sub-100µs in-memory cache lookups vary 2.5x+ between consecutive ubuntu-latest
24-
# runs; gated ratio band (0.5x1.2x) cannot bracket both without false failures.
24+
# runs; gated ratio band (0.5x to 1.2x) cannot bracket both without false failures.
2525
"test_summary_cache_lookup[hit]",
2626
"test_summary_cache_lookup[miss]",
2727
"test_composer_map_cache_lookup[hit]",

tests/test_check_benchmark_regression.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import pytest
88

99
from scripts.check_benchmark_regression import (
10+
EXCLUDED_FROM_GATE,
1011
BenchmarkDataError,
1112
check_regression,
1213
load_baseline_means,
@@ -16,6 +17,17 @@
1617

1718
GATED_BENCH = "test_fingerprint_workspace_entries[10]"
1819

20+
# Pytest benchmark node IDs (parametrize ids) that must stay in EXCLUDED_FROM_GATE.
21+
EXPECTED_EXCLUDED_FROM_GATE = (
22+
"test_summary_cache_round_trip",
23+
"test_summary_cache_lookup[hit]",
24+
"test_summary_cache_lookup[miss]",
25+
"test_composer_map_cache_lookup[hit]",
26+
"test_composer_map_cache_lookup[miss]",
27+
"test_tab_summary_cache_lookup[hit]",
28+
"test_tab_summary_cache_lookup[miss]",
29+
)
30+
1931

2032
def _write_results(path, benchmarks: list[dict]) -> None:
2133
path.write_text(
@@ -31,6 +43,43 @@ def _write_baselines(path, groups: dict[str, dict[str, float]]) -> None:
3143
)
3244

3345

46+
def test_excluded_from_gate_matches_benchmark_ids() -> None:
47+
assert EXCLUDED_FROM_GATE == frozenset(EXPECTED_EXCLUDED_FROM_GATE)
48+
49+
50+
@pytest.mark.parametrize("excluded_bench", EXPECTED_EXCLUDED_FROM_GATE)
51+
def test_excluded_benchmark_skips_regression_gate(
52+
tmp_path, excluded_bench: str
53+
) -> None:
54+
results = tmp_path / "results.json"
55+
baselines = tmp_path / "baselines.json"
56+
_write_results(
57+
results,
58+
[{"name": excluded_bench, "stats": {"mean": 1.0}}],
59+
)
60+
_write_baselines(
61+
baselines,
62+
{"summary-cache": {excluded_bench: 0.1}},
63+
)
64+
65+
assert check_regression(results, baselines) == 0
66+
67+
68+
@pytest.mark.parametrize("excluded_bench", EXPECTED_EXCLUDED_FROM_GATE)
69+
def test_excluded_benchmark_missing_result_does_not_fail(
70+
tmp_path, excluded_bench: str
71+
) -> None:
72+
results = tmp_path / "results.json"
73+
baselines = tmp_path / "baselines.json"
74+
_write_results(results, [])
75+
_write_baselines(
76+
baselines,
77+
{"summary-cache": {excluded_bench: 0.1}},
78+
)
79+
80+
assert check_regression(results, baselines) == 0
81+
82+
3483
def test_normalize_benchmark_name_strips_module_prefix() -> None:
3584
full = "tests/benchmarks/test_summary_cache_bench.py::test_summary_cache_lookup[hit]"
3685
assert normalize_benchmark_name(full) == "test_summary_cache_lookup[hit]"

tests/test_models.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,38 @@ def test_export_entry_parses(self) -> None:
9393
self.assertEqual(entry.workspace, "ws-1")
9494

9595

96+
class CursorStoragePayloadContract(unittest.TestCase):
97+
"""cursor_storage_payload() must shallow-copy _raw for API passthrough."""
98+
99+
def _assert_shallow_copy(self, model: Composer | Bubble | WorkspaceLocalComposer, raw: dict) -> None:
100+
raw["nested"] = {"k": 1}
101+
payload = model.cursor_storage_payload()
102+
self.assertIsNot(payload, raw)
103+
self.assertEqual(payload, raw)
104+
self.assertIs(payload["nested"], raw["nested"])
105+
payload["__api_mutation__"] = True
106+
self.assertNotIn("__api_mutation__", raw)
107+
108+
def test_composer_cursor_storage_payload_is_shallow_copy(self) -> None:
109+
raw = dict(GOOD_COMPOSER_RAW)
110+
composer = Composer.from_dict(raw, composer_id="cid-001")
111+
self._assert_shallow_copy(composer, raw)
112+
113+
def test_bubble_cursor_storage_payload_is_shallow_copy(self) -> None:
114+
raw = {"text": "hi", "metadata": {"model": "gpt-4"}}
115+
bubble = Bubble.from_dict(raw, bubble_id="b-1")
116+
self._assert_shallow_copy(bubble, raw)
117+
118+
def test_workspace_local_composer_cursor_storage_payload_is_shallow_copy(self) -> None:
119+
raw = {
120+
"composerId": "cid-local-1",
121+
"lastUpdatedAt": 1_715_000_500_000,
122+
"conversation": [{"bubbleId": "b-1"}],
123+
}
124+
local = WorkspaceLocalComposer.from_dict(raw)
125+
self._assert_shallow_copy(local, raw)
126+
127+
96128
class ComposerMissingFieldSchema(unittest.TestCase):
97129
def test_missing_full_conversation_headers_only_raises(self) -> None:
98130
bad = {k: v for k, v in GOOD_COMPOSER_RAW.items() if k != "fullConversationHeadersOnly"}

0 commit comments

Comments
 (0)