├── .gitignore ├── .npmignore ├── OWNERS ├── README.md ├── assets └── turbolizer.gif ├── bin └── turbolizer ├── code-view.js ├── constants.js ├── disassembly-view.js ├── edge.js ├── empty-view.js ├── expand-all.jpg ├── graph-layout.js ├── graph-view.js ├── hide-selected.png ├── hide-unselected.png ├── index.html ├── lang-disassembly.js ├── layout-icon.png ├── left-arrow.png ├── lib ├── client-loader.js ├── turbolizer.js └── turbolizer.server.js ├── live.png ├── monkey.js ├── node.js ├── package.json ├── right-arrow.png ├── schedule-view.js ├── search.png ├── search2.png ├── selection-broker.js ├── selection.js ├── text-view.js ├── turbo-visualizer.css ├── turbo-visualizer.js ├── types.png ├── upload-icon.png ├── util.js └── view.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | assets 17 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | danno@chromium.org 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Turbolizer 2 | 3 | Turbolizer tool derived from the one included with `v8/tools`. 4 | 5 |  6 | 7 | ## Installation 8 | 9 | ``` 10 | npm install -g turbolizer 11 | ``` 12 | 13 | ## Usage 14 | 15 | Run your app with the `--trace-turbo` flag, i.e. `node --trace-turbo app.js` to produce `turbo-*.json` files. 16 | 17 | Then just run `turbolizer` in the same directory and select which file (or all) you want to 18 | load and the turbolizer application will open in the browser with it preloaded. 19 | 20 | ## Alternatives 21 | 22 | If you don't want to install anything, as an alternative can then either load them one by one 23 | via the hosted browser version of this repo at [thlorenz.github.io/turbolizer](https://thlorenz.github.io/turbolizer). 24 | 25 | * * * 26 | 27 | _Original Readme from the [v8 repository](https://github.com/v8/v8)_ 28 | 29 | Turbolizer 30 | ========== 31 | 32 | Turbolizer is a HTML-based tool that visualizes optimized code along the various 33 | phases of Turbofan's optimization pipeline, allowing easy navigation between 34 | source code, Turbofan IR graphs, scheduled IR nodes and generated assembly code. 35 | 36 | Turbolizer consumes .json files that are generated per-function by d8 by passing 37 | the '--trace-turbo' command-line flag. 38 | 39 | Host the turbolizer locally by starting a web server that serves the contents of 40 | the turbolizer directory, e.g.: 41 | 42 | cd src/tools/turbolizer 43 | python -m SimpleHTTPServer 8000 44 | 45 | Optionally, profiling data generated by the perf tools in linux can be merged 46 | with the .json files using the turbolizer-perf.py file included. The following 47 | command is an example of using the perf script: 48 | 49 | perf script -i perf.data.jitted -s turbolizer-perf.py turbo-main.json 50 | 51 | The output of the above command is a json object that can be piped to a file 52 | which, when uploaded to turbolizer, will display the event counts from perf next 53 | to each instruction in the disassembly. Further detail can be found in the 54 | bottom of this document under "Using Perf with Turbo." 55 | 56 | Using the python interface in perf script requires python-dev to be installed 57 | and perf be recompiled with python support enabled. Once recompiled, the 58 | variable PERF_EXEC_PATH must be set to the location of the recompiled perf 59 | binaries. 60 | 61 | Graph visualization and manipulation based on Mike Bostock's sample code for an 62 | interactive tool for creating directed graphs. Original source is at 63 | https://github.com/metacademy/directed-graph-creator and released under the 64 | MIT/X license. 65 | 66 | Icons derived from the "White Olive Collection" created by Breezi released under 67 | the Creative Commons BY license. 68 | 69 | Using Perf with Turbo 70 | --------------------- 71 | 72 | In order to generate perf data that matches exactly with the turbofan trace, you 73 | must use either a debug build of v8 or a release build with the flag 74 | 'disassembler=on'. This flag ensures that the '--trace-turbo' will output the 75 | necessary disassembly for linking with the perf profile. 76 | 77 | The basic example of generating the required data is as follows: 78 | 79 | perf record -k mono /path/to/d8 --trace-turbo --perf-prof main.js 80 | perf inject -j -i perf.data -o perf.data.jitted 81 | perf script -i perf.data.jitted -s turbolizer-perf.py turbo-main.json 82 | 83 | These commands combined will run and profile d8, merge the output into a single 84 | 'perf.data.jitted' file, then take the event data from that and link them to the 85 | disassembly in the 'turbo-main.json'. Note that, as above, the output of the 86 | script command must be piped to a file for uploading to turbolizer. 87 | 88 | There are many options that can be added to the first command, for example '-e' 89 | can be used to specify the counting of specific events (default: cycles), as 90 | well as '--cpu' to specify which CPU to sample. 91 | -------------------------------------------------------------------------------- /assets/turbolizer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thlorenz/turbolizer/fa3377527f030c8909a6d51e03a1eb719a618b83/assets/turbolizer.gif -------------------------------------------------------------------------------- /bin/turbolizer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | 4 | const { prompt } = require('promptly') 5 | const { mapAllTurboFiles } = require('../lib/turbolizer') 6 | const { createServer, openWithFile } = require('../lib/turbolizer.server') 7 | 8 | function createPromptMsg(map) { 9 | let msg = 'Turbolizer - please select a file to view:\n\n' 10 | for (const [ selector, { entry } ] of map) { 11 | msg += `\t${selector}: ${entry}\n` 12 | } 13 | msg += '\t0: View ALL' 14 | return msg + '\n\nYour choice: ' 15 | } 16 | 17 | function createValidator(map) { 18 | return val => { 19 | if (val === '0') return val 20 | if (map.has(val)) return val 21 | throw new Error(`Invalid choice: '${val}', please select one of the given numbers`) 22 | } 23 | } 24 | 25 | const root = process.cwd() 26 | ;(async () => { 27 | try { 28 | const map = await mapAllTurboFiles(root) 29 | if (map.size === 0) { 30 | console.error('Turbolizer - Problem:\n') 31 | console.error(' Unable to find any "turbo-*.json" files in the current directory.\n') 32 | console.error(' Please run "node --trace-turbo app.js" in order to create them or follow the') 33 | console.error(' instructions at https://github.com/thlorenz/turbolizer/blob/master/README.md.') 34 | return 35 | } 36 | const msg = createPromptMsg(map) 37 | const result = await prompt(msg, { validator: createValidator(map) }) 38 | const { server, address } = createServer(root) 39 | 40 | server.on('listening', () => { 41 | const choice = result.trim() 42 | if (choice === '0') { 43 | for (const val of map.values()) { 44 | openWithFile({ address, file: val.entry }) 45 | } 46 | } else { 47 | openWithFile({ address, file: map.get(choice).entry }) 48 | } 49 | }) 50 | } catch (err) { 51 | console.error(err) 52 | } 53 | })() 54 | -------------------------------------------------------------------------------- /code-view.js: -------------------------------------------------------------------------------- 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 | "use strict"; 6 | 7 | class CodeView extends View { 8 | constructor(divID, PR, sourceText, sourcePosition, broker) { 9 | super(divID, broker, null, false); 10 | let view = this; 11 | view.PR = PR; 12 | view.mouseDown = false; 13 | view.broker = broker; 14 | view.allSpans = []; 15 | 16 | var selectionHandler = { 17 | clear: function() { broker.clear(selectionHandler); }, 18 | select: function(items, selected) { 19 | var handler = this; 20 | var broker = view.broker; 21 | for (let span of items) { 22 | if (selected) { 23 | span.classList.add("selected"); 24 | } else { 25 | span.classList.remove("selected"); 26 | } 27 | } 28 | var locations = []; 29 | for (var span of items) { 30 | locations.push({pos_start: span.start, pos_end: span.end}); 31 | } 32 | broker.clear(selectionHandler); 33 | broker.select(selectionHandler, locations, selected); 34 | }, 35 | selectionDifference: function(span1, inclusive1, span2, inclusive2) { 36 | var pos1 = span1.start; 37 | var pos2 = span2.start; 38 | var result = []; 39 | var lineListDiv = view.divNode.firstChild.firstChild.childNodes; 40 | for (var i = 0; i < lineListDiv.length; i++) { 41 | var currentLineElement = lineListDiv[i]; 42 | var spans = currentLineElement.childNodes; 43 | for (var j = 0; j < spans.length; ++j) { 44 | var currentSpan = spans[j]; 45 | if (currentSpan.start > pos1 || 46 | (inclusive1 && currentSpan.start == pos1)) { 47 | if (currentSpan.start < pos2 || 48 | (inclusive2 && currentSpan.start == pos2)) { 49 | result.push(currentSpan); 50 | } 51 | } 52 | } 53 | } 54 | return result; 55 | }, 56 | brokeredSelect: function(locations, selected) { 57 | let firstSelect = view.selection.isEmpty(); 58 | for (let location of locations) { 59 | let start = location.pos_start; 60 | let end = location.pos_end; 61 | if (start && end) { 62 | let lower = 0; 63 | let upper = view.allSpans.length; 64 | if (upper > 0) { 65 | while ((upper - lower) > 1) { 66 | var middle = Math.floor((upper + lower) / 2); 67 | var lineStart = view.allSpans[middle].start; 68 | if (lineStart < start) { 69 | lower = middle; 70 | } else if (lineStart > start) { 71 | upper = middle; 72 | } else { 73 | lower = middle; 74 | break; 75 | } 76 | } 77 | var currentSpan = view.allSpans[lower]; 78 | var currentLineElement = currentSpan.parentNode; 79 | if ((currentSpan.start <= start && start < currentSpan.end) || 80 | (currentSpan.start <= end && end < currentSpan.end)) { 81 | if (firstSelect) { 82 | makeContainerPosVisible( 83 | view.divNode, currentLineElement.offsetTop); 84 | firstSelect = false; 85 | } 86 | view.selection.select(currentSpan, selected); 87 | } 88 | } 89 | } 90 | } 91 | }, 92 | brokeredClear: function() { view.selection.clear(); }, 93 | }; 94 | view.selection = new Selection(selectionHandler); 95 | broker.addSelectionHandler(selectionHandler); 96 | 97 | view.handleSpanMouseDown = function(e) { 98 | e.stopPropagation(); 99 | if (!e.shiftKey) { 100 | view.selection.clear(); 101 | } 102 | view.selection.select(this, true); 103 | view.mouseDown = true; 104 | } 105 | 106 | view.handleSpanMouseMove = function(e) { 107 | if (view.mouseDown) { 108 | view.selection.extendTo(this); 109 | } 110 | } 111 | 112 | view.handleCodeMouseDown = function(e) { view.selection.clear(); } 113 | 114 | document.addEventListener('mouseup', function(e) { 115 | view.mouseDown = false; 116 | }, false); 117 | 118 | view.initializeCode(sourceText, sourcePosition); 119 | } 120 | 121 | initializeContent(data, rememberedSelection) { this.data = data; } 122 | 123 | initializeCode(sourceText, sourcePosition) { 124 | var view = this; 125 | var codePre = document.createElement("pre"); 126 | codePre.classList.add("prettyprint"); 127 | view.divNode.innerHTML = ""; 128 | view.divNode.appendChild(codePre); 129 | if (sourceText) { 130 | codePre.classList.add("linenums"); 131 | codePre.textContent = sourceText; 132 | try { 133 | // Wrap in try to work when offline. 134 | view.PR.prettyPrint(); 135 | } catch (e) { 136 | } 137 | 138 | view.divNode.onmousedown = this.handleCodeMouseDown; 139 | 140 | var base = sourcePosition; 141 | var current = 0; 142 | var lineListDiv = view.divNode.firstChild.firstChild.childNodes; 143 | for (let i = 0; i < lineListDiv.length; i++) { 144 | var currentLineElement = lineListDiv[i]; 145 | currentLineElement.id = "li" + i; 146 | var pos = base + current; 147 | currentLineElement.pos = pos; 148 | var spans = currentLineElement.childNodes; 149 | for (let j = 0; j < spans.length; ++j) { 150 | var currentSpan = spans[j]; 151 | if (currentSpan.nodeType == 1) { 152 | currentSpan.start = pos; 153 | currentSpan.end = pos + currentSpan.textContent.length; 154 | currentSpan.onmousedown = this.handleSpanMouseDown; 155 | currentSpan.onmousemove = this.handleSpanMouseMove; 156 | view.allSpans.push(currentSpan); 157 | } 158 | current += currentSpan.textContent.length; 159 | pos = base + current; 160 | } 161 | while ((current < sourceText.length) && 162 | (sourceText[current] == '\n' || sourceText[current] == '\r')) { 163 | ++current; 164 | } 165 | } 166 | } 167 | } 168 | 169 | deleteContent() {} 170 | } 171 | -------------------------------------------------------------------------------- /constants.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 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 | var MAX_RANK_SENTINEL = 0; 6 | var GRAPH_MARGIN = 250; 7 | var WIDTH = 'width'; 8 | var HEIGHT = 'height'; 9 | var VISIBILITY = 'visibility'; 10 | var SOURCE_PANE_ID = 'left'; 11 | var SOURCE_COLLAPSE_ID = 'source-shrink'; 12 | var SOURCE_EXPAND_ID = 'source-expand'; 13 | var INTERMEDIATE_PANE_ID = 'middle'; 14 | var EMPTY_PANE_ID = 'empty'; 15 | var GRAPH_PANE_ID = 'graph'; 16 | var SCHEDULE_PANE_ID = 'schedule'; 17 | var GENERATED_PANE_ID = 'right'; 18 | var DISASSEMBLY_PANE_ID = 'disassembly'; 19 | var DISASSEMBLY_COLLAPSE_ID = 'disassembly-shrink'; 20 | var DISASSEMBLY_EXPAND_ID = 'disassembly-expand'; 21 | var COLLAPSE_PANE_BUTTON_VISIBLE = 'button-input'; 22 | var COLLAPSE_PANE_BUTTON_INVISIBLE = 'button-input-invisible'; 23 | var UNICODE_BLOCK = '▋'; 24 | var PROF_COLS = [ 25 | { perc: 0, col: { r: 255, g: 255, b: 255 } }, 26 | { perc: 0.5, col: { r: 255, g: 255, b: 128 } }, 27 | { perc: 5, col: { r: 255, g: 128, b: 0 } }, 28 | { perc: 15, col: { r: 255, g: 0, b: 0 } }, 29 | { perc: 100, col: { r: 0, g: 0, b: 0 } } 30 | ]; 31 | -------------------------------------------------------------------------------- /disassembly-view.js: -------------------------------------------------------------------------------- 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 | "use strict"; 6 | 7 | class DisassemblyView extends TextView { 8 | constructor(id, broker) { 9 | super(id, broker, null, false); 10 | 11 | let view = this; 12 | let ADDRESS_STYLE = { 13 | css: 'tag', 14 | location: function(text) { 15 | ADDRESS_STYLE.last_address = text; 16 | return undefined; 17 | } 18 | }; 19 | let ADDRESS_LINK_STYLE = { 20 | css: 'tag', 21 | link: function(text) { 22 | view.select(function(location) { return location.address == text; }, true, true); 23 | } 24 | }; 25 | let UNCLASSIFIED_STYLE = { 26 | css: 'com' 27 | }; 28 | let NUMBER_STYLE = { 29 | css: 'lit' 30 | }; 31 | let COMMENT_STYLE = { 32 | css: 'com' 33 | }; 34 | let POSITION_STYLE = { 35 | css: 'com', 36 | location: function(text) { 37 | view.pos_start = Number(text); 38 | } 39 | }; 40 | let OPCODE_STYLE = { 41 | css: 'kwd', 42 | location: function(text) { 43 | if (BLOCK_HEADER_STYLE.block_id != undefined) { 44 | return { 45 | address: ADDRESS_STYLE.last_address, 46 | block_id: BLOCK_HEADER_STYLE.block_id 47 | }; 48 | } else { 49 | return { 50 | address: ADDRESS_STYLE.last_address 51 | }; 52 | } 53 | } 54 | }; 55 | const BLOCK_HEADER_STYLE = { 56 | css: 'com', 57 | block_id: -1, 58 | location: function(text) { 59 | let matches = /\d+/.exec(text); 60 | if (!matches) return undefined; 61 | BLOCK_HEADER_STYLE.block_id = Number(matches[0]); 62 | return { 63 | block_id: BLOCK_HEADER_STYLE.block_id 64 | }; 65 | }, 66 | }; 67 | const SOURCE_POSITION_HEADER_STYLE = { 68 | css: 'com', 69 | location: function(text) { 70 | let matches = /(\d+):(\d+)/.exec(text); 71 | if (!matches) return undefined; 72 | let li = Number(matches[1]); 73 | if (view.pos_lines === null) return undefined; 74 | let pos = view.pos_lines[li-1] + Number(matches[2]); 75 | return { 76 | pos_start: pos, 77 | pos_end: pos + 1 78 | }; 79 | }, 80 | }; 81 | view.SOURCE_POSITION_HEADER_REGEX = /^(\s*-- .+:)(\d+:\d+)( --)/; 82 | let patterns = [ 83 | [ 84 | [/^0x[0-9a-f]{8,16}/, ADDRESS_STYLE, 1], 85 | [view.SOURCE_POSITION_HEADER_REGEX, SOURCE_POSITION_HEADER_STYLE, -1], 86 | [/^\s+-- B\d+ start.*/, BLOCK_HEADER_STYLE, -1], 87 | [/^.*/, UNCLASSIFIED_STYLE, -1] 88 | ], 89 | [ 90 | [/^\s+[0-9a-f]+\s+[0-9a-f]+\s+/, NUMBER_STYLE, 2], 91 | [/^.*/, null, -1] 92 | ], 93 | [ 94 | [/^\S+\s+/, OPCODE_STYLE, 3], 95 | [/^\S+$/, OPCODE_STYLE, -1], 96 | [/^.*/, null, -1] 97 | ], 98 | [ 99 | [/^\s+/, null], 100 | [/^[^\(;]+$/, null, -1], 101 | [/^[^\(;]+/, null], 102 | [/^\(/, null, 4], 103 | [/^;/, COMMENT_STYLE, 5] 104 | ], 105 | [ 106 | [/^0x[0-9a-f]{8,16}/, ADDRESS_LINK_STYLE], 107 | [/^[^\)]/, null], 108 | [/^\)$/, null, -1], 109 | [/^\)/, null, 3] 110 | ], 111 | [ 112 | [/^; debug\: position /, COMMENT_STYLE, 6], 113 | [/^.+$/, COMMENT_STYLE, -1] 114 | ], 115 | [ 116 | [/^\d+$/, POSITION_STYLE, -1], 117 | ] 118 | ]; 119 | view.setPatterns(patterns); 120 | } 121 | 122 | lineLocation(li) { 123 | let view = this; 124 | let result = undefined; 125 | for (let i = 0; i < li.children.length; ++i) { 126 | let fragment = li.children[i]; 127 | let location = fragment.location; 128 | if (location != null) { 129 | if (location.block_id != undefined) { 130 | if (result === undefined) result = {}; 131 | result.block_id = location.block_id; 132 | } 133 | if (location.address != undefined) { 134 | if (result === undefined) result = {}; 135 | result.address = location.address; 136 | } 137 | if (location.pos_start != undefined && location.pos_end != undefined) { 138 | if (result === undefined) result = {}; 139 | result.pos_start = location.pos_start; 140 | result.pos_end = location.pos_end; 141 | } 142 | else if (view.pos_start != -1) { 143 | if (result === undefined) result = {}; 144 | result.pos_start = view.pos_start; 145 | result.pos_end = result.pos_start + 1; 146 | } 147 | } 148 | } 149 | return result; 150 | } 151 | 152 | initializeContent(data, rememberedSelection) { 153 | this.data = data; 154 | super.initializeContent(data, rememberedSelection); 155 | } 156 | 157 | initializeCode(sourceText, sourcePosition) { 158 | let view = this; 159 | view.pos_start = -1; 160 | view.addr_event_counts = null; 161 | view.total_event_counts = null; 162 | view.max_event_counts = null; 163 | view.pos_lines = new Array(); 164 | // Comment lines for line 0 include sourcePosition already, only need to 165 | // add sourcePosition for lines > 0. 166 | view.pos_lines[0] = sourcePosition; 167 | if (sourceText) { 168 | let base = sourcePosition; 169 | let current = 0; 170 | let source_lines = sourceText.split("\n"); 171 | for (let i = 1; i < source_lines.length; i++) { 172 | // Add 1 for newline character that is split off. 173 | current += source_lines[i-1].length + 1; 174 | view.pos_lines[i] = base + current; 175 | } 176 | } 177 | } 178 | 179 | initializePerfProfile(eventCounts) { 180 | let view = this; 181 | if (eventCounts !== undefined) { 182 | view.addr_event_counts = eventCounts; 183 | 184 | view.total_event_counts = {}; 185 | view.max_event_counts = {}; 186 | for (let ev_name in view.addr_event_counts) { 187 | let keys = Object.keys(view.addr_event_counts[ev_name]); 188 | let values = keys.map(key => view.addr_event_counts[ev_name][key]); 189 | view.total_event_counts[ev_name] = values.reduce((a, b) => a + b); 190 | view.max_event_counts[ev_name] = values.reduce((a, b) => Math.max(a, b)); 191 | } 192 | } 193 | else { 194 | view.addr_event_counts = null; 195 | view.total_event_counts = null; 196 | view.max_event_counts = null; 197 | } 198 | } 199 | 200 | // Shorten decimals and remove trailing zeroes for readability. 201 | humanize(num) { 202 | return num.toFixed(3).replace(/\.?0+$/, "") + "%"; 203 | } 204 | 205 | // Interpolate between the given start and end values by a fraction of val/max. 206 | interpolate(val, max, start, end) { 207 | return start + (end - start) * (val / max); 208 | } 209 | 210 | processLine(line) { 211 | let view = this; 212 | let func = function(match, p1, p2, p3) { 213 | let nums = p2.split(":"); 214 | let li = Number(nums[0]); 215 | let pos = Number(nums[1]); 216 | if(li === 0) 217 | pos -= view.pos_lines[0]; 218 | li++; 219 | return p1 + li + ":" + pos + p3; 220 | }; 221 | line = line.replace(view.SOURCE_POSITION_HEADER_REGEX, func); 222 | let fragments = super.processLine(line); 223 | 224 | // Add profiling data per instruction if available. 225 | if (view.total_event_counts) { 226 | let matches = /^(0x[0-9a-fA-F]+)\s+\d+\s+[0-9a-fA-F]+/.exec(line); 227 | if (matches) { 228 | let newFragments = []; 229 | for (let event in view.addr_event_counts) { 230 | let count = view.addr_event_counts[event][matches[1]]; 231 | let str = " "; 232 | let css_cls = "prof"; 233 | if(count !== undefined) { 234 | let perc = count / view.total_event_counts[event] * 100; 235 | 236 | let col = { r: 255, g: 255, b: 255 }; 237 | for (let i = 0; i < PROF_COLS.length; i++) { 238 | if (perc === PROF_COLS[i].perc) { 239 | col = PROF_COLS[i].col; 240 | break; 241 | } 242 | else if (perc > PROF_COLS[i].perc && perc < PROF_COLS[i + 1].perc) { 243 | let col1 = PROF_COLS[i].col; 244 | let col2 = PROF_COLS[i + 1].col; 245 | 246 | let val = perc - PROF_COLS[i].perc; 247 | let max = PROF_COLS[i + 1].perc - PROF_COLS[i].perc; 248 | 249 | col.r = Math.round(view.interpolate(val, max, col1.r, col2.r)); 250 | col.g = Math.round(view.interpolate(val, max, col1.g, col2.g)); 251 | col.b = Math.round(view.interpolate(val, max, col1.b, col2.b)); 252 | break; 253 | } 254 | } 255 | 256 | str = UNICODE_BLOCK; 257 | 258 | let fragment = view.createFragment(str, css_cls); 259 | fragment.title = event + ": " + view.humanize(perc) + " (" + count + ")"; 260 | fragment.style.color = "rgb(" + col.r + ", " + col.g + ", " + col.b + ")"; 261 | 262 | newFragments.push(fragment); 263 | } 264 | else 265 | newFragments.push(view.createFragment(str, css_cls)); 266 | 267 | } 268 | fragments = newFragments.concat(fragments); 269 | } 270 | } 271 | return fragments; 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /edge.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 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 | var MINIMUM_EDGE_SEPARATION = 20; 6 | 7 | function isEdgeInitiallyVisible(target, index, source, type) { 8 | return type == "control" && (target.cfg || source.cfg); 9 | } 10 | 11 | var Edge = function(target, index, source, type) { 12 | this.target = target; 13 | this.source = source; 14 | this.index = index; 15 | this.type = type; 16 | this.backEdgeNumber = 0; 17 | this.visible = isEdgeInitiallyVisible(target, index, source, type); 18 | }; 19 | 20 | Edge.prototype.stringID = function() { 21 | return this.source.id + "," + this.index + "," + this.target.id; 22 | }; 23 | 24 | Edge.prototype.isVisible = function() { 25 | return this.visible && this.source.visible && this.target.visible; 26 | }; 27 | 28 | Edge.prototype.getInputHorizontalPosition = function(graph) { 29 | if (this.backEdgeNumber > 0) { 30 | return graph.maxGraphNodeX + this.backEdgeNumber * MINIMUM_EDGE_SEPARATION; 31 | } 32 | var source = this.source; 33 | var target = this.target; 34 | var index = this.index; 35 | var input_x = target.x + target.getInputX(index); 36 | var inputApproach = target.getInputApproach(this.index); 37 | var outputApproach = source.getOutputApproach(graph); 38 | if (inputApproach > outputApproach) { 39 | return input_x; 40 | } else { 41 | var inputOffset = MINIMUM_EDGE_SEPARATION * (index + 1); 42 | return (target.x < source.x) 43 | ? (target.x + target.getTotalNodeWidth() + inputOffset) 44 | : (target.x - inputOffset) 45 | } 46 | } 47 | 48 | Edge.prototype.generatePath = function(graph) { 49 | var target = this.target; 50 | var source = this.source; 51 | var input_x = target.x + target.getInputX(this.index); 52 | var arrowheadHeight = 7; 53 | var input_y = target.y - 2 * DEFAULT_NODE_BUBBLE_RADIUS - arrowheadHeight; 54 | var output_x = source.x + source.getOutputX(); 55 | var output_y = source.y + graph.getNodeHeight(source) + DEFAULT_NODE_BUBBLE_RADIUS; 56 | var inputApproach = target.getInputApproach(this.index); 57 | var outputApproach = source.getOutputApproach(graph); 58 | var horizontalPos = this.getInputHorizontalPosition(graph); 59 | 60 | var result = "M" + output_x + "," + output_y + 61 | "L" + output_x + "," + outputApproach + 62 | "L" + horizontalPos + "," + outputApproach; 63 | 64 | if (horizontalPos != input_x) { 65 | result += "L" + horizontalPos + "," + inputApproach; 66 | } else { 67 | if (inputApproach < outputApproach) { 68 | inputApproach = outputApproach; 69 | } 70 | } 71 | 72 | result += "L" + input_x + "," + inputApproach + 73 | "L" + input_x + "," + input_y; 74 | return result; 75 | } 76 | 77 | Edge.prototype.isBackEdge = function() { 78 | return this.target.hasBackEdges() && (this.target.rank < this.source.rank); 79 | } 80 | -------------------------------------------------------------------------------- /empty-view.js: -------------------------------------------------------------------------------- 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 | "use strict"; 6 | 7 | class EmptyView extends View { 8 | constructor(id, broker) { 9 | super(id, broker); 10 | this.svg = this.divElement.append("svg").attr('version','1.1').attr("width", "100%"); 11 | } 12 | 13 | initializeContent(data, rememberedSelection) { 14 | this.svg.attr("height", document.documentElement.clientHeight + "px"); 15 | } 16 | 17 | deleteContent() { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /expand-all.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thlorenz/turbolizer/fa3377527f030c8909a6d51e03a1eb719a618b83/expand-all.jpg -------------------------------------------------------------------------------- /graph-layout.js: -------------------------------------------------------------------------------- 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 | var DEFAULT_NODE_ROW_SEPARATION = 130 6 | 7 | var traceLayout = false; 8 | 9 | function newGraphOccupation(graph){ 10 | var isSlotFilled = []; 11 | var maxSlot = 0; 12 | var minSlot = 0; 13 | var nodeOccupation = []; 14 | 15 | function slotToIndex(slot) { 16 | if (slot >= 0) { 17 | return slot * 2; 18 | } else { 19 | return slot * 2 + 1; 20 | } 21 | } 22 | 23 | function indexToSlot(index) { 24 | if ((index % 0) == 0) { 25 | return index / 2; 26 | } else { 27 | return -((index - 1) / 2); 28 | } 29 | } 30 | 31 | function positionToSlot(pos) { 32 | return Math.floor(pos / NODE_INPUT_WIDTH); 33 | } 34 | 35 | function slotToLeftPosition(slot) { 36 | return slot * NODE_INPUT_WIDTH 37 | } 38 | 39 | function slotToRightPosition(slot) { 40 | return (slot + 1) * NODE_INPUT_WIDTH 41 | } 42 | 43 | function findSpace(pos, width, direction) { 44 | var widthSlots = Math.floor((width + NODE_INPUT_WIDTH - 1) / 45 | NODE_INPUT_WIDTH); 46 | var currentSlot = positionToSlot(pos + width / 2); 47 | var currentScanSlot = currentSlot; 48 | var widthSlotsRemainingLeft = widthSlots; 49 | var widthSlotsRemainingRight = widthSlots; 50 | var slotsChecked = 0; 51 | while (true) { 52 | var mod = slotsChecked++ % 2; 53 | currentScanSlot = currentSlot + (mod ? -1 : 1) * (slotsChecked >> 1); 54 | if (!isSlotFilled[slotToIndex(currentScanSlot)]) { 55 | if (mod) { 56 | if (direction <= 0) --widthSlotsRemainingLeft 57 | } else { 58 | if (direction >= 0) --widthSlotsRemainingRight 59 | } 60 | if (widthSlotsRemainingLeft == 0 || 61 | widthSlotsRemainingRight == 0 || 62 | (widthSlotsRemainingLeft + widthSlotsRemainingRight) == widthSlots && 63 | (widthSlots == slotsChecked)) { 64 | if (mod) { 65 | return [currentScanSlot, widthSlots]; 66 | } else { 67 | return [currentScanSlot - widthSlots + 1, widthSlots]; 68 | } 69 | } 70 | } else { 71 | if (mod) { 72 | widthSlotsRemainingLeft = widthSlots; 73 | } else { 74 | widthSlotsRemainingRight = widthSlots; 75 | } 76 | } 77 | } 78 | } 79 | 80 | function setIndexRange(from, to, value) { 81 | if (to < from) { 82 | throw("illegal slot range"); 83 | } 84 | while (from <= to) { 85 | if (from > maxSlot) { 86 | maxSlot = from; 87 | } 88 | if (from < minSlot) { 89 | minSlot = from; 90 | } 91 | isSlotFilled[slotToIndex(from++)] = value; 92 | } 93 | } 94 | 95 | function occupySlotRange(from, to) { 96 | if (traceLayout) { 97 | console.log("Occupied [" + slotToLeftPosition(from) + " " + slotToLeftPosition(to + 1) + ")"); 98 | } 99 | setIndexRange(from, to, true); 100 | } 101 | 102 | function clearSlotRange(from, to) { 103 | if (traceLayout) { 104 | console.log("Cleared [" + slotToLeftPosition(from) + " " + slotToLeftPosition(to + 1) + ")"); 105 | } 106 | setIndexRange(from, to, false); 107 | } 108 | 109 | function occupyPositionRange(from, to) { 110 | occupySlotRange(positionToSlot(from), positionToSlot(to - 1)); 111 | } 112 | 113 | function clearPositionRange(from, to) { 114 | clearSlotRange(positionToSlot(from), positionToSlot(to - 1)); 115 | } 116 | 117 | function occupyPositionRangeWithMargin(from, to, margin) { 118 | var fromMargin = from - Math.floor(margin); 119 | var toMargin = to + Math.floor(margin); 120 | occupyPositionRange(fromMargin, toMargin); 121 | } 122 | 123 | function clearPositionRangeWithMargin(from, to, margin) { 124 | var fromMargin = from - Math.floor(margin); 125 | var toMargin = to + Math.floor(margin); 126 | clearPositionRange(fromMargin, toMargin); 127 | } 128 | 129 | var occupation = { 130 | occupyNodeInputs: function(node) { 131 | for (var i = 0; i < node.inputs.length; ++i) { 132 | if (node.inputs[i].isVisible()) { 133 | var edge = node.inputs[i]; 134 | if (!edge.isBackEdge()) { 135 | var source = edge.source; 136 | var horizontalPos = edge.getInputHorizontalPosition(graph); 137 | if (traceLayout) { 138 | console.log("Occupying input " + i + " of " + node.id + " at " + horizontalPos); 139 | } 140 | occupyPositionRangeWithMargin(horizontalPos, 141 | horizontalPos, 142 | NODE_INPUT_WIDTH / 2); 143 | } 144 | } 145 | } 146 | }, 147 | occupyNode: function(node) { 148 | var getPlacementHint = function(n) { 149 | var pos = 0; 150 | var direction = -1; 151 | var outputEdges = 0; 152 | var inputEdges = 0; 153 | for (var k = 0; k < n.outputs.length; ++k) { 154 | var outputEdge = n.outputs[k]; 155 | if (outputEdge.isVisible()) { 156 | var output = n.outputs[k].target; 157 | for (var l = 0; l < output.inputs.length; ++l) { 158 | if (output.rank > n.rank) { 159 | var inputEdge = output.inputs[l]; 160 | if (inputEdge.isVisible()) { 161 | ++inputEdges; 162 | } 163 | if (output.inputs[l].source == n) { 164 | pos += output.x + output.getInputX(l) + NODE_INPUT_WIDTH / 2; 165 | outputEdges++; 166 | if (l >= (output.inputs.length / 2)) { 167 | direction = 1; 168 | } 169 | } 170 | } 171 | } 172 | } 173 | } 174 | if (outputEdges != 0) { 175 | pos = pos / outputEdges; 176 | } 177 | if (outputEdges > 1 || inputEdges == 1) { 178 | direction = 0; 179 | } 180 | return [direction, pos]; 181 | } 182 | var width = node.getTotalNodeWidth(); 183 | var margin = MINIMUM_EDGE_SEPARATION; 184 | var paddedWidth = width + 2 * margin; 185 | var placementHint = getPlacementHint(node); 186 | var x = placementHint[1] - paddedWidth + margin; 187 | if (traceLayout) { 188 | console.log("Node " + node.id + " placement hint [" + x + ", " + (x + paddedWidth) + ")"); 189 | } 190 | var placement = findSpace(x, paddedWidth, placementHint[0]); 191 | var firstSlot = placement[0]; 192 | var slotWidth = placement[1]; 193 | var endSlotExclusive = firstSlot + slotWidth - 1; 194 | occupySlotRange(firstSlot, endSlotExclusive); 195 | nodeOccupation.push([firstSlot, endSlotExclusive]); 196 | if (placementHint[0] < 0) { 197 | return slotToLeftPosition(firstSlot + slotWidth) - width - margin; 198 | } else if (placementHint[0] > 0) { 199 | return slotToLeftPosition(firstSlot) + margin; 200 | } else { 201 | return slotToLeftPosition(firstSlot + slotWidth / 2) - (width / 2); 202 | } 203 | }, 204 | clearOccupiedNodes: function() { 205 | nodeOccupation.forEach(function(o) { 206 | clearSlotRange(o[0], o[1]); 207 | }); 208 | nodeOccupation = []; 209 | }, 210 | clearNodeOutputs: function(source) { 211 | source.outputs.forEach(function(edge) { 212 | if (edge.isVisible()) { 213 | var target = edge.target; 214 | for (var i = 0; i < target.inputs.length; ++i) { 215 | if (target.inputs[i].source === source) { 216 | var horizontalPos = edge.getInputHorizontalPosition(graph); 217 | clearPositionRangeWithMargin(horizontalPos, 218 | horizontalPos, 219 | NODE_INPUT_WIDTH / 2); 220 | } 221 | } 222 | } 223 | }); 224 | }, 225 | print: function() { 226 | var s = ""; 227 | for (var currentSlot = -40; currentSlot < 40; ++currentSlot) { 228 | if (currentSlot != 0) { 229 | s += " "; 230 | } else { 231 | s += "|"; 232 | } 233 | } 234 | console.log(s); 235 | s = ""; 236 | for (var currentSlot2 = -40; currentSlot2 < 40; ++currentSlot2) { 237 | if (isSlotFilled[slotToIndex(currentSlot2)]) { 238 | s += "*"; 239 | } else { 240 | s += " "; 241 | } 242 | } 243 | console.log(s); 244 | } 245 | } 246 | return occupation; 247 | } 248 | 249 | function layoutNodeGraph(graph) { 250 | // First determine the set of nodes that have no outputs. Those are the 251 | // basis for bottom-up DFS to determine rank and node placement. 252 | var endNodesHasNoOutputs = []; 253 | var startNodesHasNoInputs = []; 254 | graph.nodes.forEach(function(n, i){ 255 | endNodesHasNoOutputs[n.id] = true; 256 | startNodesHasNoInputs[n.id] = true; 257 | }); 258 | graph.edges.forEach(function(e, i){ 259 | endNodesHasNoOutputs[e.source.id] = false; 260 | startNodesHasNoInputs[e.target.id] = false; 261 | }); 262 | 263 | // Finialize the list of start and end nodes. 264 | var endNodes = []; 265 | var startNodes = []; 266 | var visited = []; 267 | var rank = []; 268 | graph.nodes.forEach(function(n, i){ 269 | if (endNodesHasNoOutputs[n.id]) { 270 | endNodes.push(n); 271 | } 272 | if (startNodesHasNoInputs[n.id]) { 273 | startNodes.push(n); 274 | } 275 | visited[n.id] = false; 276 | rank[n.id] = -1; 277 | n.rank = 0; 278 | n.visitOrderWithinRank = 0; 279 | n.outputApproach = MINIMUM_NODE_OUTPUT_APPROACH; 280 | }); 281 | 282 | 283 | var maxRank = 0; 284 | var visited = []; 285 | var dfsStack = []; 286 | var visitOrderWithinRank = 0; 287 | 288 | var worklist = startNodes.slice(); 289 | while (worklist.length != 0) { 290 | var n = worklist.pop(); 291 | var changed = false; 292 | if (n.rank == MAX_RANK_SENTINEL) { 293 | n.rank = 1; 294 | changed = true; 295 | } 296 | var begin = 0; 297 | var end = n.inputs.length; 298 | if (n.opcode == 'Phi' || n.opcode == 'EffectPhi') { 299 | // Keep with merge or loop node 300 | begin = n.inputs.length - 1; 301 | } else if (n.hasBackEdges()) { 302 | end = 1; 303 | } 304 | for (var l = begin; l < end; ++l) { 305 | var input = n.inputs[l].source; 306 | if (input.visible && input.rank >= n.rank) { 307 | n.rank = input.rank + 1; 308 | changed = true; 309 | } 310 | } 311 | if (changed) { 312 | var hasBackEdges = n.hasBackEdges(); 313 | for (var l = n.outputs.length - 1; l >= 0; --l) { 314 | if (hasBackEdges && (l != 0)) { 315 | worklist.unshift(n.outputs[l].target); 316 | } else { 317 | worklist.push(n.outputs[l].target); 318 | } 319 | } 320 | } 321 | if (n.rank > maxRank) { 322 | maxRank = n.rank; 323 | } 324 | } 325 | 326 | visited = []; 327 | function dfsFindRankLate(n) { 328 | if (visited[n.id]) return; 329 | visited[n.id] = true; 330 | var originalRank = n.rank; 331 | var newRank = n.rank; 332 | var firstInput = true; 333 | for (var l = 0; l < n.outputs.length; ++l) { 334 | var output = n.outputs[l].target; 335 | dfsFindRankLate(output); 336 | var outputRank = output.rank; 337 | if (output.visible && (firstInput || outputRank <= newRank) && 338 | (outputRank > originalRank)) { 339 | newRank = outputRank - 1; 340 | } 341 | firstInput = false; 342 | } 343 | if (n.opcode != "Start" && n.opcode != "Phi" && n.opcode != "EffectPhi") { 344 | n.rank = newRank; 345 | } 346 | } 347 | 348 | startNodes.forEach(dfsFindRankLate); 349 | 350 | visited = []; 351 | function dfsRankOrder(n) { 352 | if (visited[n.id]) return; 353 | visited[n.id] = true; 354 | for (var l = 0; l < n.outputs.length; ++l) { 355 | var edge = n.outputs[l]; 356 | if (edge.isVisible()) { 357 | var output = edge.target; 358 | dfsRankOrder(output); 359 | } 360 | } 361 | if (n.visitOrderWithinRank == 0) { 362 | n.visitOrderWithinRank = ++visitOrderWithinRank; 363 | } 364 | } 365 | startNodes.forEach(dfsRankOrder); 366 | 367 | endNodes.forEach(function(n) { 368 | n.rank = maxRank + 1; 369 | }); 370 | 371 | var rankSets = []; 372 | // Collect sets for each rank. 373 | graph.nodes.forEach(function(n, i){ 374 | n.y = n.rank * (DEFAULT_NODE_ROW_SEPARATION + graph.getNodeHeight(n) + 375 | 2 * DEFAULT_NODE_BUBBLE_RADIUS); 376 | if (n.visible) { 377 | if (rankSets[n.rank] === undefined) { 378 | rankSets[n.rank] = [n]; 379 | } else { 380 | rankSets[n.rank].push(n); 381 | } 382 | } 383 | }); 384 | 385 | // Iterate backwards from highest to lowest rank, placing nodes so that they 386 | // spread out from the "center" as much as possible while still being 387 | // compact and not overlapping live input lines. 388 | var occupation = newGraphOccupation(graph); 389 | var rankCount = 0; 390 | 391 | rankSets.reverse().forEach(function(rankSet) { 392 | 393 | for (var i = 0; i < rankSet.length; ++i) { 394 | occupation.clearNodeOutputs(rankSet[i]); 395 | } 396 | 397 | if (traceLayout) { 398 | console.log("After clearing outputs"); 399 | occupation.print(); 400 | } 401 | 402 | var placedCount = 0; 403 | rankSet = rankSet.sort(function(a,b) { 404 | return a.visitOrderWithinRank < b.visitOrderWithinRank; 405 | }); 406 | for (var i = 0; i < rankSet.length; ++i) { 407 | var nodeToPlace = rankSet[i]; 408 | if (nodeToPlace.visible) { 409 | nodeToPlace.x = occupation.occupyNode(nodeToPlace); 410 | if (traceLayout) { 411 | console.log("Node " + nodeToPlace.id + " is placed between [" + nodeToPlace.x + ", " + (nodeToPlace.x + nodeToPlace.getTotalNodeWidth()) + ")"); 412 | } 413 | var staggeredFlooredI = Math.floor(placedCount++ % 3); 414 | var delta = MINIMUM_EDGE_SEPARATION * staggeredFlooredI 415 | nodeToPlace.outputApproach += delta; 416 | } else { 417 | nodeToPlace.x = 0; 418 | } 419 | } 420 | 421 | if (traceLayout) { 422 | console.log("Before clearing nodes"); 423 | occupation.print(); 424 | } 425 | 426 | occupation.clearOccupiedNodes(); 427 | 428 | if (traceLayout) { 429 | console.log("After clearing nodes"); 430 | occupation.print(); 431 | } 432 | 433 | for (var i = 0; i < rankSet.length; ++i) { 434 | var node = rankSet[i]; 435 | occupation.occupyNodeInputs(node); 436 | } 437 | 438 | if (traceLayout) { 439 | console.log("After occupying inputs"); 440 | occupation.print(); 441 | } 442 | 443 | if (traceLayout) { 444 | console.log("After determining bounding box"); 445 | occupation.print(); 446 | } 447 | }); 448 | 449 | graph.maxBackEdgeNumber = 0; 450 | graph.visibleEdges.each(function (e) { 451 | if (e.isBackEdge()) { 452 | e.backEdgeNumber = ++graph.maxBackEdgeNumber; 453 | } else { 454 | e.backEdgeNumber = 0; 455 | } 456 | }); 457 | 458 | redetermineGraphBoundingBox(graph); 459 | 460 | } 461 | 462 | function redetermineGraphBoundingBox(graph) { 463 | graph.minGraphX = 0; 464 | graph.maxGraphNodeX = 1; 465 | graph.maxGraphX = undefined; // see below 466 | graph.minGraphY = 0; 467 | graph.maxGraphY = 1; 468 | 469 | for (var i = 0; i < graph.nodes.length; ++i) { 470 | var node = graph.nodes[i]; 471 | 472 | if (!node.visible) { 473 | continue; 474 | } 475 | 476 | if (node.x < graph.minGraphX) { 477 | graph.minGraphX = node.x; 478 | } 479 | if ((node.x + node.getTotalNodeWidth()) > graph.maxGraphNodeX) { 480 | graph.maxGraphNodeX = node.x + node.getTotalNodeWidth(); 481 | } 482 | if ((node.y - 50) < graph.minGraphY) { 483 | graph.minGraphY = node.y - 50; 484 | } 485 | if ((node.y + graph.getNodeHeight(node) + 50) > graph.maxGraphY) { 486 | graph.maxGraphY = node.y + graph.getNodeHeight(node) + 50; 487 | } 488 | } 489 | 490 | graph.maxGraphX = graph.maxGraphNodeX + 491 | graph.maxBackEdgeNumber * MINIMUM_EDGE_SEPARATION; 492 | 493 | } 494 | -------------------------------------------------------------------------------- /graph-view.js: -------------------------------------------------------------------------------- 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 | "use strict"; 6 | 7 | class GraphView extends View { 8 | constructor (d3, id, nodes, edges, broker) { 9 | super(id, broker); 10 | var graph = this; 11 | 12 | var svg = this.divElement.append("svg").attr('version','1.1').attr("width", "100%"); 13 | graph.svg = svg; 14 | 15 | graph.nodes = nodes || []; 16 | graph.edges = edges || []; 17 | 18 | graph.minGraphX = 0; 19 | graph.maxGraphX = 1; 20 | graph.minGraphY = 0; 21 | graph.maxGraphY = 1; 22 | 23 | graph.state = { 24 | selection: null, 25 | mouseDownNode: null, 26 | justDragged: false, 27 | justScaleTransGraph: false, 28 | lastKeyDown: -1, 29 | showTypes: false 30 | }; 31 | 32 | var selectionHandler = { 33 | clear: function() { 34 | broker.clear(selectionHandler); 35 | }, 36 | select: function(items, selected) { 37 | var locations = []; 38 | for (var d of items) { 39 | if (selected) { 40 | d.classList.add("selected"); 41 | } else { 42 | d.classList.remove("selected"); 43 | } 44 | var data = d.__data__; 45 | locations.push({ pos_start: data.pos, pos_end: data.pos + 1, node_id: data.id}); 46 | } 47 | broker.select(selectionHandler, locations, selected); 48 | }, 49 | selectionDifference: function(span1, inclusive1, span2, inclusive2) { 50 | // Should not be called 51 | }, 52 | brokeredSelect: function(locations, selected) { 53 | var test = [].entries().next(); 54 | var selection = graph.nodes 55 | .filter(function(n) { 56 | var pos = n.pos; 57 | for (var location of locations) { 58 | var start = location.pos_start; 59 | var end = location.pos_end; 60 | var id = location.node_id; 61 | if (end != undefined) { 62 | if (pos >= start && pos < end) { 63 | return true; 64 | } 65 | } else if (start != undefined) { 66 | if (pos === start) { 67 | return true; 68 | } 69 | } else { 70 | if (n.id === id) { 71 | return true; 72 | } 73 | } 74 | } 75 | return false; 76 | }); 77 | var newlySelected = new Set(); 78 | selection.forEach(function(n) { 79 | newlySelected.add(n); 80 | if (!n.visible) { 81 | n.visible = true; 82 | } 83 | }); 84 | graph.updateGraphVisibility(); 85 | graph.visibleNodes.each(function(n) { 86 | if (newlySelected.has(n)) { 87 | graph.state.selection.select(this, selected); 88 | } 89 | }); 90 | graph.updateGraphVisibility(); 91 | graph.viewSelection(); 92 | }, 93 | brokeredClear: function() { 94 | graph.state.selection.clear(); 95 | } 96 | }; 97 | broker.addSelectionHandler(selectionHandler); 98 | 99 | graph.state.selection = new Selection(selectionHandler); 100 | 101 | var defs = svg.append('svg:defs'); 102 | defs.append('svg:marker') 103 | .attr('id', 'end-arrow') 104 | .attr('viewBox', '0 -4 8 8') 105 | .attr('refX', 2) 106 | .attr('markerWidth', 2.5) 107 | .attr('markerHeight', 2.5) 108 | .attr('orient', 'auto') 109 | .append('svg:path') 110 | .attr('d', 'M0,-4L8,0L0,4'); 111 | 112 | this.graphElement = svg.append("g"); 113 | graph.visibleEdges = this.graphElement.append("g").selectAll("g"); 114 | graph.visibleNodes = this.graphElement.append("g").selectAll("g"); 115 | 116 | graph.drag = d3.behavior.drag() 117 | .origin(function(d){ 118 | return {x: d.x, y: d.y}; 119 | }) 120 | .on("drag", function(args){ 121 | graph.state.justDragged = true; 122 | graph.dragmove.call(graph, args); 123 | }) 124 | 125 | d3.select("#upload").on("click", partial(this.uploadAction, graph)); 126 | d3.select("#layout").on("click", partial(this.layoutAction, graph)); 127 | d3.select("#show-all").on("click", partial(this.showAllAction, graph)); 128 | d3.select("#hide-dead").on("click", partial(this.hideDeadAction, graph)); 129 | d3.select("#hide-unselected").on("click", partial(this.hideUnselectedAction, graph)); 130 | d3.select("#hide-selected").on("click", partial(this.hideSelectedAction, graph)); 131 | d3.select("#zoom-selection").on("click", partial(this.zoomSelectionAction, graph)); 132 | d3.select("#toggle-types").on("click", partial(this.toggleTypesAction, graph)); 133 | d3.select("#search-input").on("keydown", partial(this.searchInputAction, graph)); 134 | 135 | // listen for key events 136 | d3.select(window).on("keydown", function(e){ 137 | graph.svgKeyDown.call(graph); 138 | }) 139 | .on("keyup", function(){ 140 | graph.svgKeyUp.call(graph); 141 | }); 142 | svg.on("mousedown", function(d){graph.svgMouseDown.call(graph, d);}); 143 | svg.on("mouseup", function(d){graph.svgMouseUp.call(graph, d);}); 144 | 145 | graph.dragSvg = d3.behavior.zoom() 146 | .on("zoom", function(){ 147 | if (d3.event.sourceEvent.shiftKey){ 148 | return false; 149 | } else{ 150 | graph.zoomed.call(graph); 151 | } 152 | return true; 153 | }) 154 | .on("zoomstart", function(){ 155 | if (!d3.event.sourceEvent.shiftKey) d3.select('body').style("cursor", "move"); 156 | }) 157 | .on("zoomend", function(){ 158 | d3.select('body').style("cursor", "auto"); 159 | }); 160 | 161 | svg.call(graph.dragSvg).on("dblclick.zoom", null); 162 | } 163 | 164 | static get selectedClass() { 165 | return "selected"; 166 | } 167 | static get rectClass() { 168 | return "nodeStyle"; 169 | } 170 | static get activeEditId() { 171 | return "active-editing"; 172 | } 173 | static get nodeRadius() { 174 | return 50; 175 | } 176 | 177 | getNodeHeight(d) { 178 | if (this.state.showTypes) { 179 | return d.normalheight + d.labelbbox.height; 180 | } else { 181 | return d.normalheight; 182 | } 183 | } 184 | 185 | getEdgeFrontier(nodes, inEdges, edgeFilter) { 186 | let frontier = new Set(); 187 | nodes.forEach(function(element) { 188 | var edges = inEdges ? element.__data__.inputs : element.__data__.outputs; 189 | var edgeNumber = 0; 190 | edges.forEach(function(edge) { 191 | if (edgeFilter == undefined || edgeFilter(edge, edgeNumber)) { 192 | frontier.add(edge); 193 | } 194 | ++edgeNumber; 195 | }); 196 | }); 197 | return frontier; 198 | } 199 | 200 | getNodeFrontier(nodes, inEdges, edgeFilter) { 201 | let graph = this; 202 | var frontier = new Set(); 203 | var newState = true; 204 | var edgeFrontier = graph.getEdgeFrontier(nodes, inEdges, edgeFilter); 205 | // Control key toggles edges rather than just turning them on 206 | if (d3.event.ctrlKey) { 207 | edgeFrontier.forEach(function(edge) { 208 | if (edge.visible) { 209 | newState = false; 210 | } 211 | }); 212 | } 213 | edgeFrontier.forEach(function(edge) { 214 | edge.visible = newState; 215 | if (newState) { 216 | var node = inEdges ? edge.source : edge.target; 217 | node.visible = true; 218 | frontier.add(node); 219 | } 220 | }); 221 | graph.updateGraphVisibility(); 222 | if (newState) { 223 | return graph.visibleNodes.filter(function(n) { 224 | return frontier.has(n); 225 | }); 226 | } else { 227 | return undefined; 228 | } 229 | } 230 | 231 | dragmove(d) { 232 | var graph = this; 233 | d.x += d3.event.dx; 234 | d.y += d3.event.dy; 235 | graph.updateGraphVisibility(); 236 | } 237 | 238 | initializeContent(data, rememberedSelection) { 239 | this.createGraph(data, rememberedSelection); 240 | if (rememberedSelection != null) { 241 | this.attachSelection(rememberedSelection); 242 | this.connectVisibleSelectedNodes(); 243 | this.viewSelection(); 244 | } 245 | this.updateGraphVisibility(); 246 | } 247 | 248 | deleteContent() { 249 | if (this.visibleNodes) { 250 | this.nodes = []; 251 | this.edges = []; 252 | this.nodeMap = []; 253 | this.updateGraphVisibility(); 254 | } 255 | }; 256 | 257 | measureText(text) { 258 | var textMeasure = document.getElementById('text-measure'); 259 | textMeasure.textContent = text; 260 | return { 261 | width: textMeasure.getBBox().width, 262 | height: textMeasure.getBBox().height, 263 | }; 264 | } 265 | 266 | createGraph(data, initiallyVisibileIds) { 267 | var g = this; 268 | g.nodes = data.nodes; 269 | g.nodeMap = []; 270 | g.nodes.forEach(function(n, i){ 271 | n.__proto__ = Node; 272 | n.visible = false; 273 | n.x = 0; 274 | n.y = 0; 275 | n.rank = MAX_RANK_SENTINEL; 276 | n.inputs = []; 277 | n.outputs = []; 278 | n.rpo = -1; 279 | n.outputApproach = MINIMUM_NODE_OUTPUT_APPROACH; 280 | n.cfg = n.control; 281 | g.nodeMap[n.id] = n; 282 | n.displayLabel = n.getDisplayLabel(); 283 | n.labelbbox = g.measureText(n.displayLabel); 284 | n.typebbox = g.measureText(n.getDisplayType()); 285 | var innerwidth = Math.max(n.labelbbox.width, n.typebbox.width); 286 | n.width = Math.alignUp(innerwidth + NODE_INPUT_WIDTH * 2, 287 | NODE_INPUT_WIDTH); 288 | var innerheight = Math.max(n.labelbbox.height, n.typebbox.height); 289 | n.normalheight = innerheight + 20; 290 | }); 291 | g.edges = []; 292 | data.edges.forEach(function(e, i){ 293 | var t = g.nodeMap[e.target]; 294 | var s = g.nodeMap[e.source]; 295 | var newEdge = new Edge(t, e.index, s, e.type); 296 | t.inputs.push(newEdge); 297 | s.outputs.push(newEdge); 298 | g.edges.push(newEdge); 299 | if (e.type == 'control') { 300 | s.cfg = true; 301 | } 302 | }); 303 | g.nodes.forEach(function(n, i) { 304 | n.visible = isNodeInitiallyVisible(n); 305 | if (initiallyVisibileIds != undefined) { 306 | if (initiallyVisibileIds.has(n.id)) { 307 | n.visible = true; 308 | } 309 | } 310 | }); 311 | g.fitGraphViewToWindow(); 312 | g.updateGraphVisibility(); 313 | g.layoutGraph(); 314 | g.updateGraphVisibility(); 315 | g.viewWholeGraph(); 316 | } 317 | 318 | connectVisibleSelectedNodes() { 319 | var graph = this; 320 | graph.state.selection.selection.forEach(function(element) { 321 | var edgeNumber = 0; 322 | element.__data__.inputs.forEach(function(edge) { 323 | if (edge.source.visible && edge.target.visible) { 324 | edge.visible = true; 325 | } 326 | }); 327 | element.__data__.outputs.forEach(function(edge) { 328 | if (edge.source.visible && edge.target.visible) { 329 | edge.visible = true; 330 | } 331 | }); 332 | }); 333 | } 334 | 335 | updateInputAndOutputBubbles() { 336 | var g = this; 337 | var s = g.visibleBubbles; 338 | s.classed("filledBubbleStyle", function(c) { 339 | var components = this.id.split(','); 340 | if (components[0] == "ib") { 341 | var edge = g.nodeMap[components[3]].inputs[components[2]]; 342 | return edge.isVisible(); 343 | } else { 344 | return g.nodeMap[components[1]].areAnyOutputsVisible() == 2; 345 | } 346 | }).classed("halfFilledBubbleStyle", function(c) { 347 | var components = this.id.split(','); 348 | if (components[0] == "ib") { 349 | var edge = g.nodeMap[components[3]].inputs[components[2]]; 350 | return false; 351 | } else { 352 | return g.nodeMap[components[1]].areAnyOutputsVisible() == 1; 353 | } 354 | }).classed("bubbleStyle", function(c) { 355 | var components = this.id.split(','); 356 | if (components[0] == "ib") { 357 | var edge = g.nodeMap[components[3]].inputs[components[2]]; 358 | return !edge.isVisible(); 359 | } else { 360 | return g.nodeMap[components[1]].areAnyOutputsVisible() == 0; 361 | } 362 | }); 363 | s.each(function(c) { 364 | var components = this.id.split(','); 365 | if (components[0] == "ob") { 366 | var from = g.nodeMap[components[1]]; 367 | var x = from.getOutputX(); 368 | var y = g.getNodeHeight(from) + DEFAULT_NODE_BUBBLE_RADIUS; 369 | var transform = "translate(" + x + "," + y + ")"; 370 | this.setAttribute('transform', transform); 371 | } 372 | }); 373 | } 374 | 375 | attachSelection(s) { 376 | var graph = this; 377 | if (s.size != 0) { 378 | this.visibleNodes.each(function(n) { 379 | if (s.has(this.__data__.id)) { 380 | graph.state.selection.select(this, true); 381 | } 382 | }); 383 | } 384 | } 385 | 386 | detachSelection() { 387 | var selection = this.state.selection.detachSelection(); 388 | var s = new Set(); 389 | for (var i of selection) { 390 | s.add(i.__data__.id); 391 | }; 392 | return s; 393 | } 394 | 395 | pathMouseDown(path, d) { 396 | d3.event.stopPropagation(); 397 | this.state.selection.clear(); 398 | this.state.selection.add(path); 399 | }; 400 | 401 | nodeMouseDown(node, d) { 402 | d3.event.stopPropagation(); 403 | this.state.mouseDownNode = d; 404 | } 405 | 406 | nodeMouseUp(d3node, d) { 407 | var graph = this, 408 | state = graph.state, 409 | consts = graph.consts; 410 | 411 | var mouseDownNode = state.mouseDownNode; 412 | 413 | if (!mouseDownNode) return; 414 | 415 | if (state.justDragged) { 416 | // dragged, not clicked 417 | redetermineGraphBoundingBox(graph); 418 | state.justDragged = false; 419 | } else{ 420 | // clicked, not dragged 421 | var extend = d3.event.shiftKey; 422 | var selection = graph.state.selection; 423 | if (!extend) { 424 | selection.clear(); 425 | } 426 | selection.select(d3node[0][0], true); 427 | } 428 | } 429 | 430 | selectSourcePositions(start, end, selected) { 431 | var graph = this; 432 | var map = []; 433 | var sel = graph.nodes.filter(function(n) { 434 | var pos = (n.pos === undefined) 435 | ? -1 436 | : n.getFunctionRelativeSourcePosition(graph); 437 | if (pos >= start && pos < end) { 438 | map[n.id] = true; 439 | n.visible = true; 440 | } 441 | }); 442 | graph.updateGraphVisibility(); 443 | graph.visibleNodes.filter(function(n) { return map[n.id]; }) 444 | .each(function(n) { 445 | var selection = graph.state.selection; 446 | selection.select(d3.select(this), selected); 447 | }); 448 | } 449 | 450 | selectAllNodes(inEdges, filter) { 451 | var graph = this; 452 | if (!d3.event.shiftKey) { 453 | graph.state.selection.clear(); 454 | } 455 | graph.state.selection.select(graph.visibleNodes[0], true); 456 | graph.updateGraphVisibility(); 457 | } 458 | 459 | uploadAction(graph) { 460 | document.getElementById("hidden-file-upload").click(); 461 | } 462 | 463 | layoutAction(graph) { 464 | graph.updateGraphVisibility(); 465 | graph.layoutGraph(); 466 | graph.updateGraphVisibility(); 467 | graph.viewWholeGraph(); 468 | } 469 | 470 | showAllAction(graph) { 471 | graph.nodes.filter(function(n) { n.visible = true; }) 472 | graph.edges.filter(function(e) { e.visible = true; }) 473 | graph.updateGraphVisibility(); 474 | graph.viewWholeGraph(); 475 | } 476 | 477 | hideDeadAction(graph) { 478 | graph.nodes.filter(function(n) { if (!n.isLive()) n.visible = false; }) 479 | graph.updateGraphVisibility(); 480 | } 481 | 482 | hideUnselectedAction(graph) { 483 | var unselected = graph.visibleNodes.filter(function(n) { 484 | return !this.classList.contains("selected"); 485 | }); 486 | unselected.each(function(n) { 487 | n.visible = false; 488 | }); 489 | graph.updateGraphVisibility(); 490 | } 491 | 492 | hideSelectedAction(graph) { 493 | var selected = graph.visibleNodes.filter(function(n) { 494 | return this.classList.contains("selected"); 495 | }); 496 | selected.each(function(n) { 497 | n.visible = false; 498 | }); 499 | graph.state.selection.clear(); 500 | graph.updateGraphVisibility(); 501 | } 502 | 503 | zoomSelectionAction(graph) { 504 | graph.viewSelection(); 505 | } 506 | 507 | toggleTypesAction(graph) { 508 | graph.toggleTypes(); 509 | } 510 | 511 | searchInputAction(graph) { 512 | if (d3.event.keyCode == 13) { 513 | graph.state.selection.clear(); 514 | var query = this.value; 515 | window.sessionStorage.setItem("lastSearch", query); 516 | 517 | var reg = new RegExp(query); 518 | var filterFunction = function(n) { 519 | return (reg.exec(n.getDisplayLabel()) != null || 520 | (graph.state.showTypes && reg.exec(n.getDisplayType())) || 521 | reg.exec(n.opcode) != null); 522 | }; 523 | if (d3.event.ctrlKey) { 524 | graph.nodes.forEach(function(n, i) { 525 | if (filterFunction(n)) { 526 | n.visible = true; 527 | } 528 | }); 529 | graph.updateGraphVisibility(); 530 | } 531 | var selected = graph.visibleNodes.each(function(n) { 532 | if (filterFunction(n)) { 533 | graph.state.selection.select(this, true); 534 | } 535 | }); 536 | graph.connectVisibleSelectedNodes(); 537 | graph.updateGraphVisibility(); 538 | this.blur(); 539 | graph.viewSelection(); 540 | } 541 | d3.event.stopPropagation(); 542 | } 543 | 544 | svgMouseDown() { 545 | this.state.graphMouseDown = true; 546 | } 547 | 548 | svgMouseUp() { 549 | var graph = this, 550 | state = graph.state; 551 | if (state.justScaleTransGraph) { 552 | // Dragged 553 | state.justScaleTransGraph = false; 554 | } else { 555 | // Clicked 556 | if (state.mouseDownNode == null) { 557 | graph.state.selection.clear(); 558 | } 559 | } 560 | state.mouseDownNode = null; 561 | state.graphMouseDown = false; 562 | } 563 | 564 | svgKeyDown() { 565 | var state = this.state; 566 | var graph = this; 567 | 568 | // Don't handle key press repetition 569 | if(state.lastKeyDown !== -1) return; 570 | 571 | var showSelectionFrontierNodes = function(inEdges, filter, select) { 572 | var frontier = graph.getNodeFrontier(state.selection.selection, inEdges, filter); 573 | if (frontier != undefined) { 574 | if (select) { 575 | if (!d3.event.shiftKey) { 576 | state.selection.clear(); 577 | } 578 | state.selection.select(frontier[0], true); 579 | } 580 | graph.updateGraphVisibility(); 581 | } 582 | allowRepetition = false; 583 | } 584 | 585 | var allowRepetition = true; 586 | var eventHandled = true; // unless the below switch defaults 587 | switch(d3.event.keyCode) { 588 | case 49: 589 | case 50: 590 | case 51: 591 | case 52: 592 | case 53: 593 | case 54: 594 | case 55: 595 | case 56: 596 | case 57: 597 | // '1'-'9' 598 | showSelectionFrontierNodes(true, 599 | (edge, index) => { return index == (d3.event.keyCode - 49); }, 600 | false); 601 | break; 602 | case 97: 603 | case 98: 604 | case 99: 605 | case 100: 606 | case 101: 607 | case 102: 608 | case 103: 609 | case 104: 610 | case 105: 611 | // 'numpad 1'-'numpad 9' 612 | showSelectionFrontierNodes(true, 613 | (edge, index) => { return index == (d3.event.keyCode - 97); }, 614 | false); 615 | break; 616 | case 67: 617 | // 'c' 618 | showSelectionFrontierNodes(true, 619 | (edge, index) => { return edge.type == 'control'; }, 620 | false); 621 | break; 622 | case 69: 623 | // 'e' 624 | showSelectionFrontierNodes(true, 625 | (edge, index) => { return edge.type == 'effect'; }, 626 | false); 627 | break; 628 | case 79: 629 | // 'o' 630 | showSelectionFrontierNodes(false, undefined, false); 631 | break; 632 | case 73: 633 | // 'i' 634 | showSelectionFrontierNodes(true, undefined, false); 635 | break; 636 | case 65: 637 | // 'a' 638 | graph.selectAllNodes(); 639 | allowRepetition = false; 640 | break; 641 | case 38: 642 | case 40: { 643 | showSelectionFrontierNodes(d3.event.keyCode == 38, undefined, true); 644 | break; 645 | } 646 | case 82: 647 | // 'r' 648 | if (!d3.event.ctrlKey) { 649 | this.layoutAction(this); 650 | } else { 651 | eventHandled = false; 652 | } 653 | break; 654 | case 191: 655 | // '/' 656 | document.getElementById("search-input").focus(); 657 | document.getElementById("search-input").select(); 658 | break; 659 | default: 660 | eventHandled = false; 661 | break; 662 | } 663 | if (eventHandled) { 664 | d3.event.preventDefault(); 665 | } 666 | if (!allowRepetition) { 667 | state.lastKeyDown = d3.event.keyCode; 668 | } 669 | } 670 | 671 | svgKeyUp() { 672 | this.state.lastKeyDown = -1 673 | }; 674 | 675 | layoutEdges() { 676 | var graph = this; 677 | graph.maxGraphX = graph.maxGraphNodeX; 678 | this.visibleEdges.attr("d", function(edge){ 679 | return edge.generatePath(graph); 680 | }); 681 | } 682 | 683 | layoutGraph() { 684 | layoutNodeGraph(this); 685 | } 686 | 687 | // call to propagate changes to graph 688 | updateGraphVisibility() { 689 | 690 | var graph = this, 691 | state = graph.state; 692 | 693 | var filteredEdges = graph.edges.filter(function(e) { return e.isVisible(); }); 694 | var visibleEdges = graph.visibleEdges.data(filteredEdges, function(edge) { 695 | return edge.stringID(); 696 | }); 697 | 698 | // add new paths 699 | visibleEdges.enter() 700 | .append('path') 701 | .style('marker-end','url(#end-arrow)') 702 | .classed('hidden', function(e) { 703 | return !e.isVisible(); 704 | }) 705 | .attr("id", function(edge){ return "e," + edge.stringID(); }) 706 | .on("mousedown", function(d){ 707 | graph.pathMouseDown.call(graph, d3.select(this), d); 708 | }) 709 | .attr("adjacentToHover", "false"); 710 | 711 | // Set the correct styles on all of the paths 712 | visibleEdges.classed('value', function(e) { 713 | return e.type == 'value' || e.type == 'context'; 714 | }).classed('control', function(e) { 715 | return e.type == 'control'; 716 | }).classed('effect', function(e) { 717 | return e.type == 'effect'; 718 | }).classed('frame-state', function(e) { 719 | return e.type == 'frame-state'; 720 | }).attr('stroke-dasharray', function(e) { 721 | if (e.type == 'frame-state') return "10,10"; 722 | return (e.type == 'effect') ? "5,5" : ""; 723 | }); 724 | 725 | // remove old links 726 | visibleEdges.exit().remove(); 727 | 728 | graph.visibleEdges = visibleEdges; 729 | 730 | // update existing nodes 731 | var filteredNodes = graph.nodes.filter(function(n) { return n.visible; }); 732 | graph.visibleNodes = graph.visibleNodes.data(filteredNodes, function(d) { 733 | return d.id; 734 | }); 735 | graph.visibleNodes.attr("transform", function(n){ 736 | return "translate(" + n.x + "," + n.y + ")"; 737 | }).select('rect'). 738 | attr(HEIGHT, function(d) { return graph.getNodeHeight(d); }); 739 | 740 | // add new nodes 741 | var newGs = graph.visibleNodes.enter() 742 | .append("g"); 743 | 744 | newGs.classed("turbonode", function(n) { return true; }) 745 | .classed("control", function(n) { return n.isControl(); }) 746 | .classed("live", function(n) { return n.isLive(); }) 747 | .classed("dead", function(n) { return !n.isLive(); }) 748 | .classed("javascript", function(n) { return n.isJavaScript(); }) 749 | .classed("input", function(n) { return n.isInput(); }) 750 | .classed("simplified", function(n) { return n.isSimplified(); }) 751 | .classed("machine", function(n) { return n.isMachine(); }) 752 | .attr("transform", function(d){ return "translate(" + d.x + "," + d.y + ")";}) 753 | .on("mousedown", function(d){ 754 | graph.nodeMouseDown.call(graph, d3.select(this), d); 755 | }) 756 | .on("mouseup", function(d){ 757 | graph.nodeMouseUp.call(graph, d3.select(this), d); 758 | }) 759 | .on('mouseover', function(d){ 760 | var nodeSelection = d3.select(this); 761 | let node = graph.nodeMap[d.id]; 762 | let adjInputEdges = graph.visibleEdges.filter(e => { return e.target === node; }); 763 | let adjOutputEdges = graph.visibleEdges.filter(e => { return e.source === node; }); 764 | adjInputEdges.attr('relToHover', "input"); 765 | adjOutputEdges.attr('relToHover', "output"); 766 | let adjInputNodes = adjInputEdges.data().map(e => e.source); 767 | graph.visibleNodes.data(adjInputNodes, function(d) { 768 | return d.id; 769 | }).attr('relToHover', "input"); 770 | let adjOutputNodes = adjOutputEdges.data().map(e => e.target); 771 | graph.visibleNodes.data(adjOutputNodes, function(d) { 772 | return d.id; 773 | }).attr('relToHover', "output"); 774 | graph.updateGraphVisibility(); 775 | }) 776 | .on('mouseout', function(d){ 777 | var nodeSelection = d3.select(this); 778 | let node = graph.nodeMap[d.id]; 779 | let adjEdges = graph.visibleEdges.filter(e => { return e.target === node || e.source === node; }); 780 | adjEdges.attr('relToHover', "none"); 781 | let adjNodes = adjEdges.data().map(e => e.target).concat(adjEdges.data().map(e => e.source)); 782 | let nodes = graph.visibleNodes.data(adjNodes, function(d) { 783 | return d.id; 784 | }).attr('relToHover', "none"); 785 | graph.updateGraphVisibility(); 786 | }) 787 | .call(graph.drag); 788 | 789 | newGs.append("rect") 790 | .attr("rx", 10) 791 | .attr("ry", 10) 792 | .attr(WIDTH, function(d) { 793 | return d.getTotalNodeWidth(); 794 | }) 795 | .attr(HEIGHT, function(d) { 796 | return graph.getNodeHeight(d); 797 | }) 798 | 799 | function appendInputAndOutputBubbles(g, d) { 800 | for (var i = 0; i < d.inputs.length; ++i) { 801 | var x = d.getInputX(i); 802 | var y = -DEFAULT_NODE_BUBBLE_RADIUS; 803 | var s = g.append('circle') 804 | .classed("filledBubbleStyle", function(c) { 805 | return d.inputs[i].isVisible(); 806 | } ) 807 | .classed("bubbleStyle", function(c) { 808 | return !d.inputs[i].isVisible(); 809 | } ) 810 | .attr("id", "ib," + d.inputs[i].stringID()) 811 | .attr("r", DEFAULT_NODE_BUBBLE_RADIUS) 812 | .attr("transform", function(d) { 813 | return "translate(" + x + "," + y + ")"; 814 | }) 815 | .on("mousedown", function(d){ 816 | var components = this.id.split(','); 817 | var node = graph.nodeMap[components[3]]; 818 | var edge = node.inputs[components[2]]; 819 | var visible = !edge.isVisible(); 820 | node.setInputVisibility(components[2], visible); 821 | d3.event.stopPropagation(); 822 | graph.updateGraphVisibility(); 823 | }); 824 | } 825 | if (d.outputs.length != 0) { 826 | var x = d.getOutputX(); 827 | var y = graph.getNodeHeight(d) + DEFAULT_NODE_BUBBLE_RADIUS; 828 | var s = g.append('circle') 829 | .classed("filledBubbleStyle", function(c) { 830 | return d.areAnyOutputsVisible() == 2; 831 | } ) 832 | .classed("halFilledBubbleStyle", function(c) { 833 | return d.areAnyOutputsVisible() == 1; 834 | } ) 835 | .classed("bubbleStyle", function(c) { 836 | return d.areAnyOutputsVisible() == 0; 837 | } ) 838 | .attr("id", "ob," + d.id) 839 | .attr("r", DEFAULT_NODE_BUBBLE_RADIUS) 840 | .attr("transform", function(d) { 841 | return "translate(" + x + "," + y + ")"; 842 | }) 843 | .on("mousedown", function(d) { 844 | d.setOutputVisibility(d.areAnyOutputsVisible() == 0); 845 | d3.event.stopPropagation(); 846 | graph.updateGraphVisibility(); 847 | }); 848 | } 849 | } 850 | 851 | newGs.each(function(d){ 852 | appendInputAndOutputBubbles(d3.select(this), d); 853 | }); 854 | 855 | newGs.each(function(d){ 856 | d3.select(this).append("text") 857 | .classed("label", true) 858 | .attr("text-anchor","right") 859 | .attr("dx", 5) 860 | .attr("dy", 5) 861 | .append('tspan') 862 | .text(function(l) { 863 | return d.getDisplayLabel(); 864 | }) 865 | .append("title") 866 | .text(function(l) { 867 | return d.getTitle(); 868 | }) 869 | if (d.type != undefined) { 870 | d3.select(this).append("text") 871 | .classed("label", true) 872 | .classed("type", true) 873 | .attr("text-anchor","right") 874 | .attr("dx", 5) 875 | .attr("dy", d.labelbbox.height + 5) 876 | .append('tspan') 877 | .text(function(l) { 878 | return d.getDisplayType(); 879 | }) 880 | .append("title") 881 | .text(function(l) { 882 | return d.getType(); 883 | }) 884 | } 885 | }); 886 | 887 | graph.visibleNodes.select('.type').each(function (d) { 888 | this.setAttribute('visibility', graph.state.showTypes ? 'visible' : 'hidden'); 889 | }); 890 | 891 | // remove old nodes 892 | graph.visibleNodes.exit().remove(); 893 | 894 | graph.visibleBubbles = d3.selectAll('circle'); 895 | 896 | graph.updateInputAndOutputBubbles(); 897 | 898 | graph.layoutEdges(); 899 | 900 | graph.svg.style.height = '100%'; 901 | } 902 | 903 | getVisibleTranslation(translate, scale) { 904 | var graph = this; 905 | var height = (graph.maxGraphY - graph.minGraphY + 2 * GRAPH_MARGIN) * scale; 906 | var width = (graph.maxGraphX - graph.minGraphX + 2 * GRAPH_MARGIN) * scale; 907 | 908 | var dimensions = this.getSvgViewDimensions(); 909 | 910 | var baseY = translate[1]; 911 | var minY = (graph.minGraphY - GRAPH_MARGIN) * scale; 912 | var maxY = (graph.maxGraphY + GRAPH_MARGIN) * scale; 913 | 914 | var adjustY = 0; 915 | var adjustYCandidate = 0; 916 | if ((maxY + baseY) < dimensions[1]) { 917 | adjustYCandidate = dimensions[1] - (maxY + baseY); 918 | if ((minY + baseY + adjustYCandidate) > 0) { 919 | adjustY = (dimensions[1] / 2) - (maxY - (height / 2)) - baseY; 920 | } else { 921 | adjustY = adjustYCandidate; 922 | } 923 | } else if (-baseY < minY) { 924 | adjustYCandidate = -(baseY + minY); 925 | if ((maxY + baseY + adjustYCandidate) < dimensions[1]) { 926 | adjustY = (dimensions[1] / 2) - (maxY - (height / 2)) - baseY; 927 | } else { 928 | adjustY = adjustYCandidate; 929 | } 930 | } 931 | translate[1] += adjustY; 932 | 933 | var baseX = translate[0]; 934 | var minX = (graph.minGraphX - GRAPH_MARGIN) * scale; 935 | var maxX = (graph.maxGraphX + GRAPH_MARGIN) * scale; 936 | 937 | var adjustX = 0; 938 | var adjustXCandidate = 0; 939 | if ((maxX + baseX) < dimensions[0]) { 940 | adjustXCandidate = dimensions[0] - (maxX + baseX); 941 | if ((minX + baseX + adjustXCandidate) > 0) { 942 | adjustX = (dimensions[0] / 2) - (maxX - (width / 2)) - baseX; 943 | } else { 944 | adjustX = adjustXCandidate; 945 | } 946 | } else if (-baseX < minX) { 947 | adjustXCandidate = -(baseX + minX); 948 | if ((maxX + baseX + adjustXCandidate) < dimensions[0]) { 949 | adjustX = (dimensions[0] / 2) - (maxX - (width / 2)) - baseX; 950 | } else { 951 | adjustX = adjustXCandidate; 952 | } 953 | } 954 | translate[0] += adjustX; 955 | return translate; 956 | } 957 | 958 | translateClipped(translate, scale, transition) { 959 | var graph = this; 960 | var graphNode = this.graphElement[0][0]; 961 | var translate = this.getVisibleTranslation(translate, scale); 962 | if (transition) { 963 | graphNode.classList.add('visible-transition'); 964 | clearTimeout(graph.transitionTimout); 965 | graph.transitionTimout = setTimeout(function(){ 966 | graphNode.classList.remove('visible-transition'); 967 | }, 1000); 968 | } 969 | var translateString = "translate(" + translate[0] + "px," + translate[1] + "px) scale(" + scale + ")"; 970 | graphNode.style.transform = translateString; 971 | graph.dragSvg.translate(translate); 972 | graph.dragSvg.scale(scale); 973 | } 974 | 975 | zoomed(){ 976 | this.state.justScaleTransGraph = true; 977 | var scale = this.dragSvg.scale(); 978 | this.translateClipped(d3.event.translate, scale); 979 | } 980 | 981 | 982 | getSvgViewDimensions() { 983 | var canvasWidth = this.parentNode.clientWidth; 984 | var documentElement = document.documentElement; 985 | var canvasHeight = documentElement.clientHeight; 986 | return [canvasWidth, canvasHeight]; 987 | } 988 | 989 | 990 | minScale() { 991 | var graph = this; 992 | var dimensions = this.getSvgViewDimensions(); 993 | var width = graph.maxGraphX - graph.minGraphX; 994 | var height = graph.maxGraphY - graph.minGraphY; 995 | var minScale = dimensions[0] / (width + GRAPH_MARGIN * 2); 996 | var minScaleYCandidate = dimensions[1] / (height + GRAPH_MARGIN * 2); 997 | if (minScaleYCandidate < minScale) { 998 | minScale = minScaleYCandidate; 999 | } 1000 | this.dragSvg.scaleExtent([minScale, 1.5]); 1001 | return minScale; 1002 | } 1003 | 1004 | fitGraphViewToWindow() { 1005 | this.svg.attr("height", document.documentElement.clientHeight + "px"); 1006 | this.translateClipped(this.dragSvg.translate(), this.dragSvg.scale()); 1007 | } 1008 | 1009 | toggleTypes() { 1010 | var graph = this; 1011 | graph.state.showTypes = !graph.state.showTypes; 1012 | var element = document.getElementById('toggle-types'); 1013 | if (graph.state.showTypes) { 1014 | element.classList.add('button-input-toggled'); 1015 | } else { 1016 | element.classList.remove('button-input-toggled'); 1017 | } 1018 | graph.updateGraphVisibility(); 1019 | } 1020 | 1021 | viewSelection() { 1022 | var graph = this; 1023 | var minX, maxX, minY, maxY; 1024 | var hasSelection = false; 1025 | graph.visibleNodes.each(function(n) { 1026 | if (this.classList.contains("selected")) { 1027 | hasSelection = true; 1028 | minX = minX ? Math.min(minX, n.x) : n.x; 1029 | maxX = maxX ? Math.max(maxX, n.x + n.getTotalNodeWidth()) : 1030 | n.x + n.getTotalNodeWidth(); 1031 | minY = minY ? Math.min(minY, n.y) : n.y; 1032 | maxY = maxY ? Math.max(maxY, n.y + graph.getNodeHeight(n)) : 1033 | n.y + graph.getNodeHeight(n); 1034 | } 1035 | }); 1036 | if (hasSelection) { 1037 | graph.viewGraphRegion(minX - NODE_INPUT_WIDTH, minY - 60, 1038 | maxX + NODE_INPUT_WIDTH, maxY + 60, 1039 | true); 1040 | } 1041 | } 1042 | 1043 | viewGraphRegion(minX, minY, maxX, maxY, transition) { 1044 | var graph = this; 1045 | var dimensions = this.getSvgViewDimensions(); 1046 | var width = maxX - minX; 1047 | var height = maxY - minY; 1048 | var scale = Math.min(dimensions[0] / width, dimensions[1] / height); 1049 | scale = Math.min(1.5, scale); 1050 | scale = Math.max(graph.minScale(), scale); 1051 | var translation = [-minX*scale, -minY*scale]; 1052 | translation = graph.getVisibleTranslation(translation, scale); 1053 | graph.translateClipped(translation, scale, transition); 1054 | } 1055 | 1056 | viewWholeGraph() { 1057 | var graph = this; 1058 | var minScale = graph.minScale(); 1059 | var translation = [0, 0]; 1060 | translation = graph.getVisibleTranslation(translation, minScale); 1061 | graph.translateClipped(translation, minScale); 1062 | } 1063 | } 1064 | -------------------------------------------------------------------------------- /hide-selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thlorenz/turbolizer/fa3377527f030c8909a6d51e03a1eb719a618b83/hide-selected.png -------------------------------------------------------------------------------- /hide-unselected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thlorenz/turbolizer/fa3377527f030c8909a6d51e03a1eb719a618b83/hide-unselected.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |11 |
49 |
61 |