Charts and reproducible benchmarks, Makefile, expanded .gitignore

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 22:24:56 +02:00
co-authored by Claude Sonnet 5.5
parent 4fed40e5cd
commit 97fa625a50
15 changed files with 827 additions and 23 deletions
+4
View File
@@ -260,6 +260,10 @@ delegated with `cpuset cpu io memory pids`). The end-to-end suite passed 83 chec
| Size accounting | 80 MB written by container uid 5 in a mode-700 directory (invisible to the host user) against `--size 64m`: frozen; cleanup from a second attach thawed it |
| Attach time (5 runs) | host or image, slirp: about 40 ms each; pasta: about 65 ms; `--net none`: about 10 ms |
![Horizontal bar chart of CPU-seconds used by four busy loops in one container, without a limit and with --cpus 0.5. The limited container is highlighted. Values are in the sentence below.](charts/cpu.svg)
The chart above is generated from `docs/charts/bench.json` (`make bench`, then `make charts`); it shows <!--bench:cpu_free-->16.3<!--/bench--> CPU-seconds without a limit and <!--bench:cpu_half-->2.3<!--/bench--> with `--cpus 0.5`; the startup and size charts live in [COMPARISON](../COMPARISON.md).
### What changed in behavior (and why it is not a loss)
- Hardening can be switched off per container (`--hardening relaxed`), so nothing that needs the denied calls is lost, it is opt-in now.
+121
View File
@@ -0,0 +1,121 @@
{
"meta": {
"date": "2026-10-23",
"kernel": "7.0.0-38-generic",
"cpus": 12,
"runs": 10,
"doos": "1.1.2",
"podman": "podman version 5.7.0",
"docker": "Docker version 29.1.3, build 29.1.3-0ubuntu4.1",
"bubblewrap": "bubblewrap 0.11.1",
"src_lines": 3445,
"shared_libs": 6
},
"startup": [
{
"id": "bwrap_none",
"label": "bubblewrap (host /usr)",
"group": "none",
"kind": "other",
"ms": 8.4
},
{
"id": "bwrap_net",
"label": "bubblewrap (host /usr)",
"group": "default",
"kind": "other",
"ms": 7.4
},
{
"id": "doos_host_none",
"label": "doos host mode",
"group": "none",
"kind": "doos",
"ms": 18.7
},
{
"id": "doos_alpine_none",
"label": "doos Alpine image",
"group": "none",
"kind": "doos",
"ms": 13.8
},
{
"id": "doos_host_slirp",
"label": "doos host mode",
"group": "default",
"kind": "doos",
"ms": 56.9
},
{
"id": "doos_alpine_slirp",
"label": "doos Alpine image",
"group": "default",
"kind": "doos",
"ms": 52.1
},
{
"id": "doos_alpine_pasta",
"label": "doos Alpine, pasta",
"group": "default",
"kind": "doos",
"ms": 64.6
},
{
"id": "podman_none",
"label": "Podman Alpine",
"group": "none",
"kind": "other",
"ms": 207.8
},
{
"id": "podman_net",
"label": "Podman Alpine",
"group": "default",
"kind": "other",
"ms": 241.2
},
{
"id": "docker_none",
"label": "Docker Alpine (rootful)",
"group": "none",
"kind": "other",
"ms": 343.9
},
{
"id": "docker_net",
"label": "Docker Alpine (rootful)",
"group": "default",
"kind": "other",
"ms": 435.6
}
],
"sizes": [
{
"id": "bwrap",
"bytes": 80424
},
{
"id": "crun",
"bytes": 483392
},
{
"id": "doos",
"bytes": 565680
},
{
"id": "docker",
"bytes": 31374336
},
{
"id": "podman",
"bytes": 43180896
}
],
"cpu": {
"wall_seconds": 4.2,
"busy_loops": 4,
"unlimited": 16.26,
"half_cpu": 2.29
}
}
+48
View File
@@ -0,0 +1,48 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 200" width="760" role="img" aria-labelledby="t d" style="max-width:100%;height:auto">
<title id="t">CPU time used by four busy loops</title>
<desc id="d">Two bars, CPU seconds consumed in 4.2 seconds of wall time. Without a limit 16.3, with --cpus 0.5 2.3.</desc>
<style>
.bg { fill: var(--surface); }
.t1 { fill: var(--ink); font: 600 15px system-ui, -apple-system, "Segoe UI", sans-serif; }
.t2 { fill: var(--ink2); font: 400 12px system-ui, -apple-system, "Segoe UI", sans-serif; }
.lab { fill: var(--ink); font: 400 13px system-ui, -apple-system, "Segoe UI", sans-serif; }
.val { fill: var(--ink2); font: 400 12px system-ui, -apple-system, "Segoe UI", sans-serif; }
.tick { fill: var(--muted); font: 400 11px system-ui, -apple-system, "Segoe UI", sans-serif; }
.grid { stroke: var(--grid); stroke-width: 1; }
.axis { stroke: var(--axis); stroke-width: 1; }
.acc { fill: var(--accent); }
.oth { fill: var(--other); }
:root { --surface:#fcfcfb; --ink:#0b0b0b; --ink2:#52514e; --muted:#898781;
--grid:#e1e0d9; --axis:#c3c2b7; --accent:#2a78d6; --other:#898781; }
@media (prefers-color-scheme: dark) {
:root { --surface:#1a1a19; --ink:#f0efec; --ink2:#c3c2b7; --muted:#898781;
--grid:#2c2c2a; --axis:#383835; --accent:#3987e5; --other:#898781; }
}
</style>
<rect class="bg" x="0" y="0" width="760" height="200" rx="8"/>
<text class="t1" x="20" y="28">CPU time used by 4 busy loops in one container (CPU-seconds)</text>
<text class="t2" x="20" y="46">4.2 s of wall time. Half a CPU allows about 2.1 CPU-seconds; measured 2.3.</text>
<rect class="oth" x="20" y="55" width="12" height="12" rx="2"/>
<text class="t2" x="38" y="65">no limit</text>
<rect class="acc" x="116" y="55" width="12" height="12" rx="2"/>
<text class="t2" x="134" y="65">--cpus 0.5</text>
<line class="grid" x1="170.0" y1="84" x2="170.0" y2="146"/>
<text class="tick" x="170.0" y="162" text-anchor="middle">0</text>
<line class="grid" x1="270.0" y1="84" x2="270.0" y2="146"/>
<text class="tick" x="270.0" y="162" text-anchor="middle">4</text>
<line class="grid" x1="370.0" y1="84" x2="370.0" y2="146"/>
<text class="tick" x="370.0" y="162" text-anchor="middle">8</text>
<line class="grid" x1="470.0" y1="84" x2="470.0" y2="146"/>
<text class="tick" x="470.0" y="162" text-anchor="middle">12</text>
<line class="grid" x1="570.0" y1="84" x2="570.0" y2="146"/>
<text class="tick" x="570.0" y="162" text-anchor="middle">16</text>
<line class="grid" x1="670.0" y1="84" x2="670.0" y2="146"/>
<text class="tick" x="670.0" y="162" text-anchor="middle">20</text>
<text class="lab" x="160" y="105.0" text-anchor="end">no limit</text>
<path class="oth" d="M170.0,92.0 h402.5 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-402.5 z"><title>no limit: 16.3 CPU-seconds</title></path>
<text class="val" x="584.5" y="105.0">16.3 CPU-s</text>
<text class="lab" x="160" y="135.0" text-anchor="end">--cpus 0.5</text>
<path class="acc" d="M170.0,122.0 h53.2 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-53.2 z"><title>--cpus 0.5: 2.3 CPU-seconds</title></path>
<text class="val" x="235.2" y="135.0">2.3 CPU-s</text>
<line class="axis" x1="170" y1="84" x2="170" y2="146"/>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

+292
View File
@@ -0,0 +1,292 @@
#!/usr/bin/env python3
"""Builds the SVG charts and the generated numbers in COMPARISON.md / RESEARCH.md from bench.json.
python3 docs/charts/make_charts.py (or: make charts)
Input : docs/charts/bench.json (written by scripts/bench.sh, i.e. make bench)
Output: docs/charts/startup.svg, size.svg, cpu.svg
the blocks between <!-- bench:NAME:begin --> and <!-- bench:NAME:end --> in the markdown documents
and inline values written as <!--bench:KEY-->text<!--/bench-->
Design rules (the dataviz method): one form per job, doos in the accent hue and everything else in the neutral
gray (emphasis), bars at most 24 px with a 4 px rounded data end, direct value labels at the bar tip, a legend,
hairline solid grid, text in ink tokens (never the series color), light and dark palettes selected separately.
Colors were checked with the dataviz validator: contrast, CVD separation and the normal-vision floor pass in
both modes; its "chroma floor" flag on the gray is expected, the gray is the de-emphasis neutral by design.
Standard library only.
"""
import json
import math
import re
import sys
from pathlib import Path
from xml.sax.saxutils import escape
HERE = Path(__file__).resolve().parent
ROOT = HERE.parent.parent
DATA = HERE / "bench.json"
# ---- palette (dataviz reference instance) ---------------------------------------------------------------
LIGHT = dict(surface="#fcfcfb", ink="#0b0b0b", ink2="#52514e", muted="#898781", grid="#e1e0d9", axis="#c3c2b7",
accent="#2a78d6", other="#898781")
DARK = dict(surface="#1a1a19", ink="#f0efec", ink2="#c3c2b7", muted="#898781", grid="#2c2c2a", axis="#383835",
accent="#3987e5", other="#898781")
STYLE = f"""
.bg {{ fill: var(--surface); }}
.t1 {{ fill: var(--ink); font: 600 15px system-ui, -apple-system, "Segoe UI", sans-serif; }}
.t2 {{ fill: var(--ink2); font: 400 12px system-ui, -apple-system, "Segoe UI", sans-serif; }}
.lab {{ fill: var(--ink); font: 400 13px system-ui, -apple-system, "Segoe UI", sans-serif; }}
.val {{ fill: var(--ink2); font: 400 12px system-ui, -apple-system, "Segoe UI", sans-serif; }}
.tick {{ fill: var(--muted); font: 400 11px system-ui, -apple-system, "Segoe UI", sans-serif; }}
.grid {{ stroke: var(--grid); stroke-width: 1; }}
.axis {{ stroke: var(--axis); stroke-width: 1; }}
.acc {{ fill: var(--accent); }}
.oth {{ fill: var(--other); }}
:root {{ --surface:{LIGHT['surface']}; --ink:{LIGHT['ink']}; --ink2:{LIGHT['ink2']}; --muted:{LIGHT['muted']};
--grid:{LIGHT['grid']}; --axis:{LIGHT['axis']}; --accent:{LIGHT['accent']}; --other:{LIGHT['other']}; }}
@media (prefers-color-scheme: dark) {{
:root {{ --surface:{DARK['surface']}; --ink:{DARK['ink']}; --ink2:{DARK['ink2']}; --muted:{DARK['muted']};
--grid:{DARK['grid']}; --axis:{DARK['axis']}; --accent:{DARK['accent']}; --other:{DARK['other']}; }}
}}
"""
BAR_H = 18 # <= 24 px
ROW = 30
RADIUS = 4 # rounded data end only
def bar_path(x, y, w, h, r=RADIUS):
"""Bar growing from x to the right: square at the baseline, rounded at the data end."""
w = max(w, 1.0)
r = min(r, w, h / 2)
return (f"M{x:.1f},{y:.1f} h{w - r:.1f} a{r},{r} 0 0 1 {r},{r} v{h - 2 * r:.1f} "
f"a{r},{r} 0 0 1 {-r},{r} h{-(w - r):.1f} z")
def svg_open(w, h, title, desc):
return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" width="{w}" role="img" '
f'aria-labelledby="t d" style="max-width:100%;height:auto">\n<title id="t">{escape(title)}</title>\n'
f'<desc id="d">{escape(desc)}</desc>\n<style>{STYLE}</style>\n'
f'<rect class="bg" x="0" y="0" width="{w}" height="{h}" rx="8"/>\n')
def legend(x, y, items):
out, cx = [], x
for cls, text in items:
out.append(f'<rect class="{cls}" x="{cx}" y="{y - 9}" width="12" height="12" rx="2"/>')
out.append(f'<text class="t2" x="{cx + 18}" y="{y + 1}">{escape(text)}</text>')
cx += 18 + 7 * len(text) + 22
return "\n".join(out)
def fmt_ms(v):
return f"{v:.0f} ms" if v >= 100 else f"{v:.1f} ms".replace(".0 ms", " ms")
def fmt_bytes(b):
if b < 1 << 20:
return f"{b / 1024:.0f} KiB"
return f"{b / (1 << 20):.1f} MiB"
# ---- chart 1: startup time -----------------------------------------------------------------------------
def startup_chart(d):
rows = [r for r in d["startup"] if r["ms"] is not None]
iv = inline_values(d)
groups = [("none", "No network"), ("default", "Default network")]
left, right, top = 200, 90, 78
width = 760
plot_w = width - left - right
vmax = max(r["ms"] for r in rows)
axis_max = math.ceil(vmax / 100.0) * 100
step = 100 if axis_max <= 600 else 200
scale = plot_w / axis_max
y = top
body = []
for key, name in groups:
grp = sorted([r for r in rows if r["group"] == key], key=lambda r: r["ms"])
if not grp:
continue
body.append(f'<text class="t1" x="{left - 190 + 12}" y="{y}">{name}</text>')
y += 14
y0 = y
for t in range(0, axis_max + 1, step):
x = left + t * scale
body.append(f'<line class="grid" x1="{x:.1f}" y1="{y0 - 2}" x2="{x:.1f}" y2="{y0 + len(grp) * ROW}"/>')
for r in grp:
cls = "acc" if r["kind"] == "doos" else "oth"
w = r["ms"] * scale
by = y + (ROW - BAR_H) / 2
body.append(f'<text class="lab" x="{left - 10}" y="{by + BAR_H / 2 + 4:.1f}" text-anchor="end">{escape(r["label"])}</text>')
body.append(f'<path class="{cls}" d="{bar_path(left, by, w, BAR_H)}"><title>{escape(r["label"])}: {fmt_ms(r["ms"])}</title></path>')
body.append(f'<text class="val" x="{left + w + 8:.1f}" y="{by + BAR_H / 2 + 4:.1f}">{fmt_ms(r["ms"])}</text>')
y += ROW
body.append(f'<line class="axis" x1="{left}" y1="{y0 - 2}" x2="{left}" y2="{y}"/>')
for t in range(0, axis_max + 1, step):
body.append(f'<text class="tick" x="{left + t * scale:.1f}" y="{y + 14}" text-anchor="middle">{t}</text>')
y += 38
height = int(y - 10)
m = d["meta"]
out = svg_open(width, height, "Time to start a trivial command",
f"Horizontal bars, milliseconds, lower is better. doos: {iv['doos_range']}; Podman: {iv['podman_range']}; "
f"Docker: {iv['docker_range']}; bubblewrap: {iv['bwrap_range']}. Exact values are in the table in COMPARISON.md.")
out += '<text class="t1" x="20" y="28">Time to start a trivial command (ms, lower is better)</text>\n'
out += (f'<text class="t2" x="20" y="46">{m["runs"]} runs each, average, warm cache, {m["date"]}, kernel {escape(m["kernel"])}, '
f'{m["cpus"]} cores. Docker is a rootful daemon.</text>\n')
out += legend(20, 64, [("acc", "doos"), ("oth", "other tools")]) + "\n"
out += "\n".join(body) + "\n</svg>\n"
return out
# ---- chart 2: binary size (log scale) --------------------------------------------------------------------
def size_chart(d):
names = {"bwrap": "bubblewrap", "crun": "crun (OCI runtime)", "doos": "doos", "docker": "docker CLI",
"podman": "podman"}
rows = sorted([r for r in d["sizes"] if r["bytes"] is not None], key=lambda r: r["bytes"])
left, right, top, width = 170, 100, 96, 760
plot_w = width - left - right
lo, hi = math.log10(50 * 1024), math.log10(100 * (1 << 20))
sx = lambda b: (math.log10(b) - lo) / (hi - lo) * plot_w
ticks = [(100 * 1024, "100 KiB"), (1 << 20, "1 MiB"), (10 << 20, "10 MiB"), (100 << 20, "100 MiB")]
y0 = top + 8
body = []
for b, lab in ticks:
x = left + sx(b)
body.append(f'<line class="grid" x1="{x:.1f}" y1="{y0 - 2}" x2="{x:.1f}" y2="{y0 + len(rows) * ROW}"/>')
body.append(f'<text class="tick" x="{x:.1f}" y="{y0 + len(rows) * ROW + 16}" text-anchor="middle">{lab}</text>')
y = y0
for r in rows:
cls = "acc" if r["id"] == "doos" else "oth"
w = sx(r["bytes"])
by = y + (ROW - BAR_H) / 2
body.append(f'<text class="lab" x="{left - 10}" y="{by + BAR_H / 2 + 4:.1f}" text-anchor="end">{names.get(r["id"], r["id"])}</text>')
body.append(f'<path class="{cls}" d="{bar_path(left, by, w, BAR_H)}"><title>{names.get(r["id"], r["id"])}: {fmt_bytes(r["bytes"])}</title></path>')
body.append(f'<text class="val" x="{left + w + 8:.1f}" y="{by + BAR_H / 2 + 4:.1f}">{fmt_bytes(r["bytes"])}</text>')
y += ROW
body.append(f'<line class="axis" x1="{left}" y1="{y0 - 2}" x2="{left}" y2="{y}"/>')
height = int(y + 62)
out = svg_open(width, height, "Size of the main executable",
"Horizontal bars on a logarithmic axis: " +
", ".join(f"{names.get(r['id'], r['id'])} {fmt_bytes(r['bytes'])}" for r in rows) + ".")
out += '<text class="t1" x="20" y="28">Size of the main executable (log scale, smaller is smaller)</text>\n'
out += '<text class="t2" x="20" y="46">The docker CLI excludes dockerd and containerd.</text>\n'
out += '<text class="t2" x="20" y="62">doos excludes the helpers it calls (slirp4netns, pasta, newuidmap, curl, tar).</text>\n'
out += legend(20, 82, [("acc", "doos"), ("oth", "other tools")]) + "\n"
out += "\n".join(body) + f'\n<text class="t2" x="{left}" y="{y + 38}">Axis is logarithmic: each gridline is 10x the previous one.</text>\n</svg>\n'
return out
# ---- chart 3: cpu limit ----------------------------------------------------------------------------------
def cpu_chart(d):
c = d["cpu"]
if c["unlimited"] is None or c["half_cpu"] is None:
return None
rows = [("no limit", c["unlimited"], "oth"), ("--cpus 0.5", c["half_cpu"], "acc")]
left, right, top, width = 170, 90, 78, 760
plot_w = width - left - right
axis_max = math.ceil(c["unlimited"] / 4.0) * 4
scale = plot_w / axis_max
y0 = top + 8
body = []
for t in range(0, axis_max + 1, 4):
x = left + t * scale
body.append(f'<line class="grid" x1="{x:.1f}" y1="{y0 - 2}" x2="{x:.1f}" y2="{y0 + len(rows) * ROW}"/>')
body.append(f'<text class="tick" x="{x:.1f}" y="{y0 + len(rows) * ROW + 16}" text-anchor="middle">{t}</text>')
y = y0
for lab, v, cls in rows:
w = v * scale
by = y + (ROW - BAR_H) / 2
body.append(f'<text class="lab" x="{left - 10}" y="{by + BAR_H / 2 + 4:.1f}" text-anchor="end">{lab}</text>')
body.append(f'<path class="{cls}" d="{bar_path(left, by, w, BAR_H)}"><title>{lab}: {v:.1f} CPU-seconds</title></path>')
body.append(f'<text class="val" x="{left + w + 8:.1f}" y="{by + BAR_H / 2 + 4:.1f}">{v:.1f} CPU-s</text>')
y += ROW
body.append(f'<line class="axis" x1="{left}" y1="{y0 - 2}" x2="{left}" y2="{y}"/>')
ideal = 0.5 * c["wall_seconds"]
height = int(y + 54)
out = svg_open(width, height, "CPU time used by four busy loops",
f"Two bars, CPU seconds consumed in {c['wall_seconds']} seconds of wall time. Without a limit {c['unlimited']:.1f}, "
f"with --cpus 0.5 {c['half_cpu']:.1f}.")
out += '<text class="t1" x="20" y="28">CPU time used by 4 busy loops in one container (CPU-seconds)</text>\n'
out += (f'<text class="t2" x="20" y="46">{c["wall_seconds"]} s of wall time. Half a CPU allows about {ideal:.1f} CPU-seconds; '
f'measured {c["half_cpu"]:.1f}.</text>\n')
out += legend(20, 64, [("oth", "no limit"), ("acc", "--cpus 0.5")]) + "\n"
out += "\n".join(body) + "\n</svg>\n"
return out
# ---- generated markdown ---------------------------------------------------------------------------------
def startup_table(d):
rows = [r for r in d["startup"] if r["ms"] is not None]
lines = ["| Setup | Network | Time per run |", "|---|---|---|"]
order = {"none": 0, "default": 1}
for r in sorted(rows, key=lambda r: (order[r["group"]], r["ms"])):
net = "none" if r["group"] == "none" else "default"
lines.append(f"| {r['label']} | {net} | {fmt_ms(r['ms'])} |")
return "\n".join(lines)
def size_table(d):
names = {"bwrap": "bubblewrap", "crun": "crun", "doos": "doos", "docker": "docker CLI", "podman": "podman"}
lines = ["| Executable | Size |", "|---|---|"]
for r in sorted([r for r in d["sizes"] if r["bytes"] is not None], key=lambda r: r["bytes"]):
lines.append(f"| {names.get(r['id'], r['id'])} | {fmt_bytes(r['bytes'])} ({r['bytes']:,} bytes) |")
return "\n".join(lines)
def inline_values(d):
ms = lambda ids: [r["ms"] for r in d["startup"] if r["id"].startswith(ids) and r["ms"] is not None]
rng = lambda xs: f"{min(xs):.0f} to {max(xs):.0f} ms" if xs else "n/a"
size = {r["id"]: r["bytes"] for r in d["sizes"] if r["bytes"] is not None}
v = {
"doos_range": rng(ms("doos")),
"podman_range": rng(ms("podman")),
"docker_range": rng(ms("docker")),
"bwrap_range": rng(ms("bwrap")),
"doos_size": fmt_bytes(size["doos"]) if "doos" in size else "n/a",
"docker_size": fmt_bytes(size["docker"]) if "docker" in size else "n/a",
"podman_size": fmt_bytes(size["podman"]) if "podman" in size else "n/a",
"bench_date": d["meta"]["date"],
"src_lines": f'{d["meta"].get("src_lines", 0):,}',
"shared_libs": str(d["meta"].get("shared_libs", "n/a")),
"runs": str(d["meta"]["runs"]),
"versions": f'doos {d["meta"]["doos"]}, {d["meta"]["docker"].replace("Docker version ", "Docker ").split(",")[0]}, '
f'{d["meta"]["podman"].replace("podman version ", "Podman ")}, {d["meta"]["bubblewrap"].replace("bubblewrap ", "bubblewrap ")}',
"cpu_free": f'{d["cpu"]["unlimited"]:.1f}' if d["cpu"]["unlimited"] is not None else "n/a",
"cpu_half": f'{d["cpu"]["half_cpu"]:.1f}' if d["cpu"]["half_cpu"] is not None else "n/a",
}
return v
def patch_markdown(path, blocks, inline):
s = path.read_text()
orig = s
for name, text in blocks.items():
pat = re.compile(rf"(<!-- bench:{name}:begin -->\n).*?(\n<!-- bench:{name}:end -->)", re.S)
s = pat.sub(lambda m: m.group(1) + text + m.group(2), s)
s = re.sub(r"<!--bench:(\w+)-->.*?<!--/bench-->",
lambda m: f"<!--bench:{m.group(1)}-->{inline.get(m.group(1), 'n/a')}<!--/bench-->", s)
if s != orig:
path.write_text(s)
return True
return False
def main():
if not DATA.exists():
sys.exit("no bench.json: run `make bench` first (it needs the built binary)")
d = json.loads(DATA.read_text())
(HERE / "startup.svg").write_text(startup_chart(d))
(HERE / "size.svg").write_text(size_chart(d))
cpu = cpu_chart(d)
if cpu:
(HERE / "cpu.svg").write_text(cpu)
blocks = {"startup": startup_table(d), "sizes": size_table(d)}
inline = inline_values(d)
changed = [p.name for p in (ROOT / "COMPARISON.md", ROOT / "docs" / "RESEARCH.md") if patch_markdown(p, blocks, inline)]
print("charts: startup.svg size.svg" + (" cpu.svg" if cpu else "") + (f"; updated {', '.join(changed)}" if changed else "; docs already current"))
if __name__ == "__main__":
main()
+55
View File
@@ -0,0 +1,55 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 316" width="760" role="img" aria-labelledby="t d" style="max-width:100%;height:auto">
<title id="t">Size of the main executable</title>
<desc id="d">Horizontal bars on a logarithmic axis: bubblewrap 79 KiB, crun (OCI runtime) 472 KiB, doos 552 KiB, docker CLI 29.9 MiB, podman 41.2 MiB.</desc>
<style>
.bg { fill: var(--surface); }
.t1 { fill: var(--ink); font: 600 15px system-ui, -apple-system, "Segoe UI", sans-serif; }
.t2 { fill: var(--ink2); font: 400 12px system-ui, -apple-system, "Segoe UI", sans-serif; }
.lab { fill: var(--ink); font: 400 13px system-ui, -apple-system, "Segoe UI", sans-serif; }
.val { fill: var(--ink2); font: 400 12px system-ui, -apple-system, "Segoe UI", sans-serif; }
.tick { fill: var(--muted); font: 400 11px system-ui, -apple-system, "Segoe UI", sans-serif; }
.grid { stroke: var(--grid); stroke-width: 1; }
.axis { stroke: var(--axis); stroke-width: 1; }
.acc { fill: var(--accent); }
.oth { fill: var(--other); }
:root { --surface:#fcfcfb; --ink:#0b0b0b; --ink2:#52514e; --muted:#898781;
--grid:#e1e0d9; --axis:#c3c2b7; --accent:#2a78d6; --other:#898781; }
@media (prefers-color-scheme: dark) {
:root { --surface:#1a1a19; --ink:#f0efec; --ink2:#c3c2b7; --muted:#898781;
--grid:#2c2c2a; --axis:#383835; --accent:#3987e5; --other:#898781; }
}
</style>
<rect class="bg" x="0" y="0" width="760" height="316" rx="8"/>
<text class="t1" x="20" y="28">Size of the main executable (log scale, smaller is smaller)</text>
<text class="t2" x="20" y="46">The docker CLI excludes dockerd and containerd.</text>
<text class="t2" x="20" y="62">doos excludes the helpers it calls (slirp4netns, pasta, newuidmap, curl, tar).</text>
<rect class="acc" x="20" y="73" width="12" height="12" rx="2"/>
<text class="t2" x="38" y="83">doos</text>
<rect class="oth" x="88" y="73" width="12" height="12" rx="2"/>
<text class="t2" x="106" y="83">other tools</text>
<line class="grid" x1="214.5" y1="102" x2="214.5" y2="254"/>
<text class="tick" x="214.5" y="270" text-anchor="middle">100 KiB</text>
<line class="grid" x1="364.0" y1="102" x2="364.0" y2="254"/>
<text class="tick" x="364.0" y="270" text-anchor="middle">1 MiB</text>
<line class="grid" x1="512.0" y1="102" x2="512.0" y2="254"/>
<text class="tick" x="512.0" y="270" text-anchor="middle">10 MiB</text>
<line class="grid" x1="660.0" y1="102" x2="660.0" y2="254"/>
<text class="tick" x="660.0" y="270" text-anchor="middle">100 MiB</text>
<text class="lab" x="160" y="123.0" text-anchor="end">bubblewrap</text>
<path class="oth" d="M170.0,110.0 h25.0 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-25.0 z"><title>bubblewrap: 79 KiB</title></path>
<text class="val" x="207.0" y="123.0">79 KiB</text>
<text class="lab" x="160" y="153.0" text-anchor="end">crun (OCI runtime)</text>
<path class="oth" d="M170.0,140.0 h140.3 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-140.3 z"><title>crun (OCI runtime): 472 KiB</title></path>
<text class="val" x="322.3" y="153.0">472 KiB</text>
<text class="lab" x="160" y="183.0" text-anchor="end">doos</text>
<path class="acc" d="M170.0,170.0 h150.4 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-150.4 z"><title>doos: 552 KiB</title></path>
<text class="val" x="332.4" y="183.0">552 KiB</text>
<text class="lab" x="160" y="213.0" text-anchor="end">docker CLI</text>
<path class="oth" d="M170.0,200.0 h408.5 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-408.5 z"><title>docker CLI: 29.9 MiB</title></path>
<text class="val" x="590.5" y="213.0">29.9 MiB</text>
<text class="lab" x="160" y="243.0" text-anchor="end">podman</text>
<path class="oth" d="M170.0,230.0 h429.0 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-429.0 z"><title>podman: 41.2 MiB</title></path>
<text class="val" x="611.0" y="243.0">41.2 MiB</text>
<line class="axis" x1="170" y1="102" x2="170" y2="254"/>
<text class="t2" x="170" y="292">Axis is logarithmic: each gridline is 10x the previous one.</text>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

+90
View File
@@ -0,0 +1,90 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 502" width="760" role="img" aria-labelledby="t d" style="max-width:100%;height:auto">
<title id="t">Time to start a trivial command</title>
<desc id="d">Horizontal bars, milliseconds, lower is better. doos: 14 to 65 ms; Podman: 208 to 241 ms; Docker: 344 to 436 ms; bubblewrap: 7 to 8 ms. Exact values are in the table in COMPARISON.md.</desc>
<style>
.bg { fill: var(--surface); }
.t1 { fill: var(--ink); font: 600 15px system-ui, -apple-system, "Segoe UI", sans-serif; }
.t2 { fill: var(--ink2); font: 400 12px system-ui, -apple-system, "Segoe UI", sans-serif; }
.lab { fill: var(--ink); font: 400 13px system-ui, -apple-system, "Segoe UI", sans-serif; }
.val { fill: var(--ink2); font: 400 12px system-ui, -apple-system, "Segoe UI", sans-serif; }
.tick { fill: var(--muted); font: 400 11px system-ui, -apple-system, "Segoe UI", sans-serif; }
.grid { stroke: var(--grid); stroke-width: 1; }
.axis { stroke: var(--axis); stroke-width: 1; }
.acc { fill: var(--accent); }
.oth { fill: var(--other); }
:root { --surface:#fcfcfb; --ink:#0b0b0b; --ink2:#52514e; --muted:#898781;
--grid:#e1e0d9; --axis:#c3c2b7; --accent:#2a78d6; --other:#898781; }
@media (prefers-color-scheme: dark) {
:root { --surface:#1a1a19; --ink:#f0efec; --ink2:#c3c2b7; --muted:#898781;
--grid:#2c2c2a; --axis:#383835; --accent:#3987e5; --other:#898781; }
}
</style>
<rect class="bg" x="0" y="0" width="760" height="502" rx="8"/>
<text class="t1" x="20" y="28">Time to start a trivial command (ms, lower is better)</text>
<text class="t2" x="20" y="46">10 runs each, average, warm cache, 2026-10-23, kernel 7.0.0-38-generic, 12 cores. Docker is a rootful daemon.</text>
<rect class="acc" x="20" y="55" width="12" height="12" rx="2"/>
<text class="t2" x="38" y="65">doos</text>
<rect class="oth" x="88" y="55" width="12" height="12" rx="2"/>
<text class="t2" x="106" y="65">other tools</text>
<text class="t1" x="22" y="78">No network</text>
<line class="grid" x1="200.0" y1="90" x2="200.0" y2="242"/>
<line class="grid" x1="294.0" y1="90" x2="294.0" y2="242"/>
<line class="grid" x1="388.0" y1="90" x2="388.0" y2="242"/>
<line class="grid" x1="482.0" y1="90" x2="482.0" y2="242"/>
<line class="grid" x1="576.0" y1="90" x2="576.0" y2="242"/>
<line class="grid" x1="670.0" y1="90" x2="670.0" y2="242"/>
<text class="lab" x="190" y="111.0" text-anchor="end">bubblewrap (host /usr)</text>
<path class="oth" d="M200.0,98.0 h3.9 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-3.9 z"><title>bubblewrap (host /usr): 8.4 ms</title></path>
<text class="val" x="215.9" y="111.0">8.4 ms</text>
<text class="lab" x="190" y="141.0" text-anchor="end">doos Alpine image</text>
<path class="acc" d="M200.0,128.0 h9.0 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-9.0 z"><title>doos Alpine image: 13.8 ms</title></path>
<text class="val" x="221.0" y="141.0">13.8 ms</text>
<text class="lab" x="190" y="171.0" text-anchor="end">doos host mode</text>
<path class="acc" d="M200.0,158.0 h13.6 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-13.6 z"><title>doos host mode: 18.7 ms</title></path>
<text class="val" x="225.6" y="171.0">18.7 ms</text>
<text class="lab" x="190" y="201.0" text-anchor="end">Podman Alpine</text>
<path class="oth" d="M200.0,188.0 h191.3 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-191.3 z"><title>Podman Alpine: 208 ms</title></path>
<text class="val" x="403.3" y="201.0">208 ms</text>
<text class="lab" x="190" y="231.0" text-anchor="end">Docker Alpine (rootful)</text>
<path class="oth" d="M200.0,218.0 h319.3 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-319.3 z"><title>Docker Alpine (rootful): 344 ms</title></path>
<text class="val" x="531.3" y="231.0">344 ms</text>
<line class="axis" x1="200" y1="90" x2="200" y2="242"/>
<text class="tick" x="200.0" y="256" text-anchor="middle">0</text>
<text class="tick" x="294.0" y="256" text-anchor="middle">100</text>
<text class="tick" x="388.0" y="256" text-anchor="middle">200</text>
<text class="tick" x="482.0" y="256" text-anchor="middle">300</text>
<text class="tick" x="576.0" y="256" text-anchor="middle">400</text>
<text class="tick" x="670.0" y="256" text-anchor="middle">500</text>
<text class="t1" x="22" y="280">Default network</text>
<line class="grid" x1="200.0" y1="292" x2="200.0" y2="474"/>
<line class="grid" x1="294.0" y1="292" x2="294.0" y2="474"/>
<line class="grid" x1="388.0" y1="292" x2="388.0" y2="474"/>
<line class="grid" x1="482.0" y1="292" x2="482.0" y2="474"/>
<line class="grid" x1="576.0" y1="292" x2="576.0" y2="474"/>
<line class="grid" x1="670.0" y1="292" x2="670.0" y2="474"/>
<text class="lab" x="190" y="313.0" text-anchor="end">bubblewrap (host /usr)</text>
<path class="oth" d="M200.0,300.0 h3.0 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-3.0 z"><title>bubblewrap (host /usr): 7.4 ms</title></path>
<text class="val" x="215.0" y="313.0">7.4 ms</text>
<text class="lab" x="190" y="343.0" text-anchor="end">doos Alpine image</text>
<path class="acc" d="M200.0,330.0 h45.0 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-45.0 z"><title>doos Alpine image: 52.1 ms</title></path>
<text class="val" x="257.0" y="343.0">52.1 ms</text>
<text class="lab" x="190" y="373.0" text-anchor="end">doos host mode</text>
<path class="acc" d="M200.0,360.0 h49.5 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-49.5 z"><title>doos host mode: 56.9 ms</title></path>
<text class="val" x="261.5" y="373.0">56.9 ms</text>
<text class="lab" x="190" y="403.0" text-anchor="end">doos Alpine, pasta</text>
<path class="acc" d="M200.0,390.0 h56.7 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-56.7 z"><title>doos Alpine, pasta: 64.6 ms</title></path>
<text class="val" x="268.7" y="403.0">64.6 ms</text>
<text class="lab" x="190" y="433.0" text-anchor="end">Podman Alpine</text>
<path class="oth" d="M200.0,420.0 h222.7 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-222.7 z"><title>Podman Alpine: 241 ms</title></path>
<text class="val" x="434.7" y="433.0">241 ms</text>
<text class="lab" x="190" y="463.0" text-anchor="end">Docker Alpine (rootful)</text>
<path class="oth" d="M200.0,450.0 h405.5 a4,4 0 0 1 4,4 v10.0 a4,4 0 0 1 -4,4 h-405.5 z"><title>Docker Alpine (rootful): 436 ms</title></path>
<text class="val" x="617.5" y="463.0">436 ms</text>
<line class="axis" x1="200" y1="292" x2="200" y2="474"/>
<text class="tick" x="200.0" y="488" text-anchor="middle">0</text>
<text class="tick" x="294.0" y="488" text-anchor="middle">100</text>
<text class="tick" x="388.0" y="488" text-anchor="middle">200</text>
<text class="tick" x="482.0" y="488" text-anchor="middle">300</text>
<text class="tick" x="576.0" y="488" text-anchor="middle">400</text>
<text class="tick" x="670.0" y="488" text-anchor="middle">500</text>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB