1// Copyright 2015 the V8 project authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5 6import {MAX_RANK_SENTINEL} from "./constants.js" 7import {MINIMUM_EDGE_SEPARATION} from "./edge.js" 8import {NODE_INPUT_WIDTH, MINIMUM_NODE_OUTPUT_APPROACH, DEFAULT_NODE_BUBBLE_RADIUS} from "./node.js" 9 10 11const DEFAULT_NODE_ROW_SEPARATION = 130 12 13var traceLayout = false; 14 15function newGraphOccupation(graph) { 16 var isSlotFilled = []; 17 var maxSlot = 0; 18 var minSlot = 0; 19 var nodeOccupation = []; 20 21 function slotToIndex(slot) { 22 if (slot >= 0) { 23 return slot * 2; 24 } else { 25 return slot * 2 + 1; 26 } 27 } 28 29 function indexToSlot(index) { 30 if ((index % 0) == 0) { 31 return index / 2; 32 } else { 33 return -((index - 1) / 2); 34 } 35 } 36 37 function positionToSlot(pos) { 38 return Math.floor(pos / NODE_INPUT_WIDTH); 39 } 40 41 function slotToLeftPosition(slot) { 42 return slot * NODE_INPUT_WIDTH 43 } 44 45 function slotToRightPosition(slot) { 46 return (slot + 1) * NODE_INPUT_WIDTH 47 } 48 49 function findSpace(pos, width, direction) { 50 var widthSlots = Math.floor((width + NODE_INPUT_WIDTH - 1) / 51 NODE_INPUT_WIDTH); 52 var currentSlot = positionToSlot(pos + width / 2); 53 var currentScanSlot = currentSlot; 54 var widthSlotsRemainingLeft = widthSlots; 55 var widthSlotsRemainingRight = widthSlots; 56 var slotsChecked = 0; 57 while (true) { 58 var mod = slotsChecked++ % 2; 59 currentScanSlot = currentSlot + (mod ? -1 : 1) * (slotsChecked >> 1); 60 if (!isSlotFilled[slotToIndex(currentScanSlot)]) { 61 if (mod) { 62 if (direction <= 0)--widthSlotsRemainingLeft 63 } else { 64 if (direction >= 0)--widthSlotsRemainingRight 65 } 66 if (widthSlotsRemainingLeft == 0 || 67 widthSlotsRemainingRight == 0 || 68 (widthSlotsRemainingLeft + widthSlotsRemainingRight) == widthSlots && 69 (widthSlots == slotsChecked)) { 70 if (mod) { 71 return [currentScanSlot, widthSlots]; 72 } else { 73 return [currentScanSlot - widthSlots + 1, widthSlots]; 74 } 75 } 76 } else { 77 if (mod) { 78 widthSlotsRemainingLeft = widthSlots; 79 } else { 80 widthSlotsRemainingRight = widthSlots; 81 } 82 } 83 } 84 } 85 86 function setIndexRange(from, to, value) { 87 if (to < from) { 88 throw ("illegal slot range"); 89 } 90 while (from <= to) { 91 if (from > maxSlot) { 92 maxSlot = from; 93 } 94 if (from < minSlot) { 95 minSlot = from; 96 } 97 isSlotFilled[slotToIndex(from++)] = value; 98 } 99 } 100 101 function occupySlotRange(from, to) { 102 if (traceLayout) { 103 console.log("Occupied [" + slotToLeftPosition(from) + " " + slotToLeftPosition(to + 1) + ")"); 104 } 105 setIndexRange(from, to, true); 106 } 107 108 function clearSlotRange(from, to) { 109 if (traceLayout) { 110 console.log("Cleared [" + slotToLeftPosition(from) + " " + slotToLeftPosition(to + 1) + ")"); 111 } 112 setIndexRange(from, to, false); 113 } 114 115 function occupyPositionRange(from, to) { 116 occupySlotRange(positionToSlot(from), positionToSlot(to - 1)); 117 } 118 119 function clearPositionRange(from, to) { 120 clearSlotRange(positionToSlot(from), positionToSlot(to - 1)); 121 } 122 123 function occupyPositionRangeWithMargin(from, to, margin) { 124 var fromMargin = from - Math.floor(margin); 125 var toMargin = to + Math.floor(margin); 126 occupyPositionRange(fromMargin, toMargin); 127 } 128 129 function clearPositionRangeWithMargin(from, to, margin) { 130 var fromMargin = from - Math.floor(margin); 131 var toMargin = to + Math.floor(margin); 132 clearPositionRange(fromMargin, toMargin); 133 } 134 135 var occupation = { 136 occupyNodeInputs: function (node) { 137 for (var i = 0; i < node.inputs.length; ++i) { 138 if (node.inputs[i].isVisible()) { 139 var edge = node.inputs[i]; 140 if (!edge.isBackEdge()) { 141 var source = edge.source; 142 var horizontalPos = edge.getInputHorizontalPosition(graph); 143 if (traceLayout) { 144 console.log("Occupying input " + i + " of " + node.id + " at " + horizontalPos); 145 } 146 occupyPositionRangeWithMargin(horizontalPos, 147 horizontalPos, 148 NODE_INPUT_WIDTH / 2); 149 } 150 } 151 } 152 }, 153 occupyNode: function (node) { 154 var getPlacementHint = function (n) { 155 var pos = 0; 156 var direction = -1; 157 var outputEdges = 0; 158 var inputEdges = 0; 159 for (var k = 0; k < n.outputs.length; ++k) { 160 var outputEdge = n.outputs[k]; 161 if (outputEdge.isVisible()) { 162 var output = n.outputs[k].target; 163 for (var l = 0; l < output.inputs.length; ++l) { 164 if (output.rank > n.rank) { 165 var inputEdge = output.inputs[l]; 166 if (inputEdge.isVisible()) { 167 ++inputEdges; 168 } 169 if (output.inputs[l].source == n) { 170 pos += output.x + output.getInputX(l) + NODE_INPUT_WIDTH / 2; 171 outputEdges++; 172 if (l >= (output.inputs.length / 2)) { 173 direction = 1; 174 } 175 } 176 } 177 } 178 } 179 } 180 if (outputEdges != 0) { 181 pos = pos / outputEdges; 182 } 183 if (outputEdges > 1 || inputEdges == 1) { 184 direction = 0; 185 } 186 return [direction, pos]; 187 } 188 var width = node.getTotalNodeWidth(); 189 var margin = MINIMUM_EDGE_SEPARATION; 190 var paddedWidth = width + 2 * margin; 191 var placementHint = getPlacementHint(node); 192 var x = placementHint[1] - paddedWidth + margin; 193 if (traceLayout) { 194 console.log("Node " + node.id + " placement hint [" + x + ", " + (x + paddedWidth) + ")"); 195 } 196 var placement = findSpace(x, paddedWidth, placementHint[0]); 197 var firstSlot = placement[0]; 198 var slotWidth = placement[1]; 199 var endSlotExclusive = firstSlot + slotWidth - 1; 200 occupySlotRange(firstSlot, endSlotExclusive); 201 nodeOccupation.push([firstSlot, endSlotExclusive]); 202 if (placementHint[0] < 0) { 203 return slotToLeftPosition(firstSlot + slotWidth) - width - margin; 204 } else if (placementHint[0] > 0) { 205 return slotToLeftPosition(firstSlot) + margin; 206 } else { 207 return slotToLeftPosition(firstSlot + slotWidth / 2) - (width / 2); 208 } 209 }, 210 clearOccupiedNodes: function () { 211 nodeOccupation.forEach(function (o) { 212 clearSlotRange(o[0], o[1]); 213 }); 214 nodeOccupation = []; 215 }, 216 clearNodeOutputs: function (source) { 217 source.outputs.forEach(function (edge) { 218 if (edge.isVisible()) { 219 var target = edge.target; 220 for (var i = 0; i < target.inputs.length; ++i) { 221 if (target.inputs[i].source === source) { 222 var horizontalPos = edge.getInputHorizontalPosition(graph); 223 clearPositionRangeWithMargin(horizontalPos, 224 horizontalPos, 225 NODE_INPUT_WIDTH / 2); 226 } 227 } 228 } 229 }); 230 }, 231 print: function () { 232 var s = ""; 233 for (var currentSlot = -40; currentSlot < 40; ++currentSlot) { 234 if (currentSlot != 0) { 235 s += " "; 236 } else { 237 s += "|"; 238 } 239 } 240 console.log(s); 241 s = ""; 242 for (var currentSlot2 = -40; currentSlot2 < 40; ++currentSlot2) { 243 if (isSlotFilled[slotToIndex(currentSlot2)]) { 244 s += "*"; 245 } else { 246 s += " "; 247 } 248 } 249 console.log(s); 250 } 251 } 252 return occupation; 253} 254 255export function layoutNodeGraph(graph) { 256 // First determine the set of nodes that have no outputs. Those are the 257 // basis for bottom-up DFS to determine rank and node placement. 258 var endNodesHasNoOutputs = []; 259 var startNodesHasNoInputs = []; 260 graph.nodes.forEach(function (n, i) { 261 endNodesHasNoOutputs[n.id] = true; 262 startNodesHasNoInputs[n.id] = true; 263 }); 264 graph.edges.forEach(function (e, i) { 265 endNodesHasNoOutputs[e.source.id] = false; 266 startNodesHasNoInputs[e.target.id] = false; 267 }); 268 269 // Finialize the list of start and end nodes. 270 var endNodes = []; 271 var startNodes = []; 272 var visited = []; 273 var rank = []; 274 graph.nodes.forEach(function (n, i) { 275 if (endNodesHasNoOutputs[n.id]) { 276 endNodes.push(n); 277 } 278 if (startNodesHasNoInputs[n.id]) { 279 startNodes.push(n); 280 } 281 visited[n.id] = false; 282 rank[n.id] = -1; 283 n.rank = 0; 284 n.visitOrderWithinRank = 0; 285 n.outputApproach = MINIMUM_NODE_OUTPUT_APPROACH; 286 }); 287 288 289 var maxRank = 0; 290 var visited = []; 291 var dfsStack = []; 292 var visitOrderWithinRank = 0; 293 294 var worklist = startNodes.slice(); 295 while (worklist.length != 0) { 296 var n = worklist.pop(); 297 var changed = false; 298 if (n.rank == MAX_RANK_SENTINEL) { 299 n.rank = 1; 300 changed = true; 301 } 302 var begin = 0; 303 var end = n.inputs.length; 304 if (n.opcode == 'Phi' || n.opcode == 'EffectPhi') { 305 // Keep with merge or loop node 306 begin = n.inputs.length - 1; 307 } else if (n.hasBackEdges()) { 308 end = 1; 309 } 310 for (var l = begin; l < end; ++l) { 311 var input = n.inputs[l].source; 312 if (input.visible && input.rank >= n.rank) { 313 n.rank = input.rank + 1; 314 changed = true; 315 } 316 } 317 if (changed) { 318 var hasBackEdges = n.hasBackEdges(); 319 for (var l = n.outputs.length - 1; l >= 0; --l) { 320 if (hasBackEdges && (l != 0)) { 321 worklist.unshift(n.outputs[l].target); 322 } else { 323 worklist.push(n.outputs[l].target); 324 } 325 } 326 } 327 if (n.rank > maxRank) { 328 maxRank = n.rank; 329 } 330 } 331 332 visited = []; 333 function dfsFindRankLate(n) { 334 if (visited[n.id]) return; 335 visited[n.id] = true; 336 var originalRank = n.rank; 337 var newRank = n.rank; 338 var firstInput = true; 339 for (var l = 0; l < n.outputs.length; ++l) { 340 var output = n.outputs[l].target; 341 dfsFindRankLate(output); 342 var outputRank = output.rank; 343 if (output.visible && (firstInput || outputRank <= newRank) && 344 (outputRank > originalRank)) { 345 newRank = outputRank - 1; 346 } 347 firstInput = false; 348 } 349 if (n.opcode != "Start" && n.opcode != "Phi" && n.opcode != "EffectPhi") { 350 n.rank = newRank; 351 } 352 } 353 354 startNodes.forEach(dfsFindRankLate); 355 356 visited = []; 357 function dfsRankOrder(n) { 358 if (visited[n.id]) return; 359 visited[n.id] = true; 360 for (var l = 0; l < n.outputs.length; ++l) { 361 var edge = n.outputs[l]; 362 if (edge.isVisible()) { 363 var output = edge.target; 364 dfsRankOrder(output); 365 } 366 } 367 if (n.visitOrderWithinRank == 0) { 368 n.visitOrderWithinRank = ++visitOrderWithinRank; 369 } 370 } 371 startNodes.forEach(dfsRankOrder); 372 373 endNodes.forEach(function (n) { 374 n.rank = maxRank + 1; 375 }); 376 377 var rankSets = []; 378 // Collect sets for each rank. 379 graph.nodes.forEach(function (n, i) { 380 n.y = n.rank * (DEFAULT_NODE_ROW_SEPARATION + graph.getNodeHeight(n) + 381 2 * DEFAULT_NODE_BUBBLE_RADIUS); 382 if (n.visible) { 383 if (rankSets[n.rank] === undefined) { 384 rankSets[n.rank] = [n]; 385 } else { 386 rankSets[n.rank].push(n); 387 } 388 } 389 }); 390 391 // Iterate backwards from highest to lowest rank, placing nodes so that they 392 // spread out from the "center" as much as possible while still being 393 // compact and not overlapping live input lines. 394 var occupation = newGraphOccupation(graph); 395 var rankCount = 0; 396 397 rankSets.reverse().forEach(function (rankSet) { 398 399 for (var i = 0; i < rankSet.length; ++i) { 400 occupation.clearNodeOutputs(rankSet[i]); 401 } 402 403 if (traceLayout) { 404 console.log("After clearing outputs"); 405 occupation.print(); 406 } 407 408 var placedCount = 0; 409 rankSet = rankSet.sort(function (a, b) { 410 return a.visitOrderWithinRank < b.visitOrderWithinRank; 411 }); 412 for (var i = 0; i < rankSet.length; ++i) { 413 var nodeToPlace = rankSet[i]; 414 if (nodeToPlace.visible) { 415 nodeToPlace.x = occupation.occupyNode(nodeToPlace); 416 if (traceLayout) { 417 console.log("Node " + nodeToPlace.id + " is placed between [" + nodeToPlace.x + ", " + (nodeToPlace.x + nodeToPlace.getTotalNodeWidth()) + ")"); 418 } 419 var staggeredFlooredI = Math.floor(placedCount++ % 3); 420 var delta = MINIMUM_EDGE_SEPARATION * staggeredFlooredI 421 nodeToPlace.outputApproach += delta; 422 } else { 423 nodeToPlace.x = 0; 424 } 425 } 426 427 if (traceLayout) { 428 console.log("Before clearing nodes"); 429 occupation.print(); 430 } 431 432 occupation.clearOccupiedNodes(); 433 434 if (traceLayout) { 435 console.log("After clearing nodes"); 436 occupation.print(); 437 } 438 439 for (var i = 0; i < rankSet.length; ++i) { 440 var node = rankSet[i]; 441 occupation.occupyNodeInputs(node); 442 } 443 444 if (traceLayout) { 445 console.log("After occupying inputs"); 446 occupation.print(); 447 } 448 449 if (traceLayout) { 450 console.log("After determining bounding box"); 451 occupation.print(); 452 } 453 }); 454 455 graph.maxBackEdgeNumber = 0; 456 graph.visibleEdges.selectAll("path").each(function (e) { 457 if (e.isBackEdge()) { 458 e.backEdgeNumber = ++graph.maxBackEdgeNumber; 459 } else { 460 e.backEdgeNumber = 0; 461 } 462 }); 463 464 redetermineGraphBoundingBox(graph); 465} 466 467function redetermineGraphBoundingBox(graph) { 468 graph.minGraphX = 0; 469 graph.maxGraphNodeX = 1; 470 graph.maxGraphX = undefined; // see below 471 graph.minGraphY = 0; 472 graph.maxGraphY = 1; 473 474 for (var i = 0; i < graph.nodes.length; ++i) { 475 var node = graph.nodes[i]; 476 477 if (!node.visible) { 478 continue; 479 } 480 481 if (node.x < graph.minGraphX) { 482 graph.minGraphX = node.x; 483 } 484 if ((node.x + node.getTotalNodeWidth()) > graph.maxGraphNodeX) { 485 graph.maxGraphNodeX = node.x + node.getTotalNodeWidth(); 486 } 487 if ((node.y - 50) < graph.minGraphY) { 488 graph.minGraphY = node.y - 50; 489 } 490 if ((node.y + graph.getNodeHeight(node) + 50) > graph.maxGraphY) { 491 graph.maxGraphY = node.y + graph.getNodeHeight(node) + 50; 492 } 493 } 494 495 graph.maxGraphX = graph.maxGraphNodeX + 496 graph.maxBackEdgeNumber * MINIMUM_EDGE_SEPARATION; 497 498 const width = (graph.maxGraphX - graph.minGraphX); 499 const height = graph.maxGraphY - graph.minGraphY; 500 graph.width = width; 501 graph.height = height; 502 503 const extent = [ 504 [graph.minGraphX - width / 2, graph.minGraphY - height / 2], 505 [graph.maxGraphX + width / 2, graph.maxGraphY + height / 2] 506 ]; 507 graph.panZoom.translateExtent(extent); 508 graph.minScale(); 509} 510