#!/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()