• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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