Charts and reproducible benchmarks, Makefile, expanded .gitignore
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 |
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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()
|
||||
@@ -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 |
@@ -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 |
Reference in New Issue
Block a user