"""SLO-goodput analyzer + PD_advantage for the PD-disagg crossover study. Reads per-arm replayer output (replay_metrics.jsonl) and computes, per arm: - completion rate (error-free fraction) - raw TTFT / TPOT / E2E percentiles (over successes — reported for context only; NEVER the verdict metric, since failing arms have a small success set) - SLO-goodput: fraction of OFFERED requests that are error-free AND meet a (TTFT, TPOT) SLO. This is the verdict metric. The two arms must replay the IDENTICAL trace (same seed), so they are paired request-for-request. PD_advantage = goodput(arm) / goodput(baseline); y=1 is the crossover line — PD_advantage >= 1 means PD-disagg wins. Goodput is computed over a grid of SLO thresholds so the conclusion does not hinge on one arbitrary cutoff. Usage: python analyze_goodput.py \ --arm 8C-proxy .../8C-proxy/replay_metrics.jsonl \ --arm 4P+4D .../4P+4D/replay_metrics.jsonl \ --baseline 8C-proxy \ --ttft-slo 0.5 1 2 5 --tpot-slo 0.05 0.1 0.2 """ from __future__ import annotations import argparse import json import statistics from pathlib import Path def load_metrics(path: Path) -> list[dict]: rows = [] with path.open("r", encoding="utf-8") as fh: for line in fh: line = line.strip() if line: rows.append(json.loads(line)) return rows def percentile(sorted_vals: list[float], pct: float) -> float: n = len(sorted_vals) if n == 0: return float("nan") if n == 1: return sorted_vals[0] rank = pct * (n - 1) lo = int(rank) hi = min(lo + 1, n - 1) frac = rank - lo return sorted_vals[lo] * (1 - frac) + sorted_vals[hi] * frac def pstats(vals: list[float]) -> dict: clean = sorted(v for v in vals if v is not None) if not clean: return {"count": 0} return { "count": len(clean), "mean": statistics.fmean(clean), "p50": percentile(clean, 0.50), "p90": percentile(clean, 0.90), "p99": percentile(clean, 0.99), } def offered_window_s(rows: list[dict]) -> float: ts = [r.get("trace_timestamp_s") for r in rows if r.get("trace_timestamp_s") is not None] if len(ts) < 2: return 0.0 return max(ts) - min(ts) def meets_slo(r: dict, ttft_slo: float, tpot_slo: float) -> bool: if r.get("error") is not None: return False ttft = r.get("ttft_s") tpot = r.get("tpot_s") if ttft is None: return False if ttft > ttft_slo: return False # tpot=0 happens only for single-token outputs; treat as meeting any SLO. if tpot is not None and tpot > tpot_slo: return False return True def load_summary(jsonl_path: Path) -> dict: """Read the sibling replay_metrics.summary.json (wall-clock, amplification).""" sp = jsonl_path.with_suffix(".summary.json") if sp.exists(): try: return json.loads(sp.read_text()) except Exception: return {} return {} def summarize_arm(name: str, jsonl_path: Path, rows: list[dict]) -> dict: n = len(rows) ok = [r for r in rows if r.get("error") is None] window = offered_window_s(rows) summ = load_summary(jsonl_path) return { "name": name, "n_offered": n, "n_success": len(ok), "completion_rate": len(ok) / n if n else 0.0, "offered_window_s": window, "offered_qps": n / window if window > 0 else 0.0, # Throughput: how much longer than the offered window it took to drain. # ~1.0 = keeps up; >1 = falling behind (the cleanest PD-collapse signal). "wall_clock_s": summ.get("wall_clock_s"), "amplification": summ.get("amplification"), "ttft": pstats([r.get("ttft_s") for r in ok]), "tpot": pstats([r.get("tpot_s") for r in ok]), "e2e": pstats([r.get("latency_s") for r in ok]), "_rows": rows, } def main() -> None: p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) p.add_argument("--arm", nargs=2, action="append", metavar=("NAME", "PATH"), required=True, help="arm name + replay_metrics.jsonl path (repeatable)") p.add_argument("--baseline", required=True, help="arm name to use as PD_advantage denominator") p.add_argument("--ttft-slo", nargs="+", type=float, default=[0.5, 1.0, 2.0, 5.0]) p.add_argument("--tpot-slo", nargs="+", type=float, default=[0.05, 0.1, 0.2]) p.add_argument("--out-json", type=Path, default=None) args = p.parse_args() arms = {} for name, path in args.arm: arms[name] = summarize_arm(name, Path(path), load_metrics(Path(path))) if args.baseline not in arms: raise SystemExit(f"baseline {args.baseline!r} not among arms {list(arms)}") # ---- per-arm overview ------------------------------------------------ print("=" * 78) print("PER-ARM OVERVIEW (latency stats over successes only — context, not verdict)") print("=" * 78) hdr = f"{'arm':<12}{'offered':>8}{'compl%':>8}{'ampl':>6}{'oQPS':>7}" \ f"{'TTFTp50':>9}{'TTFTp90':>9}{'TPOTp50':>9}{'TPOTp99':>9}{'E2Ep90':>9}" print(hdr) for name, a in arms.items(): t, tp, e = a["ttft"], a["tpot"], a["e2e"] ampl = a.get("amplification") ampl_s = f"{ampl:>6.2f}" if isinstance(ampl, (int, float)) else f"{'--':>6}" print(f"{name:<12}{a['n_offered']:>8}{100*a['completion_rate']:>7.1f}%" f"{ampl_s}{a['offered_qps']:>7.2f}" f"{t.get('p50', float('nan')):>9.2f}{t.get('p90', float('nan')):>9.2f}" f"{1000*tp.get('p50', float('nan')):>8.0f}m{1000*tp.get('p99', float('nan')):>8.0f}m" f"{e.get('p90', float('nan')):>9.2f}") # ---- SLO-goodput grid + PD_advantage -------------------------------- base = arms[args.baseline] grid = [] print() print("=" * 78) print(f"SLO-GOODPUT (attainment = error-free AND TTFT<=slo AND TPOT<=slo)") print(f"PD_advantage = attainment(arm) / attainment(baseline={args.baseline}); " f">=1 means arm wins") print("=" * 78) for ttft_slo in args.ttft_slo: for tpot_slo in args.tpot_slo: row = {"ttft_slo_s": ttft_slo, "tpot_slo_s": tpot_slo, "arms": {}} base_n = sum(1 for r in base["_rows"] if meets_slo(r, ttft_slo, tpot_slo)) base_att = base_n / base["n_offered"] if base["n_offered"] else 0.0 line = f"TTFT<={ttft_slo:>4}s TPOT<={int(1000*tpot_slo):>4}ms | " cells = [] for name, a in arms.items(): n_slo = sum(1 for r in a["_rows"] if meets_slo(r, ttft_slo, tpot_slo)) att = n_slo / a["n_offered"] if a["n_offered"] else 0.0 adv = (att / base_att) if base_att > 0 else float("nan") row["arms"][name] = {"attainment": att, "pd_advantage": adv, "n_slo": n_slo} tag = "" if name == args.baseline else f" adv={adv:.2f}" cells.append(f"{name}={100*att:>5.1f}%{tag}") print(line + " ".join(cells)) grid.append(row) if args.out_json: out = { "baseline": args.baseline, "arms": {n: {k: v for k, v in a.items() if k != "_rows"} for n, a in arms.items()}, "slo_grid": grid, } args.out_json.write_text(json.dumps(out, indent=2)) print(f"\nwrote {args.out_json}") if __name__ == "__main__": main()