Add full latency grid (mean/p50/p90/p99 × TTFT/TPOT/E2E) as f6 companion

The headline f6_e2e_latency_bars only shows p90, hiding three regimes:
  - mean: unified dominates (3.3s TTFT, 7.0s E2E vs sticky 5.6s / 12.1s)
  - p50: sticky and unified are tied on first-turn TTFT (0.5s each) —
    sticky's first turn of each session is free, after which queues
    accumulate. Unified beats sticky everywhere else.
  - p99: tail amplification reveals unified's biggest gap —
    TTFT 42.3s vs sticky 74.1s; E2E 68.8s vs sticky 139.7s.

The 12-panel figure is the honest full picture; the 3-panel headline
stays for slide-friendly summary.

- analysis/characterization/window_1_results/raw_stats/{policy}.json:
  cached ttft/tpot/e2e {mean,p50,p90,p99} pulled from dash0
  /home/admin/cpfs/wjh/agentic-kv/outputs/b3_sweep_20260525_095043/
  (b3_policy_comparison.json doesn't record mean, only percentiles).
- analysis/characterization/render_window1_figures.py:
  new fig_b3_latency_full_grid renders the 4×3 grid from the cache.
- figs/f6_e2e_latency_full_grid.png: 12-panel companion.
- PAPER_OUTLINE.md §5.2: both figures embedded; main table column
  renamed from "Hotspot idx" to "Worker p90 (median / max)" to match
  the new metric convention.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 11:15:18 +08:00
parent 5e6e98aee7
commit 922d79ac95
7 changed files with 145 additions and 4 deletions

View File

@@ -89,6 +89,48 @@ def fig_b3_latency_bars(comp: dict, out: Path) -> None:
plt.close(fig)
def fig_b3_latency_full_grid(results_dir: Path, out: Path) -> None:
"""4 rows (mean / p50 / p90 / p99) × 3 cols (TTFT / TPOT / E2E) per policy.
Reads per-policy metrics.summary.json caches under raw_stats/, which
expose mean alongside the percentiles (b3_policy_comparison.json does
not record mean).
"""
raw_dir = results_dir / "raw_stats"
pols = [p for p in POLICY_ORDER if (raw_dir / f"{p}.json").exists()]
if not pols:
return
stats = {p: json.loads((raw_dir / f"{p}.json").read_text()) for p in pols}
rows = [("mean", "mean"), ("p50", "p50"), ("p90", "p90"), ("p99", "p99")]
cols = [
("TTFT (s)", "ttft", 1.0),
("TPOT (ms)", "tpot", 1000.0),
("E2E (s)", "e2e", 1.0),
]
fig, axes = plt.subplots(len(rows), len(cols), figsize=(11, 11), sharex=True)
for i, (row_label, agg_key) in enumerate(rows):
for j, (col_label, metric_key, scale) in enumerate(cols):
ax = axes[i][j]
vals = [stats[p][metric_key][agg_key] * scale for p in pols]
ax.bar(pols, vals,
color=[POLICY_COLOR.get(p, "gray") for p in pols],
edgecolor="black", linewidth=0.5)
for k, v in enumerate(vals):
ax.text(k, v, f"{v:.1f}", ha="center", va="bottom", fontsize=8)
if j == 0:
ax.set_ylabel(row_label, fontsize=11)
if i == 0:
ax.set_title(col_label, fontsize=11)
ax.grid(alpha=0.3, axis="y")
ax.tick_params(axis="x", rotation=20, labelsize=9)
ax.margins(y=0.18)
fig.suptitle("B3 latencies per policy — mean / p50 / p90 / p99")
fig.tight_layout()
fig.savefig(out, dpi=120)
plt.close(fig)
def fig_b3_apc_vs_upper(comp: dict, upper: dict, out: Path) -> None:
by = {r["policy"]: r for r in comp["rows"]}
pols = [p for p in POLICY_ORDER if p in by]
@@ -307,6 +349,9 @@ def main() -> None:
fig_b3_apc_vs_hotspot(comp, upper, args.out_dir / "fig_b3_apc_vs_hotspot.png")
fig_b3_latency_bars(comp, args.out_dir / "fig_b3_latency_bars.png")
fig_b3_latency_full_grid(
args.results_dir, args.out_dir / "fig_b3_latency_full_grid.png"
)
fig_b3_apc_vs_upper(comp, upper, args.out_dir / "fig_b3_apc_vs_upper.png")
fig_b3_failure_breakdown(comp, args.out_dir / "fig_b3_failure_breakdown.png")
fig_b3_per_worker_ttft(args.results_dir, comp,