Snapshot of zapier-decisions (maximized NS1 NATS study)

This commit is contained in:
George Lambert 2026-09-12 01:34:53 -04:00
commit 0d99c2cb6a
37 changed files with 5566 additions and 0 deletions

View file

@ -0,0 +1,247 @@
#!/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"<style>{css_text}</style>\n", encoding="utf-8")
banner.write_text(
'<div class="doc-banner">'
'<nav class="site"><a href="/">zapier.georgelambert.org</a>'
' · <a href="/index-md.html">Markdown indexes</a></nav>'
'<div class="kicker">Verae Time × Zapier · progress report</div>'
"<h1>NATS cluster message speed</h1>"
'<div class="source-path">packages/zapier-decisions/reports/nats-cluster-bench.md</div>'
"</div>\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())