#!/usr/bin/env python3 """Draw the SVG charts used on the Cold Spare measurement pages. Reads the JSON Lines emitted by compress-bench.py and sqlite-durability-bench.py and writes plain SVG - no chart library, no runtime JavaScript, no external fonts beyond the ones the page already loads. Both charts are log-scaled on the value axis because the spreads here run over four orders of magnitude and a linear axis would flatten everything interesting into the baseline. Usage: ./make-charts.py compress-results.jsonl sqlite-results.jsonl out-dir/ """ import json, math, os, sys INK, DIM, GRID = "#d1e4fa", "#9da7ba", "rgba(186,215,247,0.14)" BAR, BAR2 = "#663af3", "#b6d9fc" FONT = "'Inter', ui-sans-serif, system-ui, sans-serif" MONO = "'JetBrains Mono', ui-monospace, Menlo, monospace" def esc(s): return str(s).replace("&", "&").replace("<", "<").replace(">", ">") def log_bars(rows, title, unit, path, label_w=190, width=680, series=None, scale="log", fmt=None, notes=None): """rows: [(label, value)] or [(label, value_a, value_b)] when series is set. scale="log" for spreads over orders of magnitude, "linear" when the values sit in a narrow band and a log axis would flatten the differences away. fmt formats the value printed at the end of each bar; notes maps a row label to a short annotation drawn after that value.""" fmt = fmt or (lambda v: f"{v:,.0f}") notes = notes or {} n = len(rows) bar_h, gap = (11, 7) if series else (16, 8) group = (bar_h * (2 if series else 1) + (3 if series else 0)) top, bottom = (66 if series else 46), 44 # extra room above for the legend height = top + n * (group + gap) + bottom # reserve enough room on the right for the longest value label, which for the # annotated charts is "54.7s saves 126 MB" rather than a bare number longest = max(len(fmt(v) + (" " + notes[r[0]] if notes and r[0] in notes else "")) for r in rows for v in r[1:]) plot_w = width - label_w - max(62, longest * 6.3 + 14) vals = [v for r in rows for v in r[1:]] if scale == "linear": # round the axis up to a tick size a person would have chosen span = max(vals) * 1.18 mag = 10 ** math.floor(math.log10(span / 4)) step = next(m * mag for m in (1, 2, 2.5, 5, 10) if m * mag >= span / 4) lo, hi = 0.0, step * 4 def x(v): return label_w + plot_w * (v - lo) / (hi - lo) else: lo = max(min(vals) * 0.8, 1e-9) hi = max(vals) * 1.15 lg_lo, lg_hi = math.log10(lo), math.log10(hi) def x(v): return label_w + plot_w * (math.log10(max(v, lo)) - lg_lo) / (lg_hi - lg_lo) out = [f'', f'{esc(title)}', f'{esc(title)}'] if scale == "linear": ticks = [hi * i / 4 for i in range(5)] # hi is already 4 nice steps else: ticks = [10 ** d for d in range(math.floor(lg_lo), math.ceil(lg_hi) + 1) if lg_lo <= d <= lg_hi] for t in ticks: gx = round(x(t), 1) out.append(f'') lab = fmt(t) out.append(f'{lab}') out.append(f'{esc(unit)}') y = top for row in rows: label, vals_ = row[0], row[1:] cy = y + group / 2 out.append(f'{esc(label)}') for i, v in enumerate(vals_): by = y + i * (bar_h + 3) w = max(x(v) - label_w, 1.5) out.append(f'') tail = f" {notes[label]}" if (label in notes and i == 0) else "" out.append(f'{esc(fmt(v) + tail)}') y += group + gap if series: lx = label_w for i, name in enumerate(series): out.append(f'') out.append(f'{esc(name)}') lx += 26 + 7 * len(name) out.append("") with open(path, "w") as f: f.write("\n".join(out)) return path def main(): comp_path, sql_path, outdir = sys.argv[1], sys.argv[2], sys.argv[3] os.makedirs(outdir, exist_ok=True) comp = [json.loads(l) for l in open(comp_path)] sql = [json.loads(l) for l in open(sql_path)] meas = [r for r in comp if r["record"] == "measure"] codecs = list(dict.fromkeys(r["codec"] for r in meas)) raw_total = sum(r["raw_bytes"] for r in meas if r["codec"] == codecs[0]) # 1. the whole argument in one chart: seconds spent, MB saved annotated rows, notes = [], {} for c in codecs: rs = [r for r in meas if r["codec"] == c] secs = sum(r["compress_s"] for r in rs) saved = (raw_total - sum(r["packed_bytes"] for r in rs)) / 1e6 rows.append((c, secs)) notes[c] = f"saves {saved:.0f} MB" log_bars(rows, f"Time to compress the whole {raw_total / 1e6:.0f} MB corpus", "seconds (log scale)", os.path.join(outdir, "corpus-time.svg"), label_w=150, width=680, fmt=lambda v: f"{v:.2f}s" if v < 1 else f"{v:.1f}s", notes=notes) # 2. ratio by data type, fast setting against slowest, linear so 1.0x reads as 1.0x pair = ("zstd -3 -T0", "xz -9") order = ["code", "docs", "db", "photos", "video"] rows = [] for corpus in order: vals = {r["codec"]: r["ratio"] for r in meas if r["corpus"] == corpus} if all(p in vals for p in pair): rows.append((corpus, vals[pair[0]], vals[pair[1]])) if rows: log_bars(rows, "Compression ratio by kind of data", "times smaller (linear scale)", os.path.join(outdir, "ratio-by-type.svg"), label_w=90, width=640, series=pair, scale="linear", fmt=lambda v: f"{v:.2f}x") # 3. sqlite: fullfsync off vs on, one row per journal/synchronous pair for batch in (1, 1000): rows = [] for j in ("delete", "wal"): for s in ("full", "normal", "off"): pair = {r["fullfsync"]: r["rows_per_s"] for r in sql if r["record"] == "measure" and r["journal_mode"] == j and r["synchronous"] == s and r["rows_per_transaction"] == batch} if len(pair) == 2: rows.append((f"{j}/{s}", pair[False], pair[True])) if rows: per = "1 row per transaction" if batch == 1 else f"{batch} rows per transaction" log_bars(rows, f"SQLite insert rate - {per}", "rows per second (log scale)", os.path.join(outdir, f"sqlite-fullfsync-{batch}.svg"), label_w=130, width=650, series=("fullfsync OFF (the default)", "fullfsync ON")) for f in sorted(os.listdir(outdir)): if f.endswith(".svg"): print(f"{outdir}/{f} {os.path.getsize(os.path.join(outdir, f))} bytes") if __name__ == "__main__": main()