wegen/FrOSCon26/drawings/gen-time-scales-diagram.py
Ichthyostega f46f117eda Graph: fabricate overview of timing scales
Content generated by Knowledge-Technology

 - collect information with the help of Claude
 - validate my own measurements with sources from the net
 - find a suitable form for the presentation
 - instructed Claude to build a SVG with a logarithmic time scale
 - instructed Claude to summarise the findings as Asciidoc table
2026-08-04 19:20:45 +02:00

245 lines
10 KiB
Python

#!/usr/bin/env python3
"""Generate the FrOSCon io_uring talk 'time scales' wrapped-log-scale diagram.
Page: 254 x 142.9 mm (== LibreOffice Impress 16:9 slide, 25.4 x 14.29 cm)
User unit == 1 mm.
Structure: four bands, each spanning exactly three decades (1 -> 1000 of its
unit), all at identical horizontal scale. Each band is a separate Inkscape
layer so the whole thing can be exported step by step (Export > Page) and
stacked as transparent overlays in Impress.
@note Code generated 8/2026 by Claude -- prompted by Ichthyostega
"""
import math
W, H = 254.0, 142.9
X0, X1 = 34.0, 240.0 # axis: value 1 .. value 1000
DEC = (X1 - X0) / 3.0 # mm per decade
AXIS_END = 243.5 # slight overshoot + chevron
BANDS_Y = [40.0, 75.0, 102.0, 120.0]
# ---- colours -------------------------------------------------------------
C_AXIS = "#555555"
C_MINOR = "#b3b3b3"
C_MACH = "#14507d"
C_HUMAN = "#b4530a"
C_VAL_M = "#4a7ba6"
C_VAL_H = "#c98342"
C_MUTED = "#8a8a8a"
C_TITLE = "#222222"
# ---- type ----------------------------------------------------------------
F_NAME, F_VAL, F_BAND, F_TITLE, F_NOTE, F_DISC = 3.8, 3.2, 5.5, 8.0, 3.6, 3.0
FONT = "DejaVu Sans, Verdana, sans-serif"
# tier offsets relative to band axis Y
UP1_NAME, UP1_VAL, UP1_STUB = -10.3, -6.5, -5.0
UP2_NAME, UP2_VAL, UP2_STUB = -18.3, -14.5, -13.0
LO1_NAME, LO1_VAL, LO1_STUB = 11.2, 7.5, 5.0
BRACKET_Y = 4.0
def X(v):
return X0 + DEC * math.log10(v)
def esc(s):
return (s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;"))
def text(x, y, s, size, fill, anchor="middle", weight="normal"):
return (f'<text x="{x:.2f}" y="{y:.2f}" font-family="{FONT}" '
f'font-size="{size}" font-weight="{weight}" fill="{fill}" '
f'text-anchor="{anchor}">{esc(s)}</text>')
def line(x1, y1, x2, y2, stroke, w, dash=None):
d = f' stroke-dasharray="{dash}"' if dash else ""
return (f'<path d="M {x1:.2f},{y1:.2f} L {x2:.2f},{y2:.2f}" '
f'stroke="{stroke}" stroke-width="{w}" fill="none"{d}/>')
# ==========================================================================
# entry data
# ==========================================================================
# tier: 'up1' | 'up2' | 'lo1'
# kind: 'machine' | 'human'
# span: (v_lo, v_hi) -> draw a bracket instead of a dot
BANDS = [
dict(unit="1 ns", ay=BANDS_Y[0], layer="ns", entries=[
dict(v=1, name="L1 cache", val="1 ns", tier="up1", kind="machine"),
dict(v=4, name="L2 cache", val="4 ns", tier="up1", kind="machine"),
dict(v=10, name="full memory barrier", val="10 ns", tier="up2", kind="machine"),
dict(v=15, name="L3 cache", val="15 ns", tier="up1", kind="machine"),
dict(v=100, name="DRAM access", val="100 ns", tier="up1", kind="machine"),
dict(span=(50, 100), name="contended cache line", val="50-100 ns",
tier="lo1", kind="machine"),
dict(offscale=True, name="1 clock cycle", val="\u2248 0.3 ns",
tier="lo1", kind="machine"),
]),
dict(unit="1 \u00b5s", ay=BANDS_Y[1], layer="us", entries=[
dict(v=1, name="system call", val="1 \u00b5s", tier="up1", kind="machine"),
dict(v=2, name="context switch", val="2 \u00b5s", tier="up2", kind="machine"),
dict(v=50, name="OS timer slack", val="50 \u00b5s", tier="up1", kind="machine"),
dict(span=(10, 30), name="thread wake-up", val="10-30 \u00b5s",
tier="lo1", kind="machine"),
dict(span=(50, 100), name="SSD read", val="50-100 \u00b5s",
tier="lo1", kind="machine"),
]),
dict(unit="1 ms", ay=BANDS_Y[2], layer="ms", entries=[
dict(v=10, name="disk seek", val="10 ms", tier="up1", kind="machine"),
dict(v=40, name="one frame @ 25 fps", val="40 ms", tier="up1", kind="machine"),
dict(v=10, name="barely noticeable", val="10 ms", tier="lo1", kind="human"),
dict(v=100, name="UI feels sluggish", val="100 ms", tier="lo1", kind="human"),
]),
dict(unit="1 s", ay=BANDS_Y[3], layer="s", entries=[
dict(v=1, name="\u201cit\u2019s thinking\u2026\u201d", val="1 s",
tier="lo1", kind="human"),
dict(v=180, name="another coffee", val="3 min", tier="lo1", kind="human"),
dict(v=900, name="go do something else", val="\u2248 15 min",
tier="lo1", kind="human", anchor="end", ax=250.0),
]),
]
# ==========================================================================
# scaffold
# ==========================================================================
def band_axis(ay, unit):
o = []
o.append(line(X0, ay, AXIS_END, ay, C_AXIS, 0.6))
# continuation chevron
o.append(f'<path d="M {AXIS_END-2.4:.2f},{ay-1.6:.2f} L {AXIS_END:.2f},{ay:.2f} '
f'L {AXIS_END-2.4:.2f},{ay+1.6:.2f}" stroke="{C_AXIS}" '
f'stroke-width="0.6" fill="none"/>')
# ticks
for d in range(3):
base = 10 ** d
for m in range(1, 10):
v = base * m
x = X(v)
if m == 1:
o.append(line(x, ay - 3.0, x, ay + 3.0, C_AXIS, 0.6))
else:
o.append(line(x, ay - 1.6, x, ay + 1.6, C_MINOR, 0.25))
o.append(line(X(1000), ay - 3.0, X(1000), ay + 3.0, C_AXIS, 0.6))
o.append(text(32.0, ay + 2.0, unit, F_BAND, C_MUTED, "end", "600"))
return o
def scaffold():
o = []
o.append(text(10.0, 15.0, "Time scales", F_TITLE, C_TITLE, "start", "600"))
o.append(text(244.0, 12.5, "each row spans three decades", F_NOTE, C_MUTED, "end"))
o.append(text(244.0, 17.0, "one row down = \u00d7 1000", F_NOTE, C_MUTED, "end"))
for b in BANDS:
o += band_axis(b["ay"], b["unit"])
o.append(text(10.0, 137.0,
"Order of magnitude only \u2014 contemporary desktop / laptop x86. "
"Expect \u00b1 2\u20133\u00d7; the ratios are the point.",
F_DISC, C_MUTED, "start"))
return o
# ==========================================================================
# entries
# ==========================================================================
def render_entry(e, ay):
kind = e.get("kind", "machine")
cn = C_MACH if kind == "machine" else C_HUMAN
cv = C_VAL_M if kind == "machine" else C_VAL_H
tier = e["tier"]
anchor = e.get("anchor", "middle")
o = []
if tier == "up1":
ny, vy, sy = ay + UP1_NAME, ay + UP1_VAL, ay + UP1_STUB
elif tier == "up2":
ny, vy, sy = ay + UP2_NAME, ay + UP2_VAL, ay + UP2_STUB
else:
ny, vy, sy = ay + LO1_NAME, ay + LO1_VAL, ay + LO1_STUB
if e.get("offscale"):
# dashed stub running off the left-hand end of the scale
y = ay + BRACKET_Y + 0.5
o.append(line(X0, y, 22.5, y, cn, 0.5, dash="1.6,1.2"))
o.append(f'<path d="M 25.4,{y-1.6:.2f} L 22.0,{y:.2f} L 25.4,{y+1.6:.2f}" '
f'stroke="{cn}" stroke-width="0.6" fill="none"/>')
ax = 22.0
o.append(text(ax, vy, e["val"], F_VAL, cv, "start"))
o.append(text(ax, ny, e["name"], F_NAME, cn, "start", "600"))
return o
if "span" in e:
x1, x2 = X(e["span"][0]), X(e["span"][1])
by = ay + BRACKET_Y
o.append(line(x1, by, x2, by, cn, 0.7))
o.append(line(x1, by, x1, ay, cn, 0.7))
o.append(line(x2, by, x2, ay, cn, 0.7))
ax = e.get("ax", (x1 + x2) / 2.0)
else:
x = X(e["v"])
o.append(f'<circle cx="{x:.2f}" cy="{ay:.2f}" r="1.3" fill="{cn}"/>')
stub_from = ay - 2.0 if tier.startswith("up") else ay + 2.0
o.append(line(x, stub_from, x, sy, cn, 0.4))
ax = e.get("ax", x)
o.append(text(ax, vy, e["val"], F_VAL, cv, anchor))
o.append(text(ax, ny, e["name"], F_NAME, cn, anchor, "600"))
return o
# ==========================================================================
# assemble
# ==========================================================================
def layer(lid, label, body, display="inline"):
return (f'<g inkscape:groupmode="layer" id="{lid}" '
f'inkscape:label="{label}" style="display:{display}">\n '
+ "\n ".join(body) + "\n</g>")
def build():
layers = []
layers.append(layer("layer-1-scaffold", "1 - scaffold", scaffold()))
for i, b in enumerate(BANDS):
if b["layer"] == "ms":
mach = [x for e in b["entries"] if e["kind"] == "machine"
for x in render_entry(e, b["ay"])]
hum = [x for e in b["entries"] if e["kind"] == "human"
for x in render_entry(e, b["ay"])]
layers.append(layer("layer-4-ms-machine", "4 - ms band (machine)", mach))
layers.append(layer("layer-5-ms-human", "5 - ms band (human)", hum))
elif b["layer"] == "ns":
body = [x for e in b["entries"] for x in render_entry(e, b["ay"])]
layers.append(layer("layer-2-ns", "2 - ns band", body))
elif b["layer"] == "us":
body = [x for e in b["entries"] for x in render_entry(e, b["ay"])]
layers.append(layer("layer-3-us", "3 - us band", body))
else:
body = [x for e in b["entries"] for x in render_entry(e, b["ay"])]
layers.append(layer("layer-6-s", "6 - s band (human)", body))
dim = [f'<rect x="0" y="22" width="{W}" height="67" fill="#ffffff" '
f'fill-opacity="0.74"/>']
layers.append(layer("layer-7-dim", "7 - dim ns+us (optional)", dim,
display="none"))
return f'''<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
width="{W}mm" height="{H}mm" viewBox="0 0 {W} {H}" version="1.1">
<sodipodi:namedview inkscape:document-units="mm" units="mm"/>
<title>Time scales - ns to minutes, wrapped logarithmic</title>
{chr(10).join(layers)}
</svg>
'''
if __name__ == "__main__":
open("/mnt/user-data/outputs/time-scales.svg", "w").write(build())
print("written")