// 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. import {MAX_RANK_SENTINEL} from "./constants.js" import {MINIMUM_EDGE_SEPARATION} from "./edge.js" import {NODE_INPUT_WIDTH, MINIMUM_NODE_OUTPUT_APPROACH, DEFAULT_NODE_BUBBLE_RADIUS} from "./node.js" const 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; } export 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(n) + 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.selectAll("path").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(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(); }