blob: 0455437002ff635613a7c2866fc66acf46e07a75 [file] [log] [blame]
// Copyright 2015 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 { PROF_COLS, UNICODE_BLOCK } from "../src/constants";
import { SelectionBroker } from "../src/selection-broker";
import { TextView } from "../src/text-view";
import { MySelection } from "./selection";
import { anyToString, interpolate } from "./util";
import { InstructionSelectionHandler } from "./selection-handler";
const toolboxHTML = `<div id="disassembly-toolbox">
<form>
<label><input id="show-instruction-address" type="checkbox" name="instruction-address">Show addresses</label>
<label><input id="show-instruction-binary" type="checkbox" name="instruction-binary">Show binary literal</label>
<label><input id="highlight-gap-instructions" type="checkbox" name="instruction-binary">Highlight gap instructions</label>
</form>
</div>`;
export class DisassemblyView extends TextView {
SOURCE_POSITION_HEADER_REGEX: any;
addrEventCounts: any;
totalEventCounts: any;
maxEventCounts: any;
posLines: Array<any>;
instructionSelectionHandler: InstructionSelectionHandler;
offsetSelection: MySelection;
showInstructionAddressHandler: () => void;
showInstructionBinaryHandler: () => void;
highlightGapInstructionsHandler: () => void;
createViewElement() {
const pane = document.createElement('div');
pane.setAttribute('id', "disassembly");
pane.innerHTML =
`<pre id='disassembly-text-pre' class='prettyprint prettyprinted'>
<ul class='disassembly-list nolinenums noindent'>
</ul>
</pre>`;
return pane;
}
constructor(parentId, broker: SelectionBroker) {
super(parentId, broker);
const view = this;
const ADDRESS_STYLE = {
associateData: (text, fragment: HTMLElement) => {
const matches = text.match(/(?<address>0?x?[0-9a-fA-F]{8,16})(?<addressSpace>\s+)(?<offset>[0-9a-f]+)(?<offsetSpace>\s*)/);
const offset = Number.parseInt(matches.groups["offset"], 16);
const instructionKind = view.sourceResolver.getInstructionKindForPCOffset(offset);
fragment.dataset.instructionKind = instructionKind;
fragment.title = view.sourceResolver.instructionKindToReadableName(instructionKind);
const blockIds = view.sourceResolver.getBlockIdsForOffset(offset);
const blockIdElement = document.createElement("SPAN");
blockIdElement.className = "block-id com linkable-text";
blockIdElement.innerText = "";
if (blockIds && blockIds.length > 0) {
blockIds.forEach(blockId => view.addHtmlElementForBlockId(blockId, fragment));
blockIdElement.innerText = `B${blockIds.join(",")}:`;
blockIdElement.dataset.blockId = `${blockIds.join(",")}`;
}
fragment.appendChild(blockIdElement);
const addressElement = document.createElement("SPAN");
addressElement.className = "instruction-address";
addressElement.innerText = matches.groups["address"];
const offsetElement = document.createElement("SPAN");
offsetElement.innerText = matches.groups["offset"];
fragment.appendChild(addressElement);
fragment.appendChild(document.createTextNode(matches.groups["addressSpace"]));
fragment.appendChild(offsetElement);
fragment.appendChild(document.createTextNode(matches.groups["offsetSpace"]));
fragment.classList.add('tag');
if (!Number.isNaN(offset)) {
let pcOffset = view.sourceResolver.getKeyPcOffset(offset);
if (pcOffset == -1) pcOffset = Number(offset);
fragment.dataset.pcOffset = `${pcOffset}`;
addressElement.classList.add('linkable-text');
offsetElement.classList.add('linkable-text');
}
return true;
}
};
const UNCLASSIFIED_STYLE = {
css: 'com'
};
const NUMBER_STYLE = {
css: ['instruction-binary', 'lit']
};
const COMMENT_STYLE = {
css: 'com'
};
const OPCODE_ARGS = {
associateData: function (text, fragment) {
fragment.innerHTML = text;
const replacer = (match, hexOffset) => {
const offset = Number.parseInt(hexOffset, 16);
let keyOffset = view.sourceResolver.getKeyPcOffset(offset);
if (keyOffset == -1) keyOffset = Number(offset);
const blockIds = view.sourceResolver.getBlockIdsForOffset(offset);
let block = "";
let blockIdData = "";
if (blockIds && blockIds.length > 0) {
block = `B${blockIds.join(",")} `;
blockIdData = `data-block-id="${blockIds.join(",")}"`;
}
return `<span class="tag linkable-text" data-pc-offset="${keyOffset}" ${blockIdData}>${block}${match}</span>`;
};
const html = text.replace(/<.0?x?([0-9a-fA-F]+)>/g, replacer);
fragment.innerHTML = html;
return true;
}
};
const OPCODE_STYLE = {
css: 'kwd'
};
const BLOCK_HEADER_STYLE = {
associateData: function (text, fragment) {
if (view.sourceResolver.hasBlockStartInfo()) return false;
const matches = /\d+/.exec(text);
if (!matches) return true;
const blockId = matches[0];
fragment.dataset.blockId = blockId;
fragment.innerHTML = text;
fragment.className = "com block";
return true;
}
};
const SOURCE_POSITION_HEADER_STYLE = {
css: 'com'
};
view.SOURCE_POSITION_HEADER_REGEX = /^\s*--[^<]*<.*(not inlined|inlined\((\d+)\)):(\d+)>\s*--/;
const patterns = [
[
[/^0?x?[0-9a-fA-F]{8,16}\s+[0-9a-f]+\s+/, ADDRESS_STYLE, 1],
[view.SOURCE_POSITION_HEADER_REGEX, SOURCE_POSITION_HEADER_STYLE, -1],
[/^\s+-- B\d+ start.*/, BLOCK_HEADER_STYLE, -1],
[/^.*/, UNCLASSIFIED_STYLE, -1]
],
[
[/^\s*[0-9a-f]+\s+/, NUMBER_STYLE, 2],
[/^\s*[0-9a-f]+\s+[0-9a-f]+\s+/, NUMBER_STYLE, 2],
[/^.*/, null, -1]
],
[
[/^REX.W \S+\s+/, OPCODE_STYLE, 3],
[/^\S+\s+/, OPCODE_STYLE, 3],
[/^\S+$/, OPCODE_STYLE, -1],
[/^.*/, null, -1]
],
[
[/^\s+/, null],
[/^[^;]+$/, OPCODE_ARGS, -1],
[/^[^;]+/, OPCODE_ARGS, 4],
[/^;/, COMMENT_STYLE, 5]
],
[
[/^.+$/, COMMENT_STYLE, -1]
]
];
view.setPatterns(patterns);
const linkHandler = (e: MouseEvent) => {
if (!(e.target instanceof HTMLElement)) return;
const offsetAsString = typeof e.target.dataset.pcOffset != "undefined" ? e.target.dataset.pcOffset : e.target.parentElement.dataset.pcOffset;
const offset = Number.parseInt(offsetAsString, 10);
if ((typeof offsetAsString) != "undefined" && !Number.isNaN(offset)) {
view.offsetSelection.select([offset], true);
const nodes = view.sourceResolver.nodesForPCOffset(offset)[0];
if (nodes.length > 0) {
e.stopPropagation();
if (!e.shiftKey) {
view.selectionHandler.clear();
}
view.selectionHandler.select(nodes, true);
} else {
view.updateSelection();
}
}
return undefined;
};
view.divNode.addEventListener('click', linkHandler);
const linkHandlerBlock = e => {
const blockId = e.target.dataset.blockId;
if (typeof blockId != "undefined") {
const blockIds = blockId.split(",");
if (!e.shiftKey) {
view.selectionHandler.clear();
}
view.blockSelectionHandler.select(blockIds, true);
}
};
view.divNode.addEventListener('click', linkHandlerBlock);
this.offsetSelection = new MySelection(anyToString);
const instructionSelectionHandler = {
clear: function () {
view.offsetSelection.clear();
view.updateSelection();
broker.broadcastClear(instructionSelectionHandler);
},
select: function (instructionIds, selected) {
view.offsetSelection.select(instructionIds, selected);
view.updateSelection();
broker.broadcastBlockSelect(instructionSelectionHandler, instructionIds, selected);
},
brokeredInstructionSelect: function (instructionIds, selected) {
const firstSelect = view.offsetSelection.isEmpty();
const keyPcOffsets = view.sourceResolver.instructionsToKeyPcOffsets(instructionIds);
view.offsetSelection.select(keyPcOffsets, selected);
view.updateSelection(firstSelect);
},
brokeredClear: function () {
view.offsetSelection.clear();
view.updateSelection();
}
};
this.instructionSelectionHandler = instructionSelectionHandler;
broker.addInstructionHandler(instructionSelectionHandler);
const toolbox = document.createElement("div");
toolbox.id = "toolbox-anchor";
toolbox.innerHTML = toolboxHTML;
view.divNode.insertBefore(toolbox, view.divNode.firstChild);
const instructionAddressInput: HTMLInputElement = view.divNode.querySelector("#show-instruction-address");
const lastShowInstructionAddress = window.sessionStorage.getItem("show-instruction-address");
instructionAddressInput.checked = lastShowInstructionAddress == 'true';
const showInstructionAddressHandler = () => {
window.sessionStorage.setItem("show-instruction-address", `${instructionAddressInput.checked}`);
for (const el of view.divNode.querySelectorAll(".instruction-address")) {
el.classList.toggle("invisible", !instructionAddressInput.checked);
}
};
instructionAddressInput.addEventListener("change", showInstructionAddressHandler);
this.showInstructionAddressHandler = showInstructionAddressHandler;
const instructionBinaryInput: HTMLInputElement = view.divNode.querySelector("#show-instruction-binary");
const lastShowInstructionBinary = window.sessionStorage.getItem("show-instruction-binary");
instructionBinaryInput.checked = lastShowInstructionBinary == 'true';
const showInstructionBinaryHandler = () => {
window.sessionStorage.setItem("show-instruction-binary", `${instructionBinaryInput.checked}`);
for (const el of view.divNode.querySelectorAll(".instruction-binary")) {
el.classList.toggle("invisible", !instructionBinaryInput.checked);
}
};
instructionBinaryInput.addEventListener("change", showInstructionBinaryHandler);
this.showInstructionBinaryHandler = showInstructionBinaryHandler;
const highlightGapInstructionsInput: HTMLInputElement = view.divNode.querySelector("#highlight-gap-instructions");
const lastHighlightGapInstructions = window.sessionStorage.getItem("highlight-gap-instructions");
highlightGapInstructionsInput.checked = lastHighlightGapInstructions == 'true';
const highlightGapInstructionsHandler = () => {
window.sessionStorage.setItem("highlight-gap-instructions", `${highlightGapInstructionsInput.checked}`);
view.divNode.classList.toggle("highlight-gap-instructions", highlightGapInstructionsInput.checked);
};
highlightGapInstructionsInput.addEventListener("change", highlightGapInstructionsHandler);
this.highlightGapInstructionsHandler = highlightGapInstructionsHandler;
}
updateSelection(scrollIntoView: boolean = false) {
super.updateSelection(scrollIntoView);
const keyPcOffsets = this.sourceResolver.nodesToKeyPcOffsets(this.selection.selectedKeys());
if (this.offsetSelection) {
for (const key of this.offsetSelection.selectedKeys()) {
keyPcOffsets.push(Number(key));
}
}
for (const keyPcOffset of keyPcOffsets) {
const elementsToSelect = this.divNode.querySelectorAll(`[data-pc-offset='${keyPcOffset}']`);
for (const el of elementsToSelect) {
el.classList.toggle("selected", true);
}
}
}
initializeCode(sourceText, sourcePosition: number = 0) {
const view = this;
view.addrEventCounts = null;
view.totalEventCounts = null;
view.maxEventCounts = null;
view.posLines = new Array();
// Comment lines for line 0 include sourcePosition already, only need to
// add sourcePosition for lines > 0.
view.posLines[0] = sourcePosition;
if (sourceText && sourceText != "") {
const base = sourcePosition;
let current = 0;
const sourceLines = sourceText.split("\n");
for (let i = 1; i < sourceLines.length; i++) {
// Add 1 for newline character that is split off.
current += sourceLines[i - 1].length + 1;
view.posLines[i] = base + current;
}
}
}
initializePerfProfile(eventCounts) {
const view = this;
if (eventCounts !== undefined) {
view.addrEventCounts = eventCounts;
view.totalEventCounts = {};
view.maxEventCounts = {};
for (const evName in view.addrEventCounts) {
if (view.addrEventCounts.hasOwnProperty(evName)) {
const keys = Object.keys(view.addrEventCounts[evName]);
const values = keys.map(key => view.addrEventCounts[evName][key]);
view.totalEventCounts[evName] = values.reduce((a, b) => a + b);
view.maxEventCounts[evName] = values.reduce((a, b) => Math.max(a, b));
}
}
} else {
view.addrEventCounts = null;
view.totalEventCounts = null;
view.maxEventCounts = null;
}
}
showContent(data): void {
console.time("disassembly-view");
super.initializeContent(data, null);
this.showInstructionAddressHandler();
this.showInstructionBinaryHandler();
this.highlightGapInstructionsHandler();
console.timeEnd("disassembly-view");
}
// Shorten decimals and remove trailing zeroes for readability.
humanize(num) {
return num.toFixed(3).replace(/\.?0+$/, "") + "%";
}
processLine(line) {
const view = this;
let fragments = super.processLine(line);
// Add profiling data per instruction if available.
if (view.totalEventCounts) {
const matches = /^(0x[0-9a-fA-F]+)\s+\d+\s+[0-9a-fA-F]+/.exec(line);
if (matches) {
const newFragments = [];
for (const event in view.addrEventCounts) {
if (!view.addrEventCounts.hasOwnProperty(event)) continue;
const count = view.addrEventCounts[event][matches[1]];
let str = " ";
const cssCls = "prof";
if (count !== undefined) {
const perc = count / view.totalEventCounts[event] * 100;
let col = { r: 255, g: 255, b: 255 };
for (let i = 0; i < PROF_COLS.length; i++) {
if (perc === PROF_COLS[i].perc) {
col = PROF_COLS[i].col;
break;
} else if (perc > PROF_COLS[i].perc && perc < PROF_COLS[i + 1].perc) {
const col1 = PROF_COLS[i].col;
const col2 = PROF_COLS[i + 1].col;
const val = perc - PROF_COLS[i].perc;
const max = PROF_COLS[i + 1].perc - PROF_COLS[i].perc;
col.r = Math.round(interpolate(val, max, col1.r, col2.r));
col.g = Math.round(interpolate(val, max, col1.g, col2.g));
col.b = Math.round(interpolate(val, max, col1.b, col2.b));
break;
}
}
str = UNICODE_BLOCK;
const fragment = view.createFragment(str, cssCls);
fragment.title = event + ": " + view.humanize(perc) + " (" + count + ")";
fragment.style.color = "rgb(" + col.r + ", " + col.g + ", " + col.b + ")";
newFragments.push(fragment);
} else {
newFragments.push(view.createFragment(str, cssCls));
}
}
fragments = newFragments.concat(fragments);
}
}
return fragments;
}
detachSelection() { return null; }
public searchInputAction(searchInput: HTMLInputElement, e: Event, onlyVisible: boolean): void {
throw new Error("Method not implemented.");
}
}