Commit f423e485 authored by Danylo Boiko's avatar Danylo Boiko Committed by V8 LUCI CQ

[turbolizer] Graph layout caching

- "Remember graph layout" button
- Graph layout caching (almost 10x speed up)
- Camera position and zoom saving
- Refactored graph.ts, graph-layout.ts and graphmultiview.ts

Bug: v8:7327
Change-Id: I6a9db1ddbbaf506bff0b9d1c1e015f245c7c3974
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3714248
Commit-Queue: Danylo Boiko <danielboyko02@gmail.com>
Reviewed-by: 's avatarNico Hartmann <nicohartmann@chromium.org>
Cr-Commit-Position: refs/heads/main@{#81309}
parent c3f60e8c
This diff was suppressed by a .gitattributes entry.
......@@ -12,6 +12,7 @@ export const MINIMUM_EDGE_SEPARATION = 20;
export const MINIMUM_NODE_INPUT_APPROACH = 15 + 2 * DEFAULT_NODE_BUBBLE_RADIUS;
export const DEFAULT_NODE_ROW_SEPARATION = 150;
export const TRACE_LAYOUT = false;
export const MULTIVIEW_ID = "multiview";
export const SOURCE_PANE_ID = "left";
export const SOURCE_COLLAPSE_ID = "source-shrink";
export const SOURCE_EXPAND_ID = "source-expand";
......
......@@ -78,3 +78,13 @@ export function createElement(tag: string, cls: string, content?: string): HTMLE
if (content !== undefined) el.innerText = content;
return el;
}
export function storageGetItem(key: string, defaultValue?: any, parse: boolean = true): any {
let value = window.sessionStorage.getItem(key);
if (parse) value = JSON.parse(value);
return value === null ? defaultValue : value;
}
export function storageSetItem(key: string, value: any): void {
window.sessionStorage.setItem(key, value);
}
......@@ -6,453 +6,461 @@ import * as C from "./common/constants";
import { Graph } from "./graph";
import { GraphNode } from "./phases/graph-phase/graph-node";
import { GraphEdge } from "./phases/graph-phase/graph-edge";
import { GraphStateType } from "./phases/graph-phase";
export class GraphLayout {
graph: Graph;
graphOccupation: GraphOccupation;
startTime: number;
maxRank: number;
visitOrderWithinRank: number;
constructor(graph: Graph) {
this.graph = graph;
this.graphOccupation = new GraphOccupation(graph);
this.maxRank = 0;
this.visitOrderWithinRank = 0;
}
function newGraphOccupation(graph: Graph) {
const isSlotFilled = [];
let maxSlot = 0;
let minSlot = 0;
let nodeOccupation: Array<[number, number]> = [];
function slotToIndex(slot: number) {
if (slot >= 0) {
return slot * 2;
} else {
return slot * 2 + 1;
public rebuild(showTypes: boolean): void {
switch (this.graph.graphPhase.stateType) {
case GraphStateType.NeedToFullRebuild:
this.fullRebuild(showTypes);
break;
case GraphStateType.Cached:
this.cachedRebuild();
break;
default:
throw "Unsupported graph state type";
}
this.graph.graphPhase.rendered = true;
}
function positionToSlot(pos: number) {
return Math.floor(pos / C.NODE_INPUT_WIDTH);
public fullRebuild(showTypes: boolean): void {
this.startTime = performance.now();
this.maxRank = 0;
this.visitOrderWithinRank = 0;
const [startNodes, endNodes] = this.initNodes();
this.initWorkList(startNodes);
let visited = new Array<boolean>();
startNodes.forEach((sn: GraphNode) => this.dfsFindRankLate(visited, sn));
visited = new Array<boolean>();
startNodes.forEach((sn: GraphNode) => this.dfsRankOrder(visited, sn));
endNodes.forEach((node: GraphNode) => node.rank = this.maxRank + 1);
const rankSets = this.getRankSets(showTypes);
this.placeNodes(rankSets, showTypes);
this.calculateBackEdgeNumbers();
this.graph.graphPhase.stateType = GraphStateType.Cached;
}
function slotToLeftPosition(slot: number) {
return slot * C.NODE_INPUT_WIDTH;
public cachedRebuild(): void {
this.calculateBackEdgeNumbers();
}
function findSpace(pos: number, width: number, direction: number) {
const widthSlots = Math.floor((width + C.NODE_INPUT_WIDTH - 1) /
C.NODE_INPUT_WIDTH);
const currentSlot = positionToSlot(pos + width / 2);
let currentScanSlot = currentSlot;
let widthSlotsRemainingLeft = widthSlots;
let widthSlotsRemainingRight = widthSlots;
let slotsChecked = 0;
while (true) {
const mod = slotsChecked++ % 2;
currentScanSlot = currentSlot + (mod ? -1 : 1) * (slotsChecked >> 1);
if (!isSlotFilled[slotToIndex(currentScanSlot)]) {
if (mod) {
if (direction <= 0) --widthSlotsRemainingLeft;
} else {
if (direction >= 0) --widthSlotsRemainingRight;
private initNodes(): [Array<GraphNode>, Array<GraphNode>] {
// First determine the set of nodes that have no outputs. Those are the
// basis for bottom-up DFS to determine rank and node placement.
const endNodesHasNoOutputs = new Array<boolean>();
const startNodesHasNoInputs = new Array<boolean>();
for (const node of this.graph.nodes()) {
endNodesHasNoOutputs[node.id] = true;
startNodesHasNoInputs[node.id] = true;
}
this.graph.forEachEdge((edge: GraphEdge) => {
endNodesHasNoOutputs[edge.source.id] = false;
startNodesHasNoInputs[edge.target.id] = false;
});
// Finialize the list of start and end nodes.
const endNodes = new Array<GraphNode>();
const startNodes = new Array<GraphNode>();
const visited = new Array<boolean>();
for (const node of this.graph.nodes()) {
if (endNodesHasNoOutputs[node.id]) {
endNodes.push(node);
}
if (startNodesHasNoInputs[node.id]) {
startNodes.push(node);
}
visited[node.id] = false;
node.rank = 0;
node.visitOrderWithinRank = 0;
node.outputApproach = C.MINIMUM_NODE_OUTPUT_APPROACH;
}
this.trace("layoutGraph init");
return [startNodes, endNodes];
}
private initWorkList(startNodes: Array<GraphNode>): void {
const workList = startNodes.slice();
while (workList.length != 0) {
const node = workList.pop();
let changed = false;
if (node.rank == C.MAX_RANK_SENTINEL) {
node.rank = 1;
changed = true;
}
let begin = 0;
let end = node.inputs.length;
if (node.nodeLabel.opcode === "Phi" ||
node.nodeLabel.opcode === "EffectPhi" ||
node.nodeLabel.opcode === "InductionVariablePhi") {
// Keep with merge or loop node
begin = node.inputs.length - 1;
} else if (node.hasBackEdges()) {
end = 1;
}
for (let l = begin; l < end; ++l) {
const input = node.inputs[l].source;
if (input.visible && input.rank >= node.rank) {
node.rank = input.rank + 1;
changed = true;
}
if (widthSlotsRemainingLeft == 0 ||
widthSlotsRemainingRight == 0 ||
(widthSlotsRemainingLeft + widthSlotsRemainingRight) == widthSlots &&
(widthSlots == slotsChecked)) {
if (mod) {
return [currentScanSlot, widthSlots];
}
if (changed) {
const hasBackEdges = node.hasBackEdges();
for (let l = node.outputs.length - 1; l >= 0; --l) {
if (hasBackEdges && (l != 0)) {
workList.unshift(node.outputs[l].target);
} else {
return [currentScanSlot - widthSlots + 1, widthSlots];
workList.push(node.outputs[l].target);
}
}
} else {
if (mod) {
widthSlotsRemainingLeft = widthSlots;
} else {
widthSlotsRemainingRight = widthSlots;
}
}
this.maxRank = Math.max(node.rank, this.maxRank);
}
this.trace("layoutGraph work list");
}
function setIndexRange(from: number, to: number, value: boolean) {
if (to < from) {
throw ("illegal slot range");
}
while (from <= to) {
if (from > maxSlot) {
maxSlot = from;
}
if (from < minSlot) {
minSlot = from;
private dfsFindRankLate(visited: Array<boolean>, node: GraphNode): void {
if (visited[node.id]) return;
visited[node.id] = true;
const originalRank = node.rank;
let newRank = node.rank;
let isFirstInput = true;
for (const outputEdge of node.outputs) {
const output = outputEdge.target;
this.dfsFindRankLate(visited, output);
const outputRank = output.rank;
if (output.visible && (isFirstInput || outputRank <= newRank) &&
(outputRank > originalRank)) {
newRank = outputRank - 1;
}
isSlotFilled[slotToIndex(from++)] = value;
isFirstInput = false;
}
}
function occupySlotRange(from: number, to: number) {
if (C.TRACE_LAYOUT) {
console.log("Occupied [" + slotToLeftPosition(from) + " " + slotToLeftPosition(to + 1) + ")");
if (node.nodeLabel.opcode !== "Start" && node.nodeLabel.opcode !== "Phi"
&& node.nodeLabel.opcode !== "EffectPhi"
&& node.nodeLabel.opcode !== "InductionVariablePhi") {
node.rank = newRank;
}
setIndexRange(from, to, true);
}
function clearSlotRange(from: number, to: number) {
if (C.TRACE_LAYOUT) {
console.log("Cleared [" + slotToLeftPosition(from) + " " + slotToLeftPosition(to + 1) + ")");
private dfsRankOrder(visited: Array<boolean>, node: GraphNode): void {
if (visited[node.id]) return;
visited[node.id] = true;
for (const outputEdge of node.outputs) {
if (outputEdge.isVisible()) {
const output = outputEdge.target;
this.dfsRankOrder(visited, output);
}
}
if (node.visitOrderWithinRank == 0) {
node.visitOrderWithinRank = ++this.visitOrderWithinRank;
}
setIndexRange(from, to, false);
}
function occupyPositionRange(from: number, to: number) {
occupySlotRange(positionToSlot(from), positionToSlot(to - 1));
}
function clearPositionRange(from: number, to: number) {
clearSlotRange(positionToSlot(from), positionToSlot(to - 1));
private getRankSets(showTypes: boolean): Array<Array<GraphNode>> {
const rankSets = new Array<Array<GraphNode>>();
for (const node of this.graph.nodes()) {
node.y = node.rank * (C.DEFAULT_NODE_ROW_SEPARATION +
node.getNodeHeight(showTypes) + 2 * C.DEFAULT_NODE_BUBBLE_RADIUS);
if (node.visible) {
if (!rankSets[node.rank]) {
rankSets[node.rank] = new Array<GraphNode>(node);
} else {
rankSets[node.rank].push(node);
}
}
}
return rankSets;
}
function occupyPositionRangeWithMargin(from: number, to: number, margin: number) {
const fromMargin = from - Math.floor(margin);
const toMargin = to + Math.floor(margin);
occupyPositionRange(fromMargin, toMargin);
}
private placeNodes(rankSets: Array<Array<GraphNode>>, showTypes: boolean): void {
// 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.
rankSets.reverse().forEach((rankSet: Array<GraphNode>) => {
for (const node of rankSet) {
this.graphOccupation.clearNodeOutputs(node, showTypes);
}
function clearPositionRangeWithMargin(from: number, to: number, margin: number) {
const fromMargin = from - Math.floor(margin);
const toMargin = to + Math.floor(margin);
clearPositionRange(fromMargin, toMargin);
}
const occupation = {
occupyNodeInputs: function (node: GraphNode, showTypes: boolean) {
for (let i = 0; i < node.inputs.length; ++i) {
if (node.inputs[i].isVisible()) {
const edge = node.inputs[i];
if (!edge.isBackEdge()) {
const horizontalPos = edge.getInputHorizontalPosition(graph, showTypes);
if (C.TRACE_LAYOUT) {
console.log("Occupying input " + i + " of " + node.id + " at " + horizontalPos);
}
occupyPositionRangeWithMargin(horizontalPos,
horizontalPos,
C.NODE_INPUT_WIDTH / 2);
}
this.traceOccupation("After clearing outputs");
let placedCount = 0;
rankSet = rankSet.sort((a: GraphNode, b: GraphNode) => a.compare(b));
for (const node of rankSet) {
if (node.visible) {
node.x = this.graphOccupation.occupyNode(node);
const nodeTotalWidth = node.getTotalNodeWidth();
this.trace(`Node ${node.id} is placed between [${node.x}, ${node.x + nodeTotalWidth})`);
const staggeredFlooredI = Math.floor(placedCount++ % 3);
const delta = C.MINIMUM_EDGE_SEPARATION * staggeredFlooredI;
node.outputApproach += delta;
} else {
node.x = 0;
}
}
},
occupyNode: function (node: GraphNode) {
const getPlacementHint = function (n: GraphNode) {
let pos = 0;
let direction = -1;
let outputEdges = 0;
let inputEdges = 0;
for (const outputEdge of n.outputs) {
if (outputEdge.isVisible()) {
const output = outputEdge.target;
for (let l = 0; l < output.inputs.length; ++l) {
if (output.rank > n.rank) {
const inputEdge = output.inputs[l];
if (inputEdge.isVisible()) {
++inputEdges;
}
if (output.inputs[l].source == n) {
pos += output.x + output.getInputX(l) + C.NODE_INPUT_WIDTH / 2;
outputEdges++;
if (l >= (output.inputs.length / 2)) {
direction = 1;
}
}
}
}
}
}
if (outputEdges != 0) {
pos = pos / outputEdges;
}
if (outputEdges > 1 || inputEdges == 1) {
direction = 0;
}
return [direction, pos];
};
const width = node.getTotalNodeWidth();
const margin = C.MINIMUM_EDGE_SEPARATION;
const paddedWidth = width + 2 * margin;
const placementHint = getPlacementHint(node);
const x = placementHint[1] - paddedWidth + margin;
if (C.TRACE_LAYOUT) {
console.log("Node " + node.id + " placement hint [" + x + ", " + (x + paddedWidth) + ")");
this.traceOccupation("Before clearing nodes");
this.graphOccupation.clearOccupiedNodes();
this.traceOccupation("After clearing nodes");
for (const node of rankSet) {
this.graphOccupation.occupyNodeInputs(node, showTypes);
}
const placement = findSpace(x, paddedWidth, placementHint[0]);
const firstSlot = placement[0];
const slotWidth = placement[1];
const endSlotExclusive = firstSlot + slotWidth - 1;
occupySlotRange(firstSlot, endSlotExclusive);
nodeOccupation.push([firstSlot, endSlotExclusive]);
if (placementHint[0] < 0) {
return slotToLeftPosition(firstSlot + slotWidth) - width - margin;
} else if (placementHint[0] > 0) {
return slotToLeftPosition(firstSlot) + margin;
this.traceOccupation("After occupying inputs and determining bounding box");
});
}
private calculateBackEdgeNumbers(): void {
this.graph.maxBackEdgeNumber = 0;
this.graph.forEachEdge((edge: GraphEdge) => {
if (edge.isBackEdge()) {
edge.backEdgeNumber = ++this.graph.maxBackEdgeNumber;
} else {
return slotToLeftPosition(firstSlot + slotWidth / 2) - (width / 2);
edge.backEdgeNumber = 0;
}
},
clearOccupiedNodes: function () {
nodeOccupation.forEach(([firstSlot, endSlotExclusive]) => {
clearSlotRange(firstSlot, endSlotExclusive);
});
nodeOccupation = [];
},
clearNodeOutputs: function (source: GraphNode, showTypes: boolean) {
source.outputs.forEach(function (edge) {
if (edge.isVisible()) {
const target = edge.target;
for (const inputEdge of target.inputs) {
if (inputEdge.source === source) {
const horizontalPos = edge.getInputHorizontalPosition(graph, showTypes);
clearPositionRangeWithMargin(horizontalPos,
horizontalPos,
C.NODE_INPUT_WIDTH / 2);
}
}
}
});
},
print: function () {
let s = "";
for (let currentSlot = -40; currentSlot < 40; ++currentSlot) {
if (currentSlot != 0) {
s += " ";
} else {
s += "|";
}
}
console.log(s);
s = "";
for (let currentSlot2 = -40; currentSlot2 < 40; ++currentSlot2) {
if (isSlotFilled[slotToIndex(currentSlot2)]) {
s += "*";
} else {
s += " ";
}
}
console.log(s);
}
};
return occupation;
}
});
}
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.
const start = performance.now();
const endNodesHasNoOutputs = [];
const startNodesHasNoInputs = [];
for (const n of graph.nodes()) {
endNodesHasNoOutputs[n.id] = true;
startNodesHasNoInputs[n.id] = true;
}
graph.forEachEdge((e: GraphEdge) => {
endNodesHasNoOutputs[e.source.id] = false;
startNodesHasNoInputs[e.target.id] = false;
});
// Finialize the list of start and end nodes.
const endNodes: Array<GraphNode> = [];
const startNodes: Array<GraphNode> = [];
let visited: Array<boolean> = [];
const rank: Array<number> = [];
for (const n of graph.nodes()) {
if (endNodesHasNoOutputs[n.id]) {
endNodes.push(n);
}
if (startNodesHasNoInputs[n.id]) {
startNodes.push(n);
private trace(message: string): void {
if (C.TRACE_LAYOUT) {
console.log(`${message} ${performance.now() - this.startTime}`);
}
visited[n.id] = false;
rank[n.id] = -1;
n.rank = 0;
n.visitOrderWithinRank = 0;
n.outputApproach = C.MINIMUM_NODE_OUTPUT_APPROACH;
}
if (C.TRACE_LAYOUT) {
console.log(`layoutGraph init ${performance.now() - start}`);
private traceOccupation(message: string): void {
if (C.TRACE_LAYOUT) {
console.log(message);
this.graphOccupation.print();
}
}
}
let maxRank = 0;
visited = [];
let visitOrderWithinRank = 0;
class GraphOccupation {
graph: Graph;
filledSlots: Array<boolean>;
nodeOccupations: Array<[number, number]>;
minSlot: number;
maxSlot: number;
constructor(graph: Graph) {
this.graph = graph;
this.filledSlots = new Array<boolean>();
this.nodeOccupations = new Array<[number, number]>();
this.minSlot = 0;
this.maxSlot = 0;
}
const worklist: Array<GraphNode> = startNodes.slice();
while (worklist.length != 0) {
const n: GraphNode = worklist.pop();
let changed = false;
if (n.rank == C.MAX_RANK_SENTINEL) {
n.rank = 1;
changed = true;
}
let begin = 0;
let end = n.inputs.length;
if (n.nodeLabel.opcode == 'Phi' ||
n.nodeLabel.opcode == 'EffectPhi' ||
n.nodeLabel.opcode == 'InductionVariablePhi') {
// Keep with merge or loop node
begin = n.inputs.length - 1;
} else if (n.hasBackEdges()) {
end = 1;
}
for (let l = begin; l < end; ++l) {
const input = n.inputs[l].source;
if (input.visible && input.rank >= n.rank) {
n.rank = input.rank + 1;
changed = true;
}
}
if (changed) {
const hasBackEdges = n.hasBackEdges();
for (let l = n.outputs.length - 1; l >= 0; --l) {
if (hasBackEdges && (l != 0)) {
worklist.unshift(n.outputs[l].target);
} else {
worklist.push(n.outputs[l].target);
public clearNodeOutputs(source: GraphNode, showTypes: boolean): void {
for (const edge of source.outputs) {
if (!edge.isVisible()) continue;
for (const inputEdge of edge.target.inputs) {
if (inputEdge.source === source) {
const horizontalPos = edge.getInputHorizontalPosition(this.graph, showTypes);
this.clearPositionRangeWithMargin(horizontalPos, horizontalPos, C.NODE_INPUT_WIDTH / 2);
}
}
}
if (n.rank > maxRank) {
maxRank = n.rank;
}
public clearOccupiedNodes(): void {
for (const [firstSlot, endSlotExclusive] of this.nodeOccupations) {
this.clearSlotRange(firstSlot, endSlotExclusive);
}
this.nodeOccupations = new Array<[number, number]>();
}
if (C.TRACE_LAYOUT) {
console.log(`layoutGraph worklist ${performance.now() - start}`);
public occupyNode(node: GraphNode): number {
const width = node.getTotalNodeWidth();
const margin = C.MINIMUM_EDGE_SEPARATION;
const paddedWidth = width + 2 * margin;
const [direction, position] = this.getPlacementHint(node);
const x = position - paddedWidth + margin;
this.trace(`Node ${node.id} placement hint [${x}, ${(x + paddedWidth)})`);
const placement = this.findSpace(x, paddedWidth, direction);
const [firstSlot, slotWidth] = placement;
const endSlotExclusive = firstSlot + slotWidth - 1;
this.occupySlotRange(firstSlot, endSlotExclusive);
this.nodeOccupations.push([firstSlot, endSlotExclusive]);
if (direction < 0) {
return this.slotToLeftPosition(firstSlot + slotWidth) - width - margin;
} else if (direction > 0) {
return this.slotToLeftPosition(firstSlot) + margin;
} else {
return this.slotToLeftPosition(firstSlot + slotWidth / 2) - (width / 2);
}
}
visited = [];
function dfsFindRankLate(n: GraphNode) {
if (visited[n.id]) return;
visited[n.id] = true;
const originalRank = n.rank;
let newRank = n.rank;
let isFirstInput = true;
for (const outputEdge of n.outputs) {
const output = outputEdge.target;
dfsFindRankLate(output);
const outputRank = output.rank;
if (output.visible && (isFirstInput || outputRank <= newRank) &&
(outputRank > originalRank)) {
newRank = outputRank - 1;
public occupyNodeInputs(node: GraphNode, showTypes: boolean): void {
for (let i = 0; i < node.inputs.length; ++i) {
if (node.inputs[i].isVisible()) {
const edge = node.inputs[i];
if (!edge.isBackEdge()) {
const horizontalPos = edge.getInputHorizontalPosition(this.graph, showTypes);
this.trace(`Occupying input ${i} of ${node.id} at ${horizontalPos}`);
this.occupyPositionRangeWithMargin(horizontalPos, horizontalPos, C.NODE_INPUT_WIDTH / 2);
}
}
isFirstInput = false;
}
if (n.nodeLabel.opcode != "Start" && n.nodeLabel.opcode != "Phi" && n.nodeLabel.opcode != "EffectPhi" && n.nodeLabel.opcode != "InductionVariablePhi") {
n.rank = newRank;
}
}
startNodes.forEach(dfsFindRankLate);
visited = [];
function dfsRankOrder(n: GraphNode) {
if (visited[n.id]) return;
visited[n.id] = true;
for (const outputEdge of n.outputs) {
if (outputEdge.isVisible()) {
const output = outputEdge.target;
dfsRankOrder(output);
public print(): void {
let output = "";
for (let currentSlot = -40; currentSlot < 40; ++currentSlot) {
if (currentSlot != 0) {
output += " ";
} else {
output += "|";
}
}
if (n.visitOrderWithinRank == 0) {
n.visitOrderWithinRank = ++visitOrderWithinRank;
console.log(output);
output = "";
for (let currentSlot2 = -40; currentSlot2 < 40; ++currentSlot2) {
if (this.filledSlots[this.slotToIndex(currentSlot2)]) {
output += "*";
} else {
output += " ";
}
}
console.log(output);
}
startNodes.forEach(dfsRankOrder);
endNodes.forEach(function (n) {
n.rank = maxRank + 1;
});
const rankSets: Array<Array<GraphNode>> = [];
// Collect sets for each rank.
for (const n of graph.nodes()) {
n.y = n.rank * (C.DEFAULT_NODE_ROW_SEPARATION + n.getNodeHeight(showTypes) +
2 * C.DEFAULT_NODE_BUBBLE_RADIUS);
if (n.visible) {
if (rankSets[n.rank] === undefined) {
rankSets[n.rank] = [n];
} else {
rankSets[n.rank].push(n);
private getPlacementHint(node: GraphNode): [number, number] {
let position = 0;
let direction = -1;
let outputEdges = 0;
let inputEdges = 0;
for (const outputEdge of node.outputs) {
if (!outputEdge.isVisible()) continue;
const output = outputEdge.target;
for (let l = 0; l < output.inputs.length; ++l) {
if (output.rank > node.rank) {
const inputEdge = output.inputs[l];
if (inputEdge.isVisible()) ++inputEdges;
if (output.inputs[l].source == node) {
position += output.x + output.getInputX(l) + C.NODE_INPUT_WIDTH / 2;
outputEdges++;
if (l >= (output.inputs.length / 2)) {
direction = 1;
}
}
}
}
}
if (outputEdges != 0) {
position /= outputEdges;
}
if (outputEdges > 1 || inputEdges == 1) {
direction = 0;
}
return [direction, position];
}
// 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.
const occupation = newGraphOccupation(graph);
private occupyPositionRange(from: number, to: number): void {
this.occupySlotRange(this.positionToSlot(from), this.positionToSlot(to - 1));
}
rankSets.reverse().forEach(function (rankSet: Array<GraphNode>) {
private clearPositionRange(from: number, to: number): void {
this.clearSlotRange(this.positionToSlot(from), this.positionToSlot(to - 1));
}
for (const node of rankSet) {
occupation.clearNodeOutputs(node, showTypes);
}
private occupySlotRange(from: number, to: number): void {
this.trace(`Occupied [${this.slotToLeftPosition(from)} ${this.slotToLeftPosition(to + 1)})`);
this.setIndexRange(from, to, true);
}
if (C.TRACE_LAYOUT) {
console.log("After clearing outputs");
occupation.print();
}
private clearSlotRange(from: number, to: number): void {
this.trace(`Cleared [${this.slotToLeftPosition(from)} ${this.slotToLeftPosition(to + 1)})`);
this.setIndexRange(from, to, false);
}
let placedCount = 0;
rankSet = rankSet.sort((a: GraphNode, b: GraphNode) => {
if (a.visitOrderWithinRank < b.visitOrderWithinRank) {
return -1;
} else if (a.visitOrderWithinRank == b.visitOrderWithinRank) {
return 0;
} else {
return 1;
}
});
private clearPositionRangeWithMargin(from: number, to: number, margin: number): void {
const fromMargin = from - Math.floor(margin);
const toMargin = to + Math.floor(margin);
this.clearPositionRange(fromMargin, toMargin);
}
private occupyPositionRangeWithMargin(from: number, to: number, margin: number): void {
const fromMargin = from - Math.floor(margin);
const toMargin = to + Math.floor(margin);
this.occupyPositionRange(fromMargin, toMargin);
}
private findSpace(pos: number, width: number, direction: number): [number, number] {
const widthSlots = Math.floor((width + C.NODE_INPUT_WIDTH - 1) /
C.NODE_INPUT_WIDTH);
const currentSlot = this.positionToSlot(pos + width / 2);
let widthSlotsRemainingLeft = widthSlots;
let widthSlotsRemainingRight = widthSlots;
let slotsChecked = 0;
for (const nodeToPlace of rankSet) {
if (nodeToPlace.visible) {
nodeToPlace.x = occupation.occupyNode(nodeToPlace);
if (C.TRACE_LAYOUT) {
console.log("Node " + nodeToPlace.id + " is placed between [" + nodeToPlace.x + ", " + (nodeToPlace.x + nodeToPlace.getTotalNodeWidth()) + ")");
while (true) {
const mod = slotsChecked++ % 2;
const currentScanSlot = currentSlot + (mod ? -1 : 1) * (slotsChecked >> 1);
if (!this.filledSlots[this.slotToIndex(currentScanSlot)]) {
if (mod) {
if (direction <= 0) --widthSlotsRemainingLeft;
} else if (direction >= 0) {
--widthSlotsRemainingRight;
}
if (widthSlotsRemainingLeft == 0 || widthSlotsRemainingRight == 0 ||
(widthSlotsRemainingLeft + widthSlotsRemainingRight) == widthSlots &&
(widthSlots == slotsChecked)) {
return mod ? [currentScanSlot, widthSlots]
: [currentScanSlot - widthSlots + 1, widthSlots];
}
const staggeredFlooredI = Math.floor(placedCount++ % 3);
const delta = C.MINIMUM_EDGE_SEPARATION * staggeredFlooredI;
nodeToPlace.outputApproach += delta;
} else {
nodeToPlace.x = 0;
if (mod) {
widthSlotsRemainingLeft = widthSlots;
} else {
widthSlotsRemainingRight = widthSlots;
}
}
}
}
if (C.TRACE_LAYOUT) {
console.log("Before clearing nodes");
occupation.print();
private setIndexRange(from: number, to: number, value: boolean): void {
if (to < from) throw ("Illegal slot range");
while (from <= to) {
this.maxSlot = Math.max(from, this.maxSlot);
this.minSlot = Math.min(from, this.minSlot);
this.filledSlots[this.slotToIndex(from++)] = value;
}
}
occupation.clearOccupiedNodes();
if (C.TRACE_LAYOUT) {
console.log("After clearing nodes");
occupation.print();
}
private positionToSlot(position: number): number {
return Math.floor(position / C.NODE_INPUT_WIDTH);
}
for (const node of rankSet) {
occupation.occupyNodeInputs(node, showTypes);
}
private slotToIndex(slot: number): number {
return slot >= 0 ? slot * 2 : slot * 2 + 1;
}
if (C.TRACE_LAYOUT) {
console.log("After occupying inputs");
occupation.print();
}
private slotToLeftPosition(slot: number): number {
return slot * C.NODE_INPUT_WIDTH;
}
private trace(message): void {
if (C.TRACE_LAYOUT) {
console.log("After determining bounding box");
occupation.print();
console.log(message);
}
});
graph.maxBackEdgeNumber = 0;
graph.forEachEdge((e: GraphEdge) => {
if (e.isBackEdge()) {
e.backEdgeNumber = ++graph.maxBackEdgeNumber;
} else {
e.backEdgeNumber = 0;
}
});
}
}
......@@ -3,11 +3,12 @@
// found in the LICENSE file.
import * as C from "./common/constants";
import { GraphPhase } from "./phases/graph-phase";
import { GraphPhase, GraphStateType } from "./phases/graph-phase";
import { GraphEdge } from "./phases/graph-phase/graph-edge";
import { GraphNode } from "./phases/graph-phase/graph-node";
export class Graph {
graphPhase: GraphPhase;
nodeMap: Array<GraphNode>;
minGraphX: number;
maxGraphX: number;
......@@ -19,57 +20,41 @@ export class Graph {
height: number;
constructor(graphPhase: GraphPhase) {
this.nodeMap = [];
this.graphPhase = graphPhase;
this.nodeMap = graphPhase.nodeIdToNodeMap;
this.minGraphX = 0;
this.maxGraphX = 1;
this.minGraphY = 0;
this.maxGraphY = 1;
this.width = 1;
this.height = 1;
graphPhase.data.nodes.forEach((jsonNode: GraphNode) => {
this.nodeMap[jsonNode.id] = new GraphNode(jsonNode.nodeLabel);
});
graphPhase.data.edges.forEach((e: any) => {
const t = this.nodeMap[e.target.id];
const s = this.nodeMap[e.source.id];
const newEdge = new GraphEdge(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: GraphNode) => true) {
public *nodes(func = (n: GraphNode) => true) {
for (const node of this.nodeMap) {
if (!node || !p(node)) continue;
if (!node || !func(node)) continue;
yield node;
}
}
*filteredEdges(p: (e: GraphEdge) => boolean) {
public *filteredEdges(func: (e: GraphEdge) => boolean) {
for (const node of this.nodes()) {
for (const edge of node.inputs) {
if (p(edge)) yield edge;
if (func(edge)) yield edge;
}
}
}
forEachEdge(p: (e: GraphEdge) => void) {
public forEachEdge(func: (e: GraphEdge) => void) {
for (const node of this.nodeMap) {
if (!node) continue;
for (const edge of node.inputs) {
p(edge);
func(edge);
}
}
}
redetermineGraphBoundingBox(showTypes: boolean): [[number, number], [number, number]] {
public redetermineGraphBoundingBox(showTypes: boolean): [[number, number], [number, number]] {
this.minGraphX = 0;
this.maxGraphNodeX = 1;
this.maxGraphX = undefined; // see below
......@@ -77,36 +62,43 @@ export class Graph {
this.maxGraphY = 1;
for (const node of this.nodes()) {
if (!node.visible) {
continue;
}
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.minGraphX = Math.min(this.minGraphX, node.x);
this.maxGraphNodeX = Math.max(this.maxGraphNodeX,
node.x + node.getTotalNodeWidth());
this.minGraphY = Math.min(this.minGraphY, node.y - C.NODE_INPUT_WIDTH);
this.maxGraphY = Math.max(this.maxGraphY,
node.y + node.getNodeHeight(showTypes) + C.NODE_INPUT_WIDTH);
}
this.maxGraphX = this.maxGraphNodeX +
this.maxBackEdgeNumber * C.MINIMUM_EDGE_SEPARATION;
this.maxGraphX = this.maxGraphNodeX + this.maxBackEdgeNumber
* C.MINIMUM_EDGE_SEPARATION;
this.width = this.maxGraphX - this.minGraphX;
this.height = this.maxGraphY - this.minGraphY;
const extent: [[number, number], [number, number]] = [
return [
[this.minGraphX - this.width / 2, this.minGraphY - this.height / 2],
[this.maxGraphX + this.width / 2, this.maxGraphY + this.height / 2]
];
}
return extent;
public makeEdgesVisible(): void {
if (this.graphPhase.stateType == GraphStateType.NeedToFullRebuild) {
this.forEachEdge(edge =>
edge.visible = edge.source.visible && edge.target.visible
);
} else {
this.forEachEdge(edge =>
edge.visible = edge.visible || (this.isRendered() &&
edge.type === "control" && edge.source.visible && edge.target.visible)
);
}
}
public isRendered(): boolean {
return this.graphPhase.rendered;
}
}
......@@ -2,16 +2,17 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as C from "./common/constants";
import { GraphView } from "./views/graph-view";
import { ScheduleView } from "./views/schedule-view";
import { SequenceView } from "./views/sequence-view";
import { SourceResolver } from "./source-resolver";
import { GenericPhase, SourceResolver } from "./source-resolver";
import { SelectionBroker } from "./selection/selection-broker";
import { PhaseView, View } from "./views/view";
import { GraphPhase } from "./phases/graph-phase";
import { GraphNode } from "./phases/graph-phase/graph-node";
const multiviewID = "multiview";
import { storageGetItem, storageSetItem } from "./common/util";
import { PhaseType } from "./phases/phase";
const toolboxHTML = `
<div class="graph-toolbox">
......@@ -32,20 +33,6 @@ export class GraphMultiView extends View {
selectMenu: HTMLSelectElement;
currentPhaseView: PhaseView;
createViewElement() {
const pane = document.createElement("div");
pane.setAttribute("id", multiviewID);
pane.setAttribute("tabindex", "1");
pane.className = "viewpane";
return pane;
}
hide() {
this.container.className = "";
this.hideCurrentPhase();
super.hide();
}
constructor(id, selectionBroker, sourceResolver) {
super(id);
const view = this;
......@@ -57,9 +44,8 @@ export class GraphMultiView extends View {
view.divNode.appendChild(toolbox);
const searchInput = toolbox.querySelector("#search-input") as HTMLInputElement;
const onlyVisibleCheckbox = toolbox.querySelector("#search-only-visible") as HTMLInputElement;
searchInput.addEventListener("keyup", e => {
if (!view.currentPhaseView) return;
view.currentPhaseView.searchInputAction(searchInput, e, onlyVisibleCheckbox.checked);
searchInput.addEventListener("keyup", (e: KeyboardEvent) => {
view.currentPhaseView?.searchInputAction(searchInput, e, onlyVisibleCheckbox.checked);
});
view.divNode.addEventListener("keyup", (e: KeyboardEvent) => {
if (e.keyCode == 191) { // keyCode == '/'
......@@ -70,7 +56,7 @@ export class GraphMultiView extends View {
view.displayPreviousGraphPhase();
}
});
searchInput.setAttribute("value", window.sessionStorage.getItem("lastSearch") || "");
searchInput.setAttribute("value", storageGetItem("lastSearch", "", false));
this.graph = new GraphView(this.divNode, selectionBroker, view.displayPhaseByName.bind(this),
toolbox.querySelector(".graph-toolbox"));
this.schedule = new ScheduleView(this.divNode, selectionBroker);
......@@ -78,65 +64,65 @@ export class GraphMultiView extends View {
this.selectMenu = toolbox.querySelector("#phase-select") as HTMLSelectElement;
}
initializeSelect() {
const view = this;
view.selectMenu.innerHTML = "";
view.sourceResolver.forEachPhase(phase => {
const optionElement = document.createElement("option");
let maxNodeId = "";
if (phase instanceof GraphPhase && phase.highestNodeId != 0) {
maxNodeId = ` ${phase.highestNodeId}`;
}
optionElement.text = `${phase.name}${maxNodeId}`;
view.selectMenu.add(optionElement);
});
this.selectMenu.onchange = function (this: HTMLSelectElement) {
const phaseIndex = this.selectedIndex;
window.sessionStorage.setItem("lastSelectedPhase", phaseIndex.toString());
view.displayPhase(view.sourceResolver.getPhase(phaseIndex));
};
public createViewElement(): HTMLDivElement {
const pane = document.createElement("div");
pane.setAttribute("id", C.MULTIVIEW_ID);
pane.setAttribute("tabindex", "1");
pane.className = "viewpane";
return pane;
}
show() {
public hide(): void {
this.container.className = "";
this.hideCurrentPhase();
super.hide();
}
public show(): void {
// Insert before is used so that the display is inserted before the
// resizer for the RangeView.
this.container.insertBefore(this.divNode, this.container.firstChild);
this.initializeSelect();
const lastPhaseIndex = +window.sessionStorage.getItem("lastSelectedPhase");
const lastPhaseIndex = storageGetItem("lastSelectedPhase");
const initialPhaseIndex = this.sourceResolver.repairPhaseId(lastPhaseIndex);
this.selectMenu.selectedIndex = initialPhaseIndex;
this.displayPhase(this.sourceResolver.getPhase(initialPhaseIndex));
}
displayPhase(phase, selection?: Map<string, GraphNode>) {
if (phase.type == "graph") {
public onresize(): void {
this.currentPhaseView?.onresize();
}
private displayPhase(phase: GenericPhase, selection?: Map<string, GraphNode>): void {
if (phase.type == PhaseType.Graph) {
this.displayPhaseView(this.graph, phase, selection);
} else if (phase.type == "schedule") {
} else if (phase.type == PhaseType.Schedule) {
this.displayPhaseView(this.schedule, phase, selection);
} else if (phase.type == "sequence") {
} else if (phase.type == PhaseType.Sequence) {
this.displayPhaseView(this.sequence, phase, selection);
}
}
displayPhaseView(view: PhaseView, data, selection?: Map<string, GraphNode>) {
private displayPhaseView(view: PhaseView, data: GenericPhase, selection?: Map<string, GraphNode>):
void {
const rememberedSelection = selection ? selection : this.hideCurrentPhase();
view.initializeContent(data, rememberedSelection);
this.currentPhaseView = view;
}
displayPhaseByName(phaseName, selection?: Map<string, GraphNode>) {
private displayPhaseByName(phaseName: string, selection?: Map<string, GraphNode>): void {
const phaseId = this.sourceResolver.getPhaseIdByName(phaseName);
this.selectMenu.selectedIndex = phaseId;
this.displayPhase(this.sourceResolver.getPhase(phaseId), selection);
}
displayNextGraphPhase() {
private displayNextGraphPhase(): void {
let nextPhaseIndex = this.selectMenu.selectedIndex + 1;
while (nextPhaseIndex < this.sourceResolver.phases.length) {
const nextPhase = this.sourceResolver.getPhase(nextPhaseIndex);
if (nextPhase.type == "graph") {
if (nextPhase.type == PhaseType.Graph) {
this.selectMenu.selectedIndex = nextPhaseIndex;
window.sessionStorage.setItem("lastSelectedPhase", nextPhaseIndex.toString());
storageSetItem("lastSelectedPhase", nextPhaseIndex);
this.displayPhase(nextPhase);
break;
}
......@@ -144,13 +130,13 @@ export class GraphMultiView extends View {
}
}
displayPreviousGraphPhase() {
private displayPreviousGraphPhase(): void {
let previousPhaseIndex = this.selectMenu.selectedIndex - 1;
while (previousPhaseIndex >= 0) {
const previousPhase = this.sourceResolver.getPhase(previousPhaseIndex);
if (previousPhase.type == "graph") {
if (previousPhase.type === PhaseType.Graph) {
this.selectMenu.selectedIndex = previousPhaseIndex;
window.sessionStorage.setItem("lastSelectedPhase", previousPhaseIndex.toString());
storageSetItem("lastSelectedPhase", previousPhaseIndex);
this.displayPhase(previousPhase);
break;
}
......@@ -158,7 +144,26 @@ export class GraphMultiView extends View {
}
}
hideCurrentPhase() {
private initializeSelect(): void {
const view = this;
view.selectMenu.innerHTML = "";
view.sourceResolver.forEachPhase((phase: GenericPhase) => {
const optionElement = document.createElement("option");
let maxNodeId = "";
if (phase instanceof GraphPhase && phase.highestNodeId != 0) {
maxNodeId = ` ${phase.highestNodeId}`;
}
optionElement.text = `${phase.name}${maxNodeId}`;
view.selectMenu.add(optionElement);
});
this.selectMenu.onchange = function (this: HTMLSelectElement) {
const phaseIndex = this.selectedIndex;
storageSetItem("lastSelectedPhase", phaseIndex);
view.displayPhase(view.sourceResolver.getPhase(phaseIndex));
};
}
private hideCurrentPhase(): Map<string, GraphNode> {
let rememberedSelection = null;
if (this.currentPhaseView != null) {
rememberedSelection = this.currentPhaseView.detachSelection();
......@@ -167,12 +172,4 @@ export class GraphMultiView extends View {
}
return rememberedSelection;
}
onresize() {
if (this.currentPhaseView) this.currentPhaseView.onresize();
}
detachSelection() {
return null;
}
}
......@@ -2,21 +2,27 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { BytecodeOrigin, NodeOrigin } from "./origin";
import { BytecodePosition, SourcePosition } from "./position";
export class NodeLabel {
id: number;
label: string;
title: string;
live: boolean;
properties: string;
sourcePosition: any;
origin: any;
sourcePosition: SourcePosition | BytecodePosition;
origin: NodeOrigin | BytecodeOrigin;
opcode: string;
control: boolean;
opinfo: string;
type: string;
inplaceUpdatePhase: string;
constructor(id: number, label: string, title: string, live: boolean, properties: string, sourcePosition: any, origin: any, opcode: string, control: boolean, opinfo: string, type: string) {
constructor(id: number, label: string, title: string, live: boolean,
properties: string, sourcePosition: SourcePosition | BytecodePosition,
origin: NodeOrigin | BytecodeOrigin, opcode: string, control: boolean,
opinfo: string, type: string) {
this.id = id;
this.label = label;
this.title = title;
......@@ -31,17 +37,17 @@ export class NodeLabel {
this.inplaceUpdatePhase = null;
}
equals(that?: NodeLabel) {
public equals(that?: NodeLabel): boolean {
if (!that) return false;
if (this.id != that.id) return false;
if (this.label != that.label) return false;
if (this.title != that.title) return false;
if (this.live != that.live) return false;
if (this.properties != that.properties) return false;
if (this.opcode != that.opcode) return false;
if (this.control != that.control) return false;
if (this.opinfo != that.opinfo) return false;
return this.type == that.type;
if (this.id !== that.id) return false;
if (this.label !== that.label) return false;
if (this.title !== that.title) return false;
if (this.live !== that.live) return false;
if (this.properties !== that.properties) return false;
if (this.opcode !== that.opcode) return false;
if (this.control !== that.control) return false;
if (this.opinfo !== that.opinfo) return false;
return this.type === that.type;
}
public getTitle(): string {
......
......@@ -12,16 +12,21 @@ import { GraphEdge } from "./graph-phase/graph-edge";
export class GraphPhase extends Phase {
highestNodeId: number;
data: GraphData;
stateType: GraphStateType;
nodeLabelMap: Array<NodeLabel>;
nodeIdToNodeMap: Array<GraphNode>;
rendered: boolean;
transform: { x: number, y: number, scale: number };
constructor(name: string, highestNodeId: number, data?: GraphData,
nodeLabelMap?: Array<NodeLabel>, nodeIdToNodeMap?: Array<GraphNode>) {
super(name, PhaseType.Graph);
this.highestNodeId = highestNodeId;
this.data = data ?? new GraphData();
this.stateType = GraphStateType.NeedToFullRebuild;
this.nodeLabelMap = nodeLabelMap ?? new Array<NodeLabel>();
this.nodeIdToNodeMap = nodeIdToNodeMap ?? new Array<GraphNode>();
this.rendered = false;
}
public parseDataFromJSON(dataJson, nodeLabelMap: Array<NodeLabel>): void {
......@@ -92,3 +97,8 @@ export class GraphData {
this.edges = edges ?? new Array<GraphEdge>();
}
}
export enum GraphStateType {
NeedToFullRebuild,
Cached
}
......@@ -18,7 +18,7 @@ export class GraphEdge extends Edge<GraphNode> {
}
public getInputHorizontalPosition(graph: Graph, showTypes: boolean): number {
if (this.backEdgeNumber > 0) {
if (graph.graphPhase.rendered && this.backEdgeNumber > 0) {
return graph.maxGraphNodeX + this.backEdgeNumber * C.MINIMUM_EDGE_SEPARATION;
}
const source = this.source;
......
......@@ -58,7 +58,7 @@ export class GraphNode extends Node<GraphEdge> {
opcode.startsWith("Reference") ||
opcode.startsWith("Any") ||
opcode.endsWith("ToNumber") ||
(opcode == "AnyToBoolean") ||
(opcode === "AnyToBoolean") ||
(opcode.startsWith("Load") && opcode.length > 4) ||
(opcode.startsWith("Store") && opcode.length > 5);
}
......@@ -132,4 +132,13 @@ export class GraphNode extends Node<GraphEdge> {
this.nodeLabel.opcode === "InductionVariablePhi") &&
this.inputs[this.inputs.length - 1].source.nodeLabel.opcode === "Loop");
}
public compare(other: GraphNode): number {
if (this.visitOrderWithinRank < other.visitOrderWithinRank) {
return -1;
} else if (this.visitOrderWithinRank == other.visitOrderWithinRank) {
return 0;
}
return 1;
}
}
......@@ -13,7 +13,6 @@ export class InstructionsPhase extends Phase {
// Maps instruction offsets to PC offset.
instructionOffsetToPCOffset?: Array<[number, number]>;
codeOffsetsInfo?: CodeOffsetsInfo;
// Maps instruction numbers to PC offsets.
instructionToPCOffset: Array<TurbolizerInstructionStartInfo>;
// Maps PC offsets to instructions.
......
......@@ -43,8 +43,8 @@ export function sourcePositionValid(l) {
&& typeof l.inliningId !== undefined) || typeof l.bytecodePosition != undefined;
}
type GenericPosition = SourcePosition | BytecodePosition;
type GenericPhase = GraphPhase | TurboshaftGraphPhase | DisassemblyPhase
export type GenericPosition = SourcePosition | BytecodePosition;
export type GenericPhase = GraphPhase | TurboshaftGraphPhase | DisassemblyPhase
| InstructionsPhase | SchedulePhase | SequencePhase;
export class SourceResolver {
......
......@@ -4,15 +4,18 @@
import * as C from "../common/constants";
import * as d3 from "d3";
import { layoutNodeGraph } from "../graph-layout";
import { partial, storageGetItem, storageSetItem } from "../common/util";
import { PhaseView } from "./view";
import { MySelection } from "../selection/selection";
import { partial } from "../common/util";
import { NodeSelectionHandler, ClearableHandler } from "../selection/selection-handler";
import { ClearableHandler, NodeSelectionHandler } from "../selection/selection-handler";
import { Graph } from "../graph";
import { SelectionBroker } from "../selection/selection-broker";
import { GraphNode } from "../phases/graph-phase/graph-node";
import { GraphEdge } from "../phases/graph-phase/graph-edge";
import { GraphLayout } from "../graph-layout";
import { GraphPhase, GraphStateType } from "../phases/graph-phase";
import { BytecodePosition } from "../position";
import { BytecodeOrigin } from "../origin";
interface GraphState {
showTypes: boolean;
......@@ -37,6 +40,7 @@ export class GraphView extends PhaseView {
visibleBubbles: d3.Selection<any, any, any, any>;
transitionTimout: number;
graph: Graph;
graphLayout: GraphLayout;
broker: SelectionBroker;
phaseName: string;
toolbox: HTMLElement;
......@@ -92,8 +96,8 @@ export class GraphView extends PhaseView {
if (node.nodeLabel.sourcePosition) {
locations.push(node.nodeLabel.sourcePosition);
}
if (node.nodeLabel.origin && node.nodeLabel.origin.bytecodePosition) {
locations.push({ bytecodePosition: node.nodeLabel.origin.bytecodePosition });
if (node.nodeLabel.origin && node.nodeLabel.origin instanceof BytecodeOrigin) {
locations.push(new BytecodePosition(node.nodeLabel.origin.bytecodePosition));
}
}
view.state.selection.select(nodes, selected);
......@@ -172,7 +176,6 @@ export class GraphView extends PhaseView {
svg.call(zoomSvg).on("dblclick.zoom", null);
view.panZoom = zoomSvg;
}
getEdgeFrontier(nodes: Iterable<GraphNode>, inEdges: boolean,
......@@ -221,7 +224,7 @@ export class GraphView extends PhaseView {
}
}
initializeContent(data, rememberedSelection) {
initializeContent(data: GraphPhase, rememberedSelection) {
this.show();
function createImgInput(id: string, title: string, onClick): HTMLElement {
const input = document.createElement("input");
......@@ -233,6 +236,12 @@ export class GraphView extends PhaseView {
input.addEventListener("click", onClick);
return input;
}
function createImgToggleInput(id: string, title: string, onClick): HTMLElement {
const input = createImgInput(id, title, onClick);
const toggled = storageGetItem(id, true);
input.classList.toggle("button-input-toggled", toggled);
return input;
}
this.toolbox.appendChild(createImgInput("layout", "layout graph",
partial(this.layoutAction, this)));
this.toolbox.appendChild(createImgInput("show-all", "show all nodes",
......@@ -249,6 +258,8 @@ export class GraphView extends PhaseView {
partial(this.zoomSelectionAction, this)));
this.toolbox.appendChild(createImgInput("toggle-types", "toggle types",
partial(this.toggleTypesAction, this)));
this.toolbox.appendChild(createImgToggleInput("cache-graphs", "remember graph layout",
partial(this.toggleGraphCachingAction)));
const adaptedSelection = this.adaptSelectionToCurrentPhase(data.data, rememberedSelection);
......@@ -256,12 +267,20 @@ export class GraphView extends PhaseView {
this.createGraph(data, adaptedSelection);
this.broker.addNodeHandler(this.selectionHandler);
if (adaptedSelection != null && adaptedSelection.size > 0) {
this.attachSelection(adaptedSelection);
const selectedNodes = adaptedSelection?.size > 0
? this.attachSelection(adaptedSelection)
: null;
if (selectedNodes?.length > 0) {
this.connectVisibleSelectedNodes();
this.viewSelection();
} else {
this.viewWholeGraph();
if (this.isCachingEnabled() && data.transform) {
this.svg.call(this.panZoom.transform, d3.zoomIdentity
.translate(data.transform.x, data.transform.y)
.scale(data.transform.scale));
}
}
}
......@@ -270,37 +289,55 @@ export class GraphView extends PhaseView {
item.parentElement.removeChild(item);
}
for (const n of this.graph.nodes()) {
n.visible = false;
if (!this.isCachingEnabled()) {
this.updateGraphStateType(GraphStateType.NeedToFullRebuild);
}
this.graph.forEachEdge((e: GraphEdge) => {
e.visible = false;
});
this.graph.graphPhase.rendered = false;
this.updateGraphVisibility();
}
public hide(): void {
if (this.isCachingEnabled()) {
const matrix = this.graphElement.node().transform.baseVal.consolidate().matrix;
this.graph.graphPhase.transform = { scale: matrix.a, x: matrix.e, y: matrix.f };
} else {
this.graph.graphPhase.transform = null;
}
super.hide();
this.deleteContent();
}
createGraph(data, selection) {
this.graph = new Graph(data);
this.graphLayout = new GraphLayout(this.graph);
this.showControlAction(this);
if (!this.isCachingEnabled() ||
this.graph.graphPhase.stateType == GraphStateType.NeedToFullRebuild) {
this.updateGraphStateType(GraphStateType.NeedToFullRebuild);
this.showControlAction(this);
} else {
this.showVisible();
}
if (selection != undefined) {
for (const n of this.graph.nodes()) {
n.visible = n.visible || selection.has(n.identifier());
if (selection !== undefined) {
for (const node of this.graph.nodes()) {
node.visible = node.visible || selection.has(node.identifier());
}
}
this.graph.forEachEdge(e => e.visible = e.source.visible && e.target.visible);
this.graph.makeEdgesVisible();
this.layoutGraph();
this.updateGraphVisibility();
}
public showVisible() {
this.updateGraphVisibility();
this.viewWholeGraph();
this.focusOnSvg();
}
connectVisibleSelectedNodes() {
const view = this;
for (const n of view.state.selection) {
......@@ -358,7 +395,7 @@ export class GraphView extends PhaseView {
}
adaptSelectionToCurrentPhase(data, selection) {
const updatedGraphSelection = new Set();
const updatedGraphSelection = new Set<string>();
if (!data || !(selection instanceof Map)) return updatedGraphSelection;
// Adding survived nodes (with the same id)
for (const node of data.nodes) {
......@@ -384,12 +421,16 @@ export class GraphView extends PhaseView {
return updatedGraphSelection;
}
attachSelection(s) {
if (!(s instanceof Set)) return;
public attachSelection(selection: Set<string>): Array<GraphNode> {
if (!(selection instanceof Set)) return new Array<GraphNode>();
this.selectionHandler.clear();
const selected = [...this.graph.nodes(n =>
s.has(this.state.selection.stringKey(n)) && (!this.state.hideDead || n.isLive()))];
const selected = [
...this.graph.nodes(node =>
selection.has(this.state.selection.stringKey(node))
&& (!this.state.hideDead || node.isLive()))
];
this.selectionHandler.select(selected, true);
return selected;
}
detachSelection() {
......@@ -406,6 +447,7 @@ export class GraphView extends PhaseView {
}
layoutAction(graph: GraphView) {
graph.updateGraphStateType(GraphStateType.NeedToFullRebuild);
graph.layoutGraph();
graph.updateGraphVisibility();
graph.viewWholeGraph();
......@@ -429,11 +471,9 @@ export class GraphView extends PhaseView {
n.visible = n.cfg && (!view.state.hideDead || n.isLive());
}
view.graph.forEachEdge((e: GraphEdge) => {
e.visible = e.type == 'control' && e.source.visible && e.target.visible;
e.visible = e.type === "control" && e.source.visible && e.target.visible;
});
view.updateGraphVisibility();
view.viewWholeGraph();
view.focusOnSvg();
view.showVisible();
}
toggleHideDead(view: GraphView) {
......@@ -497,6 +537,14 @@ export class GraphView extends PhaseView {
view.focusOnSvg();
}
private toggleGraphCachingAction(): void {
const key = "cache-graphs";
const toggled = storageGetItem(key, true);
storageSetItem(key, !toggled);
const element = document.getElementById(key);
element.classList.toggle("button-input-toggled", !toggled);
}
searchInputAction(searchBar: HTMLInputElement, e: KeyboardEvent, onlyVisible: boolean) {
if (e.keyCode == 13) {
this.selectionHandler.clear();
......@@ -655,12 +703,16 @@ export class GraphView extends PhaseView {
}
layoutGraph() {
console.time("layoutGraph");
layoutNodeGraph(this.graph, this.state.showTypes);
const layoutMessage = this.graph.graphPhase.stateType == GraphStateType.Cached
? "Layout graph from cache"
: "Layout graph";
console.time(layoutMessage);
this.graphLayout.rebuild(this.state.showTypes);
const extent = this.graph.redetermineGraphBoundingBox(this.state.showTypes);
this.panZoom.translateExtent(extent);
this.minScale();
console.timeEnd("layoutGraph");
console.timeEnd(layoutMessage);
}
selectOrigins() {
......@@ -697,9 +749,10 @@ export class GraphView extends PhaseView {
const state = this.state;
if (!graph) return;
const filteredEdges = [...graph.filteredEdges(function (e) {
return e.source.visible && e.target.visible;
})];
const filteredEdges = [
...graph.filteredEdges(edge => this.graph.isRendered()
&& edge.source.visible && edge.target.visible)
];
const selEdges = view.visibleEdges.selectAll<SVGPathElement, GraphEdge>("path")
.data(filteredEdges, e => e.toString());
......@@ -738,7 +791,7 @@ export class GraphView extends PhaseView {
newAndOldEdges.classed('hidden', e => !e.isVisible());
// select existing nodes
const filteredNodes = [...graph.nodes(n => n.visible)];
const filteredNodes = [...graph.nodes(n => this.graph.isRendered() && n.visible)];
const allNodes = view.visibleNodes.selectAll<SVGGElement, GraphNode>("g");
const selNodes = allNodes.data(filteredNodes, n => n.toString());
......@@ -911,7 +964,7 @@ export class GraphView extends PhaseView {
});
}
getSvgViewDimensions() {
private getSvgViewDimensions(): [number, number] {
return [this.container.clientWidth, this.container.clientHeight];
}
......@@ -920,9 +973,9 @@ export class GraphView extends PhaseView {
}
minScale() {
const dimensions = this.getSvgViewDimensions();
const minXScale = dimensions[0] / (2 * this.graph.width);
const minYScale = dimensions[1] / (2 * this.graph.height);
const [clientWith, clientHeight] = this.getSvgViewDimensions();
const minXScale = clientWith / (2 * this.graph.width);
const minYScale = clientHeight / (2 * this.graph.height);
const minScale = Math.min(minXScale, minYScale);
this.panZoom.scaleExtent([minScale, 40]);
return minScale;
......@@ -984,4 +1037,12 @@ export class GraphView extends PhaseView {
this.graph.minGraphX + this.graph.width / 2,
this.graph.minGraphY + this.graph.height / 2);
}
private updateGraphStateType(stateType: GraphStateType): void {
this.graph.graphPhase.stateType = stateType;
}
private isCachingEnabled(): boolean {
return storageGetItem("cache-graphs", true);
}
}
......@@ -316,7 +316,7 @@ class Helper {
// There are two fixed live ranges for each register, one for normal, another for deferred.
// These are combined into a single row.
const fixedRegisterMap = new Map<string, {ranges: [Range, Range], registerIndex: number}>();
for (const [registerIndex, range] of rangeMap) {
for (const [registerIndex, range] of Object.entries(rangeMap)) {
const registerName = this.fixedRegisterName(range);
if (fixedRegisterMap.has(registerName)) {
const entry = fixedRegisterMap.get(registerName);
......@@ -572,7 +572,7 @@ class RangeViewConstructor {
private addVirtualRanges(row: number) {
const source = this.view.sequenceView.sequence.registerAllocation;
for (const [registerIndex, range] of source.liveRanges) {
for (const [registerIndex, range] of Object.entries(source.liveRanges)) {
const registerName = Helper.virtualRegisterName(registerIndex);
const registerEl = this.elementForVirtualRegister(registerName);
this.addRowToGroup(row, this.elementForRow(row, registerIndex,
......@@ -768,7 +768,7 @@ class PhaseChangeHandler {
this.view.gridAccessor.addGrid(newGrid);
const source = this.view.sequenceView.sequence.registerAllocation;
let row = 0;
for (const [registerIndex, range] of source.liveRanges) {
for (const [registerIndex, range] of Object.entries(source.liveRanges)) {
this.addnewIntervalsInRange(currentGrid, newGrid, row, registerIndex,
new RangePair([range, undefined]));
++row;
......
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