Files
kernbench2/src/kernbench/web/static/index.html
T
ywkang 624161f52f Update web viewer for router mesh topology (ADR-0019)
Remove all xbar/bridge rendering from cube detail view.
Replace 8 HBM slices with single HBM_CTRL block.
Add green dotted lines showing router-to-HBM connectivity.
Update legend, event animation, and PE view NOC destinations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 17:56:05 -07:00

1992 lines
66 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KernBench Topology Viewer</title>
<style>
:root {
--bg: #0f172a;
--surface: #1e293b;
--surface2: #334155;
--border: #475569;
--text: #e2e8f0;
--text-dim: #94a3b8;
--accent: #3b82f6;
--accent-dim: #1d4ed8;
--green: #22c55e;
--yellow: #eab308;
--red: #ef4444;
--orange: #f97316;
--cube-fill: #1e3a5f;
--cube-stroke: #3b82f6;
--hbm-fill: #1a2e1a;
--hbm-stroke: #22c55e;
--pe-fill: #2d1f3d;
--pe-stroke: #a855f7;
--io-fill: #3d2b1f;
--io-stroke: #f97316;
--router-fill: #1f2d3d;
--router-stroke: #06b6d4;
--link-color: #475569;
--link-active: #3b82f6;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
background: var(--bg);
color: var(--text);
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* ── Top Bar ── */
.topbar {
display: flex;
align-items: center;
gap: 16px;
padding: 8px 16px;
background: var(--surface);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
height: 48px;
}
.topbar h1 {
font-size: 14px;
font-weight: 600;
color: var(--accent);
white-space: nowrap;
}
.view-tabs {
display: flex;
gap: 2px;
background: var(--bg);
border-radius: 6px;
padding: 2px;
}
.view-tab {
padding: 4px 14px;
font-size: 12px;
font-family: inherit;
border: none;
background: none;
color: var(--text-dim);
cursor: pointer;
border-radius: 4px;
transition: all 0.15s;
}
.view-tab:hover { color: var(--text); background: var(--surface2); }
.view-tab.active { color: var(--text); background: var(--accent-dim); }
.breadcrumb {
font-size: 12px;
color: var(--text-dim);
display: flex;
align-items: center;
gap: 4px;
}
.breadcrumb span { cursor: pointer; }
.breadcrumb span:hover { color: var(--accent); }
.breadcrumb .sep { color: var(--border); cursor: default; }
.topbar-spacer { flex: 1; }
.sim-info {
font-size: 11px;
color: var(--text-dim);
white-space: nowrap;
}
/* ── Main Content ── */
.main {
display: flex;
flex: 1;
overflow: hidden;
}
/* ── Canvas Area ── */
.canvas-area {
flex: 1;
position: relative;
overflow: hidden;
background: var(--bg);
}
.canvas-area svg {
width: 100%;
height: 100%;
}
/* ── Detail Panel (right sidebar) ── */
.detail-panel {
width: 280px;
flex-shrink: 0;
background: var(--surface);
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow-y: auto;
}
.panel-section {
padding: 12px 14px;
border-bottom: 1px solid var(--border);
}
.panel-section h3 {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-dim);
margin-bottom: 8px;
}
.panel-section .kv {
display: flex;
justify-content: space-between;
font-size: 12px;
margin-bottom: 4px;
}
.panel-section .kv .key { color: var(--text-dim); }
.panel-section .kv .val { color: var(--text); font-weight: 500; }
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
margin-bottom: 4px;
color: var(--text-dim);
}
.legend-swatch {
width: 14px;
height: 10px;
border-radius: 2px;
border: 1px solid;
flex-shrink: 0;
}
/* ── Bottom Bar (Timeline) ── */
.bottombar {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 16px;
background: var(--surface);
border-top: 1px solid var(--border);
flex-shrink: 0;
height: 44px;
}
.play-btn {
width: 28px;
height: 28px;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--surface2);
color: var(--text);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
transition: all 0.15s;
}
.play-btn:hover { border-color: var(--accent); color: var(--accent); }
.timeline-container {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
}
.timeline-label {
font-size: 11px;
color: var(--text-dim);
white-space: nowrap;
min-width: 50px;
}
.timeline-slider {
flex: 1;
-webkit-appearance: none;
height: 4px;
background: var(--surface2);
border-radius: 2px;
outline: none;
}
.timeline-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--accent);
cursor: pointer;
border: 2px solid var(--bg);
}
.speed-btn {
font-size: 11px;
font-family: inherit;
padding: 2px 8px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--surface2);
color: var(--text-dim);
cursor: pointer;
}
.speed-btn:hover { color: var(--text); border-color: var(--accent); }
.workload-select {
font-size: 11px;
font-family: inherit;
padding: 4px 8px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--surface2);
color: var(--text);
cursor: pointer;
min-width: 180px;
}
.workload-select:hover { border-color: var(--accent); }
.workload-select:focus { outline: none; border-color: var(--accent); }
.workload-select option { background: var(--surface); color: var(--text); }
/* ── Tooltip ── */
.tooltip {
position: absolute;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 6px;
padding: 8px 12px;
font-size: 11px;
pointer-events: none;
display: none;
z-index: 100;
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
max-width: 240px;
}
.tooltip .tt-title {
font-weight: 600;
color: var(--accent);
margin-bottom: 4px;
}
.tooltip .tt-row {
display: flex;
justify-content: space-between;
gap: 12px;
color: var(--text-dim);
}
.tooltip .tt-row .tt-val { color: var(--text); }
/* ── SVG component styles ── */
.node-group { cursor: pointer; }
.node-group:hover rect,
.node-group:hover polygon { filter: brightness(1.3); }
.link-line {
stroke-linecap: round;
transition: stroke 0.2s, stroke-width 0.2s;
}
.link-line:hover {
stroke: var(--link-active);
stroke-width: 3;
}
/* Utilization color classes */
.util-idle { stroke: var(--border); }
.util-low { stroke: var(--accent); }
.util-med { stroke: var(--green); }
.util-high { stroke: var(--yellow); }
.util-sat { stroke: var(--red); }
/* Hot path highlight */
.hot-path { stroke: var(--orange); stroke-width: 3; opacity: 1; }
/* Animated packet dot */
@keyframes packet-move {
0% { offset-distance: 0%; }
100% { offset-distance: 100%; }
}
.packet-dot {
fill: var(--yellow);
r: 4;
}
/* Active component glow */
.active-component rect {
filter: brightness(1.5) drop-shadow(0 0 6px currentColor);
}
/* Hot path transition */
.link-line {
transition: stroke 0.3s ease, stroke-width 0.2s ease;
}
</style>
</head>
<body>
<!-- ── Top Bar ── -->
<div class="topbar">
<h1>KernBench</h1>
<div class="view-tabs">
<button class="view-tab active" data-view="sip">SIP</button>
<button class="view-tab" data-view="cube">CUBE</button>
<button class="view-tab" data-view="pe">PE</button>
</div>
<div class="breadcrumb" id="breadcrumb">
<span data-nav="sip">SIP 0</span>
</div>
<div class="topbar-spacer"></div>
<select class="workload-select" id="workloadSelect">
<option value="" disabled>Loading workloads...</option>
</select>
<div class="sim-info">topology.yaml | 2 SIPs | 32 Cubes</div>
</div>
<!-- ── Main ── -->
<div class="main">
<!-- Canvas -->
<div class="canvas-area" id="canvasArea">
<svg id="topoSvg" xmlns="http://www.w3.org/2000/svg"></svg>
<div class="tooltip" id="tooltip"></div>
</div>
<!-- Detail Panel -->
<div class="detail-panel">
<div class="panel-section">
<h3>Selected</h3>
<div id="selInfo">
<div class="kv"><span class="key">Click a component</span></div>
</div>
</div>
<div class="panel-section">
<h3>Legend</h3>
<div class="legend-item">
<div class="legend-swatch" style="background:var(--cube-fill);border-color:var(--cube-stroke)"></div>
Cube
</div>
<div class="legend-item">
<div class="legend-swatch" style="background:var(--io-fill);border-color:var(--io-stroke)"></div>
IO Chiplet
</div>
<div class="legend-item">
<div class="legend-swatch" style="background:var(--hbm-fill);border-color:var(--hbm-stroke)"></div>
HBM
</div>
<div class="legend-item">
<div class="legend-swatch" style="background:var(--pe-fill);border-color:var(--pe-stroke)"></div>
PE
</div>
<div class="legend-item">
<div class="legend-swatch" style="background:var(--router-fill);border-color:var(--router-stroke)"></div>
Router Mesh
</div>
</div>
<div class="panel-section">
<h3>Link Utilization</h3>
<div class="legend-item">
<div class="legend-swatch" style="background:var(--border);border-color:var(--border)"></div>
Idle (0%)
</div>
<div class="legend-item">
<div class="legend-swatch" style="background:var(--accent);border-color:var(--accent)"></div>
Low (&lt;25%)
</div>
<div class="legend-item">
<div class="legend-swatch" style="background:var(--green);border-color:var(--green)"></div>
Medium (25-50%)
</div>
<div class="legend-item">
<div class="legend-swatch" style="background:var(--yellow);border-color:var(--yellow)"></div>
High (50-75%)
</div>
<div class="legend-item">
<div class="legend-swatch" style="background:var(--red);border-color:var(--red)"></div>
Saturated (&gt;75%)
</div>
</div>
<div class="panel-section">
<h3>Topology</h3>
<div class="kv"><span class="key">Cube mesh</span><span class="val">4 x 4</span></div>
<div class="kv"><span class="key">PEs / cube</span><span class="val">8</span></div>
<div class="kv"><span class="key">HBM / cube</span><span class="val">48 GB</span></div>
<div class="kv"><span class="key">UCIe BW</span><span class="val">512 GB/s</span></div>
<div class="kv"><span class="key">HBM BW</span><span class="val">1024 GB/s</span></div>
<div class="kv"><span class="key">HBM eff.</span><span class="val">0.8</span></div>
</div>
<div class="panel-section">
<h3>Simulation</h3>
<div class="kv"><span class="key">Events</span><span class="val" id="eventCount">--</span></div>
<div class="kv"><span class="key">Duration</span><span class="val" id="simDuration">-- ns</span></div>
<div class="kv"><span class="key">In-flight</span><span class="val" id="inflight">0</span></div>
</div>
</div>
</div>
<!-- ── Bottom Bar (Timeline) ── -->
<div class="bottombar">
<button class="play-btn" id="playBtn" title="Play/Pause">&#9654;</button>
<div class="timeline-container">
<span class="timeline-label" id="timeLabel">t = 0.0 ns</span>
<input type="range" class="timeline-slider" id="timeSlider" min="0" max="1000" value="0" step="0.1">
<span class="timeline-label" id="timeEnd">0.0 ns</span>
</div>
<button class="speed-btn" id="speedBtn" title="Playback speed">1x</button>
</div>
<script>
// ═══════════════════════════════════════════
// KernBench Topology Viewer — Layout Prototype
// ═══════════════════════════════════════════
const SVG_NS = "http://www.w3.org/2000/svg";
// ── Topology constants ──
const MESH_W = 4, MESH_H = 4;
const PE_PER_CUBE = 8;
const HBM_SLICES = 8;
// ── Layout constants (SVG coordinates) ──
const PAD = 60;
const CUBE_W = 120, CUBE_H = 100;
const CUBE_GAP_X = 32, CUBE_GAP_Y = 32;
const IO_H = 44;
const IO_Y = PAD;
const GRID_Y = IO_Y + IO_H + 40;
const GRID_W = MESH_W * CUBE_W + (MESH_W - 1) * CUBE_GAP_X;
const GRID_X = PAD;
const IO_W = GRID_W;
const IO_X = PAD;
const SVG_W = PAD * 2 + GRID_W;
const SVG_H = GRID_Y + MESH_H * CUBE_H + (MESH_H - 1) * CUBE_GAP_Y + PAD;
// ── State ──
let currentView = "sip";
let selectedNode = null;
// ── SVG helpers ──
function svgEl(tag, attrs = {}) {
const el = document.createElementNS(SVG_NS, tag);
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
return el;
}
function cubePos(col, row) {
return {
x: GRID_X + col * (CUBE_W + CUBE_GAP_X),
y: GRID_Y + row * (CUBE_H + CUBE_GAP_Y)
};
}
// ── Draw SIP View ──
function drawSipView(svg) {
svg.innerHTML = "";
svg.setAttribute("viewBox", `0 0 ${SVG_W} ${SVG_H}`);
// Background
svg.appendChild(svgEl("rect", {
width: SVG_W, height: SVG_H, fill: "#0f172a"
}));
// Title
const title = svgEl("text", {
x: SVG_W / 2, y: 28,
"text-anchor": "middle",
"font-family": "monospace",
"font-size": "13",
"font-weight": "bold",
fill: "#94a3b8"
});
title.textContent = "SIP 0 — 4x4 Cube Mesh";
svg.appendChild(title);
// IO Chiplet
const ioGroup = svgEl("g", { class: "node-group", "data-id": "io0" });
ioGroup.appendChild(svgEl("rect", {
x: IO_X, y: IO_Y, width: IO_W, height: IO_H,
rx: 6, fill: "#3d2b1f", stroke: "#f97316", "stroke-width": 1.5
}));
// IO chiplet internal components
const ioLabels = [
{ name: "PCIE_EP", x: IO_X + IO_W * 0.15 },
{ name: "IO_CPU", x: IO_X + IO_W * 0.38 },
{ name: "IO_NOC", x: IO_X + IO_W * 0.62 },
{ name: "UCIe", x: IO_X + IO_W * 0.85 }
];
for (const lbl of ioLabels) {
const box = svgEl("rect", {
x: lbl.x - 32, y: IO_Y + 6, width: 64, height: 20,
rx: 3, fill: "#5a3d26", stroke: "#f9731666", "stroke-width": 1
});
ioGroup.appendChild(box);
const t = svgEl("text", {
x: lbl.x, y: IO_Y + 20,
"text-anchor": "middle",
"font-family": "monospace",
"font-size": "9",
fill: "#f97316"
});
t.textContent = lbl.name;
ioGroup.appendChild(t);
}
const ioLabel = svgEl("text", {
x: IO_X + IO_W / 2, y: IO_Y + IO_H - 4,
"text-anchor": "middle",
"font-family": "monospace",
"font-size": "9",
fill: "#f9731688"
});
ioLabel.textContent = "IO Chiplet (io0)";
ioGroup.appendChild(ioLabel);
svg.appendChild(ioGroup);
// IO → top row UCIe links
for (let c = 0; c < MESH_W; c++) {
const pos = cubePos(c, 0);
const cx = pos.x + CUBE_W / 2;
const ioBottom = IO_Y + IO_H;
const cubeTop = pos.y;
// Link line
const line = svgEl("line", {
x1: cx, y1: ioBottom, x2: cx, y2: cubeTop,
stroke: "#f9731666", "stroke-width": 1.5,
"stroke-dasharray": "4,3",
class: "link-line"
});
svg.appendChild(line);
// Label
const lt = svgEl("text", {
x: cx + 8, y: (ioBottom + cubeTop) / 2,
"font-family": "monospace",
"font-size": "7",
fill: "#f9731688"
});
lt.textContent = "512";
svg.appendChild(lt);
}
// UCIe mesh links (horizontal + vertical)
for (let r = 0; r < MESH_H; r++) {
for (let c = 0; c < MESH_W; c++) {
const pos = cubePos(c, r);
// Right link
if (c < MESH_W - 1) {
const npos = cubePos(c + 1, r);
const srcIdx = r * MESH_W + c, dstIdx = r * MESH_W + c + 1;
svg.appendChild(svgEl("line", {
x1: pos.x + CUBE_W, y1: pos.y + CUBE_H / 2,
x2: npos.x, y2: npos.y + CUBE_H / 2,
stroke: "#3b82f644", "stroke-width": 2,
class: "link-line",
"data-link": `cube${srcIdx}-cube${dstIdx}`
}));
// BW label
const lt = svgEl("text", {
x: (pos.x + CUBE_W + npos.x) / 2,
y: pos.y + CUBE_H / 2 - 4,
"text-anchor": "middle",
"font-family": "monospace",
"font-size": "7",
fill: "#3b82f666"
});
lt.textContent = "512";
svg.appendChild(lt);
}
// Down link
if (r < MESH_H - 1) {
const npos = cubePos(c, r + 1);
const srcIdx = r * MESH_W + c, dstIdx = (r + 1) * MESH_W + c;
svg.appendChild(svgEl("line", {
x1: pos.x + CUBE_W / 2, y1: pos.y + CUBE_H,
x2: npos.x + CUBE_W / 2, y2: npos.y,
stroke: "#3b82f644", "stroke-width": 2,
class: "link-line",
"data-link": `cube${srcIdx}-cube${dstIdx}`
}));
const lt = svgEl("text", {
x: pos.x + CUBE_W / 2 + 8,
y: (pos.y + CUBE_H + npos.y) / 2,
"font-family": "monospace",
"font-size": "7",
fill: "#3b82f666"
});
lt.textContent = "512";
svg.appendChild(lt);
}
}
}
// Cubes
for (let r = 0; r < MESH_H; r++) {
for (let c = 0; c < MESH_W; c++) {
const idx = r * MESH_W + c;
const pos = cubePos(c, r);
drawCubeNode(svg, pos.x, pos.y, idx);
}
}
}
function drawCubeNode(svg, x, y, idx) {
const g = svgEl("g", {
class: "node-group",
"data-id": `cube${idx}`,
"data-type": "cube"
});
// Outer rect
g.appendChild(svgEl("rect", {
x, y, width: CUBE_W, height: CUBE_H,
rx: 6, fill: "#1e3a5f", stroke: "#3b82f6", "stroke-width": 1.5
}));
// Cube label
const label = svgEl("text", {
x: x + CUBE_W / 2, y: y + 14,
"text-anchor": "middle",
"font-family": "monospace",
"font-size": "10",
"font-weight": "bold",
fill: "#3b82f6"
});
label.textContent = `C${idx}`;
g.appendChild(label);
// Mini PE indicators (4 corners)
const pePositions = [
{ cx: x + 14, cy: y + 28 }, // NW
{ cx: x + CUBE_W - 14, cy: y + 28 }, // NE
{ cx: x + 14, cy: y + CUBE_H - 28 }, // SW
{ cx: x + CUBE_W - 14, cy: y + CUBE_H - 28 } // SE
];
for (let i = 0; i < pePositions.length; i++) {
const pp = pePositions[i];
// 2 PEs per corner — draw as a small double block
g.appendChild(svgEl("rect", {
x: pp.cx - 8, y: pp.cy - 5, width: 16, height: 10,
rx: 2, fill: "#2d1f3d", stroke: "#a855f766", "stroke-width": 0.8
}));
// PE pair label
const pt = svgEl("text", {
x: pp.cx, y: pp.cy + 3,
"text-anchor": "middle",
"font-family": "monospace",
"font-size": "6",
fill: "#a855f7aa"
});
pt.textContent = `${i*2},${i*2+1}`;
g.appendChild(pt);
}
// Center block: router mesh
g.appendChild(svgEl("rect", {
x: x + 30, y: y + 30, width: CUBE_W - 60, height: CUBE_H - 56,
rx: 3, fill: "#1f2d3d", stroke: "#06b6d466", "stroke-width": 0.8
}));
const xt = svgEl("text", {
x: x + CUBE_W / 2, y: y + CUBE_H / 2 + 1,
"text-anchor": "middle",
"font-family": "monospace",
"font-size": "7",
fill: "#06b6d4aa"
});
xt.textContent = "Router Mesh";
g.appendChild(xt);
// HBM indicators (top and bottom)
const hbmY1 = y + CUBE_H - 16;
g.appendChild(svgEl("rect", {
x: x + 20, y: hbmY1, width: CUBE_W - 40, height: 10,
rx: 2, fill: "#1a2e1a", stroke: "#22c55e66", "stroke-width": 0.8
}));
const ht = svgEl("text", {
x: x + CUBE_W / 2, y: hbmY1 + 7,
"text-anchor": "middle",
"font-family": "monospace",
"font-size": "6",
fill: "#22c55eaa"
});
ht.textContent = "HBM 48GB";
g.appendChild(ht);
// UCIe port indicators (N, S, E, W) — small triangles
const ports = [
{ px: x + CUBE_W / 2, py: y, dir: "N" },
{ px: x + CUBE_W / 2, py: y + CUBE_H, dir: "S" },
{ px: x, py: y + CUBE_H / 2, dir: "W" },
{ px: x + CUBE_W, py: y + CUBE_H / 2, dir: "E" }
];
for (const p of ports) {
const sz = 4;
let points;
switch (p.dir) {
case "N": points = `${p.px},${p.py} ${p.px-sz},${p.py+sz} ${p.px+sz},${p.py+sz}`; break;
case "S": points = `${p.px},${p.py} ${p.px-sz},${p.py-sz} ${p.px+sz},${p.py-sz}`; break;
case "W": points = `${p.px},${p.py} ${p.px+sz},${p.py-sz} ${p.px+sz},${p.py+sz}`; break;
case "E": points = `${p.px},${p.py} ${p.px-sz},${p.py-sz} ${p.px-sz},${p.py+sz}`; break;
}
g.appendChild(svgEl("polygon", {
points, fill: "#3b82f6", opacity: "0.5"
}));
}
// Click to drill down
g.addEventListener("click", () => onCubeClick(idx));
svg.appendChild(g);
}
// ── Draw Cube Detail View (matches cube_mesh_view.svg architecture) ──
function drawCubeView(svg, cubeIdx) {
svg.innerHTML = "";
// Use EXACT reference SVG coordinates (viewBox 1050x980, grid origin 180,170)
const W = 1050, H = 980;
svg.setAttribute("viewBox", `0 0 ${W} ${H}`);
svg.appendChild(svgEl("rect", { width: W, height: H, fill: "#0f172a" }));
// Title
const title = svgEl("text", {
x: W / 2, y: 28, "text-anchor": "middle",
"font-family": "monospace", "font-size": "16", "font-weight": "bold", fill: "#94a3b8"
});
title.textContent = `CUBE ${cubeIdx} INTERNAL ARCHITECTURE`;
svg.appendChild(title);
const subtitle = svgEl("text", {
x: W / 2, y: 46, "text-anchor": "middle",
"font-family": "monospace", "font-size": "11", fill: "#64748b"
});
subtitle.textContent = "17.0 x 14.0 mm | 6x6 Router Mesh | 8 PEs (~5mm2) | HBM 9x5mm | UCIe N/S/E/W x4";
svg.appendChild(subtitle);
// Cube boundary
svg.appendChild(svgEl("rect", {
x: 50, y: 62, width: 950, height: 770,
rx: 10, fill: "none", stroke: "#334155", "stroke-width": 3
}));
// ── EXACT coordinates from reference SVG ──
// Grid origin matches translate(180, 170) in reference
const OX = 180, OY = 170;
const CS = 140; // column step (reference: 0,140,280,420,560,700)
const rowY = [0, 95, 215, 310, 425, 520]; // exact reference row positions
const hbmExclude = new Set(["2,2","2,3","3,2","3,3"]);
const peRouters = new Set(["0,0","0,1","1,4","1,5","4,0","4,1","5,4","5,5"]);
const ucieRouters = new Set(["0,5","4,5"]);
const specialRouters = { "2,0": { fill: "#f59e0b", stroke: "#d97706" }, "3,0": { fill: "#f59e0b", stroke: "#d97706" } };
function rXY(r, c) { return { x: OX + c * CS, y: OY + rowY[r] }; }
// ── Mesh links (horizontal) ──
// Reference uses x offsets ±30 from router center for horizontal, ±30 for vertical
const fullRows = [0, 1, 4, 5]; // rows with all 6 columns connected
const hbmRows = [2, 3]; // rows with only side connections
for (let r = 0; r < 6; r++) {
for (let c = 0; c < 5; c++) {
const k1 = `${r},${c}`, k2 = `${r},${c+1}`;
if (hbmExclude.has(k1) || hbmExclude.has(k2)) continue;
// Skip links across HBM gap for rows 2,3 (only c0-c1 and c4-c5 connected)
if (hbmRows.includes(r) && c >= 1 && c <= 3) continue;
const p1 = rXY(r, c), p2 = rXY(r, c + 1);
svg.appendChild(svgEl("line", {
x1: p1.x + 30, y1: p1.y, x2: p2.x - 30, y2: p2.y,
stroke: "#d1d5db55", "stroke-width": 1.5, class: "link-line",
"data-link": `r${r}c${c}-r${r}c${c+1}`
}));
}
}
// ── Mesh links (vertical) ──
for (let c = 0; c < 6; c++) {
for (let r = 0; r < 5; r++) {
const k1 = `${r},${c}`, k2 = `${r+1},${c}`;
if (hbmExclude.has(k1) || hbmExclude.has(k2)) continue;
// Skip vertical links into/through HBM zone for cols 2,3
if ((c === 2 || c === 3) && r >= 0 && r <= 3) continue;
const p1 = rXY(r, c), p2 = rXY(r + 1, c);
svg.appendChild(svgEl("line", {
x1: p1.x, y1: p1.y + 30, x2: p2.x, y2: p2.y - 30,
stroke: "#d1d5db55", "stroke-width": 1.5, class: "link-line",
"data-link": `r${r}c${c}-r${r+1}c${c}`
}));
}
}
// ── Router nodes ──
for (let r = 0; r < 6; r++) {
for (let c = 0; c < 6; c++) {
if (hbmExclude.has(`${r},${c}`)) continue;
const p = rXY(r, c);
const key = `${r},${c}`;
let fill = "#e2e8f0", stroke = "#94a3b8", rad = 12, textFill = "#475569";
if (peRouters.has(key)) { fill = "#3b82f6"; stroke = "#1d4ed8"; rad = 16; textFill = "white"; }
else if (ucieRouters.has(key)) { fill = "#8b5cf6"; stroke = "#6d28d9"; rad = 16; textFill = "white"; }
else if (specialRouters[key]) { fill = specialRouters[key].fill; stroke = specialRouters[key].stroke; rad = 16; textFill = "white"; }
svg.appendChild(svgEl("circle", { cx: p.x, cy: p.y, r: rad, fill, stroke, "stroke-width": 2 }));
const t = svgEl("text", {
x: p.x, y: p.y + 4, "text-anchor": "middle",
"font-family": "monospace", "font-size": peRouters.has(key) || ucieRouters.has(key) || specialRouters[key] ? "7" : "6",
"font-weight": "bold", fill: textFill
});
t.textContent = `r${r}c${c}`;
svg.appendChild(t);
}
}
// ── HBM ZONE ──
const hbmZoneX = OX + 145, hbmZoneY = OY + 195, hbmZoneW = 410, hbmZoneH = 152;
svg.appendChild(svgEl("rect", {
x: hbmZoneX, y: hbmZoneY, width: hbmZoneW, height: hbmZoneH,
rx: 8, fill: "#052e16", stroke: "#047857", "stroke-width": 2
}));
const hzmLabel = svgEl("text", {
x: hbmZoneX + hbmZoneW / 2, y: hbmZoneY + 16, "text-anchor": "middle",
"font-family": "monospace", "font-size": "9", "font-weight": "bold", fill: "#047857"
});
hzmLabel.textContent = "HBM 9.0 x 5.0 mm | hbm_ctrl_v1";
svg.appendChild(hzmLabel);
// Single HBM_CTRL block (centered in HBM zone)
const hbmCtrlG = svgEl("g", { class: "node-group", "data-id": "hbm_ctrl" });
hbmCtrlG.appendChild(svgEl("rect", {
x: hbmZoneX + 40, y: hbmZoneY + 28, width: hbmZoneW - 80, height: 40,
rx: 6, fill: "#047857", stroke: "#065f46", "stroke-width": 1.5
}));
const hbmCtrlT = svgEl("text", {
x: hbmZoneX + hbmZoneW / 2, y: hbmZoneY + 53, "text-anchor": "middle",
"font-family": "monospace", "font-size": "10", "font-weight": "bold", fill: "white"
});
hbmCtrlT.textContent = "HBM_CTRL";
hbmCtrlG.appendChild(hbmCtrlT);
svg.appendChild(hbmCtrlG);
// Exclusion zone label
const hexLabel = svgEl("text", {
x: hbmZoneX + hbmZoneW / 2, y: hbmZoneY + 85, "text-anchor": "middle",
"font-family": "monospace", "font-size": "7", fill: "#ef4444aa"
});
hexLabel.textContent = "Router exclusion: r2c2, r2c3, r3c2, r3c3";
svg.appendChild(hexLabel);
// "All routers connect to HBM" annotation
const hbmAnnot = svgEl("text", {
x: hbmZoneX + hbmZoneW / 2, y: hbmZoneY + 100, "text-anchor": "middle",
"font-family": "monospace", "font-size": "6", fill: "#059669aa"
});
hbmAnnot.textContent = "All routers → HBM_CTRL (mesh-connected)";
svg.appendChild(hbmAnnot);
// ── HBM connectivity indicators (thin green dotted lines from edge routers to HBM zone) ──
// Draw thin green dotted lines from routers adjacent to HBM zone down/up to HBM
const hbmConnRouters = [
{ r: 1, c: 2 }, { r: 1, c: 3 }, // top edge of HBM zone
{ r: 4, c: 2 }, { r: 4, c: 3 }, // bottom edge of HBM zone
{ r: 2, c: 1 }, { r: 3, c: 1 }, // left edge of HBM zone
{ r: 2, c: 4 }, { r: 3, c: 4 }, // right edge of HBM zone
];
for (const hr of hbmConnRouters) {
const rp = rXY(hr.r, hr.c);
// Draw line toward the HBM zone center
const hbmCenterX = hbmZoneX + hbmZoneW / 2;
const hbmCenterY = hbmZoneY + hbmZoneH / 2;
// Compute endpoint clipped to HBM zone edge
let ex = hbmCenterX, ey = hbmCenterY;
if (hr.r <= 1) { ey = hbmZoneY; ex = rp.x; } // top routers → top of HBM zone
else if (hr.r >= 4) { ey = hbmZoneY + hbmZoneH; ex = rp.x; } // bottom routers → bottom of HBM zone
else if (hr.c <= 1) { ex = hbmZoneX; ey = rp.y; } // left routers → left of HBM zone
else { ex = hbmZoneX + hbmZoneW; ey = rp.y; } // right routers → right of HBM zone
svg.appendChild(svgEl("line", {
x1: rp.x, y1: rp.y, x2: ex, y2: ey,
stroke: "#05966988", "stroke-width": 1, "stroke-dasharray": "3,3"
}));
}
// ── M_CPU (r2c0) and SRAM (r3c0) labels ──
const mcpuP = rXY(2, 0);
svg.appendChild(svgEl("rect", {
x: mcpuP.x - 42, y: mcpuP.y + 18, width: 84, height: 18,
rx: 4, fill: "#f59e0b", stroke: "#d97706", "stroke-width": 1.5
}));
let bt = svgEl("text", {
x: mcpuP.x, y: mcpuP.y + 31, "text-anchor": "middle",
"font-family": "monospace", "font-size": "8", "font-weight": "bold", fill: "white"
});
bt.textContent = "M_CPU";
svg.appendChild(bt);
const sramP = rXY(3, 0);
svg.appendChild(svgEl("rect", {
x: sramP.x - 42, y: sramP.y + 18, width: 84, height: 18,
rx: 4, fill: "#f59e0b", stroke: "#d97706", "stroke-width": 1.5
}));
bt = svgEl("text", {
x: sramP.x, y: sramP.y + 31, "text-anchor": "middle",
"font-family": "monospace", "font-size": "8", "font-weight": "bold", fill: "white"
});
bt.textContent = "SRAM";
svg.appendChild(bt);
// ── PEs at corners (matching reference exactly) ──
const PEW = 84, PEH = 40;
const peData = [
// PE0 NW → r0c0 (0mm, above)
{ idx: 0, corner: "NW", rx: 0, ry: 0, above: true, wire: "0mm" },
// PE1 NW → r0c1 (0mm, above)
{ idx: 1, corner: "NW", rx: 140, ry: 0, above: true, wire: "0mm" },
// PE2 NE → r1c4 (4mm wire, above mesh)
{ idx: 2, corner: "NE", rx: 560, ry: 95, above: true, wire: "4.0mm" },
// PE3 NE → r1c5 (4mm wire, above mesh)
{ idx: 3, corner: "NE", rx: 700, ry: 95, above: true, wire: "4.0mm" },
// PE4 SW → r4c0 (4mm wire, below mesh)
{ idx: 4, corner: "SW", rx: 0, ry: 425, above: false, wire: "4.0mm" },
// PE5 SW → r4c1 (4mm wire, below mesh)
{ idx: 5, corner: "SW", rx: 140, ry: 425, above: false, wire: "4.0mm" },
// PE6 SE → r5c4 (0mm, below)
{ idx: 6, corner: "SE", rx: 560, ry: 520, above: false, wire: "0mm" },
// PE7 SE → r5c5 (0mm, below)
{ idx: 7, corner: "SE", rx: 700, ry: 520, above: false, wire: "0mm" },
];
for (const pe of peData) {
const routerX = OX + pe.rx;
const routerY = OY + pe.ry;
// PE block positioned at y=-62 (above) or y=556 (below) in reference coords
const peX = routerX - PEW / 2;
const peY = pe.above ? OY - 62 : OY + 556;
// Wire from PE to router
if (pe.wire === "0mm") {
svg.appendChild(svgEl("line", {
x1: routerX, y1: pe.above ? peY + PEH : peY,
x2: routerX, y2: pe.above ? routerY - 16 : routerY + 16,
stroke: "#ef4444", "stroke-width": 2
}));
} else {
svg.appendChild(svgEl("line", {
x1: routerX, y1: pe.above ? peY + PEH : peY,
x2: routerX, y2: pe.above ? routerY - 16 : routerY + 16,
stroke: "#ef4444", "stroke-width": 2, "stroke-dasharray": "5,3"
}));
// Wire distance label
const midY = pe.above
? (peY + PEH + routerY - 16) / 2
: (routerY + 16 + peY) / 2;
const wt = svgEl("text", {
x: routerX + 12, y: midY + 3, "font-family": "monospace",
"font-size": "7", "font-weight": "bold", fill: "#dc2626"
});
wt.textContent = "4mm";
svg.appendChild(wt);
}
const peG = svgEl("g", { class: "node-group", "data-id": `pe${pe.idx}`, "data-type": "pe" });
peG.appendChild(svgEl("rect", {
x: peX, y: peY, width: PEW, height: PEH,
rx: 6, fill: "#7f1d1d", stroke: "#ef4444", "stroke-width": 1.5
}));
const pt = svgEl("text", {
x: routerX, y: peY + 17, "text-anchor": "middle",
"font-family": "monospace", "font-size": "9", "font-weight": "bold", fill: "#fecaca"
});
pt.textContent = `PE${pe.idx}`;
peG.appendChild(pt);
const ps = svgEl("text", {
x: routerX, y: peY + 30, "text-anchor": "middle",
"font-family": "monospace", "font-size": "7", fill: "#fecaca88"
});
ps.textContent = `${pe.corner} · ${pe.wire}`;
peG.appendChild(ps);
peG.addEventListener("click", (e) => { e.stopPropagation(); onPeClick(cubeIdx, pe.idx); });
svg.appendChild(peG);
}
// ── UCIe-N (horizontal, above mesh) ──
// Reference: rect at (210+OX, -108+OY) → but we use OX-relative coords inside the grid
const ucieNx = OX + 210, ucieNy = OY - 108, ucieNw = 280, ucieNh = 26;
svg.appendChild(svgEl("rect", {
x: ucieNx, y: ucieNy, width: ucieNw, height: ucieNh,
rx: 5, fill: "#1e1b4b", stroke: "#8b5cf6", "stroke-width": 1.5
}));
bt = svgEl("text", {
x: ucieNx + 30, y: ucieNy + 14,
"font-family": "monospace", "font-size": "8", "font-weight": "bold", fill: "#8b5cf6"
});
bt.textContent = "UCIe-N";
svg.appendChild(bt);
// c0-c3 sub-rectangles
const ucieNCx = [OX + 310, OX + 348, OX + 396, OX + 440];
for (let ci = 0; ci < 4; ci++) {
svg.appendChild(svgEl("rect", {
x: ucieNCx[ci], y: ucieNy + 4, width: 32, height: 16,
rx: 2, fill: "#8b5cf6", stroke: "#6d28d9", "stroke-width": 1
}));
const ct = svgEl("text", {
x: ucieNCx[ci] + 16, y: ucieNy + 15, "text-anchor": "middle",
"font-family": "monospace", "font-size": "6", fill: "white"
});
ct.textContent = `c${ci}`;
svg.appendChild(ct);
}
// N connection lines (90-degree paths)
const ucieNConns = [
{ cx: ucieNCx[0] + 16, routerX: OX, routerY: OY, vy: OY - 50 },
{ cx: ucieNCx[1] + 16, routerX: OX+140, routerY: OY, vy: OY - 44 },
{ cx: ucieNCx[2] + 16, routerX: OX+560, routerY: OY, vy: OY - 44 },
{ cx: ucieNCx[3] + 16, routerX: OX+700, routerY: OY, vy: OY - 50 },
];
for (const nc of ucieNConns) {
svg.appendChild(svgEl("path", {
d: `M ${nc.cx} ${ucieNy + ucieNh} V ${nc.vy} H ${nc.routerX} V ${nc.routerY - 16}`,
fill: "none", stroke: "#8b5cf6", "stroke-width": 1.2, "stroke-dasharray": "4,3"
}));
}
// ── UCIe-S (horizontal, below mesh) ──
const ucieSx = OX + 210, ucieSy = OY + 636, ucieSw = 280, ucieSh = 26;
svg.appendChild(svgEl("rect", {
x: ucieSx, y: ucieSy, width: ucieSw, height: ucieSh,
rx: 5, fill: "#1e1b4b", stroke: "#8b5cf6", "stroke-width": 1.5
}));
bt = svgEl("text", {
x: ucieSx + 30, y: ucieSy + 14,
"font-family": "monospace", "font-size": "8", "font-weight": "bold", fill: "#8b5cf6"
});
bt.textContent = "UCIe-S";
svg.appendChild(bt);
const ucieSCx = [OX + 310, OX + 348, OX + 396, OX + 440];
for (let ci = 0; ci < 4; ci++) {
svg.appendChild(svgEl("rect", {
x: ucieSCx[ci], y: ucieSy + 4, width: 32, height: 16,
rx: 2, fill: "#8b5cf6", stroke: "#6d28d9", "stroke-width": 1
}));
const ct = svgEl("text", {
x: ucieSCx[ci] + 16, y: ucieSy + 15, "text-anchor": "middle",
"font-family": "monospace", "font-size": "6", fill: "white"
});
ct.textContent = `c${ci}`;
svg.appendChild(ct);
}
// S connection lines
const ucieSConns = [
{ cx: ucieSCx[0] + 16, routerX: OX, routerY: OY+520, vy: OY + 572 },
{ cx: ucieSCx[1] + 16, routerX: OX+140, routerY: OY+520, vy: OY + 566 },
{ cx: ucieSCx[2] + 16, routerX: OX+560, routerY: OY+520, vy: OY + 566 },
{ cx: ucieSCx[3] + 16, routerX: OX+700, routerY: OY+520, vy: OY + 572 },
];
for (const sc of ucieSConns) {
svg.appendChild(svgEl("path", {
d: `M ${sc.cx} ${ucieSy} V ${sc.vy} H ${sc.routerX} V ${sc.routerY + 16}`,
fill: "none", stroke: "#8b5cf6", "stroke-width": 1.2, "stroke-dasharray": "4,3"
}));
}
// ── UCIe-E (vertical, right side) ──
const ucieEx = OX + 762, ucieEy = OY + 225, ucieEw = 52, ucieEh = 110;
svg.appendChild(svgEl("rect", {
x: ucieEx, y: ucieEy, width: ucieEw, height: ucieEh,
rx: 5, fill: "#1e1b4b", stroke: "#8b5cf6", "stroke-width": 1.5
}));
bt = svgEl("text", {
x: ucieEx + ucieEw / 2, y: ucieEy + 17, "text-anchor": "middle",
"font-family": "monospace", "font-size": "8", "font-weight": "bold", fill: "#8b5cf6"
});
bt.textContent = "UCIe-E";
svg.appendChild(bt);
const ucieECy = [ucieEy + 25, ucieEy + 43, ucieEy + 61, ucieEy + 79];
for (let ci = 0; ci < 4; ci++) {
svg.appendChild(svgEl("rect", {
x: ucieEx + 6, y: ucieECy[ci], width: 40, height: 14,
rx: 2, fill: "#8b5cf6", stroke: "#6d28d9", "stroke-width": 1
}));
const ct = svgEl("text", {
x: ucieEx + 26, y: ucieECy[ci] + 10, "text-anchor": "middle",
"font-family": "monospace", "font-size": "6", fill: "white"
});
ct.textContent = `c${ci}`;
svg.appendChild(ct);
}
// E connection lines (90-degree paths via vertical tracks)
const ucieEConns = [
{ cy: ucieECy[0] + 7, routerX: OX+700, routerY: OY, vx: OX + 742 },
{ cy: ucieECy[1] + 7, routerX: OX+700, routerY: OY+95, vx: OX + 730 },
{ cy: ucieECy[2] + 7, routerX: OX+700, routerY: OY+425,vx: OX + 730 },
{ cy: ucieECy[3] + 7, routerX: OX+700, routerY: OY+520,vx: OX + 742 },
];
for (const ec of ucieEConns) {
svg.appendChild(svgEl("path", {
d: `M ${ucieEx} ${ec.cy} H ${ec.vx} V ${ec.routerY} H ${ec.routerX + 16}`,
fill: "none", stroke: "#8b5cf6", "stroke-width": 1.2, "stroke-dasharray": "4,3"
}));
}
// ── UCIe-W (vertical, left side) ──
const ucieWx = OX - 124, ucieWy = OY + 225, ucieWw = 52, ucieWh = 110;
svg.appendChild(svgEl("rect", {
x: ucieWx, y: ucieWy, width: ucieWw, height: ucieWh,
rx: 5, fill: "#1e1b4b", stroke: "#8b5cf6", "stroke-width": 1.5
}));
bt = svgEl("text", {
x: ucieWx + ucieWw / 2, y: ucieWy + 17, "text-anchor": "middle",
"font-family": "monospace", "font-size": "8", "font-weight": "bold", fill: "#8b5cf6"
});
bt.textContent = "UCIe-W";
svg.appendChild(bt);
const ucieWCy = [ucieWy + 25, ucieWy + 43, ucieWy + 61, ucieWy + 79];
for (let ci = 0; ci < 4; ci++) {
svg.appendChild(svgEl("rect", {
x: ucieWx + 6, y: ucieWCy[ci], width: 40, height: 14,
rx: 2, fill: "#8b5cf6", stroke: "#6d28d9", "stroke-width": 1
}));
const ct = svgEl("text", {
x: ucieWx + 26, y: ucieWCy[ci] + 10, "text-anchor": "middle",
"font-family": "monospace", "font-size": "6", fill: "white"
});
ct.textContent = `c${ci}`;
svg.appendChild(ct);
}
// W connection lines (90-degree paths via vertical tracks)
const ucieWConns = [
{ cy: ucieWCy[0] + 7, routerX: OX, routerY: OY, vx: OX - 42 },
{ cy: ucieWCy[1] + 7, routerX: OX, routerY: OY+95, vx: OX - 30 },
{ cy: ucieWCy[2] + 7, routerX: OX, routerY: OY+425,vx: OX - 30 },
{ cy: ucieWCy[3] + 7, routerX: OX, routerY: OY+520,vx: OX - 42 },
];
for (const wc of ucieWConns) {
svg.appendChild(svgEl("path", {
d: `M ${ucieWx + ucieWw} ${wc.cy} H ${wc.vx} V ${wc.routerY} H ${wc.routerX - 16}`,
fill: "none", stroke: "#8b5cf6", "stroke-width": 1.2, "stroke-dasharray": "4,3"
}));
}
// ── Legend ──
const legY = H - 50;
const legItems = [
{ color: "#3b82f6", label: "PE Router" },
{ color: "#e2e8f0", label: "Relay", textColor: "#475569" },
{ color: "#8b5cf6", label: "UCIe Router" },
{ color: "#f59e0b", label: "M_CPU/SRAM" },
{ color: "#059669", label: "HBM Link", type: "line" },
{ color: "#047857", label: "HBM Ctrl", type: "rect" },
{ color: "#ef4444", label: "PE (~5mm2)", type: "rect" },
{ color: "#8b5cf6", label: "UCIe Port", type: "rect", rectFill: "#1e1b4b" },
];
let legX = 60;
for (const li of legItems) {
if (li.type === "line") {
svg.appendChild(svgEl("line", {
x1: legX, y1: legY, x2: legX + 25, y2: legY,
stroke: li.color, "stroke-width": 2.5, "stroke-dasharray": "8,4"
}));
legX += 30;
} else if (li.type === "rect") {
svg.appendChild(svgEl("rect", {
x: legX, y: legY - 6, width: 20, height: 12, rx: 3,
fill: li.rectFill || li.color, stroke: li.color, "stroke-width": 1
}));
legX += 25;
} else {
svg.appendChild(svgEl("circle", { cx: legX + 7, cy: legY, r: 7, fill: li.color, stroke: li.color === "#e2e8f0" ? "#94a3b8" : li.color }));
legX += 20;
}
const lt = svgEl("text", {
x: legX, y: legY + 4, "font-family": "monospace", "font-size": "8", fill: "#94a3b8"
});
lt.textContent = li.label;
svg.appendChild(lt);
legX += li.label.length * 5.5 + 16;
}
// Data path description
const dpT = svgEl("text", {
x: 60, y: legY + 24, "font-family": "monospace", "font-size": "7", fill: "#64748b"
});
dpT.textContent = "Data: PE_DMA → Router Mesh → HBM_CTRL | All traffic routed through 6x6 mesh";
svg.appendChild(dpT);
}
// ── Draw PE Detail View (ADR-0014 compliant, NOC on left side) ──
function drawPeView(svg, cubeIdx, peIdx) {
svg.innerHTML = "";
const W = 750, H = 580;
svg.setAttribute("viewBox", `0 0 ${W} ${H}`);
svg.appendChild(svgEl("rect", { width: W, height: H, fill: "#0f172a" }));
// Title
const title = svgEl("text", {
x: W / 2, y: 24, "text-anchor": "middle",
"font-family": "monospace", "font-size": "14", "font-weight": "bold", fill: "#94a3b8"
});
title.textContent = `Cube ${cubeIdx} / PE ${peIdx} — Internal Architecture`;
svg.appendChild(title);
// ── Layout constants ──
const nocX = 70; // NOC column center x
const nocW = 60; // NOC column width
const peLeft = 170; // PE boundary left
const peRight = 700; // PE boundary right
const peTop = 50;
const peBot = 500;
const peCX = (peLeft + peRight) / 2; // PE center x
// ── PE boundary ──
svg.appendChild(svgEl("rect", {
x: peLeft, y: peTop, width: peRight - peLeft, height: peBot - peTop,
rx: 8, fill: "none", stroke: "#ef444444", "stroke-width": 1.5, "stroke-dasharray": "6,4"
}));
const bndLabel = svgEl("text", {
x: peLeft + 8, y: peTop + 14, "font-family": "monospace", "font-size": "7", fill: "#ef444466"
});
bndLabel.textContent = `PE${peIdx} boundary`;
svg.appendChild(bndLabel);
// ── NOC column (left side, vertical bar) ──
const nocTop = 80, nocBot = 460;
svg.appendChild(svgEl("rect", {
x: nocX - nocW/2, y: nocTop, width: nocW, height: nocBot - nocTop,
rx: 8, fill: "#1e293b", stroke: "#3b82f6", "stroke-width": 2
}));
const nocTitle = svgEl("text", {
x: nocX, y: nocTop + 18, "text-anchor": "middle",
"font-family": "monospace", "font-size": "10", "font-weight": "bold", fill: "#3b82f6"
});
nocTitle.textContent = "NOC";
svg.appendChild(nocTitle);
const nocSub = svgEl("text", {
x: nocX, y: nocTop + 30, "text-anchor": "middle",
"font-family": "monospace", "font-size": "7", fill: "#3b82f688"
});
nocSub.textContent = "6x6 mesh";
svg.appendChild(nocSub);
// NOC destinations (inside NOC column)
const nocDests = [
{ label: "HBM", sub: "ctrl", y: nocTop + 50, fill: "#059669", bg: "#052e16" },
{ label: "SRAM", sub: "128x4", y: nocTop + 86, fill: "#f59e0b", bg: "#3d2b1f" },
{ label: "UCIe", sub: "inter", y: nocTop + 122, fill: "#8b5cf6", bg: "#1e1b4b" },
{ label: "M_CPU", sub: "cmd", y: nocTop + 158, fill: "#f59e0b", bg: "#3d2b1f" },
];
for (const nd of nocDests) {
svg.appendChild(svgEl("rect", {
x: nocX - 24, y: nd.y, width: 48, height: 28,
rx: 3, fill: nd.bg, stroke: nd.fill, "stroke-width": 1
}));
const nt = svgEl("text", {
x: nocX, y: nd.y + 12, "text-anchor": "middle",
"font-family": "monospace", "font-size": "7", "font-weight": "bold", fill: nd.fill
});
nt.textContent = nd.label;
svg.appendChild(nt);
const ns = svgEl("text", {
x: nocX, y: nd.y + 23, "text-anchor": "middle",
"font-family": "monospace", "font-size": "6", fill: nd.fill + "88"
});
ns.textContent = nd.sub;
svg.appendChild(ns);
}
// ── PE components (inside PE boundary) ──
const comps = [
{ id: "pe_cpu", label: "PE_CPU", x: peCX - 60, y: 80, w: 120, h: 44, fill: "#3d2b1f", stroke: "#f97316", sub: "overhead: 2ns", sub2: "kernel dispatch" },
{ id: "pe_sched", label: "PE_SCHEDULER", x: peCX - 75, y: 158, w: 150, h: 48, fill: "#3d2b1f", stroke: "#f97316", sub: "overhead: 1ns", sub2: "tile scheduling" },
{ id: "pe_dma", label: "PE_DMA", x: peLeft + 20, y: 250, w: 120, h: 58, fill: "#1f2d3d", stroke: "#06b6d4", sub: "RD:1 WR:1", sub2: "dual egress" },
{ id: "pe_gemm", label: "PE_GEMM", x: peCX - 60, y: 250, w: 120, h: 58, fill: "#2d1f3d", stroke: "#a855f7", sub: "8 TFLOPS F16", sub2: "shared accel_slot" },
{ id: "pe_math", label: "PE_MATH", x: peRight - 140, y: 250, w: 120, h: 58, fill: "#2d1f3d", stroke: "#a855f7", sub: "element-wise", sub2: "shared accel_slot" },
{ id: "pe_tcm", label: "PE_TCM", x: peCX - 100, y: 360, w: 200, h: 58, fill: "#052e16", stroke: "#22c55e", sub: "16 MB SRAM", sub2: "scheduler_reserved + allocatable" },
];
const compMap = {};
for (const c of comps) {
compMap[c.id] = { cx: c.x + c.w / 2, cy: c.y + c.h / 2, bottom: c.y + c.h, top: c.y, left: c.x, right: c.x + c.w, ...c };
}
// ── Internal connections (behind nodes) ──
const internalConns = [
{ from: "pe_cpu", to: "pe_sched", label: "0.5mm" },
{ from: "pe_sched", to: "pe_dma", label: "0.5mm" },
{ from: "pe_sched", to: "pe_gemm", label: "0.5mm" },
{ from: "pe_sched", to: "pe_math", label: "0.5mm" },
{ from: "pe_dma", to: "pe_tcm", label: "512 GB/s" },
{ from: "pe_gemm", to: "pe_tcm", label: "512 GB/s" },
{ from: "pe_math", to: "pe_tcm", label: "512 GB/s" },
];
for (const conn of internalConns) {
const from = compMap[conn.from];
const to = compMap[conn.to];
svg.appendChild(svgEl("line", {
x1: from.cx, y1: from.bottom, x2: to.cx, y2: to.top,
stroke: "#47556988", "stroke-width": 1.5, class: "link-line",
"data-link": `${conn.from}-${conn.to}`
}));
if (conn.label.includes("GB")) {
const midX = (from.cx + to.cx) / 2 + 8;
const midY = (from.bottom + to.top) / 2;
const lt = svgEl("text", {
x: midX, y: midY, "font-family": "monospace", "font-size": "7", fill: "#47569988"
});
lt.textContent = conn.label;
svg.appendChild(lt);
}
}
// ── Draw component blocks ──
for (const c of comps) {
const g = svgEl("g", { class: "node-group", "data-id": c.id });
g.appendChild(svgEl("rect", {
x: c.x, y: c.y, width: c.w, height: c.h,
rx: 6, fill: c.fill, stroke: c.stroke, "stroke-width": 1.5
}));
const t = svgEl("text", {
x: c.x + c.w / 2, y: c.y + 17, "text-anchor": "middle",
"font-family": "monospace", "font-size": "10", "font-weight": "bold", fill: c.stroke
});
t.textContent = c.label;
g.appendChild(t);
const sub = svgEl("text", {
x: c.x + c.w / 2, y: c.y + 30, "text-anchor": "middle",
"font-family": "monospace", "font-size": "7", fill: c.stroke + "88"
});
sub.textContent = c.sub;
g.appendChild(sub);
if (c.sub2) {
const sub2 = svgEl("text", {
x: c.x + c.w / 2, y: c.y + 42, "text-anchor": "middle",
"font-family": "monospace", "font-size": "6", fill: c.stroke + "55"
});
sub2.textContent = c.sub2;
g.appendChild(sub2);
}
svg.appendChild(g);
}
// ── NOC → PE_CPU: command path (horizontal, orange dashed) ──
const cpuComp = compMap["pe_cpu"];
const cmdY = cpuComp.cy;
svg.appendChild(svgEl("line", {
x1: nocX + nocW/2, y1: cmdY, x2: cpuComp.left, y2: cmdY,
stroke: "#f97316", "stroke-width": 2, "stroke-dasharray": "5,3",
"data-link": "noc-pe_cpu"
}));
// Arrow head
svg.appendChild(svgEl("path", {
d: `M ${cpuComp.left} ${cmdY} l -8 -4 v 8 z`,
fill: "#f97316"
}));
const cmdLabel = svgEl("text", {
x: (nocX + nocW/2 + cpuComp.left) / 2, y: cmdY - 6,
"text-anchor": "middle", "font-family": "monospace", "font-size": "7", fill: "#f97316aa"
});
cmdLabel.textContent = "cmd (M_CPU)";
svg.appendChild(cmdLabel);
// ── NOC → PE_DMA: data path (horizontal, cyan dashed) ──
const dmaComp = compMap["pe_dma"];
const dataY = dmaComp.cy;
svg.appendChild(svgEl("line", {
x1: nocX + nocW/2, y1: dataY, x2: dmaComp.left, y2: dataY,
stroke: "#06b6d4", "stroke-width": 2, "stroke-dasharray": "5,3",
"data-link": "noc-pe_dma"
}));
// Bidirectional arrows
svg.appendChild(svgEl("path", {
d: `M ${dmaComp.left} ${dataY} l -8 -4 v 8 z`,
fill: "#06b6d4"
}));
svg.appendChild(svgEl("path", {
d: `M ${nocX + nocW/2} ${dataY} l 8 -4 v 8 z`,
fill: "#06b6d4"
}));
const dataLabel = svgEl("text", {
x: (nocX + nocW/2 + dmaComp.left) / 2, y: dataY - 6,
"text-anchor": "middle", "font-family": "monospace", "font-size": "7", fill: "#06b6d4aa"
});
dataLabel.textContent = "data (256 GB/s)";
svg.appendChild(dataLabel);
// ── Legend at bottom ──
const legY = H - 28;
const legends = [
{ color: "#06b6d4", label: "Data path (PE_DMA ↔ NOC)" },
{ color: "#f97316", label: "Cmd path (M_CPU → NOC → PE_CPU)" },
{ color: "#a855f7", label: "Compute (shared accel_slot)" },
{ color: "#22c55e", label: "TCM (staging memory)" },
];
let lx = 60;
for (const lg of legends) {
svg.appendChild(svgEl("rect", {
x: lx, y: legY - 4, width: 12, height: 8, rx: 2, fill: lg.color
}));
const lt = svgEl("text", {
x: lx + 16, y: legY + 3, "font-family": "monospace", "font-size": "7", fill: "#94a3b8"
});
lt.textContent = lg.label;
svg.appendChild(lt);
lx += lg.label.length * 4.5 + 28;
}
}
// ── Navigation ──
function onCubeClick(cubeIdx) {
currentView = "cube";
updateViewTabs();
updateBreadcrumb(`SIP 0`, `Cube ${cubeIdx}`);
drawCubeView(document.getElementById("topoSvg"), cubeIdx);
updateSelection({ type: "cube", id: cubeIdx });
}
function onPeClick(cubeIdx, peIdx) {
currentView = "pe";
updateViewTabs();
updateBreadcrumb(`SIP 0`, `Cube ${cubeIdx}`, `PE ${peIdx}`);
drawPeView(document.getElementById("topoSvg"), cubeIdx, peIdx);
updateSelection({ type: "pe", id: peIdx, cube: cubeIdx });
}
function updateViewTabs() {
document.querySelectorAll(".view-tab").forEach(t => {
t.classList.toggle("active", t.dataset.view === currentView);
});
}
function updateBreadcrumb(...parts) {
const bc = document.getElementById("breadcrumb");
bc.innerHTML = "";
parts.forEach((p, i) => {
if (i > 0) {
const sep = document.createElement("span");
sep.className = "sep";
sep.textContent = ">";
bc.appendChild(sep);
}
const span = document.createElement("span");
span.textContent = p;
span.addEventListener("click", () => {
if (i === 0) {
currentView = "sip";
updateViewTabs();
updateBreadcrumb("SIP 0");
drawSipView(document.getElementById("topoSvg"));
updateSelection(null);
}
});
bc.appendChild(span);
});
}
function updateSelection(sel) {
const div = document.getElementById("selInfo");
if (!sel) {
div.innerHTML = '<div class="kv"><span class="key">Click a component</span></div>';
return;
}
let html = "";
if (sel.type === "cube") {
html = `
<div class="kv"><span class="key">Type</span><span class="val">Cube</span></div>
<div class="kv"><span class="key">ID</span><span class="val">C${sel.id}</span></div>
<div class="kv"><span class="key">PEs</span><span class="val">${PE_PER_CUBE}</span></div>
<div class="kv"><span class="key">HBM</span><span class="val">48 GB</span></div>
<div class="kv"><span class="key">HBM BW</span><span class="val">1024 GB/s</span></div>
<div class="kv"><span class="key">UCIe ports</span><span class="val">4 (N/S/E/W)</span></div>
`;
} else if (sel.type === "pe") {
html = `
<div class="kv"><span class="key">Type</span><span class="val">PE</span></div>
<div class="kv"><span class="key">ID</span><span class="val">PE${sel.id}</span></div>
<div class="kv"><span class="key">Cube</span><span class="val">C${sel.cube}</span></div>
<div class="kv"><span class="key">TCM</span><span class="val">16 MB</span></div>
<div class="kv"><span class="key">GEMM</span><span class="val">8 TFLOPS</span></div>
<div class="kv"><span class="key">DMA</span><span class="val">RD:1 WR:1</span></div>
`;
}
div.innerHTML = html;
}
// ── Tab click handlers ──
document.querySelectorAll(".view-tab").forEach(tab => {
tab.addEventListener("click", () => {
const view = tab.dataset.view;
if (view === "sip") {
currentView = "sip";
updateViewTabs();
updateBreadcrumb("SIP 0");
drawSipView(document.getElementById("topoSvg"));
updateSelection(null);
}
// cube and pe views require selecting a specific cube/pe
});
});
// ══════════════════════════════════════════════════════════
// Phase C: Replay Engine — Hot Path Visualization
// ══════════════════════════════════════════════════════════
const timeSlider = document.getElementById("timeSlider");
const timeLabel = document.getElementById("timeLabel");
const timeEnd = document.getElementById("timeEnd");
const playBtn = document.getElementById("playBtn");
const speedBtn = document.getElementById("speedBtn");
const eventCountEl = document.getElementById("eventCount");
const simDurationEl = document.getElementById("simDuration");
const inflightEl = document.getElementById("inflight");
// ── Replay state ──
let allEvents = [];
let durationNs = 0;
let currentTimeNs = 0;
let playing = false;
let speed = 1;
let lastFrameTs = 0;
let packetDots = []; // active animated dots on SVG
// ── Color ramp for utilization ──
const UTIL_COLORS = {
idle: "#475569",
low: "#3b82f6",
med: "#22c55e",
high: "#eab308",
sat: "#ef4444",
hot: "#f97316",
};
// ── Workload selector ──
const workloadSelect = document.getElementById("workloadSelect");
async function loadWorkloadList() {
try {
const resp = await fetch("/api/workloads");
const workloads = await resp.json();
workloadSelect.innerHTML = "";
// Group by category
const categories = {};
for (const w of workloads) {
const cat = w.category || "other";
if (!categories[cat]) categories[cat] = [];
categories[cat].push(w);
}
const catLabels = { probe: "Probe Cases", bench: "Benchmarks", other: "Other" };
for (const [cat, items] of Object.entries(categories)) {
const grp = document.createElement("optgroup");
grp.label = catLabels[cat] || cat;
for (const w of items) {
const opt = document.createElement("option");
opt.value = w.id;
opt.textContent = w.name;
opt.title = w.description;
grp.appendChild(opt);
}
workloadSelect.appendChild(grp);
}
// Load first workload
if (workloads.length > 0) {
workloadSelect.value = workloads[0].id;
await loadWorkloadEvents(workloads[0].id);
}
} catch (e) {
workloadSelect.innerHTML = '<option value="" disabled selected>Start server: kernbench web</option>';
console.warn("Could not load workloads:", e.message);
}
}
async function loadWorkloadEvents(workloadId) {
try {
const resp = await fetch(`/api/events/${encodeURIComponent(workloadId)}`);
allEvents = await resp.json();
durationNs = allEvents.length ? Math.max(...allEvents.map(e => e.t_ns)) : 0;
currentTimeNs = 0;
playing = false;
playBtn.innerHTML = "&#9654;";
timeSlider.max = durationNs.toFixed(1);
timeSlider.value = 0;
timeLabel.textContent = "t = 0.0 ns";
timeEnd.textContent = `${durationNs.toFixed(1)} ns`;
eventCountEl.textContent = allEvents.length;
simDurationEl.textContent = `${durationNs.toFixed(1)} ns`;
inflightEl.textContent = "0";
clearHotPaths(document.getElementById("topoSvg"));
console.log(`Loaded workload "${workloadId}": ${allEvents.length} events, ${durationNs.toFixed(1)} ns`);
} catch (e) {
console.warn("Could not load workload events:", e.message);
allEvents = [];
durationNs = 0;
}
}
workloadSelect.addEventListener("change", () => {
loadWorkloadEvents(workloadSelect.value);
});
// ── Extract cube index from node_id ──
function extractCubeIdx(nodeId) {
const m = nodeId.match(/cube(\d+)/);
return m ? parseInt(m[1]) : -1;
}
// ── Get events active at time t ──
function getActiveEvents(t, windowNs = 20) {
// Events are "active" if they started within [t - windowNs, t]
return allEvents.filter(e => e.t_ns <= t && e.t_ns >= t - windowNs);
}
// ── Get hop events that cross between two cubes ──
function getActiveSipHops(t, windowNs = 30) {
return allEvents.filter(e =>
e.type === "hop" &&
e.t_ns <= t && e.t_ns >= t - windowNs &&
e.src && e.dst
);
}
// ── Map hop event to SIP link ID ──
function hopToSipLink(hop) {
const srcCube = extractCubeIdx(hop.src);
const dstCube = extractCubeIdx(hop.dst);
if (srcCube < 0 || dstCube < 0 || srcCube === dstCube) return null;
const lo = Math.min(srcCube, dstCube);
const hi = Math.max(srcCube, dstCube);
return `cube${lo}-cube${hi}`;
}
// ── Map hop event to CUBE view link ID (router mesh) ──
function hopToCubeLink(hop) {
// Try to extract router positions from node_ids like "sip0.cube5.noc.r4c0"
const srcMatch = hop.src.match(/r(\d+)c(\d+)/);
const dstMatch = hop.dst.match(/r(\d+)c(\d+)/);
if (srcMatch && dstMatch) {
const [sr, sc] = [parseInt(srcMatch[1]), parseInt(srcMatch[2])];
const [dr, dc] = [parseInt(dstMatch[1]), parseInt(dstMatch[2])];
const loR = Math.min(sr, dr), loC = Math.min(sc, dc);
const hiR = Math.max(sr, dr), hiC = Math.max(sc, dc);
if (sr === dr) return `r${sr}c${loC}-r${sr}c${hiC}`;
if (sc === dc) return `r${loR}c${sc}-r${hiR}c${sc}`;
}
return null;
}
// ── Map hop/process to PE view link ID ──
function eventToPeLink(ev) {
if (ev.type === "hop") {
// DMA ↔ NOC
if (ev.src.includes("pe_dma") || ev.dst.includes("pe_dma")) return "noc-pe_dma";
// M_CPU → PE_CPU
if (ev.dst.includes("pe_cpu")) return "noc-pe_cpu";
}
if (ev.type === "process") {
const comp = ev.component || "";
if (comp.includes("pe_gemm")) return "pe_sched-pe_gemm";
if (comp.includes("pe_scheduler")) return "pe_cpu-pe_sched";
if (comp.includes("pe_dma")) return "pe_sched-pe_dma";
if (comp.includes("pe_cpu")) return "noc-pe_cpu";
}
return null;
}
// ── Utilization color from bytes/traffic ──
function utilColor(nbytes) {
if (nbytes <= 0) return UTIL_COLORS.idle;
if (nbytes < 1024) return UTIL_COLORS.low;
if (nbytes < 16384) return UTIL_COLORS.med;
if (nbytes < 65536) return UTIL_COLORS.high;
return UTIL_COLORS.sat;
}
// ── Clear all hot path visual effects ──
function clearHotPaths(svg) {
// Remove packet dots
svg.querySelectorAll(".packet-dot").forEach(d => d.remove());
packetDots = [];
// Reset link colors
svg.querySelectorAll(".link-line[data-link]").forEach(el => {
el.classList.remove("hot-path");
// Restore original stroke based on view
if (currentView === "sip") {
el.style.stroke = "";
el.style.strokeWidth = "";
} else if (currentView === "cube") {
el.style.stroke = "";
el.style.strokeWidth = "";
} else {
el.style.stroke = "";
el.style.strokeWidth = "";
}
});
// Reset component highlights
svg.querySelectorAll(".node-group[data-id]").forEach(g => {
g.classList.remove("active-component");
const rect = g.querySelector("rect");
if (rect) rect.style.filter = "";
});
}
// ── Apply hot path visualization at current time ──
function applyHotPaths(svg, t) {
clearHotPaths(svg);
if (!allEvents.length || durationNs <= 0) return;
const activeHops = getActiveSipHops(t, 30);
let inflight = 0;
if (currentView === "sip") {
// ── SIP VIEW: color inter-cube links by traffic ──
const linkTraffic = {}; // linkId → total bytes
for (const hop of activeHops) {
const linkId = hopToSipLink(hop);
if (linkId) {
linkTraffic[linkId] = (linkTraffic[linkId] || 0) + (hop.nbytes || 0);
}
}
for (const [linkId, bytes] of Object.entries(linkTraffic)) {
const el = svg.querySelector(`[data-link="${linkId}"]`);
if (el) {
el.style.stroke = utilColor(bytes);
el.style.strokeWidth = "3";
el.classList.add("hot-path");
inflight++;
}
}
// Add packet dots on active links
for (const hop of activeHops) {
if (hop.type !== "hop") continue;
const linkId = hopToSipLink(hop);
if (!linkId) continue;
const el = svg.querySelector(`[data-link="${linkId}"]`);
if (!el) continue;
// Calculate progress within the hop
const hopAge = t - hop.t_ns;
const hopDuration = hop.latency_ns || 1;
const progress = Math.min(1, Math.max(0, hopAge / hopDuration));
const x1 = parseFloat(el.getAttribute("x1"));
const y1 = parseFloat(el.getAttribute("y1"));
const x2 = parseFloat(el.getAttribute("x2"));
const y2 = parseFloat(el.getAttribute("y2"));
const dotX = x1 + (x2 - x1) * progress;
const dotY = y1 + (y2 - y1) * progress;
const dot = svgEl("circle", {
cx: dotX, cy: dotY, r: 4,
fill: "#eab308", class: "packet-dot", opacity: 0.9
});
svg.appendChild(dot);
packetDots.push(dot);
}
} else if (currentView === "cube") {
// ── CUBE VIEW: highlight router mesh links ──
const linkTraffic = {};
for (const hop of activeHops) {
const linkId = hopToCubeLink(hop);
if (linkId) {
linkTraffic[linkId] = (linkTraffic[linkId] || 0) + (hop.nbytes || 0);
}
}
for (const [linkId, bytes] of Object.entries(linkTraffic)) {
const el = svg.querySelector(`[data-link="${linkId}"]`);
if (el) {
el.style.stroke = utilColor(bytes);
el.style.strokeWidth = "3";
el.classList.add("hot-path");
inflight++;
}
}
// Highlight HBM component referenced in events
const activeProcesses = allEvents.filter(e =>
e.type === "process" && e.t_ns <= t && e.t_ns >= t - 30
);
for (const proc of activeProcesses) {
const comp = proc.component || "";
if (comp.includes("hbm_ctrl")) highlightComponent(svg, "hbm_ctrl");
}
} else if (currentView === "pe") {
// ── PE VIEW: highlight data/command paths ──
const activeEvts = getActiveEvents(t, 30);
const activeLinks = new Set();
for (const ev of activeEvts) {
const linkId = eventToPeLink(ev);
if (linkId) activeLinks.add(linkId);
}
for (const linkId of activeLinks) {
const el = svg.querySelector(`[data-link="${linkId}"]`);
if (el) {
el.style.strokeWidth = "3.5";
el.style.opacity = "1";
el.classList.add("hot-path");
inflight++;
}
}
// Highlight active PE components
const activeProcs = allEvents.filter(e =>
e.type === "process" && e.t_ns <= t && e.t_ns >= t - 20
);
for (const proc of activeProcs) {
const comp = proc.component || "";
for (const name of ["pe_cpu", "pe_scheduler", "pe_dma", "pe_gemm", "pe_math", "pe_tcm"]) {
if (comp.includes(name)) highlightComponent(svg, name.replace("pe_scheduler", "pe_sched"));
}
}
}
inflightEl.textContent = inflight;
}
function highlightComponent(svg, dataId) {
const g = svg.querySelector(`[data-id="${dataId}"]`);
if (g) {
g.classList.add("active-component");
const rect = g.querySelector("rect");
if (rect) rect.style.filter = "brightness(1.5) drop-shadow(0 0 6px currentColor)";
}
}
// ── Animation loop ──
function animate(ts) {
if (playing && durationNs > 0) {
if (lastFrameTs === 0) lastFrameTs = ts;
const dtMs = ts - lastFrameTs;
lastFrameTs = ts;
// Advance simulation time: dtMs * speed → ns
// 1ms real = speed ns sim (adjustable)
currentTimeNs += dtMs * speed * 0.5; // 0.5 factor for comfortable viewing
if (currentTimeNs > durationNs) {
currentTimeNs = durationNs;
playing = false;
playBtn.innerHTML = "&#9654;";
lastFrameTs = 0;
}
timeSlider.value = currentTimeNs.toFixed(1);
timeLabel.textContent = `t = ${currentTimeNs.toFixed(1)} ns`;
applyHotPaths(document.getElementById("topoSvg"), currentTimeNs);
} else {
lastFrameTs = 0;
}
requestAnimationFrame(animate);
}
// ── Timeline controls ──
timeSlider.addEventListener("input", () => {
currentTimeNs = parseFloat(timeSlider.value);
timeLabel.textContent = `t = ${currentTimeNs.toFixed(1)} ns`;
applyHotPaths(document.getElementById("topoSvg"), currentTimeNs);
});
playBtn.addEventListener("click", () => {
playing = !playing;
playBtn.innerHTML = playing ? "&#9646;&#9646;" : "&#9654;";
if (playing) lastFrameTs = 0;
});
speedBtn.addEventListener("click", () => {
const speeds = [0.5, 1, 2, 4, 10];
const idx = speeds.indexOf(speed);
speed = speeds[(idx + 1) % speeds.length];
speedBtn.textContent = `${speed}x`;
});
// ── Tooltip ──
const tooltip = document.getElementById("tooltip");
document.getElementById("canvasArea").addEventListener("mousemove", (e) => {
const target = e.target.closest(".node-group");
if (target) {
const id = target.dataset.id || "";
const type = target.dataset.type || "";
let html = `<div class="tt-title">${id || type}</div>`;
if (type === "cube") {
html += '<div class="tt-row"><span>PEs</span><span class="tt-val">8</span></div>';
html += '<div class="tt-row"><span>HBM</span><span class="tt-val">48 GB</span></div>';
html += '<div class="tt-row"><span>Click to drill down</span></div>';
}
tooltip.innerHTML = html;
tooltip.style.display = "block";
tooltip.style.left = (e.offsetX + 12) + "px";
tooltip.style.top = (e.offsetY - 10) + "px";
} else {
tooltip.style.display = "none";
}
});
// ── WebSocket client (optional live streaming) ──
let ws = null;
function connectWebSocket() {
const proto = location.protocol === "https:" ? "wss:" : "ws:";
const url = `${proto}//${location.host}/ws`;
try {
ws = new WebSocket(url);
ws.onopen = () => {
console.log("WebSocket connected");
// Can request live streaming: ws.send(JSON.stringify({cmd: "stream_demo", speed: 1}));
};
ws.onmessage = (msg) => {
const data = JSON.parse(msg.data);
if (data.type === "events") {
// Batch load
allEvents = data.events;
durationNs = allEvents.length ? Math.max(...allEvents.map(e => e.t_ns)) : 0;
timeSlider.max = durationNs.toFixed(1);
timeEnd.textContent = `${durationNs.toFixed(1)} ns`;
eventCountEl.textContent = allEvents.length;
simDurationEl.textContent = `${durationNs.toFixed(1)} ns`;
} else if (data.type === "event") {
// Streaming: append single event
allEvents.push(data);
durationNs = Math.max(durationNs, data.t_ns);
timeSlider.max = durationNs.toFixed(1);
eventCountEl.textContent = allEvents.length;
}
};
ws.onclose = () => console.log("WebSocket disconnected");
ws.onerror = () => {}; // Silently handle WS errors
} catch (e) {
// WebSocket not available (static file mode)
}
}
// ── Init ──
drawSipView(document.getElementById("topoSvg"));
loadWorkloadList();
connectWebSocket();
requestAnimationFrame(animate);
</script>
</body>
</html>