1// Copyright 2020 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 {SelectRelatedEvent} from './events.mjs'; 6import {CollapsableElement, DOM, formatBytes, formatMicroSeconds} from './helper.mjs'; 7 8DOM.defineCustomElement('view/code-panel', 9 (templateText) => 10 class CodePanel extends CollapsableElement { 11 _timeline; 12 _selectedEntries; 13 _entry; 14 15 constructor() { 16 super(templateText); 17 this._propertiesNode = this.$('#properties'); 18 this._codeSelectNode = this.$('#codeSelect'); 19 this._disassemblyNode = this.$('#disassembly'); 20 this._feedbackVectorNode = this.$('#feedbackVector'); 21 this._selectionHandler = new SelectionHandler(this._disassemblyNode); 22 23 this._codeSelectNode.onchange = this._handleSelectCode.bind(this); 24 this.$('#selectedRelatedButton').onclick = 25 this._handleSelectRelated.bind(this) 26 } 27 28 set timeline(timeline) { 29 this._timeline = timeline; 30 this.$('.panel').style.display = timeline.isEmpty() ? 'none' : 'inherit'; 31 this.requestUpdate(); 32 } 33 34 set selectedEntries(entries) { 35 this._selectedEntries = entries; 36 this.entry = entries.first(); 37 } 38 39 set entry(entry) { 40 this._entry = entry; 41 if (!entry) { 42 this._propertiesNode.propertyDict = {}; 43 } else { 44 this._propertiesNode.propertyDict = { 45 '__this__': entry, 46 functionName: entry.functionName, 47 size: formatBytes(entry.size), 48 creationTime: formatMicroSeconds(entry.time / 1000), 49 sourcePosition: entry.sourcePosition, 50 script: entry.script, 51 type: entry.type, 52 kind: entry.kindName, 53 variants: entry.variants.length > 1 ? [undefined, ...entry.variants] : 54 undefined, 55 }; 56 } 57 this.requestUpdate(); 58 } 59 60 _update() { 61 this._updateSelect(); 62 this._updateDisassembly(); 63 this._updateFeedbackVector(); 64 } 65 66 _updateFeedbackVector() { 67 if (!this._entry?.feedbackVector) { 68 this._feedbackVectorNode.propertyDict = {}; 69 } else { 70 const dict = this._entry.feedbackVector.toolTipDict; 71 delete dict.title; 72 delete dict.code; 73 this._feedbackVectorNode.propertyDict = dict; 74 } 75 } 76 77 _updateDisassembly() { 78 this._disassemblyNode.innerText = ''; 79 if (!this._entry?.code) return; 80 try { 81 this._disassemblyNode.appendChild( 82 new AssemblyFormatter(this._entry).fragment); 83 } catch (e) { 84 console.error(e); 85 this._disassemblyNode.innerText = this._entry.code; 86 } 87 } 88 89 _updateSelect() { 90 const select = this._codeSelectNode; 91 if (select.data === this._selectedEntries) return; 92 select.data = this._selectedEntries; 93 select.options.length = 0; 94 const sorted = 95 this._selectedEntries.slice().sort((a, b) => a.time - b.time); 96 for (const code of this._selectedEntries) { 97 const option = DOM.element('option'); 98 option.text = this._entrySummary(code); 99 option.data = code; 100 select.add(option); 101 } 102 } 103 _entrySummary(code) { 104 if (code.isBuiltinKind) { 105 return `${code.functionName}(...) t=${ 106 formatMicroSeconds(code.time)} size=${formatBytes(code.size)}`; 107 } 108 return `${code.functionName}(...) t=${formatMicroSeconds(code.time)} size=${ 109 formatBytes(code.size)} script=${code.script?.toString()}`; 110 } 111 112 _handleSelectCode() { 113 this.entry = this._codeSelectNode.selectedOptions[0].data; 114 } 115 116 _handleSelectRelated(e) { 117 if (!this._entry) return; 118 this.dispatchEvent(new SelectRelatedEvent(this._entry)); 119 } 120}); 121 122const kRegisters = ['rsp', 'rbp', 'rax', 'rbx', 'rcx', 'rdx', 'rsi', 'rdi']; 123// Make sure we dont match register on bytecode: Star1 or Star2 124const kAvoidBytecodeOpsRegexpSource = '(.*?[^a-zA-Z])' 125// Look for registers in strings like: movl rbx,[rcx-0x30] 126const kRegisterRegexpSource = `(?<register>${kRegisters.join('|')}|r[0-9]+)` 127const kRegisterSplitRegexp = 128 new RegExp(`${kAvoidBytecodeOpsRegexpSource}${kRegisterRegexpSource}`) 129const kIsRegisterRegexp = new RegExp(`^${kRegisterRegexpSource}$`); 130 131const kFullAddressRegexp = /(0x[0-9a-f]{8,})/; 132const kRelativeAddressRegexp = /([+-]0x[0-9a-f]+)/; 133const kAnyAddressRegexp = /(?<address>[+-]?0x[0-9a-f]+)/; 134 135const kJmpRegexp = new RegExp(`jmp ${kRegisterRegexpSource}`); 136const kMovRegexp = 137 new RegExp(`mov. ${kRegisterRegexpSource},${kAnyAddressRegexp.source}`); 138 139class AssemblyFormatter { 140 constructor(codeLogEntry) { 141 this._fragment = new DocumentFragment(); 142 this._entry = codeLogEntry; 143 this._lines = new Map(); 144 this._previousLine = undefined; 145 this._parseLines(); 146 this._format(); 147 } 148 149 get fragment() { 150 return this._fragment; 151 } 152 153 _format() { 154 let block = DOM.div(['basicBlock', 'header']); 155 this._lines.forEach(line => { 156 if (!block || line.isBlockStart) { 157 this._fragment.appendChild(block); 158 block = DOM.div('basicBlock'); 159 } 160 block.appendChild(line.format()) 161 }); 162 this._fragment.appendChild(block); 163 } 164 165 _parseLines() { 166 this._entry.code.split('\n').forEach(each => this._parseLine(each)); 167 this._findBasicBlocks(); 168 } 169 170 _parseLine(line) { 171 const parts = line.split(' '); 172 // Use unique placeholder for address: 173 let lineAddress = -this._lines.size; 174 for (let part of parts) { 175 if (kFullAddressRegexp.test(part)) { 176 lineAddress = parseInt(part); 177 break; 178 } 179 } 180 const newLine = new AssemblyLine(lineAddress, parts); 181 // special hack for: mov reg 0x...; jmp reg; 182 if (lineAddress <= 0 && this._previousLine) { 183 const jmpMatch = line.match(kJmpRegexp); 184 if (jmpMatch) { 185 const register = jmpMatch.groups.register; 186 const movMatch = this._previousLine.line.match(kMovRegexp); 187 if (movMatch.groups.register === register) { 188 newLine.outgoing.push(movMatch.groups.address); 189 } 190 } 191 } 192 this._lines.set(lineAddress, newLine); 193 this._previousLine = newLine; 194 } 195 196 _findBasicBlocks() { 197 const lines = Array.from(this._lines.values()); 198 for (let i = 0; i < lines.length; i++) { 199 const line = lines[i]; 200 let forceBasicBlock = i == 0; 201 if (i > 0 && i < lines.length - 1) { 202 const prevHasAddress = lines[i - 1].address > 0; 203 const currentHasAddress = lines[i].address > 0; 204 const nextHasAddress = lines[i + 1].address > 0; 205 if (prevHasAddress !== currentHasAddress && 206 currentHasAddress == nextHasAddress) { 207 forceBasicBlock = true; 208 } 209 } 210 if (forceBasicBlock) { 211 // Add fake-incoming address to mark a block start. 212 line.addIncoming(0); 213 } 214 line.outgoing.forEach(address => { 215 const outgoing = this._lines.get(address); 216 if (outgoing) outgoing.addIncoming(line.address); 217 }) 218 } 219 } 220} 221 222class AssemblyLine { 223 constructor(address, parts) { 224 this.address = address; 225 this.outgoing = []; 226 this.incoming = []; 227 parts.forEach(part => { 228 const fullMatch = part.match(kFullAddressRegexp); 229 if (fullMatch) { 230 let inlineAddress = parseInt(fullMatch[0]); 231 if (inlineAddress != this.address) this.outgoing.push(inlineAddress); 232 if (Number.isNaN(inlineAddress)) throw 'invalid address'; 233 } else if (kRelativeAddressRegexp.test(part)) { 234 this.outgoing.push(this._toAbsoluteAddress(part)); 235 } 236 }); 237 this.line = parts.join(' '); 238 } 239 240 get isBlockStart() { 241 return this.incoming.length > 0; 242 } 243 244 addIncoming(address) { 245 this.incoming.push(address); 246 } 247 248 format() { 249 const content = DOM.span({textContent: this.line + '\n'}); 250 let formattedCode = content.innerHTML.split(kRegisterSplitRegexp) 251 .map(part => this._formatRegisterPart(part)) 252 .join(''); 253 formattedCode = 254 formattedCode.split(kAnyAddressRegexp) 255 .map((part, index) => this._formatAddressPart(part, index)) 256 .join(''); 257 // Let's replace the base-address since it doesn't add any value. 258 // TODO 259 content.innerHTML = formattedCode; 260 return content; 261 } 262 263 _formatRegisterPart(part) { 264 if (!kIsRegisterRegexp.test(part)) return part; 265 return `<span class="reg ${part}">${part}</span>` 266 } 267 268 _formatAddressPart(part, index) { 269 if (kFullAddressRegexp.test(part)) { 270 // The first or second address must be the line address 271 if (index <= 1) { 272 return `<span class="addr line" data-addr="${part}">${part}</span>`; 273 } 274 return `<span class=addr data-addr="${part}">${part}</span>`; 275 } else if (kRelativeAddressRegexp.test(part)) { 276 return `<span class=addr data-addr="0x${ 277 this._toAbsoluteAddress(part).toString(16)}">${part}</span>`; 278 } else { 279 return part; 280 } 281 } 282 283 _toAbsoluteAddress(part) { 284 return this.address + parseInt(part); 285 } 286} 287 288class SelectionHandler { 289 _currentRegisterHovered; 290 _currentRegisterClicked; 291 292 constructor(node) { 293 this._node = node; 294 this._node.onmousemove = this._handleMouseMove.bind(this); 295 this._node.onclick = this._handleClick.bind(this); 296 } 297 298 $(query) { 299 return this._node.querySelectorAll(query); 300 } 301 302 _handleClick(event) { 303 const target = event.target; 304 if (target.classList.contains('addr')) { 305 return this._handleClickAddress(target); 306 } else if (target.classList.contains('reg')) { 307 this._handleClickRegister(target); 308 } else { 309 this._clearRegisterSelection(); 310 } 311 } 312 313 _handleClickAddress(target) { 314 let targetAddress = target.getAttribute('data-addr') ?? target.innerText; 315 // Clear any selection 316 for (let addrNode of this.$('.addr.selected')) { 317 addrNode.classList.remove('selected'); 318 } 319 // Highlight all matching addresses 320 let lineAddrNode; 321 for (let addrNode of this.$(`.addr[data-addr="${targetAddress}"]`)) { 322 addrNode.classList.add('selected'); 323 if (addrNode.classList.contains('line') && lineAddrNode == undefined) { 324 lineAddrNode = addrNode; 325 } 326 } 327 // Jump to potential target address. 328 if (lineAddrNode) { 329 lineAddrNode.scrollIntoView({behavior: 'smooth', block: 'nearest'}); 330 } 331 } 332 333 _handleClickRegister(target) { 334 this._setRegisterSelection(target.innerText); 335 this._currentRegisterClicked = this._currentRegisterHovered; 336 } 337 338 _handleMouseMove(event) { 339 if (this._currentRegisterClicked) return; 340 const target = event.target; 341 if (!target.classList.contains('reg')) { 342 this._clearRegisterSelection(); 343 } else { 344 this._setRegisterSelection(target.innerText); 345 } 346 } 347 348 _clearRegisterSelection() { 349 if (!this._currentRegisterHovered) return; 350 for (let node of this.$('.reg.selected')) { 351 node.classList.remove('selected'); 352 } 353 this._currentRegisterClicked = undefined; 354 this._currentRegisterHovered = undefined; 355 } 356 357 _setRegisterSelection(register) { 358 if (register == this._currentRegisterHovered) return; 359 this._clearRegisterSelection(); 360 this._currentRegisterHovered = register; 361 for (let node of this.$(`.reg.${register}`)) { 362 node.classList.add('selected'); 363 } 364 } 365} 366