f21e2a7f03
- Add references from CodeLogEntry to DeoptLogEntry - Add simple basic blocks in the disassembly code view Bug: v8:10644 Change-Id: I15f3b56751d515b902185b08f9454be3951ffa48 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3540142 Reviewed-by: Patrick Thier <pthier@chromium.org> Commit-Queue: Camillo Bruni <cbruni@chromium.org> Cr-Commit-Position: refs/heads/main@{#79754}
366 lines
11 KiB
JavaScript
366 lines
11 KiB
JavaScript
// 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 = `(?<register>${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 = /(?<address>[+-]?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 `<span class="reg ${part}">${part}</span>`
|
|
}
|
|
|
|
_formatAddressPart(part, index) {
|
|
if (kFullAddressRegexp.test(part)) {
|
|
// The first or second address must be the line address
|
|
if (index <= 1) {
|
|
return `<span class="addr line" data-addr="${part}">${part}</span>`;
|
|
}
|
|
return `<span class=addr data-addr="${part}">${part}</span>`;
|
|
} else if (kRelativeAddressRegexp.test(part)) {
|
|
return `<span class=addr data-addr="0x${
|
|
this._toAbsoluteAddress(part).toString(16)}">${part}</span>`;
|
|
} 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');
|
|
}
|
|
}
|
|
}
|