#!/usr/bin/env python3 """Fill the measurement pages from the benchmark output. The two measurement articles are written as index.html.tmpl with __PLACEHOLDER__ markers where a table or a chart belongs. Every number a reader sees is put there by this script, straight out of the JSON Lines the benchmarks produced, so a page cannot drift away from the run that produced it. Re-run it after any re-run of a benchmark. Usage: ./render-pages.py (paths are relative to this file) """ import json, os, subprocess, sys, tempfile HERE = os.path.dirname(os.path.abspath(__file__)) SITE = os.path.join(os.path.dirname(HERE), "site") COMP = os.path.join(HERE, "compress-results.jsonl") SQL = os.path.join(HERE, "sqlite-results.jsonl") RESTORE_BYTES = 2e12 # "time to read back 2 TB", a plausible home library def load(path): return [json.loads(l) for l in open(path) if l.strip()] def cell(v, best=False): attr = ' class="best"' if best else "" return f"{v}" def table_ratio(meas): corpora = ["code", "docs", "db", "photos", "video"] codecs = list(dict.fromkeys(r["codec"] for r in meas)) by = {(r["codec"], r["corpus"]): r for r in meas} # Highlight the winner only where winning means something. On photos and # video every setting lands within a percent of 1.00x, and marking one of # them "best" would dress up measurement noise as a result. best = {} for c in corpora: vals = [by[(k, c)]["ratio"] for k in codecs if (k, c) in by] best[c] = max(vals) if vals and max(vals) / min(vals) > 1.05 else None out = [] for k in codecs: cells = "".join( cell(f'{by[(k, c)]["ratio"]:.2f}x', best[c] is not None and by[(k, c)]["ratio"] == best[c]) if (k, c) in by else cell("—") for c in corpora) out.append(f"{k}{cells}") return "\n".join(out) def table_decomp(meas): codecs = list(dict.fromkeys(r["codec"] for r in meas)) by = {r["codec"]: r for r in meas if r["corpus"] == "code"} fastest = max(by[k]["decompress_mbs"] for k in codecs if k in by) out = [] for k in codecs: if k not in by: continue mbs = by[k]["decompress_mbs"] hours = RESTORE_BYTES / (mbs * 1e6) / 3600 span = f"{hours * 60:.0f} min" if hours < 1 else f"{hours:.1f} h" out.append(f"{k}" f"{cell(f'{mbs:,.0f} MB/s', mbs == fastest)}" f"{cell(span, mbs == fastest)}") return "\n".join(out) def table_sqlite(meas): out = [] for j in ("delete", "wal"): for s in ("full", "normal", "off"): for ff in (False, True): cells = "" for batch in (1, 100, 1000): hit = [r for r in meas if r["journal_mode"] == j and r["synchronous"] == s and r["fullfsync"] == ff and r["rows_per_transaction"] == batch] cells += cell(f'{hit[0]["rows_per_s"]:,}' if hit else "—") out.append(f"{j}{s}" f"{'ON' if ff else 'off'}{cells}") return "\n".join(out) def codec_data(meas): """The per-type ratio and throughput table the calculator runs on. Only the settings a person would plausibly choose for a backup: the level ladder within zstd, plus one representative of each of the older codecs. Every value is read straight out of the measurement file.""" keep = ["zstd -1", "zstd -3 -T0", "zstd -3", "zstd -9", "zstd -19 -T0", "gzip -6", "bzip2 -9", "xz -6 -T0", "xz -9"] types = ["photos", "video", "docs", "db", "code"] by = {(r["codec"], r["corpus"]): r for r in meas} rows = [] for k in keep: if not all((k, t) in by for t in types): continue rows.append({ "name": k, "ratio": {t: round(by[(k, t)]["ratio"], 3) for t in types}, "mbs": {t: round(by[(k, t)]["compress_mbs"], 1) for t in types}, }) if not rows: sys.exit("codec_data: no complete codec rows found") return json.dumps(rows, indent=2) def inline_svg(path): with open(path) as f: return f.read().strip() def render(tmpl, subs): src = tmpl + ".tmpl" if not tmpl.endswith(".tmpl") else tmpl out = src[:-5] text = open(src).read() for key, value in subs.items(): marker = f"__{key}__" if marker not in text: sys.exit(f"{src}: no placeholder {marker}") text = text.replace(marker, value) left = [w for w in text.split("__") if w.isupper() and "_" in w] if left: sys.exit(f"{src}: unfilled placeholders {left}") open(out, "w").write(text) print(f"rendered {out} ({len(text):,} bytes)") def main(): comp = [r for r in load(COMP) if r["record"] == "measure"] sql = [r for r in load(SQL) if r["record"] == "measure"] charts = tempfile.mkdtemp() subprocess.run([sys.executable, os.path.join(HERE, "make-charts.py"), COMP, SQL, charts], check=True, stdout=subprocess.DEVNULL) render(os.path.join(SITE, "measurements/backup-compression/index.html"), { "CHART_CORPUS_TIME": inline_svg(os.path.join(charts, "corpus-time.svg")), "CHART_RATIO_TYPE": inline_svg(os.path.join(charts, "ratio-by-type.svg")), "TABLE_RATIO": table_ratio(comp), "TABLE_DECOMP": table_decomp(comp), }) render(os.path.join(SITE, "tools/backup-window/index.html"), { "CODEC_DATA": codec_data(comp), }) render(os.path.join(SITE, "measurements/sqlite-durability/index.html"), { "CHART_SQLITE_1": inline_svg(os.path.join(charts, "sqlite-fullfsync-1.svg")), "TABLE_SQLITE": table_sqlite(sql), }) if __name__ == "__main__": main()