B&W print-friendly diagrams + tinkerpop-0001 + wave-3 proof sections. Squash of 94 local commits onto remote master.
211 lines
9.7 KiB
Python
211 lines
9.7 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
gen-donuts.py — Generate SVG donut charts for CWE-407 speedup visualization.
|
||
|
||
One donut per ecosystem: javac, minecraft-server, minecraft-client.
|
||
Each donut ring = one defect. Arc proportional to fraction of BEFORE time.
|
||
Center shows peak speedup. Minimum-ink aesthetic: grays + one accent color.
|
||
|
||
Output: svg/donut-javac.svg, svg/donut-mc-server.svg, svg/donut-mc-client.svg
|
||
"""
|
||
|
||
import math
|
||
import os
|
||
|
||
OUT_DIR = os.path.join(os.path.dirname(__file__), "svg")
|
||
os.makedirs(OUT_DIR, exist_ok=True)
|
||
|
||
# ── Palette — print-friendly B&W (no color ink) ──────────────────────────────
|
||
BEFORE_COLOR = "#555555" # dark gray — defective arc (wasted time)
|
||
AFTER_COLOR = "#dddddd" # light gray — fixed arc (efficient portion)
|
||
GAP_COLOR = "white"
|
||
LABEL_COLOR = "#000000"
|
||
CENTER_TITLE = "#000000"
|
||
CENTER_SUB = "#444444"
|
||
BG = "white"
|
||
RING_BG = "white" # no background fill — saves ink
|
||
|
||
# ── Geometry ─────────────────────────────────────────────────────────────────
|
||
CX, CY = 180, 180 # center
|
||
RING_W = 28 # ring width per defect
|
||
RING_GAP = 5 # gap between rings
|
||
R_INNER_BASE = 62 # innermost ring inner radius
|
||
LABEL_X = 390 # x position for legend labels
|
||
SVG_W, SVG_H = 560, 380
|
||
|
||
# ── Data ─────────────────────────────────────────────────────────────────────
|
||
# speedup = before_time / after_time (real measured values)
|
||
# label_speedup = display string for the ring
|
||
|
||
JAVAC_DEFECTS = [
|
||
# (id, short_name, speedup_real, annotation)
|
||
("0001", "Tarjan stack.contains()", 5.46, "5.46× — V=800"),
|
||
("0002a","findNode ArrayList scan", 6.86, "6.9× — N=200"),
|
||
("0002b","closure uncached DFS", 20.91, "20.9× — V=100"),
|
||
("0003", "TopoSorter Deque.contains()",1.51,"1.5× — V=200"),
|
||
("0004", "DependencyList.add", 4.27, "4.3× — M=200"),
|
||
("0005", "BoundSet containsAll", 2.40, "2.4× — B=100"),
|
||
]
|
||
|
||
MC_SERVER_DEFECTS = [
|
||
("mc-0001","DependencySorter.isCyclic", 110.9, "110.9× — depth 14"),
|
||
("mc-0002","PistonStructureResolver", 1.1, "1.1× — bounded ≤12"),
|
||
("mc-0003","ExpRedstoneWireEvaluator", 25.0, "~25× — N=500 wires"),
|
||
("mc-0004","MoveThroughVillageGoal", 1.0, "~1.0× — bounded ≤15"),
|
||
]
|
||
|
||
MC_CLIENT_DEFECTS = [
|
||
("mc-0001","DependencySorter (world load)", 110.9, "110.9× — depth 14"),
|
||
("mc-0002","PistonStructureResolver", 1.1, "1.1× — bounded ≤12"),
|
||
("mc-0003","ExpRedstoneWireEval (redstone)", 25.0, "~25× — N=500 wires"),
|
||
("mc-0004","MoveThroughVillageGoal (AI)", 1.0, "~1.0× — bounded ≤15"),
|
||
]
|
||
|
||
# ── SVG helpers ───────────────────────────────────────────────────────────────
|
||
|
||
def polar(cx, cy, r, angle_deg):
|
||
a = math.radians(angle_deg - 90) # 0° = top
|
||
return cx + r * math.cos(a), cy + r * math.sin(a)
|
||
|
||
def arc_path(cx, cy, r_outer, r_inner, start_deg, end_deg):
|
||
"""SVG path for a donut arc segment."""
|
||
# Clamp sweep to avoid degenerate full-circle arcs
|
||
sweep = end_deg - start_deg
|
||
if abs(sweep) >= 359.99:
|
||
end_deg = start_deg + 359.0
|
||
sweep = 359.0
|
||
large = 1 if sweep > 180 else 0
|
||
ox1, oy1 = polar(cx, cy, r_outer, start_deg)
|
||
ox2, oy2 = polar(cx, cy, r_outer, end_deg)
|
||
ix1, iy1 = polar(cx, cy, r_inner, end_deg)
|
||
ix2, iy2 = polar(cx, cy, r_inner, start_deg)
|
||
return (
|
||
f"M {ox1:.2f},{oy1:.2f} "
|
||
f"A {r_outer},{r_outer} 0 {large},1 {ox2:.2f},{oy2:.2f} "
|
||
f"L {ix1:.2f},{iy1:.2f} "
|
||
f"A {r_inner},{r_inner} 0 {large},0 {ix2:.2f},{iy2:.2f} Z"
|
||
)
|
||
|
||
def fmt_speedup(s):
|
||
if s >= 100: return f"{s:.0f}×"
|
||
if s >= 10: return f"{s:.1f}×"
|
||
return f"{s:.2f}×"
|
||
|
||
# ── Main chart builder ───────────────────────────────────────────────────────
|
||
|
||
def make_donut(title, subtitle, defects, peak_label, filename):
|
||
n = len(defects)
|
||
lines = []
|
||
lines.append(f'<svg xmlns="http://www.w3.org/2000/svg" width="{SVG_W}" height="{SVG_H}" '
|
||
f'style="background:{BG};font-family:Helvetica,Arial,sans-serif;">')
|
||
|
||
# Title
|
||
lines.append(f' <text x="20" y="26" font-size="12" font-weight="500" '
|
||
f'fill="{CENTER_TITLE}">{title}</text>')
|
||
lines.append(f' <text x="20" y="42" font-size="8.5" fill="{CENTER_SUB}">{subtitle}</text>')
|
||
|
||
# Center text
|
||
lines.append(f' <text x="{CX}" y="{CY-10}" text-anchor="middle" font-size="22" '
|
||
f'font-weight="600" fill="{CENTER_TITLE}">{peak_label}</text>')
|
||
lines.append(f' <text x="{CX}" y="{CY+8}" text-anchor="middle" font-size="8" '
|
||
f'fill="{CENTER_SUB}">peak speedup</text>')
|
||
lines.append(f' <text x="{CX}" y="{CY+20}" text-anchor="middle" font-size="7.5" '
|
||
f'fill="{CENTER_SUB}">measured</text>')
|
||
|
||
# Rings — outermost ring = index 0
|
||
for i, (did, name, speedup, annot) in enumerate(defects):
|
||
r_out = R_INNER_BASE + (n - i) * (RING_W + RING_GAP)
|
||
r_in = r_out - RING_W
|
||
|
||
# Background full ring — outline only (white fill, thin gray stroke)
|
||
bg_path = arc_path(CX, CY, r_out, r_in, 0, 359.9)
|
||
lines.append(f' <path d="{bg_path}" fill="{RING_BG}" stroke="#cccccc" stroke-width="0.5"/>')
|
||
|
||
# BEFORE arc — the whole 360° represents BEFORE time
|
||
# AFTER arc = 360° / speedup degrees (after is smaller)
|
||
# We show: red arc = fraction spent in BEFORE overhead = (1 - 1/speedup) * 360
|
||
# Green arc = 1/speedup * 360 (the fixed portion)
|
||
frac_after = min(1.0, 1.0 / speedup)
|
||
after_deg = frac_after * 359.9
|
||
before_deg = 359.9 - after_deg
|
||
|
||
# Before arc (large, red)
|
||
if before_deg > 0.5:
|
||
p = arc_path(CX, CY, r_out, r_in, after_deg, 359.9)
|
||
lines.append(f' <path d="{p}" fill="{BEFORE_COLOR}" opacity="0.75"/>')
|
||
|
||
# After arc (small, green)
|
||
if after_deg > 0.5:
|
||
p = arc_path(CX, CY, r_out, r_in, 0, after_deg)
|
||
lines.append(f' <path d="{p}" fill="{AFTER_COLOR}"/>')
|
||
|
||
# Spoke label at right side of ring
|
||
mid_r = (r_out + r_in) / 2
|
||
lx, ly = polar(CX, CY, mid_r + 6, 0) # point at top
|
||
|
||
# Legend row on the right
|
||
ly_label = 72 + i * 46
|
||
lx_swatch = LABEL_X
|
||
# Color swatch
|
||
lines.append(f' <rect x="{lx_swatch}" y="{ly_label - 8}" width="10" height="10" '
|
||
f'fill="{BEFORE_COLOR}" rx="2"/>')
|
||
lines.append(f' <rect x="{lx_swatch}" y="{ly_label + 4}" width="10" height="3" '
|
||
f'fill="{AFTER_COLOR}" rx="1"/>')
|
||
# Ring number indicator
|
||
lines.append(f' <text x="{lx_swatch + 14}" y="{ly_label}" font-size="7.5" '
|
||
f'fill="{CENTER_SUB}">{did}</text>')
|
||
lines.append(f' <text x="{lx_swatch + 14}" y="{ly_label + 11}" font-size="8.5" '
|
||
f'font-weight="500" fill="{CENTER_TITLE}">{name}</text>')
|
||
lines.append(f' <text x="{lx_swatch + 14}" y="{ly_label + 23}" font-size="8" '
|
||
f'fill="{CENTER_TITLE}" font-weight="600">{annot}</text>')
|
||
lines.append(f' <text x="{lx_swatch + 14}" y="{ly_label + 34}" font-size="7.5" '
|
||
f'fill="{CENTER_SUB}">before arc = {before_deg/3.6:.0f}% of time</text>')
|
||
|
||
# Legend header
|
||
lines.append(f' <text x="{LABEL_X}" y="60" font-size="8" fill="{CENTER_SUB}" '
|
||
f'font-weight="600">DEFECTS (outer → inner ring)</text>')
|
||
|
||
# Bottom legend
|
||
lines.append(f' <rect x="20" y="{SVG_H-28}" width="12" height="12" fill="{BEFORE_COLOR}" rx="2"/>')
|
||
lines.append(f' <text x="36" y="{SVG_H-19}" font-size="8" fill="{CENTER_SUB}">BEFORE — wasted time (dark)</text>')
|
||
lines.append(f' <rect x="180" y="{SVG_H-28}" width="12" height="12" fill="{AFTER_COLOR}" stroke="#999" stroke-width="0.5" rx="2"/>')
|
||
lines.append(f' <text x="196" y="{SVG_H-19}" font-size="8" fill="{CENTER_SUB}">AFTER — time with fix (light)</text>')
|
||
lines.append(f' <text x="350" y="{SVG_H-19}" font-size="7.5" fill="{CENTER_SUB}">'
|
||
f'Arc proportional to fraction of BEFORE time.</text>')
|
||
|
||
lines.append('</svg>')
|
||
|
||
out = os.path.join(OUT_DIR, filename)
|
||
with open(out, "w") as f:
|
||
f.write("\n".join(lines))
|
||
print(f" wrote {out}")
|
||
|
||
# ── Generate all three ────────────────────────────────────────────────────────
|
||
|
||
print("Generating donut charts...")
|
||
|
||
make_donut(
|
||
title="javac — CWE-407 Speedups",
|
||
subtitle="OpenJDK javac · 5 defects · all patched upstream · measured at production scale",
|
||
defects=JAVAC_DEFECTS,
|
||
peak_label="20.9×",
|
||
filename="donut-javac.svg",
|
||
)
|
||
|
||
make_donut(
|
||
title="Minecraft Server-26.1 — CWE-407 Speedups",
|
||
subtitle="server-26.1-all-patched.jar · 4 defects · world load / game loop / redstone",
|
||
defects=MC_SERVER_DEFECTS,
|
||
peak_label="110.9×",
|
||
filename="donut-mc-server.svg",
|
||
)
|
||
|
||
make_donut(
|
||
title="Minecraft Client — CWE-407 Impact",
|
||
subtitle="Single-player · same defective classes · modpack world load + in-game",
|
||
defects=MC_CLIENT_DEFECTS,
|
||
peak_label="110.9×",
|
||
filename="donut-mc-client.svg",
|
||
)
|
||
|
||
print("Done.")
|