OR-1 dataflow CPU sketch
at main 503 lines 15 kB view raw
1import cytoscape from "cytoscape"; 2import elk from "cytoscape-elk"; 3import svg from "cytoscape-svg"; 4import type { GraphUpdate, GraphNode, GraphEdge, GraphRegion } from "@common/types"; 5import { stylesheet } from "./style"; 6import { logicalLayout, physicalLayout } from "@common/layout"; 7import { exportSvg, exportPng, copyPng } from "@common/export"; 8 9cytoscape.use(elk); 10cytoscape.use(svg); 11 12const ROUTING_CATEGORY = "routing"; 13 14const cy = cytoscape({ 15 container: document.getElementById("graph"), 16 style: stylesheet, 17 elements: [], 18 maxZoom: 2.0, 19 minZoom: 0.1, 20}); 21 22function buildLabel(node: GraphNode): string { 23 if (node.label) return node.label; 24 if (node.const !== null) { 25 return `${node.opcode}\n${node.const}`; 26 } 27 return node.opcode; 28} 29 30function buildPhysicalLabel(node: GraphNode): string { 31 if (node.label) return node.label; 32 const lines: string[] = []; 33 if (node.const !== null) { 34 lines.push(`${node.opcode}=${node.const}`); 35 } else { 36 lines.push(node.opcode); 37 } 38 if (node.iram_offset !== null) lines.push(`iram:${node.iram_offset}`); 39 if (node.ctx !== null) lines.push(`ctx:${node.ctx}`); 40 return lines.join("\n"); 41} 42 43function buildElements(update: GraphUpdate): cytoscape.ElementDefinition[] { 44 const elements: cytoscape.ElementDefinition[] = []; 45 const regionParents = new Map<string, string>(); 46 47 for (const region of update.regions) { 48 if (region.kind === "function") { 49 elements.push({ 50 data: { id: region.tag, label: region.tag }, 51 }); 52 for (const nodeId of region.node_ids) { 53 regionParents.set(nodeId, region.tag); 54 } 55 } 56 } 57 58 for (const node of update.nodes) { 59 const el: cytoscape.ElementDefinition = { 60 data: { 61 id: node.id, 62 label: buildLabel(node), 63 colour: node.colour, 64 category: node.category, 65 pe: node.pe, 66 iram_offset: node.iram_offset, 67 ctx: node.ctx, 68 }, 69 classes: node.has_error ? "error" : undefined, 70 }; 71 const parent = regionParents.get(node.id); 72 if (parent) { 73 el.data.parent = parent; 74 } 75 elements.push(el); 76 } 77 78 for (const edge of update.edges) { 79 const sourceNode = update.nodes.find((n) => n.id === edge.source); 80 let sourceLabel: string | undefined; 81 if (sourceNode && sourceNode.category === ROUTING_CATEGORY && edge.source_port) { 82 sourceLabel = edge.source_port === "L" ? "T" : "F"; 83 } 84 85 const edgeClasses = [ 86 edge.has_error ? "error" : undefined, 87 edge.synthetic ? "synthetic" : undefined, 88 ].filter(Boolean).join(" ") || undefined; 89 90 elements.push({ 91 data: { 92 id: `${edge.source}->${edge.target}:${edge.port}`, 93 source: edge.source, 94 target: edge.target, 95 targetLabel: edge.port, 96 sourceLabel: sourceLabel ?? "", 97 }, 98 classes: edgeClasses, 99 }); 100 } 101 102 return elements; 103} 104 105function buildPhysicalElements(update: GraphUpdate): cytoscape.ElementDefinition[] { 106 const elements: cytoscape.ElementDefinition[] = []; 107 108 // Build set of edge targets for seed const detection 109 const edgeTargets = new Set<string>(); 110 for (const edge of update.edges) { 111 edgeTargets.add(edge.target); 112 } 113 114 const seedConstIds = new Set<string>(); 115 const nodePeMap = new Map<string, number | null>(); 116 const peIds = new Set<number>(); 117 118 for (const node of update.nodes) { 119 nodePeMap.set(node.id, node.pe); 120 if (node.pe !== null) peIds.add(node.pe); 121 122 if (node.category === "config" && node.opcode === "const" && !edgeTargets.has(node.id)) { 123 seedConstIds.add(node.id); 124 } 125 } 126 127 // Create PE cluster parent nodes 128 for (const peId of peIds) { 129 elements.push({ 130 data: { id: `pe-${peId}`, label: `PE ${peId}` }, 131 classes: "pe-cluster", 132 }); 133 } 134 135 // Create operation nodes 136 for (const node of update.nodes) { 137 const isSeedConst = seedConstIds.has(node.id); 138 const el: cytoscape.ElementDefinition = { 139 data: { 140 id: node.id, 141 label: buildPhysicalLabel(node), 142 colour: node.colour, 143 category: node.category, 144 pe: node.pe, 145 iram_offset: node.iram_offset, 146 ctx: node.ctx, 147 }, 148 classes: 149 [isSeedConst ? "seed-const" : undefined, node.has_error ? "error" : undefined].filter(Boolean).join(" ") || 150 undefined, 151 }; 152 // Synthetic SM nodes float freely (not parented to any PE cluster) 153 if (!node.synthetic && !isSeedConst && node.pe !== null) { 154 el.data.parent = `pe-${node.pe}`; 155 } 156 elements.push(el); 157 } 158 159 // Scan cross-PE edges (excluding seed const sources) to find PE pairs 160 type PePair = { srcPe: number; tgtPe: number }; 161 const pePairKey = (src: number, tgt: number) => `${src}->${tgt}`; 162 const crossPePairs = new Map<string, PePair>(); 163 const busEdgeCounts = new Map<string, number>(); 164 165 for (const edge of update.edges) { 166 if (seedConstIds.has(edge.source)) continue; 167 const srcPe = nodePeMap.get(edge.source) ?? null; 168 const tgtPe = nodePeMap.get(edge.target) ?? null; 169 if (srcPe !== null && tgtPe !== null && srcPe !== tgtPe) { 170 const key = pePairKey(srcPe, tgtPe); 171 if (!crossPePairs.has(key)) { 172 crossPePairs.set(key, { srcPe, tgtPe }); 173 } 174 busEdgeCounts.set(key, (busEdgeCounts.get(key) ?? 0) + 1); 175 } 176 } 177 178 // Create port nodes for each directed PE pair 179 for (const [, pair] of crossPePairs) { 180 elements.push({ 181 data: { 182 id: `port-${pair.srcPe}-to-${pair.tgtPe}-exit`, 183 parent: `pe-${pair.srcPe}`, 184 }, 185 classes: "port-node", 186 }); 187 elements.push({ 188 data: { 189 id: `port-${pair.srcPe}-to-${pair.tgtPe}-entry`, 190 parent: `pe-${pair.tgtPe}`, 191 }, 192 classes: "port-node", 193 }); 194 } 195 196 // Create bus edges (one per PE pair) 197 for (const [key, pair] of crossPePairs) { 198 const count = busEdgeCounts.get(key) ?? 1; 199 elements.push({ 200 data: { 201 id: `bus-${pair.srcPe}-to-${pair.tgtPe}`, 202 source: `port-${pair.srcPe}-to-${pair.tgtPe}-exit`, 203 target: `port-${pair.srcPe}-to-${pair.tgtPe}-entry`, 204 label: count > 1 ? `×${count}` : "", 205 }, 206 classes: "physical bus-segment", 207 }); 208 } 209 210 // Create edges 211 for (const edge of update.edges) { 212 const srcPe = nodePeMap.get(edge.source) ?? null; 213 const tgtPe = nodePeMap.get(edge.target) ?? null; 214 const errorClass = edge.has_error ? " error" : ""; 215 const syntheticClass = edge.synthetic ? " synthetic" : ""; 216 217 if (edge.synthetic) { 218 // Synthetic edges (e.g., SM request/return) — never bundled into bus 219 elements.push({ 220 data: { 221 id: `${edge.source}->${edge.target}:${edge.port}`, 222 source: edge.source, 223 target: edge.target, 224 targetLabel: edge.port, 225 sourceLabel: "", 226 }, 227 classes: `physical synthetic${errorClass}`, 228 }); 229 } else if (seedConstIds.has(edge.source)) { 230 // Seed const: direct edge, never bundled 231 elements.push({ 232 data: { 233 id: `${edge.source}->${edge.target}:${edge.port}`, 234 source: edge.source, 235 target: edge.target, 236 targetLabel: edge.port, 237 sourceLabel: "", 238 }, 239 classes: `seed-edge physical${errorClass}`, 240 }); 241 } else if (srcPe !== null && tgtPe !== null && srcPe !== tgtPe) { 242 // Cross-PE: split into exit segment + entry segment (bus already created) 243 elements.push({ 244 data: { 245 id: `${edge.source}->exit-${srcPe}-to-${tgtPe}:${edge.port}`, 246 source: edge.source, 247 target: `port-${srcPe}-to-${tgtPe}-exit`, 248 sourceLabel: "", 249 targetLabel: "", 250 }, 251 classes: `physical exit-segment${errorClass}`, 252 }); 253 elements.push({ 254 data: { 255 id: `entry-${srcPe}-to-${tgtPe}->${edge.target}:${edge.port}`, 256 source: `port-${srcPe}-to-${tgtPe}-entry`, 257 target: edge.target, 258 targetLabel: edge.port, 259 }, 260 classes: `physical entry-segment${errorClass}`, 261 }); 262 } else { 263 // Intra-PE or unplaced 264 elements.push({ 265 data: { 266 id: `${edge.source}->${edge.target}:${edge.port}`, 267 source: edge.source, 268 target: edge.target, 269 targetLabel: edge.port, 270 }, 271 classes: `physical intra-pe${errorClass}`, 272 }); 273 } 274 } 275 276 return elements; 277} 278 279function getRequiredElement(id: string): HTMLElement { 280 const element = document.getElementById(id); 281 if (!element) throw new Error(`Required element with id "${id}" not found`); 282 return element; 283} 284 285function updateErrorPanel(update: GraphUpdate): void { 286 const panel = getRequiredElement("error-panel"); 287 const list = getRequiredElement("error-list"); 288 const count = getRequiredElement("error-count"); 289 const overlay = getRequiredElement("parse-error-overlay"); 290 291 // Handle parse error (AC5.5) 292 if (update.parse_error) { 293 overlay.textContent = update.parse_error; 294 overlay.classList.add("visible"); 295 panel.classList.remove("visible"); 296 return; 297 } 298 overlay.classList.remove("visible"); 299 300 // Handle pipeline errors 301 if (update.errors.length === 0) { 302 panel.classList.remove("visible"); 303 list.innerHTML = ""; 304 return; 305 } 306 307 count.textContent = `${update.errors.length} error${update.errors.length > 1 ? "s" : ""}`; 308 list.innerHTML = ""; 309 310 for (const error of update.errors) { 311 const li = document.createElement("li"); 312 li.className = "error-item"; 313 314 // Create line:column span 315 const lineSpan = document.createElement("span"); 316 lineSpan.className = "error-line"; 317 lineSpan.textContent = `L${error.line}:${error.column}`; 318 li.appendChild(lineSpan); 319 320 // Create category span 321 const categorySpan = document.createElement("span"); 322 categorySpan.className = "error-category"; 323 categorySpan.textContent = `[${error.category}]`; 324 li.appendChild(categorySpan); 325 326 // Create message text node (safe from XSS) 327 const messageNode = document.createTextNode(error.message); 328 li.appendChild(messageNode); 329 330 if (error.suggestions.length > 0) { 331 for (const suggestion of error.suggestions) { 332 const span = document.createElement("span"); 333 span.className = "error-suggestion"; 334 span.textContent = `${suggestion}`; 335 li.appendChild(span); 336 } 337 } 338 339 li.addEventListener("click", () => { 340 // Highlight related nodes in the graph 341 cy.nodes().unselect(); 342 cy.nodes().forEach((node) => { 343 if (node.hasClass("error")) { 344 node.select(); 345 } 346 }); 347 }); 348 349 list.appendChild(li); 350 } 351 352 panel.classList.add("visible"); 353} 354 355type ViewMode = "logical" | "physical"; 356let currentView: ViewMode = "logical"; 357let latestUpdate: GraphUpdate | null = null; 358 359function routeEdges(selector?: string): void { 360 const nodes = cy.nodes().not(":parent").not(".port-node"); 361 const nodePositions: Array<{ x: number; y: number }> = []; 362 nodes.forEach((n) => { 363 nodePositions.push({ x: n.position("x"), y: n.position("y") }); 364 }); 365 366 let leftCount = 0; 367 let rightCount = 0; 368 369 const edges = selector ? cy.edges(selector) : cy.edges(); 370 edges.forEach((edge) => { 371 const sy = edge.source().position("y"); 372 const ty = edge.target().position("y"); 373 const sx = edge.source().position("x"); 374 const tx = edge.target().position("x"); 375 const span = Math.abs(ty - sy); 376 377 if (span < 80) { 378 edge.style({ "curve-style": "bezier" }); 379 return; 380 } 381 382 const blocked = nodePositions.some((p) => { 383 return p.y > Math.min(sy, ty) + 25 && p.y < Math.max(sy, ty) - 25; 384 }); 385 386 if (!blocked) { 387 edge.style({ "curve-style": "bezier" }); 388 return; 389 } 390 391 const avgX = (sx + tx) / 2; 392 const centerX = (Math.min(...nodePositions.map((p) => p.x)) + Math.max(...nodePositions.map((p) => p.x))) / 2; 393 const goLeft = avgX >= centerX; 394 395 const baseOffset = 30; 396 const stagger = goLeft ? 12 * leftCount : 12 * rightCount; 397 const offset = (goLeft ? -1 : 1) * (baseOffset + stagger); 398 399 if (goLeft) leftCount++; 400 else rightCount++; 401 402 edge.style({ 403 "curve-style": "unbundled-bezier", 404 "control-point-distances": [offset * 0.6, offset, offset * 0.6], 405 "control-point-weights": [0.2, 0.5, 0.8], 406 }); 407 }); 408} 409 410function renderLogical(update: GraphUpdate): void { 411 cy.batch(() => { 412 cy.elements().remove(); 413 cy.add(buildElements(update)); 414 }); 415 const layout = cy.layout(logicalLayout()); 416 layout.on("layoutstop", () => { 417 routeEdges(); 418 cy.fit(undefined, 40); 419 }); 420 layout.run(); 421} 422 423function renderPhysical(update: GraphUpdate): void { 424 cy.batch(() => { 425 cy.elements().remove(); 426 cy.add(buildPhysicalElements(update)); 427 }); 428 const layout = cy.layout(physicalLayout()); 429 layout.on("layoutstop", () => { 430 routeEdges(".intra-pe, .seed-edge"); 431 cy.fit(undefined, 40); 432 }); 433 layout.run(); 434} 435 436function renderUpdate(update: GraphUpdate): void { 437 latestUpdate = update; 438 if (currentView === "physical" && update.stage !== "allocate") { 439 currentView = "logical"; 440 const toggleBtn = document.getElementById("view-toggle"); 441 if (toggleBtn) { 442 toggleBtn.textContent = "Physical View"; 443 } 444 } 445 if (currentView === "logical") { 446 renderLogical(update); 447 } else { 448 renderPhysical(update); 449 } 450 updateErrorPanel(update); 451} 452 453function setupToggleButton(): void { 454 const toggleBtn = document.getElementById("view-toggle"); 455 if (toggleBtn) { 456 toggleBtn.addEventListener("click", () => { 457 if (!latestUpdate) return; 458 if (currentView === "logical") { 459 if (latestUpdate.stage !== "allocate") { 460 // Physical view not available 461 return; 462 } 463 currentView = "physical"; 464 toggleBtn.textContent = "Logical View"; 465 renderPhysical(latestUpdate); 466 } else { 467 currentView = "logical"; 468 toggleBtn.textContent = "Physical View"; 469 renderLogical(latestUpdate); 470 } 471 }); 472 } 473} 474 475function setupExportButtons(): void { 476 document.getElementById("export-svg")?.addEventListener("click", () => exportSvg(cy)); 477 document.getElementById("export-png")?.addEventListener("click", () => exportPng(cy)); 478 document.getElementById("copy-png")?.addEventListener("click", () => copyPng(cy)); 479} 480 481function connect(): void { 482 const protocol = location.protocol === "https:" ? "wss:" : "ws:"; 483 const ws = new WebSocket(`${protocol}//${location.host}/ws`); 484 485 ws.onmessage = (event: MessageEvent) => { 486 const update: GraphUpdate = JSON.parse(event.data); 487 if (update.type === "graph_update") { 488 renderUpdate(update); 489 } 490 }; 491 492 ws.onerror = (event: Event) => { 493 console.error("WebSocket error:", event); 494 }; 495 496 ws.onclose = () => { 497 setTimeout(connect, 2000); 498 }; 499} 500 501setupToggleButton(); 502setupExportButtons(); 503connect();