diff --git a/dfgraph/frontend/index.html b/dfgraph/frontend/index.html index 15c780d..9189be8 100644 --- a/dfgraph/frontend/index.html +++ b/dfgraph/frontend/index.html @@ -9,12 +9,15 @@ body { font-family: system-ui, sans-serif; height: 100vh; display: flex; flex-direction: column; } #toolbar { padding: 8px 16px; background: #1a1a2e; color: #eee; display: flex; align-items: center; gap: 12px; } #toolbar h1 { font-size: 14px; font-weight: 600; } + #view-toggle { padding: 6px 12px; background: #5c6bc0; color: #eee; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 500; } + #view-toggle:hover { background: #3f51b5; } #graph { flex: 1; }

dfgraph

+
diff --git a/dfgraph/frontend/src/layout.ts b/dfgraph/frontend/src/layout.ts index 1659fac..dc9c643 100644 --- a/dfgraph/frontend/src/layout.ts +++ b/dfgraph/frontend/src/layout.ts @@ -8,3 +8,14 @@ export function logicalLayout(): object { animate: false, }; } + +export function physicalLayout(): object { + return { + name: "dagre", + rankDir: "TB", + nodeSep: 40, + rankSep: 60, + edgeSep: 15, + animate: false, + }; +} diff --git a/dfgraph/frontend/src/main.ts b/dfgraph/frontend/src/main.ts index e6fe00c..e17d77e 100644 --- a/dfgraph/frontend/src/main.ts +++ b/dfgraph/frontend/src/main.ts @@ -2,7 +2,7 @@ import cytoscape from "cytoscape"; import dagre from "cytoscape-dagre"; import type { GraphUpdate, GraphNode, GraphEdge, GraphRegion } from "./types"; import { stylesheet } from "./style"; -import { logicalLayout } from "./layout"; +import { logicalLayout, physicalLayout } from "./layout"; cytoscape.use(dagre); @@ -21,6 +21,13 @@ function buildLabel(node: GraphNode): string { return node.opcode; } +function buildPhysicalLabel(node: GraphNode): string { + const parts = [node.opcode]; + if (node.const !== null) parts.push(`=${node.const}`); + if (node.iram_offset !== null) parts.push(`\n[iram:${node.iram_offset}, ctx:${node.ctx}]`); + return parts.join(""); +} + function buildElements( update: GraphUpdate ): cytoscape.ElementDefinition[] { @@ -80,7 +87,77 @@ function buildElements( return elements; } -function renderGraph(update: GraphUpdate): void { +function buildPhysicalElements(update: GraphUpdate): cytoscape.ElementDefinition[] { + const elements: cytoscape.ElementDefinition[] = []; + const peNodes = new Map(); + + // Group nodes by PE + for (const node of update.nodes) { + if (node.pe !== null) { + if (!peNodes.has(node.pe)) peNodes.set(node.pe, []); + peNodes.get(node.pe)!.push(node.id); + } + } + + // Create PE cluster parent nodes + for (const peId of peNodes.keys()) { + elements.push({ + data: { id: `pe-${peId}`, label: `PE ${peId}` }, + classes: "pe-cluster", + }); + } + + // Create operation nodes parented to PE clusters + for (const node of update.nodes) { + const el: cytoscape.ElementDefinition = { + data: { + id: node.id, + label: buildPhysicalLabel(node), + colour: node.colour, + category: node.category, + pe: node.pe, + iram_offset: node.iram_offset, + ctx: node.ctx, + }, + classes: node.has_error ? "error" : undefined, + }; + if (node.pe !== null) { + el.data.parent = `pe-${node.pe}`; + } + elements.push(el); + } + + // Create edges with cross-PE / intra-PE classification + const nodePeMap = new Map(); + for (const node of update.nodes) { + nodePeMap.set(node.id, node.pe); + } + + for (const edge of update.edges) { + const sourcePe = nodePeMap.get(edge.source); + const targetPe = nodePeMap.get(edge.target); + const isCrossPe = sourcePe !== null && targetPe !== null && sourcePe !== targetPe; + + elements.push({ + data: { + id: `${edge.source}->${edge.target}:${edge.port}`, + source: edge.source, + target: edge.target, + targetLabel: edge.port, + sourceLabel: "", + }, + classes: (isCrossPe ? "cross-pe" : "intra-pe") + (edge.has_error ? " error" : ""), + }); + } + + return elements; +} + +type ViewMode = "logical" | "physical"; +let currentView: ViewMode = "logical"; +let latestUpdate: GraphUpdate | null = null; + +function renderLogical(update: GraphUpdate): void { cy.batch(() => { cy.elements().remove(); cy.add(buildElements(update)); @@ -89,6 +166,53 @@ function renderGraph(update: GraphUpdate): void { cy.fit(undefined, 40); } +function renderPhysical(update: GraphUpdate): void { + cy.batch(() => { + cy.elements().remove(); + cy.add(buildPhysicalElements(update)); + }); + cy.layout(physicalLayout()).run(); + cy.fit(undefined, 40); +} + +function renderUpdate(update: GraphUpdate): void { + latestUpdate = update; + if (currentView === "physical" && update.stage !== "allocate") { + currentView = "logical"; + const toggleBtn = document.getElementById("view-toggle"); + if (toggleBtn) { + toggleBtn.textContent = "Physical View"; + } + } + if (currentView === "logical") { + renderLogical(update); + } else { + renderPhysical(update); + } +} + +function setupToggleButton(): void { + const toggleBtn = document.getElementById("view-toggle"); + if (toggleBtn) { + toggleBtn.addEventListener("click", () => { + if (!latestUpdate) return; + if (currentView === "logical") { + if (latestUpdate.stage !== "allocate") { + // Physical view not available + return; + } + currentView = "physical"; + toggleBtn.textContent = "Logical View"; + renderPhysical(latestUpdate); + } else { + currentView = "logical"; + toggleBtn.textContent = "Physical View"; + renderLogical(latestUpdate); + } + }); + } +} + function connect(): void { const protocol = location.protocol === "https:" ? "wss:" : "ws:"; const ws = new WebSocket(`${protocol}//${location.host}/ws`); @@ -96,7 +220,7 @@ function connect(): void { ws.onmessage = (event: MessageEvent) => { const update: GraphUpdate = JSON.parse(event.data); if (update.type === "graph_update") { - renderGraph(update); + renderUpdate(update); } }; @@ -105,4 +229,5 @@ function connect(): void { }; } +setupToggleButton(); connect(); diff --git a/dfgraph/frontend/src/style.ts b/dfgraph/frontend/src/style.ts index 5133139..137cfb0 100644 --- a/dfgraph/frontend/src/style.ts +++ b/dfgraph/frontend/src/style.ts @@ -74,4 +74,36 @@ export const stylesheet: cytoscape.Stylesheet[] = [ "target-arrow-color": "#e53935", }, }, + { + selector: "node.pe-cluster", + style: { + shape: "roundrectangle", + "border-width": 2, + "border-color": "#5c6bc0", + "background-color": "rgba(92, 107, 192, 0.06)", + padding: "20px", + "text-valign": "top", + "text-halign": "center", + label: "data(label)", + "font-size": 13, + "font-weight": "bold", + color: "#5c6bc0", + }, + }, + { + selector: "edge.cross-pe", + style: { + width: 3, + "line-color": "#5c6bc0", + "target-arrow-color": "#5c6bc0", + }, + }, + { + selector: "edge.intra-pe", + style: { + width: 1.5, + "line-color": "#bbb", + "target-arrow-color": "#bbb", + }, + }, ];