#!/usr/bin/env python3 """Charts + HTML + PDF for the NATS cluster speed report in zapier-decisions/reports.""" from __future__ import annotations import subprocess import sys from pathlib import Path import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt from matplotlib.ticker import FuncFormatter ROOT = Path(__file__).resolve().parents[3] PKG = Path(__file__).resolve().parents[1] REPORT_DIR = PKG / "reports" CHARTS = REPORT_DIR / "charts" MD = REPORT_DIR / "nats-cluster-bench.md" HTML = REPORT_DIR / "nats-cluster-bench.html" PDF = REPORT_DIR / "nats-cluster-bench.pdf" CSS = ROOT / "scripts" / "docs-print.css" LUA = ROOT / "scripts" / "pdf-links.lua" INDIGO = "#4f46e5" DEEP = "#312e81" TEAL = "#047857" AMBER = "#b45309" LILAC = "#7c74f0" INK = "#171a26" MUTED = "#5b6178" GRID = "#d9dce8" # Run 20260912T045131Z — parsed from nats bench logs (Sub stats, not Pub/Sub aggregate). CORE = { "labels": ["1p1s\n50k×128 B", "4p4s\n100k×128 B", "8p8s\n200k×128 B", "4p4s\n50k×1 KiB"], "agg": [1_200_836, 1_521_256, 2_007_937, 630_460], "pub": [791_094, 316_312, 333_957, 247_747], "sub": [747_461, 1_299_634, 1_790_736, 510_216], "pub_mb": [96.57, 38.61, 40.77, 241.94], "sub_mb": [91.24, 158.65, 218.60, 498.26], } JS = { "labels": ["1p 20k×128 B", "4p 50k×128 B", "4p 20k×1 KiB", "2p2s pull 20k×128 B"], "pub": [16_155, 16_607, 13_493, 10_965], "sub": [None, None, None, 10_942], } DELAY = { "labels": ["Ping\n1k×128 B", "Flood 1p\n5k×128 B", "Flood 4p\n5k×1 KiB", "Flood 4p\n10k×128 B", "Flood 8p\n20k×128 B"], "kind": ["ping", "flood", "flood", "flood", "flood"], "p50": [0.286, 248.752, 217.579, 266.672, 466.296], "p90": [0.332, 274.314, 223.268, 299.073, 499.924], "p99": [0.734, 279.398, 227.798, 304.233, 505.112], "avg": [0.307, 238.626, 211.706, 263.186, 453.749], } def style() -> None: plt.rcParams.update( { "font.family": "sans-serif", "font.size": 10, "axes.titlesize": 12, "axes.titleweight": "semibold", "axes.labelsize": 10, "axes.edgecolor": GRID, "axes.labelcolor": INK, "text.color": INK, "xtick.color": MUTED, "ytick.color": MUTED, "figure.facecolor": "white", "axes.facecolor": "white", "axes.grid": True, "grid.color": GRID, "grid.linewidth": 0.8, "legend.frameon": False, "savefig.bbox": "tight", "savefig.dpi": 160, "savefig.facecolor": "white", } ) def k_fmt(x: float, _pos: int | None = None) -> str: if x >= 1_000_000: return f" {x / 1_000_000:.2f}M".strip() if x >= 1000: return f"{x / 1000:.0f}k" return f"{x:.0f}" def save(fig: plt.Figure, name: str) -> None: CHARTS.mkdir(parents=True, exist_ok=True) path = CHARTS / name fig.savefig(path, dpi=160) plt.close(fig) print(f"wrote {path}") def chart_core_msgs() -> None: fig, ax = plt.subplots(figsize=(9.2, 4.4)) x = range(len(CORE["labels"])) w = 0.25 ax.bar([i - w for i in x], CORE["agg"], w, label="Aggregate", color=DEEP) ax.bar(list(x), CORE["pub"], w, label="Publish", color=INDIGO) ax.bar([i + w for i in x], CORE["sub"], w, label="Subscribe", color=TEAL) ax.set_xticks(list(x), CORE["labels"]) ax.set_ylabel("messages / second") ax.set_title("Core NATS throughput (fire-and-forget pub/sub)") ax.yaxis.set_major_formatter(FuncFormatter(k_fmt)) ax.legend(loc="upper left") ax.set_axisbelow(True) save(fig, "core-throughput.png") def chart_js_msgs() -> None: fig, ax = plt.subplots(figsize=(9.2, 4.4)) colors = [INDIGO, INDIGO, AMBER, LILAC] ax.bar(JS["labels"], JS["pub"], color=colors) ax.set_ylabel("durable publish messages / second") ax.set_title("JetStream file store, replicas=3 (product-stream settings)") ax.yaxis.set_major_formatter(FuncFormatter(k_fmt)) ax.set_axisbelow(True) for i, v in enumerate(JS["pub"]): ax.text(i, v + 250, f"{v:,}", ha="center", va="bottom", fontsize=9, color=MUTED) save(fig, "js-throughput.png") def chart_core_vs_js() -> None: fig, ax = plt.subplots(figsize=(9.2, 4.4)) labels = ["1 publisher\n128 B", "4 publishers\n128 B", "4 publishers\n1 KiB"] core_pub = [791_094, 316_312, 247_747] js_pub = [16_155, 16_607, 13_493] x = range(len(labels)) w = 0.35 ax.bar([i - w / 2 for i in x], core_pub, w, label="Core NATS (no disk)", color=INDIGO) ax.bar([i + w / 2 for i in x], js_pub, w, label="JetStream r=3 file", color=AMBER) ax.set_xticks(list(x), labels) ax.set_yscale("log") ax.set_ylabel("publish messages / second (log)") ax.set_title("Core vs JetStream: two different jobs") ax.legend(loc="upper right") ax.set_axisbelow(True) save(fig, "core-vs-js.png") def chart_bytes() -> None: fig, axes = plt.subplots(1, 2, figsize=(9.2, 4.2)) labels = ["128 B\n4p4s", "1 KiB\n4p4s"] msgs = [1_521_256, 630_460] mb = [185.70, 615.68] axes[0].bar(labels, msgs, color=[INDIGO, AMBER]) axes[0].set_title("Aggregate messages / second") axes[0].yaxis.set_major_formatter(FuncFormatter(k_fmt)) axes[0].set_axisbelow(True) axes[1].bar(labels, mb, color=[INDIGO, AMBER]) axes[1].set_title("Aggregate MB / second") axes[1].set_ylabel("MB/s") axes[1].set_axisbelow(True) for ax, vals, fmt in ( (axes[0], msgs, lambda v: f"{v/1e6:.2f}M"), (axes[1], mb, lambda v: f"{v:.0f}"), ): for i, v in enumerate(vals): ax.text(i, v * 1.02, fmt(v), ha="center", va="bottom", fontsize=9, color=MUTED) fig.suptitle("Core NATS: bigger payloads move more bytes, fewer messages", fontsize=12, fontweight="semibold") fig.tight_layout() save(fig, "payload-size.png") def chart_delay() -> None: fig, ax = plt.subplots(figsize=(9.2, 4.6)) x = range(len(DELAY["labels"])) w = 0.25 ax.bar([i - w for i in x], DELAY["p50"], w, label="p50", color=TEAL) ax.bar(list(x), DELAY["p90"], w, label="p90", color=INDIGO) ax.bar([i + w for i in x], DELAY["p99"], w, label="p99", color=AMBER) ax.set_xticks(list(x), DELAY["labels"]) ax.set_yscale("log") ax.set_ylabel("milliseconds (log)") ax.set_title("Round-trip delay: sequential ping vs burst flood") ax.axhline(1.0, color=GRID, linestyle="--", linewidth=1) ax.legend(loc="upper left") ax.set_axisbelow(True) save(fig, "delay-percentiles.png") def render() -> None: if not MD.exists(): raise SystemExit(f"missing {MD}") header = HTML.with_suffix(".hdr.html") banner = HTML.with_suffix(".ban.html") css_text = CSS.read_text(encoding="utf-8") header.write_text(f"\n", encoding="utf-8") banner.write_text( '
\n", encoding="utf-8", ) r = subprocess.run( [ "pandoc", str(MD), "-o", str(HTML), "--standalone", f"--resource-path={REPORT_DIR}", "--syntax-highlighting=breezedark", "--metadata=title=NATS cluster message speed", f"--include-in-header={header}", f"--include-before-body={banner}", f"--lua-filter={LUA}", ], capture_output=True, text=True, ) header.unlink(missing_ok=True) banner.unlink(missing_ok=True) if r.returncode != 0: sys.stderr.write(r.stderr) raise SystemExit(f"pandoc failed: {r.returncode}") w = subprocess.run(["weasyprint", str(HTML), str(PDF)], capture_output=True, text=True) if w.returncode != 0: sys.stderr.write(w.stderr) raise SystemExit(f"weasyprint failed: {w.returncode}") print(f"wrote {HTML}") print(f"wrote {PDF}") def main() -> int: style() chart_core_msgs() chart_js_msgs() chart_core_vs_js() chart_bytes() chart_delay() render() return 0 if __name__ == "__main__": raise SystemExit(main())