Skip to content

Commit b70ad63

Browse files
authored
benchmarks: stream and shard validation with bound receipts (#578)
* process-streaming * feat-benchmark-planning-and-sharding * feat-benchmark-batch-status
2 parents b0e186a + 58c6848 commit b70ad63

32 files changed

Lines changed: 3633 additions & 488 deletions

.github/scripts/emit-plan-receipt

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@
44
from __future__ import annotations
55

66
import argparse
7-
import hashlib
87
import json
98
import re
9+
import sys
1010
from pathlib import Path, PurePosixPath
1111
from typing import Any
1212

1313
ROOT = Path(__file__).resolve().parents[2]
14+
if str(ROOT) not in sys.path:
15+
sys.path.insert(0, str(ROOT))
16+
17+
from benchmarks.tooling.receipts import canonical_json, digest_bytes # noqa: E402
18+
1419
EVENTS = {"pull_request", "merge_group", "push", "schedule", "workflow_dispatch"}
1520
SHA = re.compile(r"[0-9a-f]{40,64}\Z")
1621
MAX_PATHS = 10_000
@@ -22,13 +27,7 @@ def fail(message: str) -> None:
2227

2328

2429
def _sha256(payload: bytes) -> str:
25-
return "sha256:" + hashlib.sha256(payload).hexdigest()
26-
27-
28-
def _canonical_json(value: Any) -> bytes:
29-
return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode(
30-
"utf-8"
31-
)
30+
return digest_bytes(payload)
3231

3332

3433
def _relative_path(value: str, label: str) -> str:
@@ -131,7 +130,7 @@ def build_receipt(
131130
),
132131
"plan": plan,
133132
}
134-
receipt["receipt_digest"] = _sha256(_canonical_json(receipt))
133+
receipt["receipt_digest"] = _sha256(canonical_json(receipt))
135134
return receipt
136135

137136

.github/scripts/manage-test-timings

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ from typing import Any
1818

1919
ROOT = Path(__file__).resolve().parents[2]
2020
CONFIG = ROOT / ".github" / "ci-config.json"
21-
SHARDED_SUITES = ("domain", "composition")
21+
CI_SHARDED_SUITES = ("domain", "composition")
22+
TIMING_SUITES = (*CI_SHARDED_SUITES, "benchmark")
2223
MAX_BYTES = 5 * 1024 * 1024
2324
MAX_ENTRIES = 10_000
2425
MAX_CANDIDATES = 5
@@ -58,8 +59,10 @@ class CrossOriginRedirectHandler(urllib.request.HTTPRedirectHandler):
5859

5960

6061
def shard_count(suite: str) -> int:
61-
if suite not in SHARDED_SUITES:
62+
if suite not in TIMING_SUITES:
6263
raise ValueError(f"unsupported timing suite: {suite}")
64+
if suite == "benchmark":
65+
return 4
6366
return int(ci_config()[f"{suite}_shard_count"])
6467

6568

@@ -75,7 +78,7 @@ def ci_config() -> dict[str, Any]:
7578
payload = json.loads(CONFIG.read_text(encoding="utf-8"))
7679
if not isinstance(payload, dict):
7780
raise ValueError("ci-config.json must contain a JSON object")
78-
for suite in SHARDED_SUITES:
81+
for suite in CI_SHARDED_SUITES:
7982
count = payload.get(f"{suite}_shard_count")
8083
if not isinstance(count, int) or isinstance(count, bool) or count < 1:
8184
raise ValueError(f"{suite}_shard_count must be a positive integer")
@@ -111,7 +114,7 @@ def emit_plan_outputs() -> None:
111114
"""Print GitHub Actions outputs for shard, Node, and pytest-split pins."""
112115

113116
config = ci_config()
114-
for suite in SHARDED_SUITES:
117+
for suite in CI_SHARDED_SUITES:
115118
count = int(config[f"{suite}_shard_count"])
116119
print(f"{suite}-shard-count={count}")
117120
print(f"{suite}-shards={json.dumps(list(range(1, count + 1)))}")
@@ -138,8 +141,9 @@ def validate_durations(value: Any, source: str, suite: str) -> dict[str, float]:
138141
raise ValueError(f"{source} exceeds {MAX_ENTRIES} timing entries")
139142

140143
durations: dict[str, float] = {}
144+
prefix = "benchmarks/validation/" if suite == "benchmark" else f"tests/{suite}/"
141145
for nodeid, duration in value.items():
142-
if not isinstance(nodeid, str) or not nodeid.startswith(f"tests/{suite}/"):
146+
if not isinstance(nodeid, str) or not nodeid.startswith(prefix):
143147
raise ValueError(f"{source} contains an invalid test node id: {nodeid!r}")
144148
if isinstance(duration, bool) or not isinstance(duration, (int, float)):
145149
raise ValueError(f"{source} contains a non-numeric duration for {nodeid}")
@@ -321,14 +325,14 @@ def main() -> None:
321325

322326
prepare_parser = subparsers.add_parser("prepare")
323327
prepare_parser.add_argument("--output", type=Path, required=True)
324-
prepare_parser.add_argument("--suite", choices=SHARDED_SUITES, required=True)
328+
prepare_parser.add_argument("--suite", choices=TIMING_SUITES, required=True)
325329

326330
merge_parser = subparsers.add_parser("merge")
327331
merge_parser.add_argument("--input", action="append", type=Path, required=True)
328332
merge_parser.add_argument("--output", type=Path, required=True)
329333
merge_parser.add_argument("--source-sha", required=True)
330334
merge_parser.add_argument("--python-version", required=True)
331-
merge_parser.add_argument("--suite", choices=SHARDED_SUITES, required=True)
335+
merge_parser.add_argument("--suite", choices=TIMING_SUITES, required=True)
332336
merge_parser.add_argument(
333337
"--pytest-split-version",
334338
default=None,

0 commit comments

Comments
 (0)