build(ci): harden preview perf gate

This commit is contained in:
matt 2025-09-30 16:01:51 -07:00
parent 8e57588f40
commit 2888d97883
5 changed files with 65 additions and 1 deletions

View file

@ -37,6 +37,17 @@ def _fetch_json(url: str) -> Dict[str, Any]:
return json.loads(data) # type: ignore[return-value]
def _fetch_json_with_retry(url: str, attempts: int = 3, delay: float = 0.6) -> Dict[str, Any]:
for attempt in range(1, attempts + 1):
try:
return _fetch_json(url)
except Exception: # pragma: no cover - network variability
if attempt < attempts:
time.sleep(delay * attempt)
else:
raise
def select_theme_slugs(base_url: str, count: int) -> List[str]:
"""Discover theme slugs for benchmarking.
@ -89,7 +100,7 @@ def fetch_all_theme_slugs(base_url: str, page_limit: int = 200) -> List[str]:
while True:
try:
url = f"{base_url.rstrip('/')}/themes/api/themes?limit={page_limit}&offset={offset}"
data = _fetch_json(url)
data = _fetch_json_with_retry(url)
except Exception as e: # pragma: no cover - network variability
raise SystemExit(f"Failed fetching themes page offset={offset}: {e}")
items = data.get("items") or []

View file

@ -21,7 +21,35 @@ import argparse
import json
import subprocess
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path
def _wait_for_service(base_url: str, attempts: int = 8, delay: float = 1.5) -> bool:
health_url = base_url.rstrip("/") + "/healthz"
last_error: Exception | None = None
for attempt in range(1, attempts + 1):
try:
with urllib.request.urlopen(health_url, timeout=5) as resp: # nosec B310 local CI
if 200 <= resp.status < 300:
return True
except urllib.error.HTTPError as exc:
last_error = exc
if 400 <= exc.code < 500 and exc.code != 429:
# Treat permanent client errors (other than rate limit) as fatal
break
except Exception as exc: # pragma: no cover - network variability
last_error = exc
time.sleep(delay)
print(json.dumps({
"event": "ci_perf_error",
"stage": "startup",
"message": "Service health check failed",
"url": health_url,
"attempts": attempts,
"error": str(last_error) if last_error else None,
}))
return False
def run(cmd: list[str]) -> subprocess.CompletedProcess:
return subprocess.run(cmd, capture_output=True, text=True, check=False)
@ -39,6 +67,9 @@ def main(argv: list[str]) -> int:
print(json.dumps({"event":"ci_perf_error","message":"Baseline not found","path":str(args.baseline)}))
return 3
if not _wait_for_service(args.url):
return 3
# Run candidate single-pass all-themes benchmark (no extra warm cycles to keep CI fast)
# If multi-pass requested, run two passes over all themes so second pass represents warmed steady-state.
passes = "2" if args.multi_pass else "1"