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 5import { PROF_COLS, UNICODE_BLOCK } from "../src/constants"; 6import { SelectionBroker } from "../src/selection-broker"; 7import { TextView } from "../src/text-view"; 8import { MySelection } from "./selection"; 9import { anyToString, interpolate } from "./util"; 10import { InstructionSelectionHandler } from "./selection-handler"; 11 12const toolboxHTML = `<div id="disassembly-toolbox"> 13<form> 14 <label><input id="show-instruction-address" type="checkbox" name="instruction-address">Show addresses</label> 15 <label><input id="show-instruction-binary" type="checkbox" name="instruction-binary">Show binary literal</label> 16 <label><input id="highlight-gap-instructions" type="checkbox" name="instruction-binary">Highlight gap instructions</label> 17</form> 18</div>`; 19 20export class DisassemblyView extends TextView { 21 SOURCE_POSITION_HEADER_REGEX: any; 22 addrEventCounts: any; 23 totalEventCounts: any; 24 maxEventCounts: any; 25 posLines: Array<any>; 26 instructionSelectionHandler: InstructionSelectionHandler; 27 offsetSelection: MySelection; 28 showInstructionAddressHandler: () => void; 29 showInstructionBinaryHandler: () => void; 30 highlightGapInstructionsHandler: () => void; 31 32 createViewElement() { 33 const pane = document.createElement('div'); 34 pane.setAttribute('id', "disassembly"); 35 pane.innerHTML = 36 `<pre id='disassembly-text-pre' class='prettyprint prettyprinted'> 37 <ul class='disassembly-list nolinenums noindent'> 38 </ul> 39 </pre>`; 40 41 return pane; 42 } 43 44 constructor(parentId, broker: SelectionBroker) { 45 super(parentId, broker); 46 const view = this; 47 const ADDRESS_STYLE = { 48 associateData: (text, fragment: HTMLElement) => { 49 const matches = text.match(/(?<address>0?x?[0-9a-fA-F]{8,16})(?<addressSpace>\s+)(?<offset>[0-9a-f]+)(?<offsetSpace>\s*)/); 50 const offset = Number.parseInt(matches.groups["offset"], 16); 51 const instructionKind = view.sourceResolver.getInstructionKindForPCOffset(offset); 52 fragment.dataset.instructionKind = instructionKind; 53 fragment.title = view.sourceResolver.instructionKindToReadableName(instructionKind); 54 const blockIds = view.sourceResolver.getBlockIdsForOffset(offset); 55 const blockIdElement = document.createElement("SPAN"); 56 blockIdElement.className = "block-id com linkable-text"; 57 blockIdElement.innerText = ""; 58 if (blockIds && blockIds.length > 0) { 59 blockIds.forEach(blockId => view.addHtmlElementForBlockId(blockId, fragment)); 60 blockIdElement.innerText = `B${blockIds.join(",")}:`; 61 blockIdElement.dataset.blockId = `${blockIds.join(",")}`; 62 } 63 fragment.appendChild(blockIdElement); 64 const addressElement = document.createElement("SPAN"); 65 addressElement.className = "instruction-address"; 66 addressElement.innerText = matches.groups["address"]; 67 const offsetElement = document.createElement("SPAN"); 68 offsetElement.innerText = matches.groups["offset"]; 69 fragment.appendChild(addressElement); 70 fragment.appendChild(document.createTextNode(matches.groups["addressSpace"])); 71 fragment.appendChild(offsetElement); 72 fragment.appendChild(document.createTextNode(matches.groups["offsetSpace"])); 73 fragment.classList.add('tag'); 74 75 if (!Number.isNaN(offset)) { 76 let pcOffset = view.sourceResolver.getKeyPcOffset(offset); 77 if (pcOffset == -1) pcOffset = Number(offset); 78 fragment.dataset.pcOffset = `${pcOffset}`; 79 addressElement.classList.add('linkable-text'); 80 offsetElement.classList.add('linkable-text'); 81 } 82 return true; 83 } 84 }; 85 const UNCLASSIFIED_STYLE = { 86 css: 'com' 87 }; 88 const NUMBER_STYLE = { 89 css: ['instruction-binary', 'lit'] 90 }; 91 const COMMENT_STYLE = { 92 css: 'com' 93 }; 94 const OPCODE_ARGS = { 95 associateData: function (text, fragment) { 96 fragment.innerHTML = text; 97 const replacer = (match, hexOffset) => { 98 const offset = Number.parseInt(hexOffset, 16); 99 let keyOffset = view.sourceResolver.getKeyPcOffset(offset); 100 if (keyOffset == -1) keyOffset = Number(offset); 101 const blockIds = view.sourceResolver.getBlockIdsForOffset(offset); 102 let block = ""; 103 let blockIdData = ""; 104 if (blockIds && blockIds.length > 0) { 105 block = `B${blockIds.join(",")} `; 106 blockIdData = `data-block-id="${blockIds.join(",")}"`; 107 } 108 return `<span class="tag linkable-text" data-pc-offset="${keyOffset}" ${blockIdData}>${block}${match}</span>`; 109 }; 110 const html = text.replace(/<.0?x?([0-9a-fA-F]+)>/g, replacer); 111 fragment.innerHTML = html; 112 return true; 113 } 114 }; 115 const OPCODE_STYLE = { 116 css: 'kwd' 117 }; 118 const BLOCK_HEADER_STYLE = { 119 associateData: function (text, fragment) { 120 if (view.sourceResolver.hasBlockStartInfo()) return false; 121 const matches = /\d+/.exec(text); 122 if (!matches) return true; 123 const blockId = matches[0]; 124 fragment.dataset.blockId = blockId; 125 fragment.innerHTML = text; 126 fragment.className = "com block"; 127 return true; 128 } 129 }; 130 const SOURCE_POSITION_HEADER_STYLE = { 131 css: 'com' 132 }; 133 view.SOURCE_POSITION_HEADER_REGEX = /^\s*--[^<]*<.*(not inlined|inlined\((\d+)\)):(\d+)>\s*--/; 134 const patterns = [ 135 [ 136 [/^0?x?[0-9a-fA-F]{8,16}\s+[0-9a-f]+\s+/, ADDRESS_STYLE, 1], 137 [view.SOURCE_POSITION_HEADER_REGEX, SOURCE_POSITION_HEADER_STYLE, -1], 138 [/^\s+-- B\d+ start.*/, BLOCK_HEADER_STYLE, -1], 139 [/^.*/, UNCLASSIFIED_STYLE, -1] 140 ], 141 [ 142 [/^\s*[0-9a-f]+\s+/, NUMBER_STYLE, 2], 143 [/^\s*[0-9a-f]+\s+[0-9a-f]+\s+/, NUMBER_STYLE, 2], 144 [/^.*/, null, -1] 145 ], 146 [ 147 [/^REX.W \S+\s+/, OPCODE_STYLE, 3], 148 [/^\S+\s+/, OPCODE_STYLE, 3], 149 [/^\S+$/, OPCODE_STYLE, -1], 150 [/^.*/, null, -1] 151 ], 152 [ 153 [/^\s+/, null], 154 [/^[^;]+$/, OPCODE_ARGS, -1], 155 [/^[^;]+/, OPCODE_ARGS, 4], 156 [/^;/, COMMENT_STYLE, 5] 157 ], 158 [ 159 [/^.+$/, COMMENT_STYLE, -1] 160 ] 161 ]; 162 view.setPatterns(patterns); 163 164 const linkHandler = (e: MouseEvent) => { 165 if (!(e.target instanceof HTMLElement)) return; 166 const offsetAsString = typeof e.target.dataset.pcOffset != "undefined" ? e.target.dataset.pcOffset : e.target.parentElement.dataset.pcOffset; 167 const offset = Number.parseInt(offsetAsString, 10); 168 if ((typeof offsetAsString) != "undefined" && !Number.isNaN(offset)) { 169 view.offsetSelection.select([offset], true); 170 const nodes = view.sourceResolver.nodesForPCOffset(offset)[0]; 171 if (nodes.length > 0) { 172 e.stopPropagation(); 173 if (!e.shiftKey) { 174 view.selectionHandler.clear(); 175 } 176 view.selectionHandler.select(nodes, true); 177 } else { 178 view.updateSelection(); 179 } 180 } 181 return undefined; 182 }; 183 view.divNode.addEventListener('click', linkHandler); 184 185 const linkHandlerBlock = e => { 186 const blockId = e.target.dataset.blockId; 187 if (typeof blockId != "undefined") { 188 const blockIds = blockId.split(","); 189 if (!e.shiftKey) { 190 view.selectionHandler.clear(); 191 } 192 view.blockSelectionHandler.select(blockIds, true); 193 } 194 }; 195 view.divNode.addEventListener('click', linkHandlerBlock); 196 197 this.offsetSelection = new MySelection(anyToString); 198 const instructionSelectionHandler = { 199 clear: function () { 200 view.offsetSelection.clear(); 201 view.updateSelection(); 202 broker.broadcastClear(instructionSelectionHandler); 203 }, 204 select: function (instructionIds, selected) { 205 view.offsetSelection.select(instructionIds, selected); 206 view.updateSelection(); 207 broker.broadcastBlockSelect(instructionSelectionHandler, instructionIds, selected); 208 }, 209 brokeredInstructionSelect: function (instructionIds, selected) { 210 const firstSelect = view.offsetSelection.isEmpty(); 211 const keyPcOffsets = view.sourceResolver.instructionsToKeyPcOffsets(instructionIds); 212 view.offsetSelection.select(keyPcOffsets, selected); 213 view.updateSelection(firstSelect); 214 }, 215 brokeredClear: function () { 216 view.offsetSelection.clear(); 217 view.updateSelection(); 218 } 219 }; 220 this.instructionSelectionHandler = instructionSelectionHandler; 221 broker.addInstructionHandler(instructionSelectionHandler); 222 223 const toolbox = document.createElement("div"); 224 toolbox.id = "toolbox-anchor"; 225 toolbox.innerHTML = toolboxHTML; 226 view.divNode.insertBefore(toolbox, view.divNode.firstChild); 227 const instructionAddressInput: HTMLInputElement = view.divNode.querySelector("#show-instruction-address"); 228 const lastShowInstructionAddress = window.sessionStorage.getItem("show-instruction-address"); 229 instructionAddressInput.checked = lastShowInstructionAddress == 'true'; 230 const showInstructionAddressHandler = () => { 231 window.sessionStorage.setItem("show-instruction-address", `${instructionAddressInput.checked}`); 232 for (const el of view.divNode.querySelectorAll(".instruction-address")) { 233 el.classList.toggle("invisible", !instructionAddressInput.checked); 234 } 235 }; 236 instructionAddressInput.addEventListener("change", showInstructionAddressHandler); 237 this.showInstructionAddressHandler = showInstructionAddressHandler; 238 239 const instructionBinaryInput: HTMLInputElement = view.divNode.querySelector("#show-instruction-binary"); 240 const lastShowInstructionBinary = window.sessionStorage.getItem("show-instruction-binary"); 241 instructionBinaryInput.checked = lastShowInstructionBinary == 'true'; 242 const showInstructionBinaryHandler = () => { 243 window.sessionStorage.setItem("show-instruction-binary", `${instructionBinaryInput.checked}`); 244 for (const el of view.divNode.querySelectorAll(".instruction-binary")) { 245 el.classList.toggle("invisible", !instructionBinaryInput.checked); 246 } 247 }; 248 instructionBinaryInput.addEventListener("change", showInstructionBinaryHandler); 249 this.showInstructionBinaryHandler = showInstructionBinaryHandler; 250 251 const highlightGapInstructionsInput: HTMLInputElement = view.divNode.querySelector("#highlight-gap-instructions"); 252 const lastHighlightGapInstructions = window.sessionStorage.getItem("highlight-gap-instructions"); 253 highlightGapInstructionsInput.checked = lastHighlightGapInstructions == 'true'; 254 const highlightGapInstructionsHandler = () => { 255 window.sessionStorage.setItem("highlight-gap-instructions", `${highlightGapInstructionsInput.checked}`); 256 view.divNode.classList.toggle("highlight-gap-instructions", highlightGapInstructionsInput.checked); 257 }; 258 259 highlightGapInstructionsInput.addEventListener("change", highlightGapInstructionsHandler); 260 this.highlightGapInstructionsHandler = highlightGapInstructionsHandler; 261 } 262 263 updateSelection(scrollIntoView: boolean = false) { 264 super.updateSelection(scrollIntoView); 265 const keyPcOffsets = this.sourceResolver.nodesToKeyPcOffsets(this.selection.selectedKeys()); 266 if (this.offsetSelection) { 267 for (const key of this.offsetSelection.selectedKeys()) { 268 keyPcOffsets.push(Number(key)); 269 } 270 } 271 for (const keyPcOffset of keyPcOffsets) { 272 const elementsToSelect = this.divNode.querySelectorAll(`[data-pc-offset='${keyPcOffset}']`); 273 for (const el of elementsToSelect) { 274 el.classList.toggle("selected", true); 275 } 276 } 277 } 278 279 initializeCode(sourceText, sourcePosition: number = 0) { 280 const view = this; 281 view.addrEventCounts = null; 282 view.totalEventCounts = null; 283 view.maxEventCounts = null; 284 view.posLines = new Array(); 285 // Comment lines for line 0 include sourcePosition already, only need to 286 // add sourcePosition for lines > 0. 287 view.posLines[0] = sourcePosition; 288 if (sourceText && sourceText != "") { 289 const base = sourcePosition; 290 let current = 0; 291 const sourceLines = sourceText.split("\n"); 292 for (let i = 1; i < sourceLines.length; i++) { 293 // Add 1 for newline character that is split off. 294 current += sourceLines[i - 1].length + 1; 295 view.posLines[i] = base + current; 296 } 297 } 298 } 299 300 initializePerfProfile(eventCounts) { 301 const view = this; 302 if (eventCounts !== undefined) { 303 view.addrEventCounts = eventCounts; 304 305 view.totalEventCounts = {}; 306 view.maxEventCounts = {}; 307 for (const evName in view.addrEventCounts) { 308 if (view.addrEventCounts.hasOwnProperty(evName)) { 309 const keys = Object.keys(view.addrEventCounts[evName]); 310 const values = keys.map(key => view.addrEventCounts[evName][key]); 311 view.totalEventCounts[evName] = values.reduce((a, b) => a + b); 312 view.maxEventCounts[evName] = values.reduce((a, b) => Math.max(a, b)); 313 } 314 } 315 } else { 316 view.addrEventCounts = null; 317 view.totalEventCounts = null; 318 view.maxEventCounts = null; 319 } 320 } 321 322 showContent(data): void { 323 console.time("disassembly-view"); 324 super.initializeContent(data, null); 325 this.showInstructionAddressHandler(); 326 this.showInstructionBinaryHandler(); 327 this.highlightGapInstructionsHandler(); 328 console.timeEnd("disassembly-view"); 329 } 330 331 // Shorten decimals and remove trailing zeroes for readability. 332 humanize(num) { 333 return num.toFixed(3).replace(/\.?0+$/, "") + "%"; 334 } 335 336 processLine(line) { 337 const view = this; 338 let fragments = super.processLine(line); 339 340 // Add profiling data per instruction if available. 341 if (view.totalEventCounts) { 342 const matches = /^(0x[0-9a-fA-F]+)\s+\d+\s+[0-9a-fA-F]+/.exec(line); 343 if (matches) { 344 const newFragments = []; 345 for (const event in view.addrEventCounts) { 346 if (!view.addrEventCounts.hasOwnProperty(event)) continue; 347 const count = view.addrEventCounts[event][matches[1]]; 348 let str = " "; 349 const cssCls = "prof"; 350 if (count !== undefined) { 351 const perc = count / view.totalEventCounts[event] * 100; 352 353 let col = { r: 255, g: 255, b: 255 }; 354 for (let i = 0; i < PROF_COLS.length; i++) { 355 if (perc === PROF_COLS[i].perc) { 356 col = PROF_COLS[i].col; 357 break; 358 } else if (perc > PROF_COLS[i].perc && perc < PROF_COLS[i + 1].perc) { 359 const col1 = PROF_COLS[i].col; 360 const col2 = PROF_COLS[i + 1].col; 361 362 const val = perc - PROF_COLS[i].perc; 363 const max = PROF_COLS[i + 1].perc - PROF_COLS[i].perc; 364 365 col.r = Math.round(interpolate(val, max, col1.r, col2.r)); 366 col.g = Math.round(interpolate(val, max, col1.g, col2.g)); 367 col.b = Math.round(interpolate(val, max, col1.b, col2.b)); 368 break; 369 } 370 } 371 372 str = UNICODE_BLOCK; 373 374 const fragment = view.createFragment(str, cssCls); 375 fragment.title = event + ": " + view.humanize(perc) + " (" + count + ")"; 376 fragment.style.color = "rgb(" + col.r + ", " + col.g + ", " + col.b + ")"; 377 378 newFragments.push(fragment); 379 } else { 380 newFragments.push(view.createFragment(str, cssCls)); 381 } 382 } 383 fragments = newFragments.concat(fragments); 384 } 385 } 386 return fragments; 387 } 388 389 detachSelection() { return null; } 390 391 public searchInputAction(searchInput: HTMLInputElement, e: Event, onlyVisible: boolean): void { 392 throw new Error("Method not implemented."); 393 } 394} 395