v8/tools/turbolizer/graph-layout.js
bgeron cc51cfcac3 [turbolizer] Redetermine graph bounding box after dragging a node.
If you dragged the node out of the bounding box, this commit allows you to see
it again after you zoom. The zoom looks jittery, but I think it's better than
not being able to see it at all.

BUG=
R=danno

Review-Url: https://codereview.chromium.org/2168713005
Cr-Commit-Position: refs/heads/master@{#38024}
2016-07-25 14:22:23 +00:00

494 lines
14 KiB
JavaScript

// Copyright 2015 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
var DEFAULT_NODE_ROW_SEPARATION = 130
var traceLayout = false;
function newGraphOccupation(graph){
var isSlotFilled = [];
var maxSlot = 0;
var minSlot = 0;
var nodeOccupation = [];
function slotToIndex(slot) {
if (slot >= 0) {
return slot * 2;
} else {
return slot * 2 + 1;
}
}
function indexToSlot(index) {
if ((index % 0) == 0) {
return index / 2;
} else {
return -((index - 1) / 2);
}
}
function positionToSlot(pos) {
return Math.floor(pos / NODE_INPUT_WIDTH);
}
function slotToLeftPosition(slot) {
return slot * NODE_INPUT_WIDTH
}
function slotToRightPosition(slot) {
return (slot + 1) * NODE_INPUT_WIDTH
}
function findSpace(pos, width, direction) {
var widthSlots = Math.floor((width + NODE_INPUT_WIDTH - 1) /
NODE_INPUT_WIDTH);
var currentSlot = positionToSlot(pos + width / 2);
var currentScanSlot = currentSlot;
var widthSlotsRemainingLeft = widthSlots;
var widthSlotsRemainingRight = widthSlots;
var slotsChecked = 0;
while (true) {
var 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
}
if (widthSlotsRemainingLeft == 0 ||
widthSlotsRemainingRight == 0 ||
(widthSlotsRemainingLeft + widthSlotsRemainingRight) == widthSlots &&
(widthSlots == slotsChecked)) {
if (mod) {
return [currentScanSlot, widthSlots];
} else {
return [currentScanSlot - widthSlots + 1, widthSlots];
}
}
} else {
if (mod) {
widthSlotsRemainingLeft = widthSlots;
} else {
widthSlotsRemainingRight = widthSlots;
}
}
}
}
function setIndexRange(from, to, value) {
if (to < from) {
throw("illegal slot range");
}
while (from <= to) {
if (from > maxSlot) {
maxSlot = from;
}
if (from < minSlot) {
minSlot = from;
}
isSlotFilled[slotToIndex(from++)] = value;
}
}
function occupySlotRange(from, to) {
if (traceLayout) {
console.log("Occupied [" + slotToLeftPosition(from) + " " + slotToLeftPosition(to + 1) + ")");
}
setIndexRange(from, to, true);
}
function clearSlotRange(from, to) {
if (traceLayout) {
console.log("Cleared [" + slotToLeftPosition(from) + " " + slotToLeftPosition(to + 1) + ")");
}
setIndexRange(from, to, false);
}
function occupyPositionRange(from, to) {
occupySlotRange(positionToSlot(from), positionToSlot(to - 1));
}
function clearPositionRange(from, to) {
clearSlotRange(positionToSlot(from), positionToSlot(to - 1));
}
function occupyPositionRangeWithMargin(from, to, margin) {
var fromMargin = from - Math.floor(margin);
var toMargin = to + Math.floor(margin);
occupyPositionRange(fromMargin, toMargin);
}
function clearPositionRangeWithMargin(from, to, margin) {
var fromMargin = from - Math.floor(margin);
var toMargin = to + Math.floor(margin);
clearPositionRange(fromMargin, toMargin);
}
var occupation = {
occupyNodeInputs: function(node) {
for (var i = 0; i < node.inputs.length; ++i) {
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);
}
occupyPositionRangeWithMargin(horizontalPos,
horizontalPos,
NODE_INPUT_WIDTH / 2);
}
}
}
},
occupyNode: function(node) {
var getPlacementHint = function(n) {
var pos = 0;
var direction = -1;
var outputEdges = 0;
var inputEdges = 0;
for (var k = 0; k < n.outputs.length; ++k) {
var outputEdge = n.outputs[k];
if (outputEdge.isVisible()) {
var output = n.outputs[k].target;
for (var l = 0; l < output.inputs.length; ++l) {
if (output.rank > n.rank) {
var inputEdge = output.inputs[l];
if (inputEdge.isVisible()) {
++inputEdges;
}
if (output.inputs[l].source == n) {
pos += output.x + output.getInputX(l) + 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];
}
var width = node.getTotalNodeWidth();
var margin = MINIMUM_EDGE_SEPARATION;
var paddedWidth = width + 2 * margin;
var placementHint = getPlacementHint(node);
var x = placementHint[1] - paddedWidth + margin;
if (traceLayout) {
console.log("Node " + node.id + " placement hint [" + x + ", " + (x + paddedWidth) + ")");
}
var placement = findSpace(x, paddedWidth, placementHint[0]);
var firstSlot = placement[0];
var slotWidth = placement[1];
var 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;
} else {
return slotToLeftPosition(firstSlot + slotWidth / 2) - (width / 2);
}
},
clearOccupiedNodes: function() {
nodeOccupation.forEach(function(o) {
clearSlotRange(o[0], o[1]);
});
nodeOccupation = [];
},
clearNodeOutputs: function(source) {
source.outputs.forEach(function(edge) {
if (edge.isVisible()) {
var target = edge.target;
for (var i = 0; i < target.inputs.length; ++i) {
if (target.inputs[i].source === source) {
var horizontalPos = edge.getInputHorizontalPosition(graph);
clearPositionRangeWithMargin(horizontalPos,
horizontalPos,
NODE_INPUT_WIDTH / 2);
}
}
}
});
},
print: function() {
var s = "";
for (var currentSlot = -40; currentSlot < 40; ++currentSlot) {
if (currentSlot != 0) {
s += " ";
} else {
s += "|";
}
}
console.log(s);
s = "";
for (var currentSlot2 = -40; currentSlot2 < 40; ++currentSlot2) {
if (isSlotFilled[slotToIndex(currentSlot2)]) {
s += "*";
} else {
s += " ";
}
}
console.log(s);
}
}
return occupation;
}
function layoutNodeGraph(graph) {
// First determine the set of nodes that have no outputs. Those are the
// basis for bottom-up DFS to determine rank and node placement.
var endNodesHasNoOutputs = [];
var startNodesHasNoInputs = [];
graph.nodes.forEach(function(n, i){
endNodesHasNoOutputs[n.id] = true;
startNodesHasNoInputs[n.id] = true;
});
graph.edges.forEach(function(e, i){
endNodesHasNoOutputs[e.source.id] = false;
startNodesHasNoInputs[e.target.id] = false;
});
// Finialize the list of start and end nodes.
var endNodes = [];
var startNodes = [];
var visited = [];
var rank = [];
graph.nodes.forEach(function(n, i){
if (endNodesHasNoOutputs[n.id]) {
endNodes.push(n);
}
if (startNodesHasNoInputs[n.id]) {
startNodes.push(n);
}
visited[n.id] = false;
rank[n.id] = -1;
n.rank = 0;
n.visitOrderWithinRank = 0;
n.outputApproach = MINIMUM_NODE_OUTPUT_APPROACH;
});
var maxRank = 0;
var visited = [];
var dfsStack = [];
var visitOrderWithinRank = 0;
var worklist = startNodes.slice();
while (worklist.length != 0) {
var n = worklist.pop();
var changed = false;
if (n.rank == MAX_RANK_SENTINEL) {
n.rank = 1;
changed = true;
}
var begin = 0;
var end = n.inputs.length;
if (n.opcode == 'Phi' || n.opcode == 'EffectPhi') {
// Keep with merge or loop node
begin = n.inputs.length - 1;
} else if (n.hasBackEdges()) {
end = 1;
}
for (var l = begin; l < end; ++l) {
var input = n.inputs[l].source;
if (input.visible && input.rank >= n.rank) {
n.rank = input.rank + 1;
changed = true;
}
}
if (changed) {
var hasBackEdges = n.hasBackEdges();
for (var 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);
}
}
}
if (n.rank > maxRank) {
maxRank = n.rank;
}
}
visited = [];
function dfsFindRankLate(n) {
if (visited[n.id]) return;
visited[n.id] = true;
var originalRank = n.rank;
var newRank = n.rank;
var firstInput = true;
for (var l = 0; l < n.outputs.length; ++l) {
var output = n.outputs[l].target;
dfsFindRankLate(output);
var outputRank = output.rank;
if (output.visible && (firstInput || outputRank <= newRank) &&
(outputRank > originalRank)) {
newRank = outputRank - 1;
}
firstInput = false;
}
if (n.opcode != "Start" && n.opcode != "Phi" && n.opcode != "EffectPhi") {
n.rank = newRank;
}
}
startNodes.forEach(dfsFindRankLate);
visited = [];
function dfsRankOrder(n) {
if (visited[n.id]) return;
visited[n.id] = true;
for (var l = 0; l < n.outputs.length; ++l) {
var edge = n.outputs[l];
if (edge.isVisible()) {
var output = edge.target;
dfsRankOrder(output);
}
}
if (n.visitOrderWithinRank == 0) {
n.visitOrderWithinRank = ++visitOrderWithinRank;
}
}
startNodes.forEach(dfsRankOrder);
endNodes.forEach(function(n) {
n.rank = maxRank + 1;
});
var rankSets = [];
// Collect sets for each rank.
graph.nodes.forEach(function(n, i){
n.y = n.rank * (DEFAULT_NODE_ROW_SEPARATION + graph.getNodeHeight() +
2 * DEFAULT_NODE_BUBBLE_RADIUS);
if (n.visible) {
if (rankSets[n.rank] === undefined) {
rankSets[n.rank] = [n];
} else {
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) {
for (var i = 0; i < rankSet.length; ++i) {
occupation.clearNodeOutputs(rankSet[i]);
}
if (traceLayout) {
console.log("After clearing outputs");
occupation.print();
}
var placedCount = 0;
rankSet = rankSet.sort(function(a,b) {
return a.visitOrderWithinRank < b.visitOrderWithinRank;
});
for (var i = 0; i < rankSet.length; ++i) {
var nodeToPlace = rankSet[i];
if (nodeToPlace.visible) {
nodeToPlace.x = occupation.occupyNode(nodeToPlace);
if (traceLayout) {
console.log("Node " + nodeToPlace.id + " is placed between [" + nodeToPlace.x + ", " + (nodeToPlace.x + nodeToPlace.getTotalNodeWidth()) + ")");
}
var staggeredFlooredI = Math.floor(placedCount++ % 3);
var delta = MINIMUM_EDGE_SEPARATION * staggeredFlooredI
nodeToPlace.outputApproach += delta;
} else {
nodeToPlace.x = 0;
}
}
if (traceLayout) {
console.log("Before clearing nodes");
occupation.print();
}
occupation.clearOccupiedNodes();
if (traceLayout) {
console.log("After clearing nodes");
occupation.print();
}
for (var i = 0; i < rankSet.length; ++i) {
var node = rankSet[i];
occupation.occupyNodeInputs(node);
}
if (traceLayout) {
console.log("After occupying inputs");
occupation.print();
}
if (traceLayout) {
console.log("After determining bounding box");
occupation.print();
}
});
graph.maxBackEdgeNumber = 0;
graph.visibleEdges.each(function (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() + 50) > graph.maxGraphY) {
graph.maxGraphY = node.y + graph.getNodeHeight() + 50;
}
}
graph.maxGraphX = graph.maxGraphNodeX +
graph.maxBackEdgeNumber * MINIMUM_EDGE_SEPARATION;
}