From b53dcfd5a630aca74edd1963fb6a8c7d3f318a63 Mon Sep 17 00:00:00 2001 From: Sigurd Schneider Date: Fri, 4 Jan 2019 14:33:57 +0100 Subject: [PATCH] [turbolizer] Split Graph class from GraphView This CL splits out a Graph class from the GraphView, which improves maintainability and is a first step towards preserving node positions during phase view changes. This CL also removes duplication of node storage on the graph and provides a generator function instead. The only storage for nodes in the graph is now the {nodeMap}. Bug: v8:7327 Notry: true Change-Id: I1659ecfe46f62a12d2fb3c40ccd6f4936f081b53 Reviewed-on: https://chromium-review.googlesource.com/c/1396087 Commit-Queue: Sigurd Schneider Reviewed-by: Georg Neis Cr-Commit-Position: refs/heads/master@{#58549} --- tools/turbolizer/src/edge.ts | 15 +- tools/turbolizer/src/graph-layout.ts | 69 +---- tools/turbolizer/src/graph-view.ts | 379 +++++++++++---------------- tools/turbolizer/src/graph.ts | 127 +++++++++ tools/turbolizer/src/node.ts | 16 +- tools/turbolizer/src/util.ts | 11 + tools/turbolizer/tsconfig.json | 1 + 7 files changed, 316 insertions(+), 302 deletions(-) create mode 100644 tools/turbolizer/src/graph.ts diff --git a/tools/turbolizer/src/edge.ts b/tools/turbolizer/src/edge.ts index 81119d6351..fb99f711b2 100644 --- a/tools/turbolizer/src/edge.ts +++ b/tools/turbolizer/src/edge.ts @@ -2,7 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import {GNode, DEFAULT_NODE_BUBBLE_RADIUS} from "../src/node" +import { GNode, DEFAULT_NODE_BUBBLE_RADIUS } from "../src/node" +import { Graph } from "./graph"; export const MINIMUM_EDGE_SEPARATION = 20; @@ -32,7 +33,7 @@ export class Edge { return this.visible && this.source.visible && this.target.visible; }; - getInputHorizontalPosition(graph) { + getInputHorizontalPosition(graph: Graph, showTypes: boolean) { if (this.backEdgeNumber > 0) { return graph.maxGraphNodeX + this.backEdgeNumber * MINIMUM_EDGE_SEPARATION; } @@ -41,7 +42,7 @@ export class Edge { var index = this.index; var input_x = target.x + target.getInputX(index); var inputApproach = target.getInputApproach(this.index); - var outputApproach = source.getOutputApproach(graph); + var outputApproach = source.getOutputApproach(showTypes); if (inputApproach > outputApproach) { return input_x; } else { @@ -52,17 +53,17 @@ export class Edge { } } - generatePath(graph) { + generatePath(graph: Graph, showTypes: boolean) { var target = this.target; var source = this.source; var input_x = target.x + target.getInputX(this.index); var arrowheadHeight = 7; var input_y = target.y - 2 * DEFAULT_NODE_BUBBLE_RADIUS - arrowheadHeight; var output_x = source.x + source.getOutputX(); - var output_y = source.y + graph.getNodeHeight(source) + DEFAULT_NODE_BUBBLE_RADIUS; + var output_y = source.y + source.getNodeHeight(showTypes) + DEFAULT_NODE_BUBBLE_RADIUS; var inputApproach = target.getInputApproach(this.index); - var outputApproach = source.getOutputApproach(graph); - var horizontalPos = this.getInputHorizontalPosition(graph); + var outputApproach = source.getOutputApproach(showTypes); + var horizontalPos = this.getInputHorizontalPosition(graph, showTypes); var result = "M" + output_x + "," + output_y + "L" + output_x + "," + outputApproach + diff --git a/tools/turbolizer/src/graph-layout.ts b/tools/turbolizer/src/graph-layout.ts index 63e3c94dbe..46cbc5bd4f 100644 --- a/tools/turbolizer/src/graph-layout.ts +++ b/tools/turbolizer/src/graph-layout.ts @@ -6,13 +6,14 @@ import { MAX_RANK_SENTINEL } from "../src/constants" import { MINIMUM_EDGE_SEPARATION, Edge } from "../src/edge" import { NODE_INPUT_WIDTH, MINIMUM_NODE_OUTPUT_APPROACH, DEFAULT_NODE_BUBBLE_RADIUS, GNode } from "../src/node" +import { Graph } from "./graph"; const DEFAULT_NODE_ROW_SEPARATION = 130 var traceLayout = false; -function newGraphOccupation(graph) { +function newGraphOccupation(graph:Graph) { var isSlotFilled = []; var maxSlot = 0; var minSlot = 0; @@ -138,7 +139,6 @@ function newGraphOccupation(graph) { if (node.inputs[i].isVisible()) { var edge = node.inputs[i]; if (!edge.isBackEdge()) { - var source = edge.source; var horizontalPos = edge.getInputHorizontalPosition(graph); if (traceLayout) { console.log("Occupying input " + i + " of " + node.id + " at " + horizontalPos); @@ -252,7 +252,7 @@ function newGraphOccupation(graph) { return occupation; } -export function layoutNodeGraph(graph) { +export function layoutNodeGraph(graph: Graph, showTypes: boolean): void { // First determine the set of nodes that have no outputs. Those are the // basis for bottom-up DFS to determine rank and node placement. @@ -260,10 +260,10 @@ export function layoutNodeGraph(graph) { const endNodesHasNoOutputs = []; const startNodesHasNoInputs = []; - graph.nodes.forEach(function (n: GNode) { + for (const n of graph.nodes()) { endNodesHasNoOutputs[n.id] = true; startNodesHasNoInputs[n.id] = true; - }); + }; graph.forEachEdge((e: Edge) => { endNodesHasNoOutputs[e.source.id] = false; startNodesHasNoInputs[e.target.id] = false; @@ -274,7 +274,7 @@ export function layoutNodeGraph(graph) { var startNodes = []; var visited = []; var rank = []; - graph.nodes.forEach(function (n, i) { + for (const n of graph.nodes()) { if (endNodesHasNoOutputs[n.id]) { endNodes.push(n); } @@ -286,7 +286,7 @@ export function layoutNodeGraph(graph) { n.rank = 0; n.visitOrderWithinRank = 0; n.outputApproach = MINIMUM_NODE_OUTPUT_APPROACH; - }); + }; if (traceLayout) { console.log(`layoutGraph init ${performance.now() - start}`); @@ -385,8 +385,8 @@ export function layoutNodeGraph(graph) { var rankSets = []; // Collect sets for each rank. - graph.nodes.forEach(function (n, i) { - n.y = n.rank * (DEFAULT_NODE_ROW_SEPARATION + graph.getNodeHeight(n) + + for (const n of graph.nodes()) { + n.y = n.rank * (DEFAULT_NODE_ROW_SEPARATION + n.getNodeHeight(showTypes) + 2 * DEFAULT_NODE_BUBBLE_RADIUS); if (n.visible) { if (rankSets[n.rank] === undefined) { @@ -395,13 +395,12 @@ export function layoutNodeGraph(graph) { rankSets[n.rank].push(n); } } - }); + }; // Iterate backwards from highest to lowest rank, placing nodes so that they // spread out from the "center" as much as possible while still being // compact and not overlapping live input lines. var occupation = newGraphOccupation(graph); - var rankCount = 0; rankSets.reverse().forEach(function (rankSet) { @@ -462,57 +461,11 @@ export function layoutNodeGraph(graph) { }); graph.maxBackEdgeNumber = 0; - graph.visibleEdges.selectAll("path").each(function (e) { + graph.forEachEdge((e) => { if (e.isBackEdge()) { e.backEdgeNumber = ++graph.maxBackEdgeNumber; } else { e.backEdgeNumber = 0; } }); - - redetermineGraphBoundingBox(graph); -} - -function redetermineGraphBoundingBox(graph) { - graph.minGraphX = 0; - graph.maxGraphNodeX = 1; - graph.maxGraphX = undefined; // see below - graph.minGraphY = 0; - graph.maxGraphY = 1; - - for (var i = 0; i < graph.nodes.length; ++i) { - var node = graph.nodes[i]; - - if (!node.visible) { - continue; - } - - if (node.x < graph.minGraphX) { - graph.minGraphX = node.x; - } - if ((node.x + node.getTotalNodeWidth()) > graph.maxGraphNodeX) { - graph.maxGraphNodeX = node.x + node.getTotalNodeWidth(); - } - if ((node.y - 50) < graph.minGraphY) { - graph.minGraphY = node.y - 50; - } - if ((node.y + graph.getNodeHeight(node) + 50) > graph.maxGraphY) { - graph.maxGraphY = node.y + graph.getNodeHeight(node) + 50; - } - } - - graph.maxGraphX = graph.maxGraphNodeX + - graph.maxBackEdgeNumber * MINIMUM_EDGE_SEPARATION; - - const width = (graph.maxGraphX - graph.minGraphX); - const height = graph.maxGraphY - graph.minGraphY; - graph.width = width; - graph.height = height; - - const extent = [ - [graph.minGraphX - width / 2, graph.minGraphY - height / 2], - [graph.maxGraphX + width / 2, graph.maxGraphY + height / 2] - ]; - graph.panZoom.translateExtent(extent); - graph.minScale(); } diff --git a/tools/turbolizer/src/graph-view.ts b/tools/turbolizer/src/graph-view.ts index 87615b92de..aa609f4785 100644 --- a/tools/turbolizer/src/graph-view.ts +++ b/tools/turbolizer/src/graph-view.ts @@ -13,6 +13,7 @@ import { View, PhaseView } from "../src/view" import { MySelection } from "../src/selection" import { partial, alignUp } from "../src/util" import { NodeSelectionHandler, ClearableHandler } from "./selection-handler"; +import { Graph } from "./graph"; function nodeToStringKey(n) { return "" + n.id; @@ -33,23 +34,17 @@ export class GraphView extends View implements PhaseView { svg: d3.Selection; showPhaseByName: (string) => void; state: GraphState; - nodes: Array; selectionHandler: NodeSelectionHandler & ClearableHandler; graphElement: d3.Selection; visibleNodes: d3.Selection; visibleEdges: d3.Selection; - minGraphX: number; - maxGraphX: number; - minGraphY: number; - maxGraphY: number; width: number; height: number; - maxGraphNodeX: number; drag: d3.DragBehavior; panZoom: d3.ZoomBehavior; - nodeMap: Array; visibleBubbles: d3.Selection; transitionTimout: number; + graph: Graph; createViewElement() { const pane = document.createElement('div'); @@ -57,43 +52,20 @@ export class GraphView extends View implements PhaseView { return pane; } - *filteredEdges(p: (e: Edge) => boolean) { - for (const node of this.nodes) { - for (const edge of node.inputs) { - if (p(edge)) yield edge; - } - } - } - - forEachEdge(p: (e: Edge) => void) { - for (const node of this.nodes) { - for (const edge of node.inputs) { - p(edge); - } - } - } - constructor(id, broker, showPhaseByName: (string) => void) { super(id); - var graph = this; + const view = this; this.showPhaseByName = showPhaseByName; this.divElement = d3.select(this.divNode); const svg = this.divElement.append("svg").attr('version', '1.1') .attr("width", "100%") .attr("height", "100%"); svg.on("click", function (d) { - graph.selectionHandler.clear(); + view.selectionHandler.clear(); }); - graph.svg = svg; + view.svg = svg; - graph.nodes = []; - - graph.minGraphX = 0; - graph.maxGraphX = 1; - graph.minGraphY = 0; - graph.maxGraphY = 1; - - graph.state = { + this.state = { selection: null, mouseDownNode: null, justDragged: false, @@ -105,9 +77,9 @@ export class GraphView extends View implements PhaseView { this.selectionHandler = { clear: function () { - graph.state.selection.clear(); + view.state.selection.clear(); broker.broadcastClear(this); - graph.updateGraphVisibility(); + view.updateGraphVisibility(); }, select: function (nodes, selected) { let locations = []; @@ -119,39 +91,38 @@ export class GraphView extends View implements PhaseView { locations.push({ bytecodePosition: node.origin.bytecodePosition }); } } - graph.state.selection.select(nodes, selected); + view.state.selection.select(nodes, selected); broker.broadcastSourcePositionSelect(this, locations, selected); - graph.updateGraphVisibility(); + view.updateGraphVisibility(); }, brokeredNodeSelect: function (locations, selected) { - let selection = graph.nodes - .filter(function (n) { - return locations.has(nodeToStringKey(n)) - && (!graph.state.hideDead || n.isLive()); - }); - graph.state.selection.select(selection, selected); + let selection = view.graph.nodes((n) => { + return locations.has(nodeToStringKey(n)) + && (!view.state.hideDead || n.isLive()); + }); + view.state.selection.select(selection, selected); // Update edge visibility based on selection. - graph.nodes.forEach((n) => { - if (graph.state.selection.isSelected(n)) { + for (const n of view.graph.nodes()) { + if (view.state.selection.isSelected(n)) { n.visible = true; n.inputs.forEach((e) => { - e.visible = e.visible || graph.state.selection.isSelected(e.source); + e.visible = e.visible || view.state.selection.isSelected(e.source); }); n.outputs.forEach((e) => { - e.visible = e.visible || graph.state.selection.isSelected(e.target); + e.visible = e.visible || view.state.selection.isSelected(e.target); }); } - }); - graph.updateGraphVisibility(); + }; + view.updateGraphVisibility(); }, brokeredClear: function () { - graph.state.selection.clear(); - graph.updateGraphVisibility(); + view.state.selection.clear(); + view.updateGraphVisibility(); } }; broker.addNodeHandler(this.selectionHandler); - graph.state.selection = new MySelection(nodeToStringKey); + view.state.selection = new MySelection(nodeToStringKey); const defs = svg.append('svg:defs'); defs.append('svg:marker') @@ -165,27 +136,27 @@ export class GraphView extends View implements PhaseView { .attr('d', 'M0,-4L8,0L0,4'); this.graphElement = svg.append("g"); - graph.visibleEdges = this.graphElement.append("g"); - graph.visibleNodes = this.graphElement.append("g"); + view.visibleEdges = this.graphElement.append("g"); + view.visibleNodes = this.graphElement.append("g"); - graph.drag = d3.drag() + view.drag = d3.drag() .on("drag", function (d) { d.x += d3.event.dx; d.y += d3.event.dy; - graph.updateGraphVisibility(); + view.updateGraphVisibility(); }); // listen for key events d3.select(window).on("keydown", function (e) { - graph.svgKeyDown.call(graph); + view.svgKeyDown.call(view); }).on("keyup", function () { - graph.svgKeyUp.call(graph); + view.svgKeyUp.call(view); }); function zoomed() { if (d3.event.shiftKey) return false; - graph.graphElement.attr("transform", d3.event.transform); + view.graphElement.attr("transform", d3.event.transform); } const zoomSvg = d3.zoom() @@ -201,7 +172,7 @@ export class GraphView extends View implements PhaseView { svg.call(zoomSvg).on("dblclick.zoom", null); - graph.panZoom = zoomSvg; + view.panZoom = zoomSvg; } @@ -219,14 +190,6 @@ export class GraphView extends View implements PhaseView { return 50; } - getNodeHeight(d): number { - if (this.state.showTypes) { - return d.normalheight + d.labelbbox.height; - } else { - return d.normalheight; - } - } - getEdgeFrontier(nodes, inEdges, edgeFilter) { let frontier = new Set(); for (const n of nodes) { @@ -243,10 +206,10 @@ export class GraphView extends View implements PhaseView { } getNodeFrontier(nodes, inEdges, edgeFilter) { - let graph = this; + const view = this; var frontier = new Set(); var newState = true; - var edgeFrontier = graph.getEdgeFrontier(nodes, inEdges, edgeFilter); + var edgeFrontier = view.getEdgeFrontier(nodes, inEdges, edgeFilter); // Control key toggles edges rather than just turning them on if (d3.event.ctrlKey) { edgeFrontier.forEach(function (edge) { @@ -263,7 +226,7 @@ export class GraphView extends View implements PhaseView { frontier.add(node); } }); - graph.updateGraphVisibility(); + view.updateGraphVisibility(); if (newState) { return frontier; } else { @@ -296,76 +259,23 @@ export class GraphView extends View implements PhaseView { deleteContent() { if (this.visibleNodes) { - this.nodes = []; - this.nodeMap = []; this.updateGraphVisibility(); } }; - measureText(text) { - const textMeasure = document.getElementById('text-measure'); - if (textMeasure instanceof SVGTSpanElement) { - textMeasure.textContent = text; - return { - width: textMeasure.getBBox().width, - height: textMeasure.getBBox().height, - }; - } - } - createGraph(data, rememberedSelection) { - var g = this; - g.nodes = []; - g.nodeMap = []; - data.nodes.forEach(function (n, i) { - n.__proto__ = GNode.prototype; - n.visible = false; - n.x = 0; - n.y = 0; - if (typeof n.pos === "number") { - // Backwards compatibility. - n.sourcePosition = { scriptOffset: n.pos, inliningId: -1 }; - } - n.rank = MAX_RANK_SENTINEL; - n.inputs = []; - n.outputs = []; - n.rpo = -1; - n.outputApproach = MINIMUM_NODE_OUTPUT_APPROACH; - // Every control node is a CFG node. - n.cfg = n.control; - g.nodeMap[n.id] = n; - n.displayLabel = n.getDisplayLabel(); - n.labelbbox = g.measureText(n.displayLabel); - n.typebbox = g.measureText(n.getDisplayType()); - var innerwidth = Math.max(n.labelbbox.width, n.typebbox.width); - n.width = alignUp(innerwidth + NODE_INPUT_WIDTH * 2, - NODE_INPUT_WIDTH); - var innerheight = Math.max(n.labelbbox.height, n.typebbox.height); - n.normalheight = innerheight + 20; - g.nodes.push(n); - }); - data.edges.forEach((e: any) => { - var t = g.nodeMap[e.target]; - var s = g.nodeMap[e.source]; - var newEdge = new Edge(t, e.index, s, e.type); - t.inputs.push(newEdge); - s.outputs.push(newEdge); - if (e.type == 'control') { - // Every source of a control edge is a CFG node. - s.cfg = true; - } - }); - g.nodes.forEach(function (n, i) { - n.visible = n.cfg && (!g.state.hideDead || n.isLive()); + this.graph = new Graph(data); + for (const n of this.graph.nodes()) { + n.visible = n.cfg && (!this.state.hideDead || n.isLive()); if (rememberedSelection != undefined && rememberedSelection.has(nodeToStringKey(n))) { n.visible = true; } - }); - g.forEachEdge((e: Edge) => { + }; + this.graph.forEachEdge((e: Edge) => { e.visible = e.type == 'control' && e.source.visible && e.target.visible; }); - g.layoutGraph(); - g.updateGraphVisibility(); + this.layoutGraph(); + this.updateGraphVisibility(); } connectVisibleSelectedNodes() { @@ -385,8 +295,9 @@ export class GraphView extends View implements PhaseView { } updateInputAndOutputBubbles() { - var g = this; - var s = g.visibleBubbles; + const view = this; + const g = this.graph; + const s = this.visibleBubbles; s.classed("filledBubbleStyle", function (c) { var components = this.id.split(','); if (components[0] == "ib") { @@ -417,7 +328,7 @@ export class GraphView extends View implements PhaseView { if (components[0] == "ob") { var from = g.nodeMap[components[1]]; var x = from.getOutputX(); - var y = g.getNodeHeight(from) + DEFAULT_NODE_BUBBLE_RADIUS; + var y = from.getNodeHeight(view.state.showTypes) + DEFAULT_NODE_BUBBLE_RADIUS; var transform = "translate(" + x + "," + y + ")"; this.setAttribute('transform', transform); } @@ -425,12 +336,11 @@ export class GraphView extends View implements PhaseView { } attachSelection(s) { - const graph = this; if (!(s instanceof Set)) return; - graph.selectionHandler.clear(); - const selected = graph.nodes.filter((n) => - s.has(graph.state.selection.stringKey(n)) && (!graph.state.hideDead || n.isLive())); - graph.selectionHandler.select(selected, true); + this.selectionHandler.clear(); + const selected = [...this.graph.nodes((n) => + s.has(this.state.selection.stringKey(n)) && (!this.state.hideDead || n.isLive()))]; + this.selectionHandler.select(selected, true); } detachSelection() { @@ -438,80 +348,77 @@ export class GraphView extends View implements PhaseView { } selectAllNodes() { - var graph = this; if (!d3.event.shiftKey) { - graph.state.selection.clear(); + this.state.selection.clear(); } - const allVisibleNodes = graph.nodes.filter((n) => n.visible); - graph.state.selection.select(allVisibleNodes, true); - graph.updateGraphVisibility(); + const allVisibleNodes = [...this.graph.nodes((n) => n.visible)]; + this.state.selection.select(allVisibleNodes, true); + this.updateGraphVisibility(); } - layoutAction(graph) { + layoutAction(graph: GraphView) { graph.layoutGraph(); graph.updateGraphVisibility(); graph.viewWholeGraph(); } - showAllAction(graph) { - graph.nodes.forEach((n: GNode) => { - n.visible = !graph.state.hideDead || n.isLive(); - }); - graph.forEachEdge((e: Edge) => { + showAllAction(view: GraphView) { + for (const n of view.graph.nodes()) { + n.visible = !view.state.hideDead || n.isLive(); + }; + view.graph.forEachEdge((e: Edge) => { e.visible = e.source.visible || e.target.visible; }); - graph.updateGraphVisibility(); - graph.viewWholeGraph(); + view.updateGraphVisibility(); + view.viewWholeGraph(); } - toggleHideDead(graph) { - graph.state.hideDead = !graph.state.hideDead; - if (graph.state.hideDead) graph.hideDead(); + toggleHideDead(view: GraphView) { + view.state.hideDead = !view.state.hideDead; + if (view.state.hideDead) view.hideDead(); var element = document.getElementById('toggle-hide-dead'); - element.classList.toggle('button-input-toggled', graph.state.hideDead); + element.classList.toggle('button-input-toggled', view.state.hideDead); } hideDead() { - const graph = this; - graph.nodes.filter(function (n) { + for (const n of this.graph.nodes()) { if (!n.isLive()) { n.visible = false; - graph.state.selection.select([n], false); + this.state.selection.select([n], false); } - }); - graph.updateGraphVisibility(); + }; + this.updateGraphVisibility(); } - hideUnselectedAction(graph) { - graph.nodes.forEach(function (n) { - if (!graph.state.selection.isSelected(n)) { + hideUnselectedAction(view: GraphView) { + for (const n of view.graph.nodes()) { + if (!view.state.selection.isSelected(n)) { n.visible = false; } - }); - graph.updateGraphVisibility(); + }; + view.updateGraphVisibility(); } - hideSelectedAction(graph) { - graph.nodes.forEach(function (n) { - if (graph.state.selection.isSelected(n)) { + hideSelectedAction(view: GraphView) { + for (const n of view.graph.nodes()) { + if (view.state.selection.isSelected(n)) { n.visible = false; } - }); - graph.selectionHandler.clear(); + }; + view.selectionHandler.clear(); } - zoomSelectionAction(graph) { - graph.viewSelection(); + zoomSelectionAction(view: GraphView) { + view.viewSelection(); } - toggleTypesAction(graph) { - graph.toggleTypes(); + toggleTypesAction(view: GraphView) { + view.toggleTypes(); } searchInputAction(searchBar, e: KeyboardEvent) { - const graph = this; if (e.keyCode == 13) { - graph.selectionHandler.clear(); + this.selectionHandler.clear(); var query = searchBar.value; window.sessionStorage.setItem("lastSearch", query); if (query.length == 0) return; @@ -519,38 +426,38 @@ export class GraphView extends View implements PhaseView { var reg = new RegExp(query); var filterFunction = function (n) { return (reg.exec(n.getDisplayLabel()) != null || - (graph.state.showTypes && reg.exec(n.getDisplayType())) || + (this.state.showTypes && reg.exec(n.getDisplayType())) || (reg.exec(n.getTitle())) || reg.exec(n.opcode) != null); }; - const selection = graph.nodes.filter( - function (n, i) { - if ((e.ctrlKey || n.visible) && filterFunction(n)) { - if (e.ctrlKey) n.visible = true; - return true; - } - return false; - }); + const selection = this.graph.nodes((n) => { + if ((e.ctrlKey || n.visible) && filterFunction(n)) { + if (e.ctrlKey) n.visible = true; + return true; + } + return false; + }); - graph.selectionHandler.select(selection, true); - graph.connectVisibleSelectedNodes(); - graph.updateGraphVisibility(); + this.selectionHandler.select(selection, true); + this.connectVisibleSelectedNodes(); + this.updateGraphVisibility(); searchBar.blur(); - graph.viewSelection(); + this.viewSelection(); } e.stopPropagation(); } svgKeyDown() { - var state = this.state; - var graph = this; + const view = this; + const state = this.state; + const graph = this.graph; // Don't handle key press repetition if (state.lastKeyDown !== -1) return; var showSelectionFrontierNodes = function (inEdges, filter, select) { - var frontier = graph.getNodeFrontier(state.selection, inEdges, filter); + var frontier = view.getNodeFrontier(state.selection, inEdges, filter); if (frontier != undefined && frontier.size) { if (select) { if (!d3.event.shiftKey) { @@ -558,7 +465,7 @@ export class GraphView extends View implements PhaseView { } state.selection.select(frontier, true); } - graph.updateGraphVisibility(); + view.updateGraphVisibility(); } allowRepetition = false; } @@ -616,7 +523,7 @@ export class GraphView extends View implements PhaseView { break; case 65: // 'a' - graph.selectAllNodes(); + view.selectAllNodes(); allowRepetition = false; break; case 38: @@ -634,7 +541,7 @@ export class GraphView extends View implements PhaseView { break; case 83: // 's' - graph.selectOrigins(); + view.selectOrigins(); break; case 191: // '/' @@ -658,7 +565,12 @@ export class GraphView extends View implements PhaseView { layoutGraph() { console.time("layoutGraph"); - layoutNodeGraph(this); + layoutNodeGraph(this.graph, this.state.showTypes); + const [[width, height], extent] = this.graph.redetermineGraphBoundingBox(this.state.showTypes); + this.width = width; + this.height = height; + this.panZoom.translateExtent(extent); + this.minScale(); console.timeEnd("layoutGraph"); } @@ -668,7 +580,7 @@ export class GraphView extends View implements PhaseView { let phase = null; for (const n of state.selection) { if (n.origin) { - const node = this.nodeMap[n.origin.nodeId]; + const node = this.graph.nodeMap[n.origin.nodeId]; origins.push(node); phase = n.origin.phase; } @@ -684,13 +596,14 @@ export class GraphView extends View implements PhaseView { // call to propagate changes to graph updateGraphVisibility() { - let graph = this; - let state = graph.state; + const view = this; + const graph = this.graph; + const state = this.state; var filteredEdges = [...graph.filteredEdges(function (e) { return e.source.visible && e.target.visible; })]; - const selEdges = graph.visibleEdges.selectAll("path").data(filteredEdges, edgeToStr); + const selEdges = view.visibleEdges.selectAll("path").data(filteredEdges, edgeToStr); // remove old links selEdges.exit().remove(); @@ -704,9 +617,9 @@ export class GraphView extends View implements PhaseView { .on("click", function (edge) { d3.event.stopPropagation(); if (!d3.event.shiftKey) { - graph.selectionHandler.clear(); + view.selectionHandler.clear(); } - graph.selectionHandler.select([edge.source, edge.target], true); + view.selectionHandler.select([edge.source, edge.target], true); }) .attr("adjacentToHover", "false") .classed('value', function (e) { @@ -727,8 +640,8 @@ export class GraphView extends View implements PhaseView { newAndOldEdges.classed('hidden', (e) => !e.isVisible()); // select existing nodes - const filteredNodes = graph.nodes.filter(n => n.visible); - const allNodes = graph.visibleNodes.selectAll("g"); + const filteredNodes = [...graph.nodes(n => n.visible)]; + const allNodes = view.visibleNodes.selectAll("g"); const selNodes = allNodes.data(filteredNodes, nodeToStr); // remove old nodes @@ -747,36 +660,36 @@ export class GraphView extends View implements PhaseView { .classed("simplified", function (n) { return n.isSimplified(); }) .classed("machine", function (n) { return n.isMachine(); }) .on('mouseenter', function (node) { - const visibleEdges = graph.visibleEdges.selectAll('path'); + const visibleEdges = view.visibleEdges.selectAll('path'); const adjInputEdges = visibleEdges.filter(e => { return e.target === node; }); const adjOutputEdges = visibleEdges.filter(e => { return e.source === node; }); adjInputEdges.attr('relToHover', "input"); adjOutputEdges.attr('relToHover', "output"); const adjInputNodes = adjInputEdges.data().map(e => e.source); - const visibleNodes = graph.visibleNodes.selectAll("g"); + const visibleNodes = view.visibleNodes.selectAll("g"); const input = visibleNodes.data(adjInputNodes, nodeToStr) .attr('relToHover', "input"); const adjOutputNodes = adjOutputEdges.data().map(e => e.target); const output = visibleNodes.data(adjOutputNodes, nodeToStr) .attr('relToHover', "output"); - graph.updateGraphVisibility(); + view.updateGraphVisibility(); }) .on('mouseleave', function (node) { - const visibleEdges = graph.visibleEdges.selectAll('path'); + const visibleEdges = view.visibleEdges.selectAll('path'); const adjEdges = visibleEdges.filter(e => { return e.target === node || e.source === node; }); adjEdges.attr('relToHover', "none"); const adjNodes = adjEdges.data().map(e => e.target).concat(adjEdges.data().map(e => e.source)); - const visibleNodes = graph.visibleNodes.selectAll("g"); + const visibleNodes = view.visibleNodes.selectAll("g"); const nodes = visibleNodes.data(adjNodes, nodeToStr) .attr('relToHover', "none"); - graph.updateGraphVisibility(); + view.updateGraphVisibility(); }) .on("click", (d) => { - if (!d3.event.shiftKey) graph.selectionHandler.clear(); - graph.selectionHandler.select([d], undefined); + if (!d3.event.shiftKey) view.selectionHandler.clear(); + view.selectionHandler.select([d], undefined); d3.event.stopPropagation(); }) - .call(graph.drag) + .call(view.drag) newGs.append("rect") .attr("rx", 10) @@ -785,7 +698,7 @@ export class GraphView extends View implements PhaseView { return d.getTotalNodeWidth(); }) .attr('height', function (d) { - return graph.getNodeHeight(d); + return d.getNodeHeight(view.state.showTypes); }) function appendInputAndOutputBubbles(g, d) { @@ -811,13 +724,13 @@ export class GraphView extends View implements PhaseView { var visible = !edge.isVisible(); node.setInputVisibility(components[2], visible); d3.event.stopPropagation(); - graph.updateGraphVisibility(); + view.updateGraphVisibility(); }); } if (d.outputs.length != 0) { - var x = d.getOutputX(); - var y = graph.getNodeHeight(d) + DEFAULT_NODE_BUBBLE_RADIUS; - var s = g.append('circle') + const x = d.getOutputX(); + const y = d.getNodeHeight(view.state.showTypes) + DEFAULT_NODE_BUBBLE_RADIUS; + g.append('circle') .classed("filledBubbleStyle", function (c) { return d.areAnyOutputsVisible() == 2; }) @@ -835,7 +748,7 @@ export class GraphView extends View implements PhaseView { .on("click", function (d) { d.setOutputVisibility(d.areAnyOutputsVisible() == 0); d3.event.stopPropagation(); - graph.updateGraphVisibility(); + view.updateGraphVisibility(); }); } } @@ -879,7 +792,7 @@ export class GraphView extends View implements PhaseView { const newAndOldNodes = newGs.merge(selNodes); newAndOldNodes.select('.type').each(function (d) { - this.setAttribute('visibility', graph.state.showTypes ? 'visible' : 'hidden'); + this.setAttribute('visibility', view.state.showTypes ? 'visible' : 'hidden'); }); newAndOldNodes @@ -889,15 +802,15 @@ export class GraphView extends View implements PhaseView { }) .attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; }) .select('rect') - .attr('height', function (d) { return graph.getNodeHeight(d); }); + .attr('height', function (d) { return d.getNodeHeight(view.state.showTypes); }); - graph.visibleBubbles = d3.selectAll('circle'); + view.visibleBubbles = d3.selectAll('circle'); - graph.updateInputAndOutputBubbles(); + view.updateInputAndOutputBubbles(); graph.maxGraphX = graph.maxGraphNodeX; newAndOldEdges.attr("d", function (edge) { - return edge.generatePath(graph); + return edge.generatePath(graph, view.state.showTypes); }); } @@ -934,22 +847,22 @@ export class GraphView extends View implements PhaseView { } viewSelection() { - var graph = this; + var view = this; var minX, maxX, minY, maxY; var hasSelection = false; - graph.visibleNodes.selectAll("g").each(function (n) { - if (graph.state.selection.isSelected(n)) { + view.visibleNodes.selectAll("g").each(function (n) { + if (view.state.selection.isSelected(n)) { hasSelection = true; minX = minX ? Math.min(minX, n.x) : n.x; maxX = maxX ? Math.max(maxX, n.x + n.getTotalNodeWidth()) : n.x + n.getTotalNodeWidth(); minY = minY ? Math.min(minY, n.y) : n.y; - maxY = maxY ? Math.max(maxY, n.y + graph.getNodeHeight(n)) : - n.y + graph.getNodeHeight(n); + maxY = maxY ? Math.max(maxY, n.y + n.getNodeHeight(view.state.showTypes)) : + n.y + n.getNodeHeight(view.state.showTypes); } }); if (hasSelection) { - graph.viewGraphRegion(minX - NODE_INPUT_WIDTH, minY - 60, + view.viewGraphRegion(minX - NODE_INPUT_WIDTH, minY - 60, maxX + NODE_INPUT_WIDTH, maxY + 60, true); } @@ -971,6 +884,6 @@ export class GraphView extends View implements PhaseView { viewWholeGraph() { this.panZoom.scaleTo(this.svg, 0); - this.panZoom.translateTo(this.svg, this.minGraphX + this.width / 2, this.minGraphY + this.height / 2) + this.panZoom.translateTo(this.svg, this.graph.minGraphX + this.width / 2, this.graph.minGraphY + this.height / 2) } } diff --git a/tools/turbolizer/src/graph.ts b/tools/turbolizer/src/graph.ts new file mode 100644 index 0000000000..3a801f0c25 --- /dev/null +++ b/tools/turbolizer/src/graph.ts @@ -0,0 +1,127 @@ +import { GNode, MINIMUM_NODE_OUTPUT_APPROACH, NODE_INPUT_WIDTH } from "./node"; +import { MAX_RANK_SENTINEL } from "./constants"; +import { alignUp, measureText } from "./util"; +import { Edge, MINIMUM_EDGE_SEPARATION } from "./edge"; + +export class Graph { + nodeMap: Array; + minGraphX: number; + maxGraphX: number; + minGraphY: number; + maxGraphY: number; + maxGraphNodeX: number; + maxBackEdgeNumber: number; + + constructor(data: any) { + this.nodeMap = []; + + this.minGraphX = 0; + this.maxGraphX = 1; + this.minGraphY = 0; + this.maxGraphY = 1; + + data.nodes.forEach((n) => { + n.__proto__ = GNode.prototype; + n.visible = false; + n.x = 0; + n.y = 0; + if (typeof n.pos === "number") { + // Backwards compatibility. + n.sourcePosition = { scriptOffset: n.pos, inliningId: -1 }; + } + n.rank = MAX_RANK_SENTINEL; + n.inputs = []; + n.outputs = []; + n.outputApproach = MINIMUM_NODE_OUTPUT_APPROACH; + // Every control node is a CFG node. + n.cfg = n.control; + this.nodeMap[n.id] = n; + n.displayLabel = n.getDisplayLabel(); + n.labelbbox = measureText(n.displayLabel); + const typebbox = measureText(n.getDisplayType()); + const innerwidth = Math.max(n.labelbbox.width, typebbox.width); + n.width = alignUp(innerwidth + NODE_INPUT_WIDTH * 2, + NODE_INPUT_WIDTH); + const innerheight = Math.max(n.labelbbox.height, typebbox.height); + n.normalheight = innerheight + 20; + }); + + data.edges.forEach((e: any) => { + var t = this.nodeMap[e.target]; + var s = this.nodeMap[e.source]; + var newEdge = new Edge(t, e.index, s, e.type); + t.inputs.push(newEdge); + s.outputs.push(newEdge); + if (e.type == 'control') { + // Every source of a control edge is a CFG node. + s.cfg = true; + } + }); + + } + + *nodes(p = (n: GNode) => true) { + for (const node of this.nodeMap) { + if (!node || !p(node)) continue; + yield node; + } + } + + *filteredEdges(p: (e: Edge) => boolean) { + for (const node of this.nodes()) { + for (const edge of node.inputs) { + if (p(edge)) yield edge; + } + } + } + + forEachEdge(p: (e: Edge) => void) { + for (const node of this.nodeMap) { + if (!node) continue; + for (const edge of node.inputs) { + p(edge); + } + } + } + + redetermineGraphBoundingBox(showTypes: boolean): [[number, number], [[number, number], [number, number]]] { + this.minGraphX = 0; + this.maxGraphNodeX = 1; + this.maxGraphX = undefined; // see below + this.minGraphY = 0; + this.maxGraphY = 1; + + for (const node of this.nodes()) { + if (!node.visible) { + continue; + } + + if (node.x < this.minGraphX) { + this.minGraphX = node.x; + } + if ((node.x + node.getTotalNodeWidth()) > this.maxGraphNodeX) { + this.maxGraphNodeX = node.x + node.getTotalNodeWidth(); + } + if ((node.y - 50) < this.minGraphY) { + this.minGraphY = node.y - 50; + } + if ((node.y + node.getNodeHeight(showTypes) + 50) > this.maxGraphY) { + this.maxGraphY = node.y + node.getNodeHeight(showTypes) + 50; + } + } + + this.maxGraphX = this.maxGraphNodeX + + this.maxBackEdgeNumber * MINIMUM_EDGE_SEPARATION; + + const width = (this.maxGraphX - this.minGraphX); + const height = this.maxGraphY - this.minGraphY; + + const extent: [[number, number], [number, number]] = [ + [this.minGraphX - width / 2, this.minGraphY - height / 2], + [this.maxGraphX + width / 2, this.maxGraphY + height / 2] + ]; + + return [[width, height], extent]; + } + +} diff --git a/tools/turbolizer/src/node.ts b/tools/turbolizer/src/node.ts index 6880494167..79386dc235 100644 --- a/tools/turbolizer/src/node.ts +++ b/tools/turbolizer/src/node.ts @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import {NodeOrigin} from "../src/source-resolver" -import {MINIMUM_EDGE_SEPARATION, Edge} from "../src/edge" +import { NodeOrigin } from "../src/source-resolver" +import { MINIMUM_EDGE_SEPARATION, Edge } from "../src/edge" export const DEFAULT_NODE_BUBBLE_RADIUS = 12; export const NODE_INPUT_WIDTH = 50; @@ -42,6 +42,7 @@ export class GNode { labelbbox: { width: number, height: number }; visitOrderWithinRank: number; cfg: boolean; + normalheight: number; isControl() { return this.control; @@ -158,8 +159,15 @@ export class GNode { return this.y - MINIMUM_NODE_INPUT_APPROACH - (index % 4) * MINIMUM_EDGE_SEPARATION - DEFAULT_NODE_BUBBLE_RADIUS } - getOutputApproach(graph) { - return this.y + this.outputApproach + graph.getNodeHeight(this) + + getNodeHeight(showTypes:boolean): number { + if (showTypes) { + return this.normalheight + this.labelbbox.height; + } else { + return this.normalheight; + } + } + getOutputApproach(showTypes:boolean) { + return this.y + this.outputApproach + this.getNodeHeight(showTypes) + + DEFAULT_NODE_BUBBLE_RADIUS; } getInputX(index) { diff --git a/tools/turbolizer/src/util.ts b/tools/turbolizer/src/util.ts index ef877e508c..ed9a7e7ad2 100644 --- a/tools/turbolizer/src/util.ts +++ b/tools/turbolizer/src/util.ts @@ -114,3 +114,14 @@ export function isIterable(obj: any): obj is Iterable { export function alignUp(raw:number, multiple:number):number { return Math.floor((raw + multiple - 1) / multiple) * multiple; } + +export function measureText(text: string) { + const textMeasure = document.getElementById('text-measure'); + if (textMeasure instanceof SVGTSpanElement) { + textMeasure.textContent = text; + return { + width: textMeasure.getBBox().width, + height: textMeasure.getBBox().height, + }; + } +} \ No newline at end of file diff --git a/tools/turbolizer/tsconfig.json b/tools/turbolizer/tsconfig.json index c54157280f..8e02d86a19 100644 --- a/tools/turbolizer/tsconfig.json +++ b/tools/turbolizer/tsconfig.json @@ -14,6 +14,7 @@ "src/lang-disassembly.ts", "src/node.ts", "src/edge.ts", + "src/graph.ts", "src/source-resolver.ts", "src/selection.ts", "src/selection-broker.ts",