├── .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 | ![turbolizer](./assets/turbolizer.gif) 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 | Turbolizer 5 | 6 | 7 | 8 |
9 |
10 |
 11 |       
12 |
13 |
14 |
15 |
16 | 17 | 19 | 21 | 23 | 25 | 27 | 29 | 31 | 34 | 37 | 38 |
39 | 40 |
41 | 42 | 44 |
45 |
46 |
47 |
48 |
 49 |           
    50 |
51 |
52 |
53 |
54 | 55 |
56 |
57 |
58 | 66 |
67 | 69 | 71 |
72 |
73 | 75 | 77 |
78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /lang-disassembly.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 | PR.registerLangHandler( 6 | PR.createSimpleLexer( 7 | [ 8 | [PR.PR_STRING, /^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$))/, null, '\''], 9 | [PR.PR_PLAIN, /^\s+/, null, ' \r\n\t\xA0'] 10 | ], 11 | [ // fallthroughStylePatterns 12 | [PR.PR_COMMENT, /;; debug: position \d+/, null], 13 | ]), 14 | ['disassembly']); 15 | -------------------------------------------------------------------------------- /layout-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thlorenz/turbolizer/fa3377527f030c8909a6d51e03a1eb719a618b83/layout-icon.png -------------------------------------------------------------------------------- /left-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thlorenz/turbolizer/fa3377527f030c8909a6d51e03a1eb719a618b83/left-arrow.png -------------------------------------------------------------------------------- /lib/client-loader.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* global location fetch */ 4 | 5 | async function preload() { 6 | try { 7 | if (!(/^\?preload=/).test(location.search)) return 8 | const fileUrl = location.search.replace(/^\?preload=/, '') 9 | const fileData = await fetch(fileUrl, { mode: 'no-cors' }) 10 | const txt = await fileData.text() 11 | window.renderTurbolizerData(txt) 12 | } catch (err) { 13 | console.error(err) 14 | } 15 | } 16 | 17 | preload() 18 | -------------------------------------------------------------------------------- /lib/turbolizer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { promisify } = require('util') 4 | const path = require('path') 5 | const fs = require('fs') 6 | const readdir = promisify(fs.readdir) 7 | const stat = promisify(fs.stat) 8 | const access = promisify(fs.access) 9 | 10 | async function canRead(p) { 11 | try { 12 | await access(p, fs.constants.R_OK) 13 | return true 14 | } catch (err) { 15 | return false 16 | } 17 | } 18 | 19 | async function findAllTurboFiles(root) { 20 | const allEntries = await readdir(root) 21 | const turboFiles = [] 22 | for (const entry of allEntries) { 23 | if (!(/^turbo-.+\.json$/).test(entry)) continue 24 | 25 | const fullPath = path.join(root, entry) 26 | if (!(await canRead(fullPath))) continue 27 | if (!(await stat(fullPath)).isFile()) continue 28 | turboFiles.push({ fullPath, entry }) 29 | } 30 | 31 | return turboFiles 32 | } 33 | 34 | function makeSelectable(arr) { 35 | const map = new Map() 36 | for (var i = 0; i < arr.length; i++) { 37 | map.set(`${i + 1}`, arr[i]) 38 | } 39 | return map 40 | } 41 | 42 | async function mapAllTurboFiles(root) { 43 | const arr = await findAllTurboFiles(root) 44 | return makeSelectable(arr) 45 | } 46 | 47 | module.exports = { 48 | findAllTurboFiles 49 | , mapAllTurboFiles 50 | } 51 | 52 | -------------------------------------------------------------------------------- /lib/turbolizer.server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const ecstatic = require('ecstatic') 5 | const connect = require('connect') 6 | const opener = require('opener') 7 | 8 | function createServer(turboFilesDir, PORT = null) { 9 | const appDir = path.join(__dirname, '..') 10 | const ecstaticApp = ecstatic({ root: appDir }) 11 | const ecstaticFiles = ecstatic({ root: turboFilesDir }) 12 | const app = connect() 13 | .use('/', ecstaticApp) 14 | .use('/data/', ecstaticFiles) 15 | 16 | const server = app.listen(PORT) 17 | const { port } = server.address() 18 | const address = `http://localhost:${port}` 19 | return { server, address } 20 | } 21 | 22 | function openWithFile({ address, file }) { 23 | const url = `${address}?preload=${address}/data/${file}` 24 | opener(url) 25 | } 26 | 27 | module.exports = { createServer, openWithFile } 28 | -------------------------------------------------------------------------------- /live.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thlorenz/turbolizer/fa3377527f030c8909a6d51e03a1eb719a618b83/live.png -------------------------------------------------------------------------------- /monkey.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 | Math.alignUp = function(raw, multiple) { 6 | return Math.floor((raw + multiple - 1) / multiple) * multiple; 7 | } 8 | -------------------------------------------------------------------------------- /node.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 TYPE_HEIGHT = 25; 6 | var DEFAULT_NODE_BUBBLE_RADIUS = 12; 7 | var NODE_INPUT_WIDTH = 50; 8 | var MINIMUM_NODE_INPUT_APPROACH = 15 + 2 * DEFAULT_NODE_BUBBLE_RADIUS; 9 | var MINIMUM_NODE_OUTPUT_APPROACH = 15; 10 | 11 | function isNodeInitiallyVisible(node) { 12 | return node.cfg; 13 | } 14 | 15 | var Node = { 16 | isControl: function() { 17 | return this.control; 18 | }, 19 | isInput: function() { 20 | return this.opcode == 'Parameter' || this.opcode.endsWith('Constant'); 21 | }, 22 | isLive: function() { 23 | return this.live !== false; 24 | }, 25 | isJavaScript: function() { 26 | return this.opcode.startsWith('JS'); 27 | }, 28 | isSimplified: function() { 29 | if (this.isJavaScript()) return false; 30 | return this.opcode.endsWith('Phi') || 31 | this.opcode.startsWith('Boolean') || 32 | this.opcode.startsWith('Number') || 33 | this.opcode.startsWith('String') || 34 | this.opcode.startsWith('Change') || 35 | this.opcode.startsWith('Object') || 36 | this.opcode.startsWith('Reference') || 37 | this.opcode.startsWith('Any') || 38 | this.opcode.endsWith('ToNumber') || 39 | (this.opcode == 'AnyToBoolean') || 40 | (this.opcode.startsWith('Load') && this.opcode.length > 4) || 41 | (this.opcode.startsWith('Store') && this.opcode.length > 5); 42 | }, 43 | isMachine: function() { 44 | return !(this.isControl() || this.isInput() || 45 | this.isJavaScript() || this.isSimplified()); 46 | }, 47 | getTotalNodeWidth: function() { 48 | var inputWidth = this.inputs.length * NODE_INPUT_WIDTH; 49 | return Math.max(inputWidth, this.width); 50 | }, 51 | getTitle: function() { 52 | var propsString; 53 | if (this.properties === undefined) { 54 | propsString = ""; 55 | } else if (this.properties === "") { 56 | propsString = "no properties"; 57 | } else { 58 | propsString = "[" + this.properties + "]"; 59 | } 60 | return this.title + "\n" + propsString + "\n" + this.opinfo; 61 | }, 62 | getDisplayLabel: function() { 63 | var result = this.id + ":" + this.label; 64 | if (result.length > 40) { 65 | return this.id + ":" + this.opcode; 66 | } else { 67 | return result; 68 | } 69 | }, 70 | getType: function() { 71 | return this.type; 72 | }, 73 | getDisplayType: function() { 74 | var type_string = this.type; 75 | if (type_string == undefined) return ""; 76 | if (type_string.length > 24) { 77 | type_string = type_string.substr(0, 25) + "..."; 78 | } 79 | return type_string; 80 | }, 81 | deepestInputRank: function() { 82 | var deepestRank = 0; 83 | this.inputs.forEach(function(e) { 84 | if (e.isVisible() && !e.isBackEdge()) { 85 | if (e.source.rank > deepestRank) { 86 | deepestRank = e.source.rank; 87 | } 88 | } 89 | }); 90 | return deepestRank; 91 | }, 92 | areAnyOutputsVisible: function() { 93 | var visibleCount = 0; 94 | this.outputs.forEach(function(e) { if (e.isVisible()) ++visibleCount; }); 95 | if (this.outputs.length == visibleCount) return 2; 96 | if (visibleCount != 0) return 1; 97 | return 0; 98 | }, 99 | setOutputVisibility: function(v) { 100 | var result = false; 101 | this.outputs.forEach(function(e) { 102 | e.visible = v; 103 | if (v) { 104 | if (!e.target.visible) { 105 | e.target.visible = true; 106 | result = true; 107 | } 108 | } 109 | }); 110 | return result; 111 | }, 112 | setInputVisibility: function(i, v) { 113 | var edge = this.inputs[i]; 114 | edge.visible = v; 115 | if (v) { 116 | if (!edge.source.visible) { 117 | edge.source.visible = true; 118 | return true; 119 | } 120 | } 121 | return false; 122 | }, 123 | getInputApproach: function(index) { 124 | return this.y - MINIMUM_NODE_INPUT_APPROACH - 125 | (index % 4) * MINIMUM_EDGE_SEPARATION - DEFAULT_NODE_BUBBLE_RADIUS 126 | }, 127 | getOutputApproach: function(graph, index) { 128 | return this.y + this.outputApproach + graph.getNodeHeight(this) + 129 | + DEFAULT_NODE_BUBBLE_RADIUS; 130 | }, 131 | getInputX: function(index) { 132 | var result = this.getTotalNodeWidth() - (NODE_INPUT_WIDTH / 2) + 133 | (index - this.inputs.length + 1) * NODE_INPUT_WIDTH; 134 | return result; 135 | }, 136 | getOutputX: function() { 137 | return this.getTotalNodeWidth() - (NODE_INPUT_WIDTH / 2); 138 | }, 139 | getFunctionRelativeSourcePosition: function(graph) { 140 | return this.pos - graph.sourcePosition; 141 | }, 142 | hasBackEdges: function() { 143 | return (this.opcode == "Loop") || 144 | ((this.opcode == "Phi" || this.opcode == "EffectPhi") && 145 | this.inputs[this.inputs.length - 1].source.opcode == "Loop"); 146 | } 147 | }; 148 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "turbolizer", 3 | "version": "0.2.0", 4 | "description": "Turbolizer tool from the v8 repository with added support to preload a profile", 5 | "main": "lib/turbolizer.js", 6 | "bin": "bin/turbolizer", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/thlorenz/turbolizer.git" 10 | }, 11 | "homepage": "https://github.com/thlorenz/turbolizer", 12 | "dependencies": { 13 | "connect": "~3.6.6", 14 | "ecstatic": "~4.1.4", 15 | "opener": "~1.4.3", 16 | "promptly": "~3.0.3" 17 | }, 18 | "devDependencies": {}, 19 | "keywords": [], 20 | "author": { 21 | "name": "Thorsten Lorenz", 22 | "email": "thlorenz@gmx.de", 23 | "url": "http://thlorenz.com" 24 | }, 25 | "license": { 26 | "type": "MIT", 27 | "url": "https://github.com/thlorenz/turbolizer/blob/master/LICENSE" 28 | }, 29 | "engine": { 30 | "node": ">=8" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /right-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thlorenz/turbolizer/fa3377527f030c8909a6d51e03a1eb719a618b83/right-arrow.png -------------------------------------------------------------------------------- /schedule-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 ScheduleView extends TextView { 8 | constructor(id, broker) { 9 | super(id, broker, null, false); 10 | let view = this; 11 | let BLOCK_STYLE = { 12 | css: 'tag' 13 | }; 14 | const BLOCK_HEADER_STYLE = { 15 | css: 'com', 16 | block_id: -1, 17 | location: function(text) { 18 | let matches = /\d+/.exec(text); 19 | if (!matches) return undefined; 20 | BLOCK_HEADER_STYLE.block_id = Number(matches[0]); 21 | return { 22 | block_id: BLOCK_HEADER_STYLE.block_id 23 | }; 24 | }, 25 | }; 26 | const BLOCK_LINK_STYLE = { 27 | css: 'tag', 28 | link: function(text) { 29 | let id = Number(text.substr(1)); 30 | view.select(function(location) { return location.block_id == id; }, true, true); 31 | } 32 | }; 33 | const ID_STYLE = { 34 | css: 'tag', 35 | location: function(text) { 36 | let matches = /\d+/.exec(text); 37 | return { 38 | node_id: Number(matches[0]), 39 | block_id: BLOCK_HEADER_STYLE.block_id 40 | }; 41 | }, 42 | }; 43 | const ID_LINK_STYLE = { 44 | css: 'tag', 45 | link: function(text) { 46 | let id = Number(text); 47 | view.select(function(location) { return location.node_id == id; }, true, true); 48 | } 49 | }; 50 | const NODE_STYLE = { css: 'kwd' }; 51 | const GOTO_STYLE = { css: 'kwd', 52 | goto_id: -2, 53 | location: function(text) { 54 | return { 55 | node_id: GOTO_STYLE.goto_id--, 56 | block_id: BLOCK_HEADER_STYLE.block_id 57 | }; 58 | } 59 | } 60 | const ARROW_STYLE = { css: 'kwd' }; 61 | let patterns = [ 62 | [ 63 | [/^--- BLOCK B\d+/, BLOCK_HEADER_STYLE, 1], 64 | [/^\s+\d+: /, ID_STYLE, 2], 65 | [/^\s+Goto/, GOTO_STYLE, 6], 66 | [/^.*/, null, -1] 67 | ], 68 | [ 69 | [/^ +/, null], 70 | [/^\(deferred\)/, BLOCK_HEADER_STYLE], 71 | [/^B\d+/, BLOCK_LINK_STYLE], 72 | [/^<-/, ARROW_STYLE], 73 | [/^->/, ARROW_STYLE], 74 | [/^,/, null], 75 | [/^---/, BLOCK_HEADER_STYLE, -1] 76 | ], 77 | // Parse opcode including [] 78 | [ 79 | [/^[A-Za-z0-9_]+(\[.*\])?$/, NODE_STYLE, -1], 80 | [/^[A-Za-z0-9_]+(\[(\[.*?\]|.)*?\])?/, NODE_STYLE, 3] 81 | ], 82 | // Parse optional parameters 83 | [ 84 | [/^ /, null, 4], 85 | [/^\(/, null], 86 | [/^\d+/, ID_LINK_STYLE], 87 | [/^, /, null], 88 | [/^\)$/, null, -1], 89 | [/^\)/, null, 4], 90 | ], 91 | [ 92 | [/^ -> /, ARROW_STYLE, 5], 93 | [/^.*/, null, -1] 94 | ], 95 | [ 96 | [/^B\d+$/, BLOCK_LINK_STYLE, -1], 97 | [/^B\d+/, BLOCK_LINK_STYLE], 98 | [/^, /, null] 99 | ], 100 | [ 101 | [/^ -> /, ARROW_STYLE], 102 | [/^B\d+$/, BLOCK_LINK_STYLE, -1] 103 | ] 104 | ]; 105 | this.setPatterns(patterns); 106 | } 107 | 108 | initializeContent(data, rememberedSelection) { 109 | super.initializeContent(data, rememberedSelection); 110 | var graph = this; 111 | var locations = []; 112 | for (var id of rememberedSelection) { 113 | locations.push({ node_id : id }); 114 | } 115 | this.selectLocations(locations, true, true); 116 | } 117 | 118 | detachSelection() { 119 | var selection = this.selection.detachSelection(); 120 | var s = new Set(); 121 | for (var i of selection) { 122 | if (i.location.node_id != undefined && i.location.node_id > 0) { 123 | s.add(i.location.node_id); 124 | } 125 | }; 126 | return s; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thlorenz/turbolizer/fa3377527f030c8909a6d51e03a1eb719a618b83/search.png -------------------------------------------------------------------------------- /search2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thlorenz/turbolizer/fa3377527f030c8909a6d51e03a1eb719a618b83/search2.png -------------------------------------------------------------------------------- /selection-broker.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 SelectionBroker = function() { 6 | this.brokers = []; 7 | this.dispatching = false; 8 | this.lastDispatchingHandler = null; 9 | this.nodePositionMap = []; 10 | this.sortedPositionList = []; 11 | this.positionNodeMap = []; 12 | }; 13 | 14 | SelectionBroker.prototype.addSelectionHandler = function(handler) { 15 | this.brokers.push(handler); 16 | } 17 | 18 | SelectionBroker.prototype.setNodePositionMap = function(map) { 19 | let broker = this; 20 | if (!map) return; 21 | broker.nodePositionMap = map; 22 | broker.positionNodeMap = []; 23 | broker.sortedPositionList = []; 24 | let next = 0; 25 | for (let i in broker.nodePositionMap) { 26 | broker.sortedPositionList[next] = Number(broker.nodePositionMap[i]); 27 | broker.positionNodeMap[next++] = i; 28 | } 29 | broker.sortedPositionList = sortUnique(broker.sortedPositionList, 30 | function(a,b) { return a - b; }); 31 | this.positionNodeMap.sort(function(a,b) { 32 | let result = broker.nodePositionMap[a] - broker.nodePositionMap[b]; 33 | if (result != 0) return result; 34 | return a - b; 35 | }); 36 | } 37 | 38 | SelectionBroker.prototype.select = function(from, locations, selected) { 39 | let broker = this; 40 | if (!broker.dispatching) { 41 | broker.lastDispatchingHandler = from; 42 | try { 43 | broker.dispatching = true; 44 | let enrichLocations = function(locations) { 45 | result = []; 46 | for (let location of locations) { 47 | let newLocation = {}; 48 | if (location.pos_start != undefined) { 49 | newLocation.pos_start = location.pos_start; 50 | } 51 | if (location.pos_end != undefined) { 52 | newLocation.pos_end = location.pos_end; 53 | } 54 | if (location.node_id != undefined) { 55 | newLocation.node_id = location.node_id; 56 | } 57 | if (location.block_id != undefined) { 58 | newLocation.block_id = location.block_id; 59 | } 60 | if (newLocation.pos_start == undefined && 61 | newLocation.pos_end == undefined && 62 | newLocation.node_id != undefined) { 63 | if (broker.nodePositionMap && broker.nodePositionMap[location.node_id]) { 64 | newLocation.pos_start = broker.nodePositionMap[location.node_id]; 65 | newLocation.pos_end = location.pos_start + 1; 66 | } 67 | } 68 | result.push(newLocation); 69 | } 70 | return result; 71 | } 72 | locations = enrichLocations(locations); 73 | for (var b of this.brokers) { 74 | if (b != from) { 75 | b.brokeredSelect(locations, selected); 76 | } 77 | } 78 | } 79 | finally { 80 | broker.dispatching = false; 81 | } 82 | } 83 | } 84 | 85 | SelectionBroker.prototype.clear = function(from) { 86 | this.lastDispatchingHandler = null; 87 | if (!this.dispatching) { 88 | try { 89 | this.dispatching = true; 90 | this.brokers.forEach(function(b) { 91 | if (b != from) { 92 | b.brokeredClear(); 93 | } 94 | }); 95 | } finally { 96 | this.dispatching = false; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /selection.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 Selection = function(handler) { 6 | this.handler = handler; 7 | this.selectionBase = null; 8 | this.lastSelection = null; 9 | this.selection = new Set(); 10 | } 11 | 12 | 13 | Selection.prototype.isEmpty = function() { 14 | return this.selection.size == 0; 15 | } 16 | 17 | 18 | Selection.prototype.clear = function() { 19 | var handler = this.handler; 20 | this.selectionBase = null; 21 | this.lastSelection = null; 22 | handler.select(this.selection, false); 23 | handler.clear(); 24 | this.selection = new Set(); 25 | } 26 | 27 | 28 | count = 0; 29 | 30 | Selection.prototype.select = function(s, isSelected) { 31 | var handler = this.handler; 32 | if (!(Symbol.iterator in Object(s))) { s = [s]; } 33 | if (isSelected) { 34 | let first = true; 35 | for (let i of s) { 36 | if (first) { 37 | this.selectionBase = i; 38 | this.lastSelection = i; 39 | first = false; 40 | } 41 | this.selection.add(i); 42 | } 43 | handler.select(this.selection, true); 44 | } else { 45 | let unselectSet = new Set(); 46 | for (let i of s) { 47 | if (this.selection.has(i)) { 48 | unselectSet.add(i); 49 | this.selection.delete(i); 50 | } 51 | } 52 | handler.select(unselectSet, false); 53 | } 54 | } 55 | 56 | 57 | Selection.prototype.extendTo = function(pos) { 58 | if (pos == this.lastSelection || this.lastSelection === null) return; 59 | 60 | var handler = this.handler; 61 | var pos_diff = handler.selectionDifference(pos, true, this.lastSelection, false); 62 | var unselect_diff = []; 63 | if (pos_diff.length == 0) { 64 | pos_diff = handler.selectionDifference(this.selectionBase, false, pos, true); 65 | if (pos_diff.length != 0) { 66 | unselect_diff = handler.selectionDifference(this.lastSelection, true, this.selectionBase, false); 67 | this.selection = new Set(); 68 | this.selection.add(this.selectionBase); 69 | for (var d of pos_diff) { 70 | this.selection.add(d); 71 | } 72 | } else { 73 | unselect_diff = handler.selectionDifference(this.lastSelection, true, pos, false); 74 | for (var d of unselect_diff) { 75 | this.selection.delete(d); 76 | } 77 | } 78 | } else { 79 | unselect_diff = handler.selectionDifference(this.selectionBase, false, this.lastSelection, true); 80 | if (unselect_diff != 0) { 81 | pos_diff = handler.selectionDifference(pos, true, this.selectionBase, false); 82 | if (pos_diff.length == 0) { 83 | unselect_diff = handler.selectionDifference(pos, false, this.lastSelection, true); 84 | } 85 | for (var d of unselect_diff) { 86 | this.selection.delete(d); 87 | } 88 | } 89 | if (pos_diff.length != 0) { 90 | for (var d of pos_diff) { 91 | this.selection.add(d); 92 | } 93 | } 94 | } 95 | handler.select(unselect_diff, false); 96 | handler.select(pos_diff, true); 97 | this.lastSelection = pos; 98 | } 99 | 100 | 101 | Selection.prototype.detachSelection = function() { 102 | var result = new Set(); 103 | for (var i of this.selection) { 104 | result.add(i); 105 | } 106 | this.clear(); 107 | return result; 108 | } 109 | -------------------------------------------------------------------------------- /text-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 TextView extends View { 8 | constructor(id, broker, patterns, allowSpanSelection) { 9 | super(id, broker); 10 | let view = this; 11 | view.hide(); 12 | view.textListNode = view.divNode.getElementsByTagName('ul')[0]; 13 | view.fillerSvgElement = view.divElement.append("svg").attr('version','1.1').attr("width", "0"); 14 | view.patterns = patterns; 15 | view.allowSpanSelection = allowSpanSelection; 16 | view.nodeToLineMap = []; 17 | var selectionHandler = { 18 | clear: function() { 19 | broker.clear(selectionHandler); 20 | }, 21 | select: function(items, selected) { 22 | for (let i of items) { 23 | if (selected) { 24 | i.classList.add("selected"); 25 | } else { 26 | i.classList.remove("selected"); 27 | } 28 | } 29 | broker.clear(selectionHandler); 30 | broker.select(selectionHandler, view.getLocations(items), selected); 31 | }, 32 | selectionDifference: function(span1, inclusive1, span2, inclusive2) { 33 | return null; 34 | }, 35 | brokeredSelect: function(locations, selected) { 36 | view.selectLocations(locations, selected, true); 37 | }, 38 | brokeredClear: function() { 39 | view.selection.clear(); 40 | } 41 | }; 42 | view.selection = new Selection(selectionHandler); 43 | broker.addSelectionHandler(selectionHandler); 44 | } 45 | 46 | setPatterns(patterns) { 47 | let view = this; 48 | view.patterns = patterns; 49 | } 50 | 51 | clearText() { 52 | let view = this; 53 | while (view.textListNode.firstChild) { 54 | view.textListNode.removeChild(view.textListNode.firstChild); 55 | } 56 | } 57 | 58 | sameLocation(l1, l2) { 59 | let view = this; 60 | if (l1.block_id != undefined && l2.block_id != undefined && 61 | l1.block_id == l2.block_id && l1.node_id === undefined) { 62 | return true; 63 | } 64 | 65 | if (l1.address != undefined && l1.address == l2.address) { 66 | return true; 67 | } 68 | 69 | let node1 = l1.node_id; 70 | let node2 = l2.node_id; 71 | 72 | if (node1 === undefined || node2 == undefined) { 73 | if (l1.pos_start === undefined || l2.pos_start == undefined) { 74 | return false; 75 | } 76 | if (l1.pos_start == -1 || l2.pos_start == -1) { 77 | return false; 78 | } 79 | if (l1.pos_start < l2.pos_start) { 80 | return l1.pos_end > l2.pos_start; 81 | } { 82 | return l1.pos_start < l2.pos_end; 83 | } 84 | } 85 | 86 | return l1.node_id == l2.node_id; 87 | } 88 | 89 | selectLocations(locations, selected, makeVisible) { 90 | let view = this; 91 | let s = new Set(); 92 | for (let l of locations) { 93 | for (let i = 0; i < view.textListNode.children.length; ++i) { 94 | let child = view.textListNode.children[i]; 95 | if (child.location != undefined && view.sameLocation(l, child.location)) { 96 | s.add(child); 97 | } 98 | } 99 | } 100 | view.selectCommon(s, selected, makeVisible); 101 | } 102 | 103 | getLocations(items) { 104 | let result = []; 105 | let lastObject = null; 106 | for (let i of items) { 107 | if (i.location) { 108 | result.push(i.location); 109 | } 110 | } 111 | return result; 112 | } 113 | 114 | createFragment(text, style) { 115 | let view = this; 116 | let span = document.createElement("SPAN"); 117 | span.onmousedown = function(e) { 118 | view.mouseDownSpan(span, e); 119 | } 120 | if (style != undefined) { 121 | span.classList.add(style); 122 | } 123 | span.innerHTML = text; 124 | return span; 125 | } 126 | 127 | appendFragment(li, fragment) { 128 | li.appendChild(fragment); 129 | } 130 | 131 | processLine(line) { 132 | let view = this; 133 | let result = []; 134 | let patternSet = 0; 135 | while (true) { 136 | let beforeLine = line; 137 | for (let pattern of view.patterns[patternSet]) { 138 | let matches = line.match(pattern[0]); 139 | if (matches != null) { 140 | if (matches[0] != '') { 141 | let style = pattern[1] != null ? pattern[1] : {}; 142 | let text = matches[0]; 143 | if (text != '') { 144 | let fragment = view.createFragment(matches[0], style.css); 145 | if (style.link) { 146 | fragment.classList.add('linkable-text'); 147 | fragment.link = style.link; 148 | } 149 | result.push(fragment); 150 | if (style.location != undefined) { 151 | let location = style.location(text); 152 | if (location != undefined) { 153 | fragment.location = location; 154 | } 155 | } 156 | } 157 | line = line.substr(matches[0].length); 158 | } 159 | let nextPatternSet = patternSet; 160 | if (pattern.length > 2) { 161 | nextPatternSet = pattern[2]; 162 | } 163 | if (line == "") { 164 | if (nextPatternSet != -1) { 165 | throw("illegal parsing state in text-view in patternSet" + patternSet); 166 | } 167 | return result; 168 | } 169 | patternSet = nextPatternSet; 170 | break; 171 | } 172 | } 173 | if (beforeLine == line) { 174 | throw("input not consumed in text-view in patternSet" + patternSet); 175 | } 176 | } 177 | } 178 | 179 | select(s, selected, makeVisible) { 180 | let view = this; 181 | view.selection.clear(); 182 | view.selectCommon(s, selected, makeVisible); 183 | } 184 | 185 | selectCommon(s, selected, makeVisible) { 186 | let view = this; 187 | let firstSelect = makeVisible && view.selection.isEmpty(); 188 | if ((typeof s) === 'function') { 189 | for (let i = 0; i < view.textListNode.children.length; ++i) { 190 | let child = view.textListNode.children[i]; 191 | if (child.location && s(child.location)) { 192 | if (firstSelect) { 193 | makeContainerPosVisible(view.parentNode, child.offsetTop); 194 | firstSelect = false; 195 | } 196 | view.selection.select(child, selected); 197 | } 198 | } 199 | } else if (typeof s[Symbol.iterator] === 'function') { 200 | if (firstSelect) { 201 | for (let i of s) { 202 | makeContainerPosVisible(view.parentNode, i.offsetTop); 203 | break; 204 | } 205 | } 206 | view.selection.select(s, selected); 207 | } else { 208 | if (firstSelect) { 209 | makeContainerPosVisible(view.parentNode, s.offsetTop); 210 | } 211 | view.selection.select(s, selected); 212 | } 213 | } 214 | 215 | mouseDownLine(li, e) { 216 | let view = this; 217 | e.stopPropagation(); 218 | if (!e.shiftKey) { 219 | view.selection.clear(); 220 | } 221 | if (li.location != undefined) { 222 | view.selectLocations([li.location], true, false); 223 | } 224 | } 225 | 226 | mouseDownSpan(span, e) { 227 | let view = this; 228 | if (view.allowSpanSelection) { 229 | e.stopPropagation(); 230 | if (!e.shiftKey) { 231 | view.selection.clear(); 232 | } 233 | select(li, true); 234 | } else if (span.link) { 235 | span.link(span.textContent); 236 | e.stopPropagation(); 237 | } 238 | } 239 | 240 | processText(text) { 241 | let view = this; 242 | let textLines = text.split(/[\n]/); 243 | let lineNo = 0; 244 | for (let line of textLines) { 245 | let li = document.createElement("LI"); 246 | li.onmousedown = function(e) { 247 | view.mouseDownLine(li, e); 248 | } 249 | li.className = "nolinenums"; 250 | li.lineNo = lineNo++; 251 | let fragments = view.processLine(line); 252 | for (let fragment of fragments) { 253 | view.appendFragment(li, fragment); 254 | } 255 | let lineLocation = view.lineLocation(li); 256 | if (lineLocation != undefined) { 257 | li.location = lineLocation; 258 | } 259 | view.textListNode.appendChild(li); 260 | } 261 | } 262 | 263 | initializeContent(data, rememberedSelection) { 264 | let view = this; 265 | view.selection.clear(); 266 | view.clearText(); 267 | view.processText(data); 268 | var fillerSize = document.documentElement.clientHeight - 269 | view.textListNode.clientHeight; 270 | if (fillerSize < 0) { 271 | fillerSize = 0; 272 | } 273 | view.fillerSvgElement.attr("height", fillerSize); 274 | } 275 | 276 | deleteContent() { 277 | } 278 | 279 | isScrollable() { 280 | return true; 281 | } 282 | 283 | detachSelection() { 284 | return null; 285 | } 286 | 287 | lineLocation(li) { 288 | let view = this; 289 | for (let i = 0; i < li.children.length; ++i) { 290 | let fragment = li.children[i]; 291 | if (fragment.location != undefined && !view.allowSpanSelection) { 292 | return fragment.location; 293 | } 294 | } 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /turbo-visualizer.css: -------------------------------------------------------------------------------- 1 | .visible-transition { 2 | transition-delay: 0s; 3 | transition-duration: 1s; 4 | transition-property: all; 5 | transition-timing-function: ease; 6 | } 7 | 8 | .collapse-pane { 9 | background: #A0A0A0; 10 | bottom: 0; 11 | position: absolute; 12 | margin-bottom: 0.5em; 13 | margin-right: 0.5em; 14 | margin-left: 0.5em; 15 | border-radius: 5px; 16 | padding: 0.5em; 17 | z-index: 5; 18 | opacity: 0.7; 19 | cursor: pointer; 20 | } 21 | 22 | .search-input { 23 | vertical-align: middle; 24 | width: 145px; 25 | opacity: 1; 26 | } 27 | 28 | .button-input { 29 | vertical-align: middle; 30 | width: 24px; 31 | opacity: 0.4; 32 | cursor: pointer; 33 | } 34 | 35 | .button-input-toggled { 36 | border-radius: 5px; 37 | background-color: #505050; 38 | } 39 | 40 | .button-input:focus { 41 | outline: none; 42 | } 43 | 44 | .invisible { 45 | display: none; 46 | } 47 | 48 | 49 | .selected { 50 | background-color: #FFFF33; 51 | } 52 | 53 | .prettyprint ol.linenums > li { 54 | list-style-type: decimal; 55 | !important 56 | } 57 | 58 | 59 | body { 60 | margin: 0; 61 | padding: 0; 62 | height: 100vh; 63 | width: 100vw; 64 | overflow:hidden; 65 | -webkit-touch-callout: none; 66 | -webkit-user-select: none; 67 | -khtml-user-select: none; 68 | -moz-user-select: none; 69 | -ms-user-select: none; 70 | user-select: none; 71 | } 72 | 73 | p { 74 | text-align: center; 75 | overflow: overlay; 76 | position: relative; 77 | } 78 | 79 | marker { 80 | fill: #080808; 81 | } 82 | 83 | g rect { 84 | fill: #F0F0F0; 85 | stroke: #080808; 86 | stroke-width: 2px; 87 | } 88 | 89 | g.dead { 90 | opacity: .5; 91 | } 92 | 93 | g.unsorted rect { 94 | opacity: 0.5; 95 | } 96 | 97 | div.scrollable { 98 | overflow-y: _croll; overflow-x: hidden; 99 | } 100 | 101 | g.turbonode[relToHover="input"] rect { 102 | stroke: #67e62c; 103 | stroke-width: 16px; 104 | } 105 | 106 | g.turbonode[relToHover="output"] rect { 107 | stroke: #d23b14; 108 | stroke-width: 16px; 109 | } 110 | 111 | path[relToHover="input"] { 112 | stroke: #67e62c; 113 | stroke-width: 16px; 114 | } 115 | 116 | path[relToHover="output"] { 117 | stroke: #d23b14; 118 | stroke-width: 16px; 119 | } 120 | 121 | 122 | g.turbonode:hover rect { 123 | stroke: #000000; 124 | stroke-width: 7px; 125 | } 126 | 127 | g.control rect { 128 | fill: #EFCC00; 129 | stroke: #080808; 130 | stroke-width: 5px; 131 | } 132 | 133 | g.javascript rect { 134 | fill: #DD7E6B; 135 | } 136 | 137 | g.simplified rect { 138 | fill: #3C78D8; 139 | } 140 | 141 | g.machine rect { 142 | fill: #6AA84F; 143 | } 144 | 145 | g.input rect { 146 | fill: #CFE2F3; 147 | } 148 | 149 | g.selected rect { 150 | fill: #FFFF33; 151 | } 152 | 153 | circle.bubbleStyle { 154 | fill: #080808; 155 | fill-opacity: 0.0; 156 | stroke: #080808; 157 | stroke-width: 2px; 158 | } 159 | 160 | circle.bubbleStyle:hover { 161 | stroke-width: 3px; 162 | } 163 | 164 | circle.filledBubbleStyle { 165 | fill: #080808; 166 | stroke: #080808; 167 | stroke-width: 2px; 168 | } 169 | 170 | circle.filledBubbleStyle:hover { 171 | fill: #080808; 172 | stroke-width: 3px; 173 | } 174 | 175 | circle.halfFilledBubbleStyle { 176 | fill: #808080; 177 | stroke: #101010; 178 | stroke-width: 2px; 179 | } 180 | 181 | circle.halfFilledBubbleStyle:hover { 182 | fill: #808080; 183 | stroke-width: 3px; 184 | } 185 | 186 | path { 187 | fill: none; 188 | stroke: #080808; 189 | stroke-width: 4px; 190 | cursor: default; 191 | } 192 | 193 | path:hover { 194 | stroke-width: 6px; 195 | } 196 | 197 | path.hidden { 198 | fill: none; 199 | stroke-width: 0; 200 | } 201 | 202 | path.link.selected { 203 | stroke: #FFFF33; 204 | } 205 | 206 | pre.prettyprint { 207 | border: none !important; 208 | padding: 0px; 209 | } 210 | 211 | li.L1, 212 | li.L3, 213 | li.L5, 214 | li.L7, 215 | li.L9 { 216 | background: none !important 217 | } 218 | 219 | li.nolinenums { 220 | list-style-type:none; 221 | } 222 | 223 | ul.noindent { 224 | -webkit-padding-start: 0px; 225 | -webkit-margin-before: 0px; 226 | -webkit-margin-after: 0px; 227 | } 228 | 229 | input:hover, .collapse-pane:hover input { 230 | opacity: 1; 231 | cursor: pointer; 232 | } 233 | 234 | span.linkable-text { 235 | text-decoration: underline; 236 | } 237 | 238 | span.linkable-text:hover { 239 | cursor: pointer; 240 | font-weight: bold; 241 | } 242 | 243 | 244 | #left { 245 | float: left; 246 | } 247 | 248 | #middle { 249 | float:left; background-color: #F8F8F8; 250 | } 251 | 252 | #right { 253 | float: right; 254 | } 255 | 256 | .viewpane { 257 | height: 100vh; 258 | background-color: #FFFFFF; 259 | } 260 | 261 | 262 | #disassembly-collapse { 263 | right: 0; 264 | } 265 | 266 | #source-collapse { 267 | left: 0; 268 | } 269 | 270 | #graph-toolbox-anchor { 271 | height: 0px; 272 | } 273 | 274 | #graph-toolbox { 275 | position: relative; 276 | top: 1em; 277 | left: 25px; 278 | border: 2px solid #eee8d5; 279 | border-radius: 5px; 280 | padding: 0.7em; 281 | z-index: 5; 282 | background: rgba(100%, 100%, 100%, 0.7); 283 | } 284 | 285 | #disassembly-toolbox { 286 | position: relative; 287 | top: 1em; 288 | left: 0.7em; 289 | border: 2px solid #eee8d5; 290 | border-radius: 5px; 291 | padding: 0.7em; 292 | z-index: 5; 293 | } 294 | 295 | #load-file { 296 | position: absolute; 297 | top: 0; 298 | right: 0; 299 | margin-top: 0.5em; 300 | margin-right: 0.5em; 301 | z-index: 5; 302 | opacity: 0.7; 303 | } 304 | 305 | #load-file input { 306 | background: #A0A0A0; 307 | border-radius: 5px; 308 | padding: 0.5em; 309 | } 310 | 311 | #hidden-file-upload { 312 | display: none; 313 | } 314 | 315 | .prof { 316 | cursor: default; 317 | } 318 | 319 | tspan { 320 | font-size: 500%; 321 | font-family: sans-serif; 322 | } 323 | 324 | text { 325 | dominant-baseline: text-before-edge; 326 | } 327 | 328 | .resizer-left { 329 | position:absolute; 330 | width: 4px; 331 | height:100%; 332 | background: #a0a0a0; 333 | cursor: pointer; 334 | } 335 | 336 | .resizer-left.snapped { 337 | width: 12px; 338 | } 339 | 340 | .resizer-left:hover { 341 | background: orange; 342 | } 343 | 344 | .resizer-left.dragged { 345 | background: orange; 346 | } 347 | 348 | .resizer-right { 349 | position:absolute; 350 | width: 4px; 351 | height:100%; 352 | background: #a0a0a0; 353 | cursor: pointer; 354 | } 355 | 356 | .resizer-right.snapped { 357 | width: 12px; 358 | } 359 | 360 | .resizer-right:hover { 361 | background: orange; 362 | } 363 | 364 | .resizer-right.dragged { 365 | background: orange; 366 | } -------------------------------------------------------------------------------- /turbo-visualizer.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 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 | class Snapper { 6 | 7 | constructor(resizer) { 8 | let snapper = this; 9 | snapper.resizer = resizer; 10 | snapper.sourceExpand = d3.select("#" + SOURCE_EXPAND_ID); 11 | snapper.sourceCollapse = d3.select("#" + SOURCE_COLLAPSE_ID); 12 | snapper.disassemblyExpand = d3.select("#" + DISASSEMBLY_EXPAND_ID); 13 | snapper.disassemblyCollapse = d3.select("#" + DISASSEMBLY_COLLAPSE_ID); 14 | 15 | d3.select("#source-collapse").on("click", function(){ 16 | resizer.snapper.toggleSourceExpanded(); 17 | }); 18 | d3.select("#disassembly-collapse").on("click", function(){ 19 | resizer.snapper.toggleDisassemblyExpanded(); 20 | }); 21 | } 22 | 23 | getLastExpandedState(type, default_state) { 24 | var state = window.sessionStorage.getItem("expandedState-"+type); 25 | if (state === null) return default_state; 26 | return state === 'true'; 27 | } 28 | 29 | setLastExpandedState(type, state) { 30 | window.sessionStorage.setItem("expandedState-"+type, state); 31 | } 32 | 33 | toggleSourceExpanded() { 34 | this.setSourceExpanded(!this.sourceExpand.classed("invisible")); 35 | } 36 | 37 | sourceExpandUpdate(newState) { 38 | this.setLastExpandedState("source", newState); 39 | this.sourceExpand.classed("invisible", newState); 40 | this.sourceCollapse.classed("invisible", !newState); 41 | } 42 | 43 | setSourceExpanded(newState) { 44 | if (this.sourceExpand.classed("invisible") === newState) return; 45 | this.sourceExpandUpdate(newState); 46 | let resizer = this.resizer; 47 | if (newState) { 48 | resizer.sep_left = resizer.sep_left_snap; 49 | resizer.sep_left_snap = 0; 50 | } else { 51 | resizer.sep_left_snap = resizer.sep_left; 52 | resizer.sep_left = 0; 53 | } 54 | resizer.updatePanes(); 55 | } 56 | 57 | toggleDisassemblyExpanded() { 58 | this.setDisassemblyExpanded(!this.disassemblyExpand.classed("invisible")); 59 | } 60 | 61 | disassemblyExpandUpdate(newState) { 62 | this.setLastExpandedState("disassembly", newState); 63 | this.disassemblyExpand.classed("invisible", newState); 64 | this.disassemblyCollapse.classed("invisible", !newState); 65 | } 66 | 67 | setDisassemblyExpanded(newState) { 68 | if (this.disassemblyExpand.classed("invisible") === newState) return; 69 | this.disassemblyExpandUpdate(newState); 70 | let resizer = this.resizer; 71 | if (newState) { 72 | resizer.sep_right = resizer.sep_right_snap; 73 | resizer.sep_right_snap = resizer.client_width; 74 | } else { 75 | resizer.sep_right_snap = resizer.sep_right; 76 | resizer.sep_right = resizer.client_width; 77 | } 78 | resizer.updatePanes(); 79 | } 80 | 81 | panesUpated() { 82 | this.sourceExpandUpdate(this.resizer.sep_left > this.resizer.dead_width); 83 | this.disassemblyExpandUpdate(this.resizer.sep_right < 84 | (this.resizer.client_width - this.resizer.dead_width)); 85 | } 86 | } 87 | 88 | class Resizer { 89 | constructor(panes_updated_callback, dead_width) { 90 | let resizer = this; 91 | resizer.snapper = new Snapper(resizer) 92 | resizer.panes_updated_callback = panes_updated_callback; 93 | resizer.dead_width = dead_width 94 | resizer.client_width = d3.select("body").node().getBoundingClientRect().width; 95 | resizer.left = d3.select("#" + SOURCE_PANE_ID); 96 | resizer.middle = d3.select("#" + INTERMEDIATE_PANE_ID); 97 | resizer.right = d3.select("#" + GENERATED_PANE_ID); 98 | resizer.resizer_left = d3.select('.resizer-left'); 99 | resizer.resizer_right = d3.select('.resizer-right'); 100 | resizer.sep_left = resizer.client_width/3; 101 | resizer.sep_right = resizer.client_width/3*2; 102 | resizer.sep_left_snap = 0; 103 | resizer.sep_right_snap = 0; 104 | // Offset to prevent resizers from sliding slightly over one another. 105 | resizer.sep_width_offset = 7; 106 | 107 | let dragResizeLeft = d3.behavior.drag() 108 | .on('drag', function() { 109 | let x = d3.mouse(this.parentElement)[0]; 110 | resizer.sep_left = Math.min(Math.max(0,x), resizer.sep_right-resizer.sep_width_offset); 111 | resizer.updatePanes(); 112 | }) 113 | .on('dragstart', function() { 114 | resizer.resizer_left.classed("dragged", true); 115 | let x = d3.mouse(this.parentElement)[0]; 116 | if (x > dead_width) { 117 | resizer.sep_left_snap = resizer.sep_left; 118 | } 119 | }) 120 | .on('dragend', function() { 121 | resizer.resizer_left.classed("dragged", false); 122 | }); 123 | resizer.resizer_left.call(dragResizeLeft); 124 | 125 | let dragResizeRight = d3.behavior.drag() 126 | .on('drag', function() { 127 | let x = d3.mouse(this.parentElement)[0]; 128 | resizer.sep_right = Math.max(resizer.sep_left+resizer.sep_width_offset, Math.min(x, resizer.client_width)); 129 | resizer.updatePanes(); 130 | }) 131 | .on('dragstart', function() { 132 | resizer.resizer_right.classed("dragged", true); 133 | let x = d3.mouse(this.parentElement)[0]; 134 | if (x < (resizer.client_width-dead_width)) { 135 | resizer.sep_right_snap = resizer.sep_right; 136 | } 137 | }) 138 | .on('dragend', function() { 139 | resizer.resizer_right.classed("dragged", false); 140 | });; 141 | resizer.resizer_right.call(dragResizeRight); 142 | window.onresize = function(){ 143 | resizer.updateWidths(); 144 | /*fitPanesToParents();*/ 145 | resizer.updatePanes(); 146 | }; 147 | } 148 | 149 | updatePanes() { 150 | let left_snapped = this.sep_left === 0; 151 | let right_snapped = this.sep_right >= this.client_width - 1; 152 | this.resizer_left.classed("snapped", left_snapped); 153 | this.resizer_right.classed("snapped", right_snapped); 154 | this.left.style('width', this.sep_left + 'px'); 155 | this.middle.style('width', (this.sep_right-this.sep_left) + 'px'); 156 | this.right.style('width', (this.client_width - this.sep_right) + 'px'); 157 | this.resizer_left.style('left', this.sep_left + 'px'); 158 | this.resizer_right.style('right', (this.client_width - this.sep_right - 1) + 'px'); 159 | 160 | this.snapper.panesUpated(); 161 | this.panes_updated_callback(); 162 | } 163 | 164 | updateWidths() { 165 | this.client_width = d3.select("body").node().getBoundingClientRect().width; 166 | this.sep_right = Math.min(this.sep_right, this.client_width); 167 | this.sep_left = Math.min(Math.max(0, this.sep_left), this.sep_right); 168 | } 169 | } 170 | 171 | document.onload = (function(d3){ 172 | "use strict"; 173 | var jsonObj; 174 | var svg = null; 175 | var graph = null; 176 | var schedule = null; 177 | var empty = null; 178 | var currentPhaseView = null; 179 | var disassemblyView = null; 180 | var sourceView = null; 181 | var selectionBroker = null; 182 | let resizer = new Resizer(panesUpdatedCallback, 100); 183 | 184 | function renderTurbolizerData(txtRes) { 185 | // If the JSON isn't properly terminated, assume compiler crashed and 186 | // add best-guess empty termination 187 | if (txtRes[txtRes.length-2] == ',') { 188 | txtRes += '{"name":"disassembly","type":"disassembly","data":""}]}'; 189 | } 190 | try{ 191 | jsonObj = JSON.parse(txtRes); 192 | 193 | hideCurrentPhase(); 194 | 195 | selectionBroker.setNodePositionMap(jsonObj.nodePositions); 196 | 197 | sourceView.initializeCode(jsonObj.source, jsonObj.sourcePosition); 198 | disassemblyView.initializeCode(jsonObj.source); 199 | 200 | var selectMenu = document.getElementById('display-selector'); 201 | var disassemblyPhase = null; 202 | selectMenu.innerHTML = ''; 203 | for (var i = 0; i < jsonObj.phases.length; ++i) { 204 | var optionElement = document.createElement("option"); 205 | optionElement.text = jsonObj.phases[i].name; 206 | if (optionElement.text == 'disassembly') { 207 | disassemblyPhase = jsonObj.phases[i]; 208 | } else { 209 | selectMenu.add(optionElement, null); 210 | } 211 | } 212 | 213 | disassemblyView.initializePerfProfile(jsonObj.eventCounts); 214 | disassemblyView.show(disassemblyPhase.data, null); 215 | 216 | var initialPhaseIndex = +window.sessionStorage.getItem("lastSelectedPhase"); 217 | if (!(initialPhaseIndex in jsonObj.phases)) { 218 | initialPhaseIndex = 0; 219 | } 220 | 221 | // We wish to show the remembered phase {lastSelectedPhase}, but 222 | // this will crash if the first view we switch to is a 223 | // ScheduleView. So we first switch to the first phase, which 224 | // should never be a ScheduleView. 225 | displayPhase(jsonObj.phases[0]); 226 | displayPhase(jsonObj.phases[initialPhaseIndex]); 227 | selectMenu.selectedIndex = initialPhaseIndex; 228 | 229 | selectMenu.onchange = function(item) { 230 | window.sessionStorage.setItem("lastSelectedPhase", selectMenu.selectedIndex); 231 | displayPhase(jsonObj.phases[selectMenu.selectedIndex]); 232 | } 233 | 234 | fitPanesToParents(); 235 | 236 | d3.select("#search-input").attr("value", window.sessionStorage.getItem("lastSearch") || ""); 237 | 238 | } 239 | catch(err) { 240 | window.console.log("caught exception, clearing session storage just in case"); 241 | window.sessionStorage.clear(); // just in case 242 | window.console.log("showing error"); 243 | window.alert("Invalid TurboFan JSON file\n" + 244 | "error: " + err.message); 245 | return; 246 | } 247 | } 248 | 249 | window.renderTurbolizerData = renderTurbolizerData 250 | 251 | function panesUpdatedCallback() { 252 | graph.fitGraphViewToWindow(); 253 | } 254 | 255 | function hideCurrentPhase() { 256 | var rememberedSelection = null; 257 | if (currentPhaseView != null) { 258 | rememberedSelection = currentPhaseView.detachSelection(); 259 | currentPhaseView.hide(); 260 | currentPhaseView = null; 261 | } 262 | return rememberedSelection; 263 | } 264 | 265 | function displayPhaseView(view, data) { 266 | var rememberedSelection = hideCurrentPhase(); 267 | view.show(data, rememberedSelection); 268 | d3.select("#middle").classed("scrollable", view.isScrollable()); 269 | currentPhaseView = view; 270 | } 271 | 272 | function displayPhase(phase) { 273 | if (phase.type == 'graph') { 274 | displayPhaseView(graph, phase.data); 275 | } else if (phase.type == 'schedule') { 276 | displayPhaseView(schedule, phase.data); 277 | } else { 278 | displayPhaseView(empty, null); 279 | } 280 | } 281 | 282 | function fitPanesToParents() { 283 | d3.select("#left").classed("scrollable", false) 284 | d3.select("#right").classed("scrollable", false); 285 | 286 | graph.fitGraphViewToWindow(); 287 | 288 | d3.select("#left").classed("scrollable", true); 289 | d3.select("#right").classed("scrollable", true); 290 | } 291 | 292 | selectionBroker = new SelectionBroker(); 293 | 294 | function initializeHandlers(g) { 295 | d3.select("#hidden-file-upload").on("change", function() { 296 | if (window.File && window.FileReader && window.FileList) { 297 | var uploadFile = this.files[0]; 298 | var filereader = new window.FileReader(); 299 | var consts = Node.consts; 300 | filereader.onload = function(){ 301 | var txtRes = filereader.result; 302 | renderTurbolizerData(txtRes); 303 | }; 304 | filereader.readAsText(uploadFile); 305 | } else { 306 | alert("Can't load graph"); 307 | } 308 | }); 309 | } 310 | 311 | sourceView = new CodeView(SOURCE_PANE_ID, PR, "", 0, selectionBroker); 312 | disassemblyView = new DisassemblyView(DISASSEMBLY_PANE_ID, selectionBroker); 313 | graph = new GraphView(d3, GRAPH_PANE_ID, [], [], selectionBroker); 314 | schedule = new ScheduleView(SCHEDULE_PANE_ID, selectionBroker); 315 | empty = new EmptyView(EMPTY_PANE_ID, selectionBroker); 316 | 317 | initializeHandlers(graph); 318 | 319 | resizer.snapper.setSourceExpanded(resizer.snapper.getLastExpandedState("source", true)); 320 | resizer.snapper.setDisassemblyExpanded(resizer.snapper.getLastExpandedState("disassembly", false)); 321 | 322 | displayPhaseView(empty, null); 323 | fitPanesToParents(); 324 | resizer.updatePanes(); 325 | 326 | })(window.d3); 327 | -------------------------------------------------------------------------------- /types.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thlorenz/turbolizer/fa3377527f030c8909a6d51e03a1eb719a618b83/types.png -------------------------------------------------------------------------------- /upload-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thlorenz/turbolizer/fa3377527f030c8909a6d51e03a1eb719a618b83/upload-icon.png -------------------------------------------------------------------------------- /util.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 | function makeContainerPosVisible(container, pos) { 8 | var height = container.offsetHeight; 9 | var margin = Math.floor(height / 4); 10 | if (pos < container.scrollTop + margin) { 11 | pos -= margin; 12 | if (pos < 0) pos = 0; 13 | container.scrollTop = pos; 14 | return; 15 | } 16 | if (pos > (container.scrollTop + 3 * margin)) { 17 | pos = pos - 3 * margin; 18 | if (pos < 0) pos = 0; 19 | container.scrollTop = pos; 20 | } 21 | } 22 | 23 | 24 | function lowerBound(a, value, compare, lookup) { 25 | let first = 0; 26 | let count = a.length; 27 | while (count > 0) { 28 | let step = Math.floor(count / 2); 29 | let middle = first + step; 30 | let middle_value = (lookup === undefined) ? a[middle] : lookup(a, middle); 31 | let result = (compare === undefined) ? (middle_value < value) : compare(middle_value, value); 32 | if (result) { 33 | first = middle + 1; 34 | count -= step + 1; 35 | } else { 36 | count = step; 37 | } 38 | } 39 | return first; 40 | } 41 | 42 | 43 | function upperBound(a, value, compare, lookup) { 44 | let first = 0; 45 | let count = a.length; 46 | while (count > 0) { 47 | let step = Math.floor(count / 2); 48 | let middle = first + step; 49 | let middle_value = (lookup === undefined) ? a[middle] : lookup(a, middle); 50 | let result = (compare === undefined) ? (value < middle_value) : compare(value, middle_value); 51 | if (!result) { 52 | first = middle + 1; 53 | count -= step + 1; 54 | } else { 55 | count = step; 56 | } 57 | } 58 | return first; 59 | } 60 | 61 | 62 | function sortUnique(arr, f) { 63 | arr = arr.sort(f); 64 | let ret = [arr[0]]; 65 | for (var i = 1; i < arr.length; i++) { 66 | if (arr[i-1] !== arr[i]) { 67 | ret.push(arr[i]); 68 | } 69 | } 70 | return ret; 71 | } 72 | 73 | // Partial application without binding the receiver 74 | function partial(f) { 75 | var arguments1 = Array.prototype.slice.call(arguments, 1); 76 | return function() { 77 | var arguments2 = Array.from(arguments); 78 | f.apply(this, arguments1.concat(arguments2)); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /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 View { 8 | constructor(id, broker) { 9 | this.divElement = d3.select("#" + id); 10 | this.divNode = this.divElement[0][0]; 11 | this.parentNode = this.divNode.parentNode; 12 | } 13 | 14 | isScrollable() { 15 | return false; 16 | } 17 | 18 | show(data, rememberedSelection) { 19 | this.parentNode.appendChild(this.divElement[0][0]); 20 | this.initializeContent(data, rememberedSelection); 21 | this.divElement.attr(VISIBILITY, 'visible'); 22 | } 23 | 24 | hide() { 25 | this.divElement.attr(VISIBILITY, 'hidden'); 26 | this.deleteContent(); 27 | this.parentNode.removeChild(this.divNode); 28 | } 29 | 30 | detachSelection() { 31 | return null; 32 | } 33 | } 34 | --------------------------------------------------------------------------------