Repository: thlorenz/turbolizer Branch: master Commit: fa3377527f03 Files: 28 Total size: 116.3 KB Directory structure: gitextract_t6om2rjh/ ├── .gitignore ├── .npmignore ├── OWNERS ├── README.md ├── bin/ │ └── turbolizer ├── code-view.js ├── constants.js ├── disassembly-view.js ├── edge.js ├── empty-view.js ├── graph-layout.js ├── graph-view.js ├── index.html ├── lang-disassembly.js ├── lib/ │ ├── client-loader.js │ ├── turbolizer.js │ └── turbolizer.server.js ├── monkey.js ├── node.js ├── package.json ├── schedule-view.js ├── selection-broker.js ├── selection.js ├── text-view.js ├── turbo-visualizer.css ├── turbo-visualizer.js ├── util.js └── view.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ lib-cov *.seed *.log *.csv *.dat *.out *.pid *.gz pids logs results npm-debug.log node_modules ================================================ FILE: .npmignore ================================================ lib-cov *.seed *.log *.csv *.dat *.out *.pid *.gz pids logs results npm-debug.log node_modules assets ================================================ FILE: OWNERS ================================================ danno@chromium.org ================================================ FILE: README.md ================================================ # Turbolizer Turbolizer tool derived from the one included with `v8/tools`.  ## Installation ``` npm install -g turbolizer ``` ## Usage Run your app with the `--trace-turbo` flag, i.e. `node --trace-turbo app.js` to produce `turbo-*.json` files. Then just run `turbolizer` in the same directory and select which file (or all) you want to load and the turbolizer application will open in the browser with it preloaded. ## Alternatives If you don't want to install anything, as an alternative can then either load them one by one via the hosted browser version of this repo at [thlorenz.github.io/turbolizer](https://thlorenz.github.io/turbolizer). * * * _Original Readme from the [v8 repository](https://github.com/v8/v8)_ Turbolizer ========== Turbolizer is a HTML-based tool that visualizes optimized code along the various phases of Turbofan's optimization pipeline, allowing easy navigation between source code, Turbofan IR graphs, scheduled IR nodes and generated assembly code. Turbolizer consumes .json files that are generated per-function by d8 by passing the '--trace-turbo' command-line flag. Host the turbolizer locally by starting a web server that serves the contents of the turbolizer directory, e.g.: cd src/tools/turbolizer python -m SimpleHTTPServer 8000 Optionally, profiling data generated by the perf tools in linux can be merged with the .json files using the turbolizer-perf.py file included. The following command is an example of using the perf script: perf script -i perf.data.jitted -s turbolizer-perf.py turbo-main.json The output of the above command is a json object that can be piped to a file which, when uploaded to turbolizer, will display the event counts from perf next to each instruction in the disassembly. Further detail can be found in the bottom of this document under "Using Perf with Turbo." Using the python interface in perf script requires python-dev to be installed and perf be recompiled with python support enabled. Once recompiled, the variable PERF_EXEC_PATH must be set to the location of the recompiled perf binaries. Graph visualization and manipulation based on Mike Bostock's sample code for an interactive tool for creating directed graphs. Original source is at https://github.com/metacademy/directed-graph-creator and released under the MIT/X license. Icons derived from the "White Olive Collection" created by Breezi released under the Creative Commons BY license. Using Perf with Turbo --------------------- In order to generate perf data that matches exactly with the turbofan trace, you must use either a debug build of v8 or a release build with the flag 'disassembler=on'. This flag ensures that the '--trace-turbo' will output the necessary disassembly for linking with the perf profile. The basic example of generating the required data is as follows: perf record -k mono /path/to/d8 --trace-turbo --perf-prof main.js perf inject -j -i perf.data -o perf.data.jitted perf script -i perf.data.jitted -s turbolizer-perf.py turbo-main.json These commands combined will run and profile d8, merge the output into a single 'perf.data.jitted' file, then take the event data from that and link them to the disassembly in the 'turbo-main.json'. Note that, as above, the output of the script command must be piped to a file for uploading to turbolizer. There are many options that can be added to the first command, for example '-e' can be used to specify the counting of specific events (default: cycles), as well as '--cpu' to specify which CPU to sample. ================================================ FILE: bin/turbolizer ================================================ #!/usr/bin/env node 'use strict' const { prompt } = require('promptly') const { mapAllTurboFiles } = require('../lib/turbolizer') const { createServer, openWithFile } = require('../lib/turbolizer.server') function createPromptMsg(map) { let msg = 'Turbolizer - please select a file to view:\n\n' for (const [ selector, { entry } ] of map) { msg += `\t${selector}: ${entry}\n` } msg += '\t0: View ALL' return msg + '\n\nYour choice: ' } function createValidator(map) { return val => { if (val === '0') return val if (map.has(val)) return val throw new Error(`Invalid choice: '${val}', please select one of the given numbers`) } } const root = process.cwd() ;(async () => { try { const map = await mapAllTurboFiles(root) if (map.size === 0) { console.error('Turbolizer - Problem:\n') console.error(' Unable to find any "turbo-*.json" files in the current directory.\n') console.error(' Please run "node --trace-turbo app.js" in order to create them or follow the') console.error(' instructions at https://github.com/thlorenz/turbolizer/blob/master/README.md.') return } const msg = createPromptMsg(map) const result = await prompt(msg, { validator: createValidator(map) }) const { server, address } = createServer(root) server.on('listening', () => { const choice = result.trim() if (choice === '0') { for (const val of map.values()) { openWithFile({ address, file: val.entry }) } } else { openWithFile({ address, file: map.get(choice).entry }) } }) } catch (err) { console.error(err) } })() ================================================ FILE: code-view.js ================================================ // 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. "use strict"; class CodeView extends View { constructor(divID, PR, sourceText, sourcePosition, broker) { super(divID, broker, null, false); let view = this; view.PR = PR; view.mouseDown = false; view.broker = broker; view.allSpans = []; var selectionHandler = { clear: function() { broker.clear(selectionHandler); }, select: function(items, selected) { var handler = this; var broker = view.broker; for (let span of items) { if (selected) { span.classList.add("selected"); } else { span.classList.remove("selected"); } } var locations = []; for (var span of items) { locations.push({pos_start: span.start, pos_end: span.end}); } broker.clear(selectionHandler); broker.select(selectionHandler, locations, selected); }, selectionDifference: function(span1, inclusive1, span2, inclusive2) { var pos1 = span1.start; var pos2 = span2.start; var result = []; var lineListDiv = view.divNode.firstChild.firstChild.childNodes; for (var i = 0; i < lineListDiv.length; i++) { var currentLineElement = lineListDiv[i]; var spans = currentLineElement.childNodes; for (var j = 0; j < spans.length; ++j) { var currentSpan = spans[j]; if (currentSpan.start > pos1 || (inclusive1 && currentSpan.start == pos1)) { if (currentSpan.start < pos2 || (inclusive2 && currentSpan.start == pos2)) { result.push(currentSpan); } } } } return result; }, brokeredSelect: function(locations, selected) { let firstSelect = view.selection.isEmpty(); for (let location of locations) { let start = location.pos_start; let end = location.pos_end; if (start && end) { let lower = 0; let upper = view.allSpans.length; if (upper > 0) { while ((upper - lower) > 1) { var middle = Math.floor((upper + lower) / 2); var lineStart = view.allSpans[middle].start; if (lineStart < start) { lower = middle; } else if (lineStart > start) { upper = middle; } else { lower = middle; break; } } var currentSpan = view.allSpans[lower]; var currentLineElement = currentSpan.parentNode; if ((currentSpan.start <= start && start < currentSpan.end) || (currentSpan.start <= end && end < currentSpan.end)) { if (firstSelect) { makeContainerPosVisible( view.divNode, currentLineElement.offsetTop); firstSelect = false; } view.selection.select(currentSpan, selected); } } } } }, brokeredClear: function() { view.selection.clear(); }, }; view.selection = new Selection(selectionHandler); broker.addSelectionHandler(selectionHandler); view.handleSpanMouseDown = function(e) { e.stopPropagation(); if (!e.shiftKey) { view.selection.clear(); } view.selection.select(this, true); view.mouseDown = true; } view.handleSpanMouseMove = function(e) { if (view.mouseDown) { view.selection.extendTo(this); } } view.handleCodeMouseDown = function(e) { view.selection.clear(); } document.addEventListener('mouseup', function(e) { view.mouseDown = false; }, false); view.initializeCode(sourceText, sourcePosition); } initializeContent(data, rememberedSelection) { this.data = data; } initializeCode(sourceText, sourcePosition) { var view = this; var codePre = document.createElement("pre"); codePre.classList.add("prettyprint"); view.divNode.innerHTML = ""; view.divNode.appendChild(codePre); if (sourceText) { codePre.classList.add("linenums"); codePre.textContent = sourceText; try { // Wrap in try to work when offline. view.PR.prettyPrint(); } catch (e) { } view.divNode.onmousedown = this.handleCodeMouseDown; var base = sourcePosition; var current = 0; var lineListDiv = view.divNode.firstChild.firstChild.childNodes; for (let i = 0; i < lineListDiv.length; i++) { var currentLineElement = lineListDiv[i]; currentLineElement.id = "li" + i; var pos = base + current; currentLineElement.pos = pos; var spans = currentLineElement.childNodes; for (let j = 0; j < spans.length; ++j) { var currentSpan = spans[j]; if (currentSpan.nodeType == 1) { currentSpan.start = pos; currentSpan.end = pos + currentSpan.textContent.length; currentSpan.onmousedown = this.handleSpanMouseDown; currentSpan.onmousemove = this.handleSpanMouseMove; view.allSpans.push(currentSpan); } current += currentSpan.textContent.length; pos = base + current; } while ((current < sourceText.length) && (sourceText[current] == '\n' || sourceText[current] == '\r')) { ++current; } } } } deleteContent() {} } ================================================ FILE: constants.js ================================================ // Copyright 2014 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 MAX_RANK_SENTINEL = 0; var GRAPH_MARGIN = 250; var WIDTH = 'width'; var HEIGHT = 'height'; var VISIBILITY = 'visibility'; var SOURCE_PANE_ID = 'left'; var SOURCE_COLLAPSE_ID = 'source-shrink'; var SOURCE_EXPAND_ID = 'source-expand'; var INTERMEDIATE_PANE_ID = 'middle'; var EMPTY_PANE_ID = 'empty'; var GRAPH_PANE_ID = 'graph'; var SCHEDULE_PANE_ID = 'schedule'; var GENERATED_PANE_ID = 'right'; var DISASSEMBLY_PANE_ID = 'disassembly'; var DISASSEMBLY_COLLAPSE_ID = 'disassembly-shrink'; var DISASSEMBLY_EXPAND_ID = 'disassembly-expand'; var COLLAPSE_PANE_BUTTON_VISIBLE = 'button-input'; var COLLAPSE_PANE_BUTTON_INVISIBLE = 'button-input-invisible'; var UNICODE_BLOCK = '▋'; var PROF_COLS = [ { perc: 0, col: { r: 255, g: 255, b: 255 } }, { perc: 0.5, col: { r: 255, g: 255, b: 128 } }, { perc: 5, col: { r: 255, g: 128, b: 0 } }, { perc: 15, col: { r: 255, g: 0, b: 0 } }, { perc: 100, col: { r: 0, g: 0, b: 0 } } ]; ================================================ FILE: disassembly-view.js ================================================ // 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. "use strict"; class DisassemblyView extends TextView { constructor(id, broker) { super(id, broker, null, false); let view = this; let ADDRESS_STYLE = { css: 'tag', location: function(text) { ADDRESS_STYLE.last_address = text; return undefined; } }; let ADDRESS_LINK_STYLE = { css: 'tag', link: function(text) { view.select(function(location) { return location.address == text; }, true, true); } }; let UNCLASSIFIED_STYLE = { css: 'com' }; let NUMBER_STYLE = { css: 'lit' }; let COMMENT_STYLE = { css: 'com' }; let POSITION_STYLE = { css: 'com', location: function(text) { view.pos_start = Number(text); } }; let OPCODE_STYLE = { css: 'kwd', location: function(text) { if (BLOCK_HEADER_STYLE.block_id != undefined) { return { address: ADDRESS_STYLE.last_address, block_id: BLOCK_HEADER_STYLE.block_id }; } else { return { address: ADDRESS_STYLE.last_address }; } } }; const BLOCK_HEADER_STYLE = { css: 'com', block_id: -1, location: function(text) { let matches = /\d+/.exec(text); if (!matches) return undefined; BLOCK_HEADER_STYLE.block_id = Number(matches[0]); return { block_id: BLOCK_HEADER_STYLE.block_id }; }, }; const SOURCE_POSITION_HEADER_STYLE = { css: 'com', location: function(text) { let matches = /(\d+):(\d+)/.exec(text); if (!matches) return undefined; let li = Number(matches[1]); if (view.pos_lines === null) return undefined; let pos = view.pos_lines[li-1] + Number(matches[2]); return { pos_start: pos, pos_end: pos + 1 }; }, }; view.SOURCE_POSITION_HEADER_REGEX = /^(\s*-- .+:)(\d+:\d+)( --)/; let patterns = [ [ [/^0x[0-9a-f]{8,16}/, ADDRESS_STYLE, 1], [view.SOURCE_POSITION_HEADER_REGEX, SOURCE_POSITION_HEADER_STYLE, -1], [/^\s+-- B\d+ start.*/, BLOCK_HEADER_STYLE, -1], [/^.*/, UNCLASSIFIED_STYLE, -1] ], [ [/^\s+[0-9a-f]+\s+[0-9a-f]+\s+/, NUMBER_STYLE, 2], [/^.*/, null, -1] ], [ [/^\S+\s+/, OPCODE_STYLE, 3], [/^\S+$/, OPCODE_STYLE, -1], [/^.*/, null, -1] ], [ [/^\s+/, null], [/^[^\(;]+$/, null, -1], [/^[^\(;]+/, null], [/^\(/, null, 4], [/^;/, COMMENT_STYLE, 5] ], [ [/^0x[0-9a-f]{8,16}/, ADDRESS_LINK_STYLE], [/^[^\)]/, null], [/^\)$/, null, -1], [/^\)/, null, 3] ], [ [/^; debug\: position /, COMMENT_STYLE, 6], [/^.+$/, COMMENT_STYLE, -1] ], [ [/^\d+$/, POSITION_STYLE, -1], ] ]; view.setPatterns(patterns); } lineLocation(li) { let view = this; let result = undefined; for (let i = 0; i < li.children.length; ++i) { let fragment = li.children[i]; let location = fragment.location; if (location != null) { if (location.block_id != undefined) { if (result === undefined) result = {}; result.block_id = location.block_id; } if (location.address != undefined) { if (result === undefined) result = {}; result.address = location.address; } if (location.pos_start != undefined && location.pos_end != undefined) { if (result === undefined) result = {}; result.pos_start = location.pos_start; result.pos_end = location.pos_end; } else if (view.pos_start != -1) { if (result === undefined) result = {}; result.pos_start = view.pos_start; result.pos_end = result.pos_start + 1; } } } return result; } initializeContent(data, rememberedSelection) { this.data = data; super.initializeContent(data, rememberedSelection); } initializeCode(sourceText, sourcePosition) { let view = this; view.pos_start = -1; view.addr_event_counts = null; view.total_event_counts = null; view.max_event_counts = null; view.pos_lines = new Array(); // Comment lines for line 0 include sourcePosition already, only need to // add sourcePosition for lines > 0. view.pos_lines[0] = sourcePosition; if (sourceText) { let base = sourcePosition; let current = 0; let source_lines = sourceText.split("\n"); for (let i = 1; i < source_lines.length; i++) { // Add 1 for newline character that is split off. current += source_lines[i-1].length + 1; view.pos_lines[i] = base + current; } } } initializePerfProfile(eventCounts) { let view = this; if (eventCounts !== undefined) { view.addr_event_counts = eventCounts; view.total_event_counts = {}; view.max_event_counts = {}; for (let ev_name in view.addr_event_counts) { let keys = Object.keys(view.addr_event_counts[ev_name]); let values = keys.map(key => view.addr_event_counts[ev_name][key]); view.total_event_counts[ev_name] = values.reduce((a, b) => a + b); view.max_event_counts[ev_name] = values.reduce((a, b) => Math.max(a, b)); } } else { view.addr_event_counts = null; view.total_event_counts = null; view.max_event_counts = null; } } // Shorten decimals and remove trailing zeroes for readability. humanize(num) { return num.toFixed(3).replace(/\.?0+$/, "") + "%"; } // Interpolate between the given start and end values by a fraction of val/max. interpolate(val, max, start, end) { return start + (end - start) * (val / max); } processLine(line) { let view = this; let func = function(match, p1, p2, p3) { let nums = p2.split(":"); let li = Number(nums[0]); let pos = Number(nums[1]); if(li === 0) pos -= view.pos_lines[0]; li++; return p1 + li + ":" + pos + p3; }; line = line.replace(view.SOURCE_POSITION_HEADER_REGEX, func); let fragments = super.processLine(line); // Add profiling data per instruction if available. if (view.total_event_counts) { let matches = /^(0x[0-9a-fA-F]+)\s+\d+\s+[0-9a-fA-F]+/.exec(line); if (matches) { let newFragments = []; for (let event in view.addr_event_counts) { let count = view.addr_event_counts[event][matches[1]]; let str = " "; let css_cls = "prof"; if(count !== undefined) { let perc = count / view.total_event_counts[event] * 100; let col = { r: 255, g: 255, b: 255 }; for (let i = 0; i < PROF_COLS.length; i++) { if (perc === PROF_COLS[i].perc) { col = PROF_COLS[i].col; break; } else if (perc > PROF_COLS[i].perc && perc < PROF_COLS[i + 1].perc) { let col1 = PROF_COLS[i].col; let col2 = PROF_COLS[i + 1].col; let val = perc - PROF_COLS[i].perc; let max = PROF_COLS[i + 1].perc - PROF_COLS[i].perc; col.r = Math.round(view.interpolate(val, max, col1.r, col2.r)); col.g = Math.round(view.interpolate(val, max, col1.g, col2.g)); col.b = Math.round(view.interpolate(val, max, col1.b, col2.b)); break; } } str = UNICODE_BLOCK; let fragment = view.createFragment(str, css_cls); fragment.title = event + ": " + view.humanize(perc) + " (" + count + ")"; fragment.style.color = "rgb(" + col.r + ", " + col.g + ", " + col.b + ")"; newFragments.push(fragment); } else newFragments.push(view.createFragment(str, css_cls)); } fragments = newFragments.concat(fragments); } } return fragments; } } ================================================ FILE: edge.js ================================================ // Copyright 2014 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 MINIMUM_EDGE_SEPARATION = 20; function isEdgeInitiallyVisible(target, index, source, type) { return type == "control" && (target.cfg || source.cfg); } var Edge = function(target, index, source, type) { this.target = target; this.source = source; this.index = index; this.type = type; this.backEdgeNumber = 0; this.visible = isEdgeInitiallyVisible(target, index, source, type); }; Edge.prototype.stringID = function() { return this.source.id + "," + this.index + "," + this.target.id; }; Edge.prototype.isVisible = function() { return this.visible && this.source.visible && this.target.visible; }; Edge.prototype.getInputHorizontalPosition = function(graph) { if (this.backEdgeNumber > 0) { return graph.maxGraphNodeX + this.backEdgeNumber * MINIMUM_EDGE_SEPARATION; } var source = this.source; var target = this.target; var index = this.index; var input_x = target.x + target.getInputX(index); var inputApproach = target.getInputApproach(this.index); var outputApproach = source.getOutputApproach(graph); if (inputApproach > outputApproach) { return input_x; } else { var inputOffset = MINIMUM_EDGE_SEPARATION * (index + 1); return (target.x < source.x) ? (target.x + target.getTotalNodeWidth() + inputOffset) : (target.x - inputOffset) } } Edge.prototype.generatePath = function(graph) { var target = this.target; var source = this.source; var input_x = target.x + target.getInputX(this.index); var arrowheadHeight = 7; var input_y = target.y - 2 * DEFAULT_NODE_BUBBLE_RADIUS - arrowheadHeight; var output_x = source.x + source.getOutputX(); var output_y = source.y + graph.getNodeHeight(source) + DEFAULT_NODE_BUBBLE_RADIUS; var inputApproach = target.getInputApproach(this.index); var outputApproach = source.getOutputApproach(graph); var horizontalPos = this.getInputHorizontalPosition(graph); var result = "M" + output_x + "," + output_y + "L" + output_x + "," + outputApproach + "L" + horizontalPos + "," + outputApproach; if (horizontalPos != input_x) { result += "L" + horizontalPos + "," + inputApproach; } else { if (inputApproach < outputApproach) { inputApproach = outputApproach; } } result += "L" + input_x + "," + inputApproach + "L" + input_x + "," + input_y; return result; } Edge.prototype.isBackEdge = function() { return this.target.hasBackEdges() && (this.target.rank < this.source.rank); } ================================================ FILE: empty-view.js ================================================ // 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. "use strict"; class EmptyView extends View { constructor(id, broker) { super(id, broker); this.svg = this.divElement.append("svg").attr('version','1.1').attr("width", "100%"); } initializeContent(data, rememberedSelection) { this.svg.attr("height", document.documentElement.clientHeight + "px"); } deleteContent() { } } ================================================ FILE: graph-layout.js ================================================ // 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(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.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; } ================================================ FILE: graph-view.js ================================================ // 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. "use strict"; class GraphView extends View { constructor (d3, id, nodes, edges, broker) { super(id, broker); var graph = this; var svg = this.divElement.append("svg").attr('version','1.1').attr("width", "100%"); graph.svg = svg; graph.nodes = nodes || []; graph.edges = edges || []; graph.minGraphX = 0; graph.maxGraphX = 1; graph.minGraphY = 0; graph.maxGraphY = 1; graph.state = { selection: null, mouseDownNode: null, justDragged: false, justScaleTransGraph: false, lastKeyDown: -1, showTypes: false }; var selectionHandler = { clear: function() { broker.clear(selectionHandler); }, select: function(items, selected) { var locations = []; for (var d of items) { if (selected) { d.classList.add("selected"); } else { d.classList.remove("selected"); } var data = d.__data__; locations.push({ pos_start: data.pos, pos_end: data.pos + 1, node_id: data.id}); } broker.select(selectionHandler, locations, selected); }, selectionDifference: function(span1, inclusive1, span2, inclusive2) { // Should not be called }, brokeredSelect: function(locations, selected) { var test = [].entries().next(); var selection = graph.nodes .filter(function(n) { var pos = n.pos; for (var location of locations) { var start = location.pos_start; var end = location.pos_end; var id = location.node_id; if (end != undefined) { if (pos >= start && pos < end) { return true; } } else if (start != undefined) { if (pos === start) { return true; } } else { if (n.id === id) { return true; } } } return false; }); var newlySelected = new Set(); selection.forEach(function(n) { newlySelected.add(n); if (!n.visible) { n.visible = true; } }); graph.updateGraphVisibility(); graph.visibleNodes.each(function(n) { if (newlySelected.has(n)) { graph.state.selection.select(this, selected); } }); graph.updateGraphVisibility(); graph.viewSelection(); }, brokeredClear: function() { graph.state.selection.clear(); } }; broker.addSelectionHandler(selectionHandler); graph.state.selection = new Selection(selectionHandler); var defs = svg.append('svg:defs'); defs.append('svg:marker') .attr('id', 'end-arrow') .attr('viewBox', '0 -4 8 8') .attr('refX', 2) .attr('markerWidth', 2.5) .attr('markerHeight', 2.5) .attr('orient', 'auto') .append('svg:path') .attr('d', 'M0,-4L8,0L0,4'); this.graphElement = svg.append("g"); graph.visibleEdges = this.graphElement.append("g").selectAll("g"); graph.visibleNodes = this.graphElement.append("g").selectAll("g"); graph.drag = d3.behavior.drag() .origin(function(d){ return {x: d.x, y: d.y}; }) .on("drag", function(args){ graph.state.justDragged = true; graph.dragmove.call(graph, args); }) d3.select("#upload").on("click", partial(this.uploadAction, graph)); d3.select("#layout").on("click", partial(this.layoutAction, graph)); d3.select("#show-all").on("click", partial(this.showAllAction, graph)); d3.select("#hide-dead").on("click", partial(this.hideDeadAction, graph)); d3.select("#hide-unselected").on("click", partial(this.hideUnselectedAction, graph)); d3.select("#hide-selected").on("click", partial(this.hideSelectedAction, graph)); d3.select("#zoom-selection").on("click", partial(this.zoomSelectionAction, graph)); d3.select("#toggle-types").on("click", partial(this.toggleTypesAction, graph)); d3.select("#search-input").on("keydown", partial(this.searchInputAction, graph)); // listen for key events d3.select(window).on("keydown", function(e){ graph.svgKeyDown.call(graph); }) .on("keyup", function(){ graph.svgKeyUp.call(graph); }); svg.on("mousedown", function(d){graph.svgMouseDown.call(graph, d);}); svg.on("mouseup", function(d){graph.svgMouseUp.call(graph, d);}); graph.dragSvg = d3.behavior.zoom() .on("zoom", function(){ if (d3.event.sourceEvent.shiftKey){ return false; } else{ graph.zoomed.call(graph); } return true; }) .on("zoomstart", function(){ if (!d3.event.sourceEvent.shiftKey) d3.select('body').style("cursor", "move"); }) .on("zoomend", function(){ d3.select('body').style("cursor", "auto"); }); svg.call(graph.dragSvg).on("dblclick.zoom", null); } static get selectedClass() { return "selected"; } static get rectClass() { return "nodeStyle"; } static get activeEditId() { return "active-editing"; } static get nodeRadius() { return 50; } getNodeHeight(d) { if (this.state.showTypes) { return d.normalheight + d.labelbbox.height; } else { return d.normalheight; } } getEdgeFrontier(nodes, inEdges, edgeFilter) { let frontier = new Set(); nodes.forEach(function(element) { var edges = inEdges ? element.__data__.inputs : element.__data__.outputs; var edgeNumber = 0; edges.forEach(function(edge) { if (edgeFilter == undefined || edgeFilter(edge, edgeNumber)) { frontier.add(edge); } ++edgeNumber; }); }); return frontier; } getNodeFrontier(nodes, inEdges, edgeFilter) { let graph = this; var frontier = new Set(); var newState = true; var edgeFrontier = graph.getEdgeFrontier(nodes, inEdges, edgeFilter); // Control key toggles edges rather than just turning them on if (d3.event.ctrlKey) { edgeFrontier.forEach(function(edge) { if (edge.visible) { newState = false; } }); } edgeFrontier.forEach(function(edge) { edge.visible = newState; if (newState) { var node = inEdges ? edge.source : edge.target; node.visible = true; frontier.add(node); } }); graph.updateGraphVisibility(); if (newState) { return graph.visibleNodes.filter(function(n) { return frontier.has(n); }); } else { return undefined; } } dragmove(d) { var graph = this; d.x += d3.event.dx; d.y += d3.event.dy; graph.updateGraphVisibility(); } initializeContent(data, rememberedSelection) { this.createGraph(data, rememberedSelection); if (rememberedSelection != null) { this.attachSelection(rememberedSelection); this.connectVisibleSelectedNodes(); this.viewSelection(); } this.updateGraphVisibility(); } deleteContent() { if (this.visibleNodes) { this.nodes = []; this.edges = []; this.nodeMap = []; this.updateGraphVisibility(); } }; measureText(text) { var textMeasure = document.getElementById('text-measure'); textMeasure.textContent = text; return { width: textMeasure.getBBox().width, height: textMeasure.getBBox().height, }; } createGraph(data, initiallyVisibileIds) { var g = this; g.nodes = data.nodes; g.nodeMap = []; g.nodes.forEach(function(n, i){ n.__proto__ = Node; n.visible = false; n.x = 0; n.y = 0; n.rank = MAX_RANK_SENTINEL; n.inputs = []; n.outputs = []; n.rpo = -1; n.outputApproach = MINIMUM_NODE_OUTPUT_APPROACH; n.cfg = n.control; g.nodeMap[n.id] = n; n.displayLabel = n.getDisplayLabel(); n.labelbbox = g.measureText(n.displayLabel); n.typebbox = g.measureText(n.getDisplayType()); var innerwidth = Math.max(n.labelbbox.width, n.typebbox.width); n.width = Math.alignUp(innerwidth + NODE_INPUT_WIDTH * 2, NODE_INPUT_WIDTH); var innerheight = Math.max(n.labelbbox.height, n.typebbox.height); n.normalheight = innerheight + 20; }); g.edges = []; data.edges.forEach(function(e, i){ var t = g.nodeMap[e.target]; var s = g.nodeMap[e.source]; var newEdge = new Edge(t, e.index, s, e.type); t.inputs.push(newEdge); s.outputs.push(newEdge); g.edges.push(newEdge); if (e.type == 'control') { s.cfg = true; } }); g.nodes.forEach(function(n, i) { n.visible = isNodeInitiallyVisible(n); if (initiallyVisibileIds != undefined) { if (initiallyVisibileIds.has(n.id)) { n.visible = true; } } }); g.fitGraphViewToWindow(); g.updateGraphVisibility(); g.layoutGraph(); g.updateGraphVisibility(); g.viewWholeGraph(); } connectVisibleSelectedNodes() { var graph = this; graph.state.selection.selection.forEach(function(element) { var edgeNumber = 0; element.__data__.inputs.forEach(function(edge) { if (edge.source.visible && edge.target.visible) { edge.visible = true; } }); element.__data__.outputs.forEach(function(edge) { if (edge.source.visible && edge.target.visible) { edge.visible = true; } }); }); } updateInputAndOutputBubbles() { var g = this; var s = g.visibleBubbles; s.classed("filledBubbleStyle", function(c) { var components = this.id.split(','); if (components[0] == "ib") { var edge = g.nodeMap[components[3]].inputs[components[2]]; return edge.isVisible(); } else { return g.nodeMap[components[1]].areAnyOutputsVisible() == 2; } }).classed("halfFilledBubbleStyle", function(c) { var components = this.id.split(','); if (components[0] == "ib") { var edge = g.nodeMap[components[3]].inputs[components[2]]; return false; } else { return g.nodeMap[components[1]].areAnyOutputsVisible() == 1; } }).classed("bubbleStyle", function(c) { var components = this.id.split(','); if (components[0] == "ib") { var edge = g.nodeMap[components[3]].inputs[components[2]]; return !edge.isVisible(); } else { return g.nodeMap[components[1]].areAnyOutputsVisible() == 0; } }); s.each(function(c) { var components = this.id.split(','); if (components[0] == "ob") { var from = g.nodeMap[components[1]]; var x = from.getOutputX(); var y = g.getNodeHeight(from) + DEFAULT_NODE_BUBBLE_RADIUS; var transform = "translate(" + x + "," + y + ")"; this.setAttribute('transform', transform); } }); } attachSelection(s) { var graph = this; if (s.size != 0) { this.visibleNodes.each(function(n) { if (s.has(this.__data__.id)) { graph.state.selection.select(this, true); } }); } } detachSelection() { var selection = this.state.selection.detachSelection(); var s = new Set(); for (var i of selection) { s.add(i.__data__.id); }; return s; } pathMouseDown(path, d) { d3.event.stopPropagation(); this.state.selection.clear(); this.state.selection.add(path); }; nodeMouseDown(node, d) { d3.event.stopPropagation(); this.state.mouseDownNode = d; } nodeMouseUp(d3node, d) { var graph = this, state = graph.state, consts = graph.consts; var mouseDownNode = state.mouseDownNode; if (!mouseDownNode) return; if (state.justDragged) { // dragged, not clicked redetermineGraphBoundingBox(graph); state.justDragged = false; } else{ // clicked, not dragged var extend = d3.event.shiftKey; var selection = graph.state.selection; if (!extend) { selection.clear(); } selection.select(d3node[0][0], true); } } selectSourcePositions(start, end, selected) { var graph = this; var map = []; var sel = graph.nodes.filter(function(n) { var pos = (n.pos === undefined) ? -1 : n.getFunctionRelativeSourcePosition(graph); if (pos >= start && pos < end) { map[n.id] = true; n.visible = true; } }); graph.updateGraphVisibility(); graph.visibleNodes.filter(function(n) { return map[n.id]; }) .each(function(n) { var selection = graph.state.selection; selection.select(d3.select(this), selected); }); } selectAllNodes(inEdges, filter) { var graph = this; if (!d3.event.shiftKey) { graph.state.selection.clear(); } graph.state.selection.select(graph.visibleNodes[0], true); graph.updateGraphVisibility(); } uploadAction(graph) { document.getElementById("hidden-file-upload").click(); } layoutAction(graph) { graph.updateGraphVisibility(); graph.layoutGraph(); graph.updateGraphVisibility(); graph.viewWholeGraph(); } showAllAction(graph) { graph.nodes.filter(function(n) { n.visible = true; }) graph.edges.filter(function(e) { e.visible = true; }) graph.updateGraphVisibility(); graph.viewWholeGraph(); } hideDeadAction(graph) { graph.nodes.filter(function(n) { if (!n.isLive()) n.visible = false; }) graph.updateGraphVisibility(); } hideUnselectedAction(graph) { var unselected = graph.visibleNodes.filter(function(n) { return !this.classList.contains("selected"); }); unselected.each(function(n) { n.visible = false; }); graph.updateGraphVisibility(); } hideSelectedAction(graph) { var selected = graph.visibleNodes.filter(function(n) { return this.classList.contains("selected"); }); selected.each(function(n) { n.visible = false; }); graph.state.selection.clear(); graph.updateGraphVisibility(); } zoomSelectionAction(graph) { graph.viewSelection(); } toggleTypesAction(graph) { graph.toggleTypes(); } searchInputAction(graph) { if (d3.event.keyCode == 13) { graph.state.selection.clear(); var query = this.value; window.sessionStorage.setItem("lastSearch", query); var reg = new RegExp(query); var filterFunction = function(n) { return (reg.exec(n.getDisplayLabel()) != null || (graph.state.showTypes && reg.exec(n.getDisplayType())) || reg.exec(n.opcode) != null); }; if (d3.event.ctrlKey) { graph.nodes.forEach(function(n, i) { if (filterFunction(n)) { n.visible = true; } }); graph.updateGraphVisibility(); } var selected = graph.visibleNodes.each(function(n) { if (filterFunction(n)) { graph.state.selection.select(this, true); } }); graph.connectVisibleSelectedNodes(); graph.updateGraphVisibility(); this.blur(); graph.viewSelection(); } d3.event.stopPropagation(); } svgMouseDown() { this.state.graphMouseDown = true; } svgMouseUp() { var graph = this, state = graph.state; if (state.justScaleTransGraph) { // Dragged state.justScaleTransGraph = false; } else { // Clicked if (state.mouseDownNode == null) { graph.state.selection.clear(); } } state.mouseDownNode = null; state.graphMouseDown = false; } svgKeyDown() { var state = this.state; var graph = this; // Don't handle key press repetition if(state.lastKeyDown !== -1) return; var showSelectionFrontierNodes = function(inEdges, filter, select) { var frontier = graph.getNodeFrontier(state.selection.selection, inEdges, filter); if (frontier != undefined) { if (select) { if (!d3.event.shiftKey) { state.selection.clear(); } state.selection.select(frontier[0], true); } graph.updateGraphVisibility(); } allowRepetition = false; } var allowRepetition = true; var eventHandled = true; // unless the below switch defaults switch(d3.event.keyCode) { case 49: case 50: case 51: case 52: case 53: case 54: case 55: case 56: case 57: // '1'-'9' showSelectionFrontierNodes(true, (edge, index) => { return index == (d3.event.keyCode - 49); }, false); break; case 97: case 98: case 99: case 100: case 101: case 102: case 103: case 104: case 105: // 'numpad 1'-'numpad 9' showSelectionFrontierNodes(true, (edge, index) => { return index == (d3.event.keyCode - 97); }, false); break; case 67: // 'c' showSelectionFrontierNodes(true, (edge, index) => { return edge.type == 'control'; }, false); break; case 69: // 'e' showSelectionFrontierNodes(true, (edge, index) => { return edge.type == 'effect'; }, false); break; case 79: // 'o' showSelectionFrontierNodes(false, undefined, false); break; case 73: // 'i' showSelectionFrontierNodes(true, undefined, false); break; case 65: // 'a' graph.selectAllNodes(); allowRepetition = false; break; case 38: case 40: { showSelectionFrontierNodes(d3.event.keyCode == 38, undefined, true); break; } case 82: // 'r' if (!d3.event.ctrlKey) { this.layoutAction(this); } else { eventHandled = false; } break; case 191: // '/' document.getElementById("search-input").focus(); document.getElementById("search-input").select(); break; default: eventHandled = false; break; } if (eventHandled) { d3.event.preventDefault(); } if (!allowRepetition) { state.lastKeyDown = d3.event.keyCode; } } svgKeyUp() { this.state.lastKeyDown = -1 }; layoutEdges() { var graph = this; graph.maxGraphX = graph.maxGraphNodeX; this.visibleEdges.attr("d", function(edge){ return edge.generatePath(graph); }); } layoutGraph() { layoutNodeGraph(this); } // call to propagate changes to graph updateGraphVisibility() { var graph = this, state = graph.state; var filteredEdges = graph.edges.filter(function(e) { return e.isVisible(); }); var visibleEdges = graph.visibleEdges.data(filteredEdges, function(edge) { return edge.stringID(); }); // add new paths visibleEdges.enter() .append('path') .style('marker-end','url(#end-arrow)') .classed('hidden', function(e) { return !e.isVisible(); }) .attr("id", function(edge){ return "e," + edge.stringID(); }) .on("mousedown", function(d){ graph.pathMouseDown.call(graph, d3.select(this), d); }) .attr("adjacentToHover", "false"); // Set the correct styles on all of the paths visibleEdges.classed('value', function(e) { return e.type == 'value' || e.type == 'context'; }).classed('control', function(e) { return e.type == 'control'; }).classed('effect', function(e) { return e.type == 'effect'; }).classed('frame-state', function(e) { return e.type == 'frame-state'; }).attr('stroke-dasharray', function(e) { if (e.type == 'frame-state') return "10,10"; return (e.type == 'effect') ? "5,5" : ""; }); // remove old links visibleEdges.exit().remove(); graph.visibleEdges = visibleEdges; // update existing nodes var filteredNodes = graph.nodes.filter(function(n) { return n.visible; }); graph.visibleNodes = graph.visibleNodes.data(filteredNodes, function(d) { return d.id; }); graph.visibleNodes.attr("transform", function(n){ return "translate(" + n.x + "," + n.y + ")"; }).select('rect'). attr(HEIGHT, function(d) { return graph.getNodeHeight(d); }); // add new nodes var newGs = graph.visibleNodes.enter() .append("g"); newGs.classed("turbonode", function(n) { return true; }) .classed("control", function(n) { return n.isControl(); }) .classed("live", function(n) { return n.isLive(); }) .classed("dead", function(n) { return !n.isLive(); }) .classed("javascript", function(n) { return n.isJavaScript(); }) .classed("input", function(n) { return n.isInput(); }) .classed("simplified", function(n) { return n.isSimplified(); }) .classed("machine", function(n) { return n.isMachine(); }) .attr("transform", function(d){ return "translate(" + d.x + "," + d.y + ")";}) .on("mousedown", function(d){ graph.nodeMouseDown.call(graph, d3.select(this), d); }) .on("mouseup", function(d){ graph.nodeMouseUp.call(graph, d3.select(this), d); }) .on('mouseover', function(d){ var nodeSelection = d3.select(this); let node = graph.nodeMap[d.id]; let adjInputEdges = graph.visibleEdges.filter(e => { return e.target === node; }); let adjOutputEdges = graph.visibleEdges.filter(e => { return e.source === node; }); adjInputEdges.attr('relToHover', "input"); adjOutputEdges.attr('relToHover', "output"); let adjInputNodes = adjInputEdges.data().map(e => e.source); graph.visibleNodes.data(adjInputNodes, function(d) { return d.id; }).attr('relToHover', "input"); let adjOutputNodes = adjOutputEdges.data().map(e => e.target); graph.visibleNodes.data(adjOutputNodes, function(d) { return d.id; }).attr('relToHover', "output"); graph.updateGraphVisibility(); }) .on('mouseout', function(d){ var nodeSelection = d3.select(this); let node = graph.nodeMap[d.id]; let adjEdges = graph.visibleEdges.filter(e => { return e.target === node || e.source === node; }); adjEdges.attr('relToHover', "none"); let adjNodes = adjEdges.data().map(e => e.target).concat(adjEdges.data().map(e => e.source)); let nodes = graph.visibleNodes.data(adjNodes, function(d) { return d.id; }).attr('relToHover', "none"); graph.updateGraphVisibility(); }) .call(graph.drag); newGs.append("rect") .attr("rx", 10) .attr("ry", 10) .attr(WIDTH, function(d) { return d.getTotalNodeWidth(); }) .attr(HEIGHT, function(d) { return graph.getNodeHeight(d); }) function appendInputAndOutputBubbles(g, d) { for (var i = 0; i < d.inputs.length; ++i) { var x = d.getInputX(i); var y = -DEFAULT_NODE_BUBBLE_RADIUS; var s = g.append('circle') .classed("filledBubbleStyle", function(c) { return d.inputs[i].isVisible(); } ) .classed("bubbleStyle", function(c) { return !d.inputs[i].isVisible(); } ) .attr("id", "ib," + d.inputs[i].stringID()) .attr("r", DEFAULT_NODE_BUBBLE_RADIUS) .attr("transform", function(d) { return "translate(" + x + "," + y + ")"; }) .on("mousedown", function(d){ var components = this.id.split(','); var node = graph.nodeMap[components[3]]; var edge = node.inputs[components[2]]; var visible = !edge.isVisible(); node.setInputVisibility(components[2], visible); d3.event.stopPropagation(); graph.updateGraphVisibility(); }); } if (d.outputs.length != 0) { var x = d.getOutputX(); var y = graph.getNodeHeight(d) + DEFAULT_NODE_BUBBLE_RADIUS; var s = g.append('circle') .classed("filledBubbleStyle", function(c) { return d.areAnyOutputsVisible() == 2; } ) .classed("halFilledBubbleStyle", function(c) { return d.areAnyOutputsVisible() == 1; } ) .classed("bubbleStyle", function(c) { return d.areAnyOutputsVisible() == 0; } ) .attr("id", "ob," + d.id) .attr("r", DEFAULT_NODE_BUBBLE_RADIUS) .attr("transform", function(d) { return "translate(" + x + "," + y + ")"; }) .on("mousedown", function(d) { d.setOutputVisibility(d.areAnyOutputsVisible() == 0); d3.event.stopPropagation(); graph.updateGraphVisibility(); }); } } newGs.each(function(d){ appendInputAndOutputBubbles(d3.select(this), d); }); newGs.each(function(d){ d3.select(this).append("text") .classed("label", true) .attr("text-anchor","right") .attr("dx", 5) .attr("dy", 5) .append('tspan') .text(function(l) { return d.getDisplayLabel(); }) .append("title") .text(function(l) { return d.getTitle(); }) if (d.type != undefined) { d3.select(this).append("text") .classed("label", true) .classed("type", true) .attr("text-anchor","right") .attr("dx", 5) .attr("dy", d.labelbbox.height + 5) .append('tspan') .text(function(l) { return d.getDisplayType(); }) .append("title") .text(function(l) { return d.getType(); }) } }); graph.visibleNodes.select('.type').each(function (d) { this.setAttribute('visibility', graph.state.showTypes ? 'visible' : 'hidden'); }); // remove old nodes graph.visibleNodes.exit().remove(); graph.visibleBubbles = d3.selectAll('circle'); graph.updateInputAndOutputBubbles(); graph.layoutEdges(); graph.svg.style.height = '100%'; } getVisibleTranslation(translate, scale) { var graph = this; var height = (graph.maxGraphY - graph.minGraphY + 2 * GRAPH_MARGIN) * scale; var width = (graph.maxGraphX - graph.minGraphX + 2 * GRAPH_MARGIN) * scale; var dimensions = this.getSvgViewDimensions(); var baseY = translate[1]; var minY = (graph.minGraphY - GRAPH_MARGIN) * scale; var maxY = (graph.maxGraphY + GRAPH_MARGIN) * scale; var adjustY = 0; var adjustYCandidate = 0; if ((maxY + baseY) < dimensions[1]) { adjustYCandidate = dimensions[1] - (maxY + baseY); if ((minY + baseY + adjustYCandidate) > 0) { adjustY = (dimensions[1] / 2) - (maxY - (height / 2)) - baseY; } else { adjustY = adjustYCandidate; } } else if (-baseY < minY) { adjustYCandidate = -(baseY + minY); if ((maxY + baseY + adjustYCandidate) < dimensions[1]) { adjustY = (dimensions[1] / 2) - (maxY - (height / 2)) - baseY; } else { adjustY = adjustYCandidate; } } translate[1] += adjustY; var baseX = translate[0]; var minX = (graph.minGraphX - GRAPH_MARGIN) * scale; var maxX = (graph.maxGraphX + GRAPH_MARGIN) * scale; var adjustX = 0; var adjustXCandidate = 0; if ((maxX + baseX) < dimensions[0]) { adjustXCandidate = dimensions[0] - (maxX + baseX); if ((minX + baseX + adjustXCandidate) > 0) { adjustX = (dimensions[0] / 2) - (maxX - (width / 2)) - baseX; } else { adjustX = adjustXCandidate; } } else if (-baseX < minX) { adjustXCandidate = -(baseX + minX); if ((maxX + baseX + adjustXCandidate) < dimensions[0]) { adjustX = (dimensions[0] / 2) - (maxX - (width / 2)) - baseX; } else { adjustX = adjustXCandidate; } } translate[0] += adjustX; return translate; } translateClipped(translate, scale, transition) { var graph = this; var graphNode = this.graphElement[0][0]; var translate = this.getVisibleTranslation(translate, scale); if (transition) { graphNode.classList.add('visible-transition'); clearTimeout(graph.transitionTimout); graph.transitionTimout = setTimeout(function(){ graphNode.classList.remove('visible-transition'); }, 1000); } var translateString = "translate(" + translate[0] + "px," + translate[1] + "px) scale(" + scale + ")"; graphNode.style.transform = translateString; graph.dragSvg.translate(translate); graph.dragSvg.scale(scale); } zoomed(){ this.state.justScaleTransGraph = true; var scale = this.dragSvg.scale(); this.translateClipped(d3.event.translate, scale); } getSvgViewDimensions() { var canvasWidth = this.parentNode.clientWidth; var documentElement = document.documentElement; var canvasHeight = documentElement.clientHeight; return [canvasWidth, canvasHeight]; } minScale() { var graph = this; var dimensions = this.getSvgViewDimensions(); var width = graph.maxGraphX - graph.minGraphX; var height = graph.maxGraphY - graph.minGraphY; var minScale = dimensions[0] / (width + GRAPH_MARGIN * 2); var minScaleYCandidate = dimensions[1] / (height + GRAPH_MARGIN * 2); if (minScaleYCandidate < minScale) { minScale = minScaleYCandidate; } this.dragSvg.scaleExtent([minScale, 1.5]); return minScale; } fitGraphViewToWindow() { this.svg.attr("height", document.documentElement.clientHeight + "px"); this.translateClipped(this.dragSvg.translate(), this.dragSvg.scale()); } toggleTypes() { var graph = this; graph.state.showTypes = !graph.state.showTypes; var element = document.getElementById('toggle-types'); if (graph.state.showTypes) { element.classList.add('button-input-toggled'); } else { element.classList.remove('button-input-toggled'); } graph.updateGraphVisibility(); } viewSelection() { var graph = this; var minX, maxX, minY, maxY; var hasSelection = false; graph.visibleNodes.each(function(n) { if (this.classList.contains("selected")) { hasSelection = true; minX = minX ? Math.min(minX, n.x) : n.x; maxX = maxX ? Math.max(maxX, n.x + n.getTotalNodeWidth()) : n.x + n.getTotalNodeWidth(); minY = minY ? Math.min(minY, n.y) : n.y; maxY = maxY ? Math.max(maxY, n.y + graph.getNodeHeight(n)) : n.y + graph.getNodeHeight(n); } }); if (hasSelection) { graph.viewGraphRegion(minX - NODE_INPUT_WIDTH, minY - 60, maxX + NODE_INPUT_WIDTH, maxY + 60, true); } } viewGraphRegion(minX, minY, maxX, maxY, transition) { var graph = this; var dimensions = this.getSvgViewDimensions(); var width = maxX - minX; var height = maxY - minY; var scale = Math.min(dimensions[0] / width, dimensions[1] / height); scale = Math.min(1.5, scale); scale = Math.max(graph.minScale(), scale); var translation = [-minX*scale, -minY*scale]; translation = graph.getVisibleTranslation(translation, scale); graph.translateClipped(translation, scale, transition); } viewWholeGraph() { var graph = this; var minScale = graph.minScale(); var translation = [0, 0]; translation = graph.getVisibleTranslation(translation, minScale); graph.translateClipped(translation, minScale); } } ================================================ FILE: index.html ================================================