// Copyright 2020 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import {SelectRelatedEvent} from './events.mjs'; import {CollapsableElement, DOM, formatBytes, formatMicroSeconds} from './helper.mjs'; DOM.defineCustomElement('view/code-panel', (templateText) => class CodePanel extends CollapsableElement { _timeline; _selectedEntries; _entry; constructor() { super(templateText); this._propertiesNode = this.$('#properties'); this._codeSelectNode = this.$('#codeSelect'); this._disassemblyNode = this.$('#disassembly'); this._feedbackVectorNode = this.$('#feedbackVector'); this._selectionHandler = new SelectionHandler(this._disassemblyNode); this._codeSelectNode.onchange = this._handleSelectCode.bind(this); this.$('#selectedRelatedButton').onclick = this._handleSelectRelated.bind(this) } set timeline(timeline) { this._timeline = timeline; this.$('.panel').style.display = timeline.isEmpty() ? 'none' : 'inherit'; this.requestUpdate(); } set selectedEntries(entries) { this._selectedEntries = entries; this.entry = entries.first(); } set entry(entry) { this._entry = entry; if (!entry) { this._propertiesNode.propertyDict = {}; } else { this._propertiesNode.propertyDict = { '__this__': entry, functionName: entry.functionName, size: formatBytes(entry.size), creationTime: formatMicroSeconds(entry.time / 1000), sourcePosition: entry.sourcePosition, script: entry.script, type: entry.type, kind: entry.kindName, variants: entry.variants.length > 1 ? [undefined, ...entry.variants] : undefined, }; } this.requestUpdate(); } _update() { this._updateSelect(); this._updateDisassembly(); this._updateFeedbackVector(); } _updateFeedbackVector() { if (!this._entry?.feedbackVector) { this._feedbackVectorNode.propertyDict = {}; } else { const dict = this._entry.feedbackVector.toolTipDict; delete dict.title; delete dict.code; this._feedbackVectorNode.propertyDict = dict; } } _updateDisassembly() { this._disassemblyNode.innerText = ''; if (!this._entry?.code) return; try { this._disassemblyNode.appendChild( new AssemblyFormatter(this._entry).fragment); } catch (e) { console.error(e); this._disassemblyNode.innerText = this._entry.code; } } _updateSelect() { const select = this._codeSelectNode; if (select.data === this._selectedEntries) return; select.data = this._selectedEntries; select.options.length = 0; const sorted = this._selectedEntries.slice().sort((a, b) => a.time - b.time); for (const code of this._selectedEntries) { const option = DOM.element('option'); option.text = this._entrySummary(code); option.data = code; select.add(option); } } _entrySummary(code) { if (code.isBuiltinKind) { return `${code.functionName}(...) t=${ formatMicroSeconds(code.time)} size=${formatBytes(code.size)}`; } return `${code.functionName}(...) t=${formatMicroSeconds(code.time)} size=${ formatBytes(code.size)} script=${code.script?.toString()}`; } _handleSelectCode() { this.entry = this._codeSelectNode.selectedOptions[0].data; } _handleSelectRelated(e) { if (!this._entry) return; this.dispatchEvent(new SelectRelatedEvent(this._entry)); } }); const kRegisters = ['rsp', 'rbp', 'rax', 'rbx', 'rcx', 'rdx', 'rsi', 'rdi']; // Make sure we dont match register on bytecode: Star1 or Star2 const kAvoidBytecodeOpsRegexpSource = '(.*?[^a-zA-Z])' // Look for registers in strings like: movl rbx,[rcx-0x30] const kRegisterRegexpSource = `(?${kRegisters.join('|')}|r[0-9]+)` const kRegisterSplitRegexp = new RegExp(`${kAvoidBytecodeOpsRegexpSource}${kRegisterRegexpSource}`) const kIsRegisterRegexp = new RegExp(`^${kRegisterRegexpSource}$`); const kFullAddressRegexp = /(0x[0-9a-f]{8,})/; const kRelativeAddressRegexp = /([+-]0x[0-9a-f]+)/; const kAnyAddressRegexp = /(?
[+-]?0x[0-9a-f]+)/; const kJmpRegexp = new RegExp(`jmp ${kRegisterRegexpSource}`); const kMovRegexp = new RegExp(`mov. ${kRegisterRegexpSource},${kAnyAddressRegexp.source}`); class AssemblyFormatter { constructor(codeLogEntry) { this._fragment = new DocumentFragment(); this._entry = codeLogEntry; this._lines = new Map(); this._previousLine = undefined; this._parseLines(); this._format(); } get fragment() { return this._fragment; } _format() { let block = DOM.div(['basicBlock', 'header']); this._lines.forEach(line => { if (!block || line.isBlockStart) { this._fragment.appendChild(block); block = DOM.div('basicBlock'); } block.appendChild(line.format()) }); this._fragment.appendChild(block); } _parseLines() { this._entry.code.split('\n').forEach(each => this._parseLine(each)); this._findBasicBlocks(); } _parseLine(line) { const parts = line.split(' '); // Use unique placeholder for address: let lineAddress = -this._lines.size; for (let part of parts) { if (kFullAddressRegexp.test(part)) { lineAddress = parseInt(part); break; } } const newLine = new AssemblyLine(lineAddress, parts); // special hack for: mov reg 0x...; jmp reg; if (lineAddress <= 0 && this._previousLine) { const jmpMatch = line.match(kJmpRegexp); if (jmpMatch) { const register = jmpMatch.groups.register; const movMatch = this._previousLine.line.match(kMovRegexp); if (movMatch.groups.register === register) { newLine.outgoing.push(movMatch.groups.address); } } } this._lines.set(lineAddress, newLine); this._previousLine = newLine; } _findBasicBlocks() { const lines = Array.from(this._lines.values()); for (let i = 0; i < lines.length; i++) { const line = lines[i]; let forceBasicBlock = i == 0; if (i > 0 && i < lines.length - 1) { const prevHasAddress = lines[i - 1].address > 0; const currentHasAddress = lines[i].address > 0; const nextHasAddress = lines[i + 1].address > 0; if (prevHasAddress !== currentHasAddress && currentHasAddress == nextHasAddress) { forceBasicBlock = true; } } if (forceBasicBlock) { // Add fake-incoming address to mark a block start. line.addIncoming(0); } line.outgoing.forEach(address => { const outgoing = this._lines.get(address); if (outgoing) outgoing.addIncoming(line.address); }) } } } class AssemblyLine { constructor(address, parts) { this.address = address; this.outgoing = []; this.incoming = []; parts.forEach(part => { const fullMatch = part.match(kFullAddressRegexp); if (fullMatch) { let inlineAddress = parseInt(fullMatch[0]); if (inlineAddress != this.address) this.outgoing.push(inlineAddress); if (Number.isNaN(inlineAddress)) throw 'invalid address'; } else if (kRelativeAddressRegexp.test(part)) { this.outgoing.push(this._toAbsoluteAddress(part)); } }); this.line = parts.join(' '); } get isBlockStart() { return this.incoming.length > 0; } addIncoming(address) { this.incoming.push(address); } format() { const content = DOM.span({textContent: this.line + '\n'}); let formattedCode = content.innerHTML.split(kRegisterSplitRegexp) .map(part => this._formatRegisterPart(part)) .join(''); formattedCode = formattedCode.split(kAnyAddressRegexp) .map((part, index) => this._formatAddressPart(part, index)) .join(''); // Let's replace the base-address since it doesn't add any value. // TODO content.innerHTML = formattedCode; return content; } _formatRegisterPart(part) { if (!kIsRegisterRegexp.test(part)) return part; return `${part}` } _formatAddressPart(part, index) { if (kFullAddressRegexp.test(part)) { // The first or second address must be the line address if (index <= 1) { return `${part}`; } return `${part}`; } else if (kRelativeAddressRegexp.test(part)) { return `${part}`; } else { return part; } } _toAbsoluteAddress(part) { return this.address + parseInt(part); } } class SelectionHandler { _currentRegisterHovered; _currentRegisterClicked; constructor(node) { this._node = node; this._node.onmousemove = this._handleMouseMove.bind(this); this._node.onclick = this._handleClick.bind(this); } $(query) { return this._node.querySelectorAll(query); } _handleClick(event) { const target = event.target; if (target.classList.contains('addr')) { return this._handleClickAddress(target); } else if (target.classList.contains('reg')) { this._handleClickRegister(target); } else { this._clearRegisterSelection(); } } _handleClickAddress(target) { let targetAddress = target.getAttribute('data-addr') ?? target.innerText; // Clear any selection for (let addrNode of this.$('.addr.selected')) { addrNode.classList.remove('selected'); } // Highlight all matching addresses let lineAddrNode; for (let addrNode of this.$(`.addr[data-addr="${targetAddress}"]`)) { addrNode.classList.add('selected'); if (addrNode.classList.contains('line') && lineAddrNode == undefined) { lineAddrNode = addrNode; } } // Jump to potential target address. if (lineAddrNode) { lineAddrNode.scrollIntoView({behavior: 'smooth', block: 'nearest'}); } } _handleClickRegister(target) { this._setRegisterSelection(target.innerText); this._currentRegisterClicked = this._currentRegisterHovered; } _handleMouseMove(event) { if (this._currentRegisterClicked) return; const target = event.target; if (!target.classList.contains('reg')) { this._clearRegisterSelection(); } else { this._setRegisterSelection(target.innerText); } } _clearRegisterSelection() { if (!this._currentRegisterHovered) return; for (let node of this.$('.reg.selected')) { node.classList.remove('selected'); } this._currentRegisterClicked = undefined; this._currentRegisterHovered = undefined; } _setRegisterSelection(register) { if (register == this._currentRegisterHovered) return; this._clearRegisterSelection(); this._currentRegisterHovered = register; for (let node of this.$(`.reg.${register}`)) { node.classList.add('selected'); } } }