Snapshot of zapier-decisions (maximized NS1 NATS study)
This commit is contained in:
commit
0d99c2cb6a
37 changed files with 5566 additions and 0 deletions
247
scripts/build-nats-bench-report.py
Normal file
247
scripts/build-nats-bench-report.py
Normal 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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue