Commit b53dcfd5 authored by Sigurd Schneider's avatar Sigurd Schneider Committed by Commit Bot

[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 <sigurds@chromium.org>
Reviewed-by: 's avatarGeorg Neis <neis@chromium.org>
Cr-Commit-Position: refs/heads/master@{#58549}
parent a53332fe
......@@ -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 +
......
......@@ -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();
}
This diff is collapsed.
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<GNode>;
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];
}
}
......@@ -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) {
......
......@@ -114,3 +114,14 @@ export function isIterable(obj: any): obj is Iterable<any> {
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
......@@ -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",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment