- Figures with numbered captions: SAX stepper, json_view layout and edit stepper, exception map, JSON pointer and patch walkers, QA pipeline, scaling and binary size charts, roadmap lanes, release timeline, integration decision tree and namespace builder - Table captions (nj-table-caption) on 9 pages - Compile times: modules and PCH charts, filesystem section, re-measured Figure 1, links between features/modules.md and compile_times.md - Interactive macro chooser for features/arbitrary_types.md - Link check: exclude URLs that answer the plugin with 400/401/419 or are dead (customers.md) Signed-off-by: Niels Lohmann <mail@nlohmann.me>
281 lines
16 KiB
Python
281 lines
16 KiB
Python
"""Draws the measurement charts as inline SVGs into docs/images/.
|
|
|
|
Generated files:
|
|
|
|
- compile_times.svg: percent change in compile time per option at -O0 (integration/compile_times.md)
|
|
- compile_times_pch.svg, compile_times_modules.svg: percent change per translation unit with a precompiled header or
|
|
`import nlohmann.json;` at -O0, plus the one-off build cost (integration/compile_times.md)
|
|
- binary_sizes_<file>.svg: encoded size per binary format relative to minified JSON, one panel per test file
|
|
(features/binary_formats/index.md, reused on features/performance.md)
|
|
- ordered_json_scaling.svg: parse time of a flat object for json vs. ordered_json (features/performance.md)
|
|
|
|
Every number below is copied from the tables on those pages (or, where noted, computed from the milliseconds they
|
|
report); the tables stay on the pages as the accessible fallback. Update both together. The SVGs are not images:
|
|
the pages include their text with `--8<-- "images/<name>.svg"`, so they pick up the colours and fonts of the page
|
|
(see the `nj-charts` rules in docs/css/fig-charts.css). Run `python3 scripts/figures/charts.py` from docs/mkdocs after
|
|
changing data or drawing, and commit the generated files. In SVG text write the word JSON as `J​SON`,
|
|
otherwise the abbreviation extension wraps it in an <abbr> and the text disappears.
|
|
"""
|
|
import html
|
|
import math
|
|
import re
|
|
from pathlib import Path
|
|
|
|
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
|
|
MINUS = "−"
|
|
JSON = "J​SON"
|
|
|
|
|
|
# words the abbreviation extension (includes/glossary.md) would wrap in <abbr> inside the SVG text
|
|
ABBR = re.compile(r"(?<![\w;])(BJData|BON8|BSON|CBOR|GCC|UBJSON|MsgPack|JSON|DOM|SAX)(?!\w)")
|
|
|
|
|
|
def protect(text):
|
|
return ABBR.sub(lambda m: m.group(1)[0] + "​" + m.group(1)[1:], text)
|
|
|
|
|
|
def esc(s):
|
|
return html.escape(s, quote=True).replace("J&#8203;SON", JSON).replace("&#8722;", MINUS)
|
|
|
|
|
|
def write(name, W, H, cls, tid, title, desc, body):
|
|
s = ['<svg class="nj-fig nj-charts %s" viewBox="0 0 %g %g" role="img" aria-labelledby="%s %s-d" '
|
|
'xmlns="http://www.w3.org/2000/svg">' % (cls, W, H, tid, tid),
|
|
'<title id="%s">%s</title>' % (tid, esc(title)),
|
|
'<desc id="%s-d">%s</desc>' % (tid, esc(desc))]
|
|
s += body
|
|
s.append('</svg>')
|
|
(OUT / name).write_text(protect("\n".join(s)) + "\n")
|
|
|
|
|
|
def hbar(x0, x1, y, h, r=4):
|
|
"""Horizontal bar from the baseline x0 to x1, rounded only at the data end."""
|
|
if abs(x1 - x0) < 0.5:
|
|
return "M%g %g h0.5 v%g h-0.5 Z" % (x0, y, h)
|
|
r = min(r, h / 2, abs(x1 - x0))
|
|
s = 1 if x1 > x0 else -1
|
|
return ("M%g %g H%g Q%g %g %g %g V%g Q%g %g %g %g H%g Z"
|
|
% (x0, y, x1 - s * r, x1, y, x1, y + r, y + h - r, x1, y + h, x1 - s * r, y + h, x0))
|
|
|
|
|
|
def pct(v):
|
|
if v == 0:
|
|
return "0 %"
|
|
return ("+" if v > 0 else MINUS) + "%g %%" % abs(v)
|
|
|
|
|
|
# ---------- compile times (integration/compile_times.md, -O0 columns) ----------
|
|
# (label, translation unit, Apple clang %, GCC 16 %, computed?), ordered by saving
|
|
COMPILE = [
|
|
("json_fwd.hpp instead of json.hpp", "header-only TU", -62, -82, False),
|
|
("extern template", "parsing TU", -7, -21, False),
|
|
("JSON_NO_AUTOMATIC_UDLS", "model TU", -11, -11, False),
|
|
("JSON_NO_AUTOMATIC_UDLS", "parsing TU", -1, -2, False),
|
|
("JSON_HAS_FILESYSTEM=0 + JSON_NO_IO", "model TU", -8, -11, False),
|
|
("JSON_HAS_FILESYSTEM=0", "model TU", -5, -9, False),
|
|
# computed from the ms in "Options without effect on compile times": 1034/1051, 1756/1801, 1055/1051, 1783/1801
|
|
("JSON_NO_IO", "model TU", round((1034 / 1051 - 1) * 100), round((1756 / 1801 - 1) * 100), True),
|
|
("JSON_USE_GLOBAL_UDLS=0", "model TU", round((1055 / 1051 - 1) * 100), round((1783 / 1801 - 1) * 100), True),
|
|
]
|
|
|
|
|
|
def compile_times():
|
|
W, L, R = 470, 8, 402
|
|
lo, hi = -90, 10
|
|
X = lambda v: L + (v - lo) / (hi - lo) * (R - L)
|
|
top, pitch, bh = 64, 52, 12
|
|
H = top + pitch * len(COMPILE) + 30
|
|
b = []
|
|
# legend
|
|
b.append('<rect class="nj-charts-clang" x="%g" y="10" width="12" height="12" rx="3"/>' % L)
|
|
b.append('<text class="nj-charts-leg" x="%g" y="20.5">Apple clang</text>' % (L + 18))
|
|
b.append('<rect class="nj-charts-gcc" x="%g" y="10" width="12" height="12" rx="3"/>' % (L + 118))
|
|
b.append('<text class="nj-charts-leg" x="%g" y="20.5">GCC 16</text>' % (L + 136))
|
|
# grid and ticks
|
|
axis_y = top + pitch * len(COMPILE) - 8
|
|
for t in (-80, -60, -40, -20):
|
|
b.append('<line class="nj-charts-grid" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (X(t), top - 6, X(t), axis_y))
|
|
b.append('<text class="nj-charts-tick" x="%g" y="%g">%s</text>' % (X(t), axis_y + 16, pct(t)))
|
|
b.append('<text class="nj-charts-tick" x="%g" y="%g">0 %%</text>' % (X(0), axis_y + 16))
|
|
b.append('<text class="nj-charts-tick nj-charts-tick--l" x="%g" y="%g">← faster</text>' % (L, axis_y + 34))
|
|
b.append('<text class="nj-charts-tick nj-charts-tick--r" x="%g" y="%g">slower →</text>' % (W - 8, axis_y + 34))
|
|
for i, (label, tu, clang, gcc, computed) in enumerate(COMPILE):
|
|
y = top + i * pitch
|
|
b.append('<text class="nj-charts-rowlabel" x="%g" y="%g"><tspan class="nj-charts-code">%s</tspan>'
|
|
'<tspan class="nj-charts-sub"> · %s%s</tspan></text>'
|
|
% (L, y - 2, esc(label), tu, "*" if computed else ""))
|
|
for j, (v, cls, name) in enumerate(((clang, "clang", "Apple clang"), (gcc, "gcc", "GCC 16"))):
|
|
by = y + 6 + j * (bh + 2)
|
|
b.append('<path class="nj-charts-%s" d="%s"><title>%s, %s (%s): %s</title></path>'
|
|
% (cls, hbar(X(0), X(v), by, bh), esc(label), tu, name, esc(pct(v))))
|
|
b.append('<text class="nj-charts-val" x="%g" y="%g">%s</text>' % (max(X(0), X(v)) + 5, by + bh - 1.5, pct(v)))
|
|
b.append('<line class="nj-charts-zero" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (X(0), top - 6, X(0), axis_y))
|
|
H = axis_y + 44
|
|
desc = "; ".join("%s on a %s: Apple clang %s, GCC 16 %s" % (label, tu.replace("TU", "translation unit"),
|
|
pct(c).replace(MINUS, "-"), pct(g).replace(MINUS, "-")) for label, tu, c, g, _ in COMPILE)
|
|
write("compile_times.svg", W, H, "nj-charts--compile", "nj-charts-ct",
|
|
"Change in compile time against the default configuration at -O0, Apple clang and GCC 16",
|
|
"Grouped horizontal bars, negative is faster. " + desc + ".", b)
|
|
|
|
|
|
# ---------- precompiled headers and C++20 modules (integration/compile_times.md, -O0 columns) ----------
|
|
# Per-TU medians of the wall-clock time in ms from the tables "Use precompiled headers" (-std=c++17) and
|
|
# "Use C++20 modules" (-std=c++20): {translation unit: {compiler: (default ms, with PCH/module ms)}}, plus the one-off
|
|
# build cost in ms per compiler. None instead of a value means "not measured"; NOT_AVAILABLE then gives the reason.
|
|
PCH = {
|
|
"model TU": {"Apple clang": (1045, 594), "GCC 16": (1789, 1060)},
|
|
"parsing TU": {"Apple clang": (1148, 714), "GCC 16": (2498, 1884)},
|
|
}
|
|
PCH_BUILD = {"Apple clang": 771, "GCC 16": 2254}
|
|
MODULES = {
|
|
"model TU": {"Apple clang": (1311, 680), "GCC 16": (2156, 1153)},
|
|
"parsing TU": {"Apple clang": (1387, 800), "GCC 16": (2838, 1810)},
|
|
}
|
|
MODULES_BUILD = {"Apple clang": 1630, "GCC 16": 2538}
|
|
NOT_AVAILABLE = {} # compiler: reason
|
|
|
|
|
|
def tradeoff(name, tid, rows, build, what, title, desc_intro):
|
|
"""Percent change per TU (bars) and the one-off build cost (text rows below) for two compilers."""
|
|
W, L, R = 440, 8, 372
|
|
lo, hi = -60, 0
|
|
X = lambda v: L + (v - lo) / (hi - lo) * (R - L)
|
|
top, pitch, bh = 64, 52, 12
|
|
comps = (("Apple clang", "clang"), ("GCC 16", "gcc"))
|
|
b, desc = [], []
|
|
b.append('<rect class="nj-charts-clang" x="%g" y="10" width="12" height="12" rx="3"/>' % L)
|
|
b.append('<text class="nj-charts-leg" x="%g" y="20.5">Apple clang</text>' % (L + 18))
|
|
b.append('<rect class="nj-charts-gcc" x="%g" y="10" width="12" height="12" rx="3"/>' % (L + 118))
|
|
b.append('<text class="nj-charts-leg" x="%g" y="20.5">GCC 16</text>' % (L + 136))
|
|
axis_y = top + pitch * len(rows) - 8
|
|
for t in (-50, -25):
|
|
b.append('<line class="nj-charts-grid" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (X(t), top - 6, X(t), axis_y))
|
|
b.append('<text class="nj-charts-tick" x="%g" y="%g">%s</text>' % (X(t), axis_y + 16, pct(t)))
|
|
b.append('<text class="nj-charts-tick" x="%g" y="%g">0 %%</text>' % (X(0), axis_y + 16))
|
|
b.append('<text class="nj-charts-tick nj-charts-tick--l" x="%g" y="%g">← faster per TU</text>' % (L, axis_y + 34))
|
|
for i, (tu, vals) in enumerate(rows.items()):
|
|
y = top + i * pitch
|
|
b.append('<text class="nj-charts-rowlabel" x="%g" y="%g">%s<tspan class="nj-charts-sub"> · %s</tspan></text>'
|
|
% (L, y - 2, tu, esc(what)))
|
|
for j, (comp, cls) in enumerate(comps):
|
|
by = y + 6 + j * (bh + 2)
|
|
pair = vals[comp]
|
|
if pair is None:
|
|
b.append('<text class="nj-charts-note" x="%g" y="%g">%s: not available: %s</text>'
|
|
% (L, by + bh - 1.5, comp, esc(NOT_AVAILABLE.get(comp, "not measured"))))
|
|
desc.append("%s, %s: not available" % (tu, comp))
|
|
continue
|
|
d, w = pair
|
|
v = round((w / d - 1) * 100)
|
|
b.append('<path class="nj-charts-%s" d="%s"><title>%s, %s: %d ms by default, %d ms %s (%s)</title></path>'
|
|
% (cls, hbar(X(0), X(v), by, bh), tu, comp, d, w, esc(what), esc(pct(v))))
|
|
b.append('<text class="nj-charts-val" x="%g" y="%g">%s</text>' % (X(0) + 5, by + bh - 1.5, pct(v)))
|
|
desc.append("%s, %s: %s (%d ms to %d ms)" % (tu, comp, pct(v).replace(MINUS, "-"), d, w))
|
|
b.append('<line class="nj-charts-zero" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (X(0), top - 6, X(0), axis_y))
|
|
# one-off cost
|
|
y = axis_y + 62
|
|
b.append('<text class="nj-charts-rowlabel" x="%g" y="%g">One-off cost<tspan class="nj-charts-sub"> · built once, '
|
|
'reused by every TU</tspan></text>' % (L, y))
|
|
for j, (comp, cls) in enumerate(comps):
|
|
yy = y + 20 + j * 20
|
|
b.append('<rect class="nj-charts-%s" x="%g" y="%g" width="10" height="10" rx="2.5"/>' % (cls, L, yy - 9.5))
|
|
ms = build[comp]
|
|
saved = sorted(vals[comp][0] - vals[comp][1] for vals in rows.values() if vals[comp] is not None)
|
|
txt = ("%s: %d ms once, saves %d to %d ms per TU" % (comp, ms, saved[0], saved[-1])) if ms is not None else ("%s: not available: %s" % (comp, NOT_AVAILABLE.get(comp, "not measured")))
|
|
b.append('<text class="nj-charts-val" x="%g" y="%g">%s</text>' % (L + 16, yy, esc(txt)))
|
|
desc.append("one-off build, %s: %s" % (comp, ("%d ms" % ms) if ms is not None else "not available"))
|
|
H = y + 20 + 20 * len(comps)
|
|
write(name, W, H, "nj-charts--compile", tid, title, desc_intro + " " + "; ".join(desc) + ".", b)
|
|
|
|
|
|
# ---------- binary sizes (features/binary_formats/index.md, "Sizes") ----------
|
|
FORMATS = ["BJData", "BON8", "BSON", "CBOR", "MessagePack", "UBJSON"] # colour slots 1-6, same in every panel
|
|
SIZES = {
|
|
"canada.json": [53.2, 50.5, 85.8, 50.5, 50.5, 53.2],
|
|
"twitter.json": [91.1, 83.8, 95.2, 86.3, 86.0, 91.3],
|
|
"citm_catalog.json": [78.1, 63.5, 95.8, 68.4, 68.5, 78.2],
|
|
"jeopardy.json": [96.6, 87.5, 106.7, 88.0, 87.9, 96.6],
|
|
}
|
|
|
|
|
|
def binary_sizes():
|
|
W, L, R = 340, 104, 286
|
|
X = lambda v: L + v / 110 * (R - L)
|
|
top, pitch, bh = 46, 26, 16
|
|
for fname, vals in SIZES.items():
|
|
b, vl = [], [] # value labels go on top of the 100 % line
|
|
lo = min(vals)
|
|
b.append('<text class="nj-charts-panel" x="8" y="18">%s</text>' % fname)
|
|
bottom = top + pitch * len(FORMATS) - 6
|
|
for t in (0, 50):
|
|
b.append('<line class="nj-charts-grid" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (X(t), top - 6, X(t), bottom))
|
|
b.append('<text class="nj-charts-tick" x="%g" y="%g">%d %%</text>' % (X(t), bottom + 15, t))
|
|
for i, (fmt, v) in enumerate(zip(FORMATS, vals)):
|
|
y = top + i * pitch
|
|
best = v == lo
|
|
b.append('<text class="nj-charts-rowlabel nj-charts-fmt%s" x="%g" y="%g">%s</text>'
|
|
% (" nj-charts-best" if best else "", L - 8, y + bh - 3.5, fmt))
|
|
b.append('<path class="nj-charts-c%d%s" d="%s"><title>%s, %s: %g %% of minified %s%s</title></path>'
|
|
% (i + 1, "" if best else " nj-charts-dim", hbar(X(0), X(v), y, bh), fname, fmt, v, JSON,
|
|
" (smallest)" if best else ""))
|
|
vl.append('<text class="nj-charts-val%s" x="%g" y="%g">%.1f %%</text>'
|
|
% (" nj-charts-best" if best else "", X(v) + 5, y + bh - 3.5, v))
|
|
b.append('<line class="nj-charts-ref" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (X(100), top - 10, X(100), bottom))
|
|
b.append('<text class="nj-charts-tick nj-charts-tick--r" x="%g" y="%g">minified %s = 100 %%</text>'
|
|
% (X(100) + 2, top - 14, JSON))
|
|
b.append('<text class="nj-charts-tick" x="%g" y="%g">100 %%</text>' % (X(100), bottom + 15))
|
|
b += vl
|
|
H = bottom + 24
|
|
best = [f for f, v in zip(FORMATS, vals) if v == lo]
|
|
desc = "; ".join("%s %g %%" % (f, v) for f, v in zip(FORMATS, vals))
|
|
stem = fname.split(".")[0]
|
|
write("binary_sizes_%s.svg" % stem, W, H, "nj-charts--sizes", "nj-charts-bs-" + stem,
|
|
"Size of %s in each binary format relative to minified %s" % (fname, JSON),
|
|
"%s. Smallest: %s." % (desc, " and ".join(best)), b)
|
|
|
|
|
|
# ---------- json vs. ordered_json (api/ordered_map.md, cited on features/performance.md) ----------
|
|
SCALING = [(2000, 0.7, 3.6, 5), (4000, 0.8, 14.0, 19), (8000, 1.6, 67.8, 43), (16000, 3.3, 181.6, 54)]
|
|
|
|
|
|
def scaling():
|
|
W, L, R, T, B = 440, 66, 330, 16, 210
|
|
xs = [n for n, *_ in SCALING]
|
|
X = lambda n: L + math.log2(n / xs[0]) / math.log2(xs[-1] / xs[0]) * (R - L)
|
|
Y = lambda ms: B - (math.log10(ms) - math.log10(0.3)) / (math.log10(300) - math.log10(0.3)) * (B - T)
|
|
b = []
|
|
for t in (1, 10, 100):
|
|
b.append('<line class="nj-charts-grid" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (L, Y(t), R, Y(t)))
|
|
b.append('<text class="nj-charts-tick nj-charts-tick--r" x="%g" y="%g">%g ms</text>' % (L - 6, Y(t) + 4, t))
|
|
b.append('<line class="nj-charts-axis" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (L, B, R, B))
|
|
for n, a, o, f in SCALING:
|
|
b.append('<text class="nj-charts-tick" x="%g" y="%g">%s</text>' % (X(n), B + 16, "{:,}".format(n).replace(",", " ")))
|
|
for key, idx, cls, name in (("json", 1, "json", "json"), ("ojson", 2, "ojson", "ordered_json")):
|
|
pts = [(X(r[0]), Y(r[idx])) for r in SCALING]
|
|
b.append('<polyline class="nj-charts-%s" points="%s"/>' % (cls, " ".join("%g,%g" % p for p in pts)))
|
|
for (x, y), r in zip(pts, SCALING):
|
|
b.append('<circle class="nj-charts-%s-dot" cx="%g" cy="%g" r="4.5"><title>%s, %d keys: %g ms</title></circle>'
|
|
% (cls, x, y, name, r[0], r[idx]))
|
|
last = SCALING[-1]
|
|
b.append('<text class="nj-charts-val nj-charts-name-%s" x="%g" y="%g">%s</text>' % (cls, pts[-1][0] + 10, pts[-1][1] - 2, name))
|
|
b.append('<text class="nj-charts-val" x="%g" y="%g">%g ms</text>' % (pts[-1][0] + 10, pts[-1][1] + 13, last[idx]))
|
|
first = SCALING[0]
|
|
dy = 16 if key == "json" else 20
|
|
b.append('<text class="nj-charts-val" x="%g" y="%g">%g ms</text>' % (pts[0][0] + 6, pts[0][1] + dy, first[idx]))
|
|
write("ordered_json_scaling.svg", W, B + 28, "nj-charts--scaling", "nj-charts-sc",
|
|
"Time to parse a flat object of n keys, json against ordered_json, logarithmic axes",
|
|
"2000 keys: json 0.7 ms, ordered_json 3.6 ms (5x). 4000 keys: 0.8 ms and 14.0 ms (19x). "
|
|
"8000 keys: 1.6 ms and 67.8 ms (43x). 16000 keys: 3.3 ms and 181.6 ms (54x).", b)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
compile_times()
|
|
tradeoff("compile_times_pch.svg", "nj-charts-pch", PCH, PCH_BUILD, "with PCH",
|
|
"Compile time per translation unit with a precompiled json.hpp at -O0, and the one-off cost of the PCH",
|
|
"Horizontal bars, negative is faster, medians of nine runs, -std=c++17.")
|
|
tradeoff("compile_times_modules.svg", "nj-charts-mod", MODULES, MODULES_BUILD, "import nlohmann.json",
|
|
"Compile time per translation unit with import nlohmann.json at -O0, and the one-off cost of the module",
|
|
"Horizontal bars, negative is faster, medians of nine runs, -std=c++20 for both.")
|
|
binary_sizes()
|
|
scaling()
|