// 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 "../src/constants"; import { Edge } from "../src/edge"; import { GNode, MINIMUM_EDGE_SEPARATION, NODE_INPUT_WIDTH, MINIMUM_NODE_OUTPUT_APPROACH, DEFAULT_NODE_BUBBLE_RADIUS } from "../src/node"; import { Graph } from "./graph"; const DEFAULT_NODE_ROW_SEPARATION = 150; const traceLayout = false; 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; } } function positionToSlot(pos: number) { return Math.floor(pos / NODE_INPUT_WIDTH); } function slotToLeftPosition(slot: number) { return slot * NODE_INPUT_WIDTH; } function findSpace(pos: number, width: number, direction: number) { const widthSlots = Math.floor((width + NODE_INPUT_WIDTH - 1) / 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; } 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: 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; } isSlotFilled[slotToIndex(from++)] = value; } } function occupySlotRange(from: number, to: number) { if (traceLayout) { console.log("Occupied [" + slotToLeftPosition(from) + " " + slotToLeftPosition(to + 1) + ")"); } setIndexRange(from, to, true); } function clearSlotRange(from: number, to: number) { if (traceLayout) { console.log("Cleared [" + slotToLeftPosition(from) + " " + slotToLeftPosition(to + 1) + ")"); } 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)); } function occupyPositionRangeWithMargin(from: number, to: number, margin: number) { const fromMargin = from - Math.floor(margin); const toMargin = to + Math.floor(margin); occupyPositionRange(fromMargin, toMargin); } 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: GNode, 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 (traceLayout) { console.log("Occupying input " + i + " of " + node.id + " at " + horizontalPos); } occupyPositionRangeWithMargin(horizontalPos, horizontalPos, NODE_INPUT_WIDTH / 2); } } } }, occupyNode: function (node: GNode) { const getPlacementHint = function (n: GNode) { 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) + 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 = MINIMUM_EDGE_SEPARATION; const paddedWidth = width + 2 * margin; const placementHint = getPlacementHint(node); const x = placementHint[1] - paddedWidth + margin; if (traceLayout) { console.log("Node " + node.id + " placement hint [" + x + ", " + (x + paddedWidth) + ")"); } 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; } else { return slotToLeftPosition(firstSlot + slotWidth / 2) - (width / 2); } }, clearOccupiedNodes: function () { nodeOccupation.forEach(([firstSlot, endSlotExclusive]) => { clearSlotRange(firstSlot, endSlotExclusive); }); nodeOccupation = []; }, clearNodeOutputs: function (source: GNode, 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, 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: Edge) => { endNodesHasNoOutputs[e.source.id] = false; startNodesHasNoInputs[e.target.id] = false; }); // Finialize the list of start and end nodes. const endNodes: Array = []; const startNodes: Array = []; let visited: Array = []; const rank: Array = []; for (const n of graph.nodes()) { 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; } if (traceLayout) { console.log(`layoutGraph init ${performance.now() - start}`); } let maxRank = 0; visited = []; let visitOrderWithinRank = 0; const worklist: Array = startNodes.slice(); while (worklist.length != 0) { const n: GNode = worklist.pop(); let changed = false; if (n.rank == 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); } } } if (n.rank > maxRank) { maxRank = n.rank; } } if (traceLayout) { console.log(`layoutGraph worklist ${performance.now() - start}`); } visited = []; function dfsFindRankLate(n: GNode) { 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; } 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: GNode) { if (visited[n.id]) return; visited[n.id] = true; for (const outputEdge of n.outputs) { if (outputEdge.isVisible()) { const output = outputEdge.target; dfsRankOrder(output); } } if (n.visitOrderWithinRank == 0) { n.visitOrderWithinRank = ++visitOrderWithinRank; } } startNodes.forEach(dfsRankOrder); endNodes.forEach(function (n) { n.rank = maxRank + 1; }); const rankSets: Array> = []; // Collect sets for each rank. 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) { 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. const occupation = newGraphOccupation(graph); rankSets.reverse().forEach(function (rankSet: Array) { for (const node of rankSet) { occupation.clearNodeOutputs(node, showTypes); } if (traceLayout) { console.log("After clearing outputs"); occupation.print(); } let placedCount = 0; rankSet = rankSet.sort((a: GNode, b: GNode) => { if (a.visitOrderWithinRank < b.visitOrderWithinRank) { return -1; } else if (a.visitOrderWithinRank == b.visitOrderWithinRank) { return 0; } else { return 1; } }); for (const nodeToPlace of rankSet) { if (nodeToPlace.visible) { nodeToPlace.x = occupation.occupyNode(nodeToPlace); if (traceLayout) { console.log("Node " + nodeToPlace.id + " is placed between [" + nodeToPlace.x + ", " + (nodeToPlace.x + nodeToPlace.getTotalNodeWidth()) + ")"); } const staggeredFlooredI = Math.floor(placedCount++ % 3); const 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 (const node of rankSet) { occupation.occupyNodeInputs(node, showTypes); } if (traceLayout) { console.log("After occupying inputs"); occupation.print(); } if (traceLayout) { console.log("After determining bounding box"); occupation.print(); } }); graph.maxBackEdgeNumber = 0; graph.forEachEdge((e: Edge) => { if (e.isBackEdge()) { e.backEdgeNumber = ++graph.maxBackEdgeNumber; } else { e.backEdgeNumber = 0; } }); }