"""Summarise runs/*/record.json into results/summary.json and results/summary.md.""" import json import statistics from collections import defaultdict from pathlib import Path root = Path(__file__).parent records = [json.loads(p.read_text()) for p in sorted((root / "runs").glob("*/record.json"))] by = defaultdict(list) for r in records: by[r["scenario"]].append(r) def ev(r, worker=None, event=None): return [e for e in r["timeline"] if (worker is None or e["worker"] == worker) and (event is None or e["event"] == event)] def first_t(r, worker, event): hits = ev(r, worker, event) return hits[0]["t"] if hits else None def per_run(r): result = json.loads(r["task"]["result"]) if r.get("task") and r["task"].get("result") else None effects = r["state"]["effects"] requests = r["state"]["requests"] a_finish = first_t(r, "A", "attempt.finish") b_start = first_t(r, "B", "attempt.start") interrupt = r["marks"].get("interruptAt") a_exit = next((e for e in ev(r, "A", "process.exit")), None) owners_seen = sorted({s["owner"] for s in r.get("storageTimeline", []) if s["owner"]}) renewals_a = sum(1 for s in r.get("storageTimeline", []) if s["owner"] == "A") - 1 return { "rep": r["rep"], "verdict": r["verdict"], "finalStatus": r["task"]["status"] if r.get("task") else None, "storedResultFrom": result["worker"] if result else None, "attempts": {w: len(ev(r, w, "attempt.start")) for w in ("A", "B")}, "requests": len(requests), "effects": len(effects), "duplicateReplies": sum(1 for q in requests if q["outcome"] == "duplicate"), "ownersPersisted": owners_seen, "leaseRenewalsByA": max(renewals_a, 0) if "A" in owners_seen else 0, "bStartedWhileAAlive": b_start is not None and (interrupt is None or b_start < interrupt) and (a_finish is None or b_start < a_finish), "takeoverMs": (b_start - interrupt) if (b_start is not None and interrupt is not None and b_start > interrupt) else None, "aExit": {"code": a_exit["code"], "signal": a_exit["signal"]} if a_exit else None, "aAbortReason": next((e.get("reason") for e in ev(r, "A", "attempt.aborted")), None), "retryCount": r["task"]["retryCount"] if r.get("task") else None, } summary = {} for sid, runs in sorted(by.items()): rows = [per_run(r) for r in sorted(runs, key=lambda r: r["rep"])] takeovers = [x["takeoverMs"] for x in rows if x["takeoverMs"] is not None] summary[sid] = { "variant": runs[0]["variant"], "versions": runs[0]["versions"], "args": {k: v for k, v in runs[0]["args"].items() if k != "operationId"}, "action": runs[0]["action"], "runs": len(rows), "verdicts": sorted({x["verdict"] for x in rows}), "effects": [x["effects"] for x in rows], "requests": [x["requests"] for x in rows], "storedResultFrom": [x["storedResultFrom"] for x in rows], "attemptsA": [x["attempts"]["A"] for x in rows], "attemptsB": [x["attempts"]["B"] for x in rows], "bStartedWhileAAlive": [x["bStartedWhileAAlive"] for x in rows], "ownersPersisted": [x["ownersPersisted"] for x in rows], "takeoverMs": {"min": min(takeovers), "median": statistics.median(takeovers), "max": max(takeovers)} if takeovers else None, "detail": rows, } out = root / "results" out.mkdir(exist_ok=True) (out / "summary.json").write_text(json.dumps(summary, indent=2)) lines = ["| Scenario | Runs | Attempts A / B | Requests | Effects | Stored result from | Owner persisted | B ran while A alive | Takeover after interrupt (ms) |", "|---|---|---|---|---|---|---|---|---|"] for sid, s in summary.items(): t = s["takeoverMs"] lines.append(f"| {sid} | {s['runs']} | {sorted(set(s['attemptsA']))} / {sorted(set(s['attemptsB']))} | {sorted(set(s['requests']))} | {sorted(set(s['effects']))} | " f"{sorted(set(map(str, s['storedResultFrom'])))} | " f"{sorted({','.join(o) or 'none' for o in s['ownersPersisted']})} | {sorted(set(s['bStartedWhileAAlive']))} | " f"{'-' if not t else f'{t['min']}–{t['max']} (median {t['median']})'} |") (out / "summary.md").write_text("\n".join(lines) + "\n") print("\n".join(lines))