Commit 65667531 authored by Camillo Bruni's avatar Camillo Bruni Committed by V8 LUCI CQ

[tools] Improve system analyzer

Profiler:
  - Track profiler tick durations
  - Various speedups due to low-level hacking
Improve code-panel:
  - Better register highlighting
  - Added address navigation and highlighting
  - Removed obsolete inline source-view
Improve script-panel:
  - Keep current source position focused when showing related entries
  - Better tool-tip with buttons to focus on grouped entries per
    source postion
  - Focus by default on other views when showing related entries
Improve timeline-panel:
  - Initialise event handlers late to avoid errors
  - Lazy initialise chunks to avoid errors when zooming-in and trying to
    create tooltips at the same time


Change-Id: I3f3c0fd51985aaa490d62f786ab52a4be1eed292
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3492521Reviewed-by: 's avatarPatrick Thier <pthier@chromium.org>
Commit-Queue: Camillo Bruni <cbruni@chromium.org>
Cr-Commit-Position: refs/heads/main@{#79329}
parent 123c38a5
......@@ -27,6 +27,15 @@
import { SplayTree } from "./splaytree.mjs";
/**
* The number of alignment bits in a page address.
*/
const kPageAlignment = 12;
/**
* Page size in bytes.
*/
const kPageSize = 1 << kPageAlignment;
/**
* Constructs a mapper that maps addresses into code entries.
*
......@@ -56,19 +65,7 @@ export class CodeMap {
/**
* Map of memory pages occupied with static code.
*/
pages_ = [];
/**
* The number of alignment bits in a page address.
*/
static PAGE_ALIGNMENT = 12;
/**
* Page size in bytes.
*/
static PAGE_SIZE = 1 << CodeMap.PAGE_ALIGNMENT;
pages_ = new Set();
/**
......@@ -130,9 +127,8 @@ export class CodeMap {
* @private
*/
markPages_(start, end) {
for (let addr = start; addr <= end;
addr += CodeMap.PAGE_SIZE) {
this.pages_[(addr / CodeMap.PAGE_SIZE)|0] = 1;
for (let addr = start; addr <= end; addr += kPageSize) {
this.pages_.add((addr / kPageSize) | 0);
}
}
......@@ -144,7 +140,7 @@ export class CodeMap {
let addr = end - 1;
while (addr >= start) {
const node = tree.findGreatestLessThan(addr);
if (!node) break;
if (node === null) break;
const start2 = node.key, end2 = start2 + node.value.size;
if (start2 < end && start < end2) to_delete.push(start2);
addr = start2 - 1;
......@@ -164,7 +160,7 @@ export class CodeMap {
*/
findInTree_(tree, addr) {
const node = tree.findGreatestLessThan(addr);
return node && this.isAddressBelongsTo_(addr, node) ? node : null;
return node !== null && this.isAddressBelongsTo_(addr, node) ? node : null;
}
/**
......@@ -175,22 +171,23 @@ export class CodeMap {
* @param {number} addr Address.
*/
findAddress(addr) {
const pageAddr = (addr / CodeMap.PAGE_SIZE)|0;
if (pageAddr in this.pages_) {
const pageAddr = (addr / kPageSize) | 0;
if (this.pages_.has(pageAddr)) {
// Static code entries can contain "holes" of unnamed code.
// In this case, the whole library is assigned to this address.
let result = this.findInTree_(this.statics_, addr);
if (!result) {
if (result === null) {
result = this.findInTree_(this.libraries_, addr);
if (!result) return null;
if (result === null) return null;
}
return {entry: result.value, offset: addr - result.key};
}
const min = this.dynamics_.findMin();
const max = this.dynamics_.findMax();
if (max != null && addr < (max.key + max.value.size) && addr >= min.key) {
if (max === null) return null;
const min = this.dynamics_.findMin();
if (addr >= min.key && addr < (max.key + max.value.size)) {
const dynaEntry = this.findInTree_(this.dynamics_, addr);
if (dynaEntry == null) return null;
if (dynaEntry === null) return null;
// Dedupe entry name.
const entry = dynaEntry.value;
if (!entry.nameUpdated_) {
......@@ -210,7 +207,7 @@ export class CodeMap {
*/
findEntry(addr) {
const result = this.findAddress(addr);
return result ? result.entry : null;
return result !== null ? result.entry : null;
}
/**
......@@ -220,7 +217,7 @@ export class CodeMap {
*/
findDynamicEntryByStartAddress(addr) {
const node = this.dynamics_.find(addr);
return node ? node.value : null;
return node !== null ? node.value : null;
}
/**
......
......@@ -38,13 +38,11 @@ export class CsvParser {
escapeField(string) {
let nextPos = string.indexOf("\\");
if (nextPos === -1) return string;
let result = string.substring(0, nextPos);
// Escape sequences of the form \x00 and \u0000;
let endPos = string.length;
let pos = 0;
while (nextPos !== -1) {
let escapeIdentifier = string.charAt(nextPos + 1);
const escapeIdentifier = string.charAt(nextPos + 1);
pos = nextPos + 2;
if (escapeIdentifier === 'n') {
result += '\n';
......@@ -61,7 +59,7 @@ export class CsvParser {
nextPos = pos + 4;
}
// Convert the selected escape sequence to a single character.
let escapeChars = string.substring(pos, nextPos);
const escapeChars = string.substring(pos, nextPos);
if (escapeChars === '2C') {
result += ',';
} else {
......@@ -75,6 +73,7 @@ export class CsvParser {
// If there are no more escape sequences consume the rest of the string.
if (nextPos === -1) {
result += string.substr(pos);
break;
} else if (pos !== nextPos) {
result += string.substring(pos, nextPos);
}
......
......@@ -14,6 +14,7 @@ export class CppProcessor extends LogReader {
constructor(cppEntriesProvider, timedRange, pairwiseTimedRange) {
super({}, timedRange, pairwiseTimedRange);
this.dispatchTable_ = {
__proto__: null,
'shared-library': {
parsers: [parseString, parseInt, parseInt, parseInt],
processor: this.processSharedLibrary }
......
......@@ -148,7 +148,7 @@ export class FileReader extends V8CustomElement {
export class DOM {
static element(type, options) {
const node = document.createElement(type);
if (options !== undefined) {
if (options === undefined) return node;
if (typeof options === 'string') {
// Old behaviour: options = class string
node.className = options;
......@@ -161,7 +161,7 @@ export class DOM {
if (key == 'className') {
node.className = value;
} else if (key == 'classList') {
node.classList = value;
DOM.addClasses(node, value);
} else if (key == 'textContent') {
node.textContent = value;
} else if (key == 'children') {
......@@ -173,7 +173,6 @@ export class DOM {
}
}
}
}
return node;
}
......@@ -196,6 +195,10 @@ export class DOM {
static button(label, clickHandler) {
const button = DOM.element('button');
button.innerText = label;
if (typeof clickHandler != 'function') {
throw new Error(
`DOM.button: Expected function but got clickHandler=${clickHandler}`);
}
button.onclick = clickHandler;
return button;
}
......
......@@ -179,16 +179,6 @@ export class LogReader {
return fullStack;
}
/**
* Returns whether a particular dispatch must be skipped.
*
* @param {!Object} dispatch Dispatch record.
* @return {boolean} True if dispatch must be skipped.
*/
skipDispatch(dispatch) {
return false;
}
/**
* Does a dispatch of a log record.
*
......@@ -200,14 +190,12 @@ export class LogReader {
const command = fields[0];
const dispatch = this.dispatchTable_[command];
if (dispatch === undefined) return;
if (dispatch === null || this.skipDispatch(dispatch)) {
return;
}
const parsers = dispatch.parsers;
const length = parsers.length;
// Parse fields.
const parsedFields = [];
for (let i = 0; i < dispatch.parsers.length; ++i) {
const parser = dispatch.parsers[i];
for (let i = 0; i < length; ++i) {
const parser = parsers[i];
if (parser === parseString) {
parsedFields.push(fields[1 + i]);
} else if (typeof parser == 'function') {
......
......@@ -261,6 +261,10 @@ class SourceInfo {
}
}
const kProfileOperationMove = 0;
const kProfileOperationDelete = 1;
const kProfileOperationTick = 2;
/**
* Creates a profile object for processing profiling-related events
* and calculating function execution times.
......@@ -271,9 +275,10 @@ export class Profile {
codeMap_ = new CodeMap();
topDownTree_ = new CallTree();
bottomUpTree_ = new CallTree();
c_entries_ = {};
c_entries_ = {__proto__:null};
scripts_ = [];
urlToScript_ = new Map();
warnings = new Set();
serializeVMSymbols() {
let result = this.codeMap_.getAllStaticEntriesWithAddresses();
......@@ -300,9 +305,9 @@ export class Profile {
* @enum {number}
*/
static Operation = {
MOVE: 0,
DELETE: 1,
TICK: 2
MOVE: kProfileOperationMove,
DELETE: kProfileOperationDelete,
TICK: kProfileOperationTick
}
/**
......@@ -454,7 +459,7 @@ export class Profile {
// As code and functions are in the same address space,
// it is safe to put them in a single code map.
let func = this.codeMap_.findDynamicEntryByStartAddress(funcAddr);
if (!func) {
if (func === null) {
func = new FunctionEntry(name);
this.codeMap_.addCode(funcAddr, func);
} else if (func.name !== name) {
......@@ -462,7 +467,7 @@ export class Profile {
func.name = name;
}
let entry = this.codeMap_.findDynamicEntryByStartAddress(start);
if (entry) {
if (entry !== null) {
if (entry.size === size && entry.func === func) {
// Entry state has changed.
entry.state = state;
......@@ -471,7 +476,7 @@ export class Profile {
entry = null;
}
}
if (!entry) {
if (entry === null) {
entry = new DynamicFuncCodeEntry(size, type, func, state);
this.codeMap_.addCode(start, entry);
}
......@@ -488,7 +493,7 @@ export class Profile {
try {
this.codeMap_.moveCode(from, to);
} catch (e) {
this.handleUnknownCode(Profile.Operation.MOVE, from);
this.handleUnknownCode(kProfileOperationMove, from);
}
}
......@@ -505,7 +510,7 @@ export class Profile {
try {
this.codeMap_.deleteCode(start);
} catch (e) {
this.handleUnknownCode(Profile.Operation.DELETE, start);
this.handleUnknownCode(kProfileOperationDelete, start);
}
}
......@@ -516,16 +521,16 @@ export class Profile {
inliningPositions, inlinedFunctions) {
const script = this.getOrCreateScript(scriptId);
const entry = this.codeMap_.findDynamicEntryByStartAddress(start);
if (!entry) return;
if (entry === null) return;
// Resolve the inlined functions list.
if (inlinedFunctions.length > 0) {
inlinedFunctions = inlinedFunctions.substring(1).split("S");
for (let i = 0; i < inlinedFunctions.length; i++) {
const funcAddr = parseInt(inlinedFunctions[i]);
const func = this.codeMap_.findDynamicEntryByStartAddress(funcAddr);
if (!func || func.funcId === undefined) {
if (func === null || func.funcId === undefined) {
// TODO: fix
console.warn(`Could not find function ${inlinedFunctions[i]}`);
this.warnings.add(`Could not find function ${inlinedFunctions[i]}`);
inlinedFunctions[i] = null;
} else {
inlinedFunctions[i] = func.funcId;
......@@ -542,7 +547,9 @@ export class Profile {
addDisassemble(start, kind, disassemble) {
const entry = this.codeMap_.findDynamicEntryByStartAddress(start);
if (entry) this.getOrCreateSourceInfo(entry).setDisassemble(disassemble);
if (entry !== null) {
this.getOrCreateSourceInfo(entry).setDisassemble(disassemble);
}
return entry;
}
......@@ -558,7 +565,7 @@ export class Profile {
getOrCreateScript(id) {
let script = this.scripts_[id];
if (!script) {
if (script === undefined) {
script = new Script(id);
this.scripts_[id] = script;
}
......@@ -618,7 +625,7 @@ export class Profile {
for (let i = 0; i < stack.length; ++i) {
const pc = stack[i];
const entry = this.codeMap_.findEntry(pc);
if (entry) {
if (entry !== null) {
entryStack.push(entry);
const name = entry.getName();
if (i === 0 && (entry.type === 'CPP' || entry.type === 'SHARED_LIB')) {
......@@ -631,12 +638,13 @@ export class Profile {
nameStack.push(name);
}
} else {
this.handleUnknownCode(Profile.Operation.TICK, pc, i);
this.handleUnknownCode(kProfileOperationTick, pc, i);
if (i === 0) nameStack.push("UNKNOWN");
entryStack.push(pc);
}
if (look_for_first_c_function && i > 0 &&
(!entry || entry.type !== 'CPP') && last_seen_c_function !== '') {
(entry === null || entry.type !== 'CPP')
&& last_seen_c_function !== '') {
if (this.c_entries_[last_seen_c_function] === undefined) {
this.c_entries_[last_seen_c_function] = 0;
}
......@@ -711,7 +719,7 @@ export class Profile {
getFlatProfile(opt_label) {
const counters = new CallTree();
const rootLabel = opt_label || CallTree.ROOT_NODE_LABEL;
const precs = {};
const precs = {__proto__:null};
precs[rootLabel] = 0;
const root = counters.findOrAddChild(rootLabel);
......@@ -963,9 +971,7 @@ class CallTree {
* @param {Array<string>} path Call path.
*/
addPath(path) {
if (path.length == 0) {
return;
}
if (path.length == 0) return;
let curr = this.root_;
for (let i = 0; i < path.length; ++i) {
curr = curr.findOrAddChild(path[i]);
......@@ -1079,21 +1085,14 @@ class CallTree {
* @param {CallTreeNode} opt_parent Node parent.
*/
class CallTreeNode {
/**
* Node self weight (how many times this node was the last node in
* a call path).
* @type {number}
*/
selfWeight = 0;
/**
* Node total weight (includes weights of all children).
* @type {number}
*/
totalWeight = 0;
children = {};
constructor(label, opt_parent) {
// Node self weight (how many times this node was the last node in
// a call path).
this.selfWeight = 0;
// Node total weight (includes weights of all children).
this.totalWeight = 0;
this. children = { __proto__:null };
this.label = label;
this.parent = opt_parent;
}
......@@ -1136,7 +1135,8 @@ class CallTreeNode {
* @param {string} label Child node label.
*/
findChild(label) {
return this.children[label] || null;
const found = this.children[label];
return found === undefined ? null : found;
}
/**
......@@ -1146,7 +1146,9 @@ class CallTreeNode {
* @param {string} label Child node label.
*/
findOrAddChild(label) {
return this.findChild(label) || this.addChild(label);
const found = this.findChild(label)
if (found === null) return this.addChild(label);
return found;
}
/**
......@@ -1166,7 +1168,7 @@ class CallTreeNode {
* @param {function(CallTreeNode)} f Visitor function.
*/
walkUpToRoot(f) {
for (let curr = this; curr != null; curr = curr.parent) {
for (let curr = this; curr !== null; curr = curr.parent) {
f(curr);
}
}
......
......@@ -49,7 +49,7 @@ export class SplayTree {
* @return {boolean} Whether the tree is empty.
*/
isEmpty() {
return !this.root_;
return this.root_ === null;
}
/**
......@@ -100,7 +100,7 @@ export class SplayTree {
throw Error(`Key not found: ${key}`);
}
const removed = this.root_;
if (!this.root_.left) {
if (this.root_.left === null) {
this.root_ = this.root_.right;
} else {
const { right } = this.root_;
......@@ -133,7 +133,7 @@ export class SplayTree {
findMin() {
if (this.isEmpty()) return null;
let current = this.root_;
while (current.left) {
while (current.left !== null) {
current = current.left;
}
return current;
......@@ -145,7 +145,7 @@ export class SplayTree {
findMax(opt_startNode) {
if (this.isEmpty()) return null;
let current = opt_startNode || this.root_;
while (current.right) {
while (current.right !== null) {
current = current.right;
}
return current;
......@@ -164,7 +164,7 @@ export class SplayTree {
// the left subtree.
if (this.root_.key <= key) {
return this.root_;
} else if (this.root_.left) {
} else if (this.root_.left !== null) {
return this.findMax(this.root_.left);
} else {
return null;
......@@ -186,7 +186,7 @@ export class SplayTree {
*/
exportValues() {
const result = [];
this.traverse_(function(node) { result.push(node.value); });
this.traverse_(function(node) { result.push(node.value) });
return result;
}
......@@ -212,36 +212,28 @@ export class SplayTree {
let current = this.root_;
while (true) {
if (key < current.key) {
if (!current.left) {
break;
}
if (current.left === null) break;
if (key < current.left.key) {
// Rotate right.
const tmp = current.left;
current.left = tmp.right;
tmp.right = current;
current = tmp;
if (!current.left) {
break;
}
if (current.left === null) break;
}
// Link right.
right.left = current;
right = current;
current = current.left;
} else if (key > current.key) {
if (!current.right) {
break;
}
if (current.right === null) break;
if (key > current.right.key) {
// Rotate left.
const tmp = current.right;
current.right = tmp.left;
tmp.left = current;
current = tmp;
if (!current.right) {
break;
}
if (current.right === null) break;
}
// Link left.
left.right = current;
......@@ -269,9 +261,7 @@ export class SplayTree {
const nodesToVisit = [this.root_];
while (nodesToVisit.length > 0) {
const node = nodesToVisit.shift();
if (node == null) {
continue;
}
if (node === null) continue;
f(node);
nodesToVisit.push(node.left);
nodesToVisit.push(node.right);
......
......@@ -64,4 +64,13 @@ export function groupBy(array, keyFunction, collect = false) {
return groups.sort((a, b) => b.length - a.length);
}
export function arrayEquals(left, right) {
if (left == right) return true;
if (left.length != right.length) return false;
for (let i = 0; i < left.length; i++) {
if (left[i] != right[i]) return false;
}
return true;
}
export * from '../js/helper.mjs'
......@@ -95,7 +95,7 @@ class App {
document.addEventListener(
SelectionEvent.name, e => this.handleSelectEntries(e))
document.addEventListener(
FocusEvent.name, e => this.handleFocusLogEntryl(e));
FocusEvent.name, e => this.handleFocusLogEntry(e));
document.addEventListener(
SelectTimeEvent.name, e => this.handleTimeRangeSelect(e));
document.addEventListener(ToolTipEvent.name, e => this.handleToolTip(e));
......@@ -151,7 +151,7 @@ class App {
handleSelectEntries(e) {
e.stopImmediatePropagation();
this.showEntries(e.entries);
this.selectEntries(e.entries);
}
selectEntries(entries) {
......@@ -160,29 +160,30 @@ class App {
this.selectEntriesOfSingleType(group.entries);
missingTypes.delete(group.key);
});
missingTypes.forEach(type => this.selectEntriesOfSingleType([], type));
missingTypes.forEach(
type => this.selectEntriesOfSingleType([], type, false));
}
selectEntriesOfSingleType(entries, type) {
selectEntriesOfSingleType(entries, type, focusView = true) {
const entryType = entries[0]?.constructor ?? type;
switch (entryType) {
case Script:
entries = entries.flatMap(script => script.sourcePositions);
return this.showSourcePositions(entries);
return this.showSourcePositions(entries, focusView);
case SourcePosition:
return this.showSourcePositions(entries);
return this.showSourcePositions(entries, focusView);
case MapLogEntry:
return this.showMapEntries(entries);
return this.showMapEntries(entries, focusView);
case IcLogEntry:
return this.showIcEntries(entries);
return this.showIcEntries(entries, focusView);
case ApiLogEntry:
return this.showApiEntries(entries);
return this.showApiEntries(entries, focusView);
case CodeLogEntry:
return this.showCodeEntries(entries);
return this.showCodeEntries(entries, focusView);
case DeoptLogEntry:
return this.showDeoptEntries(entries);
return this.showDeoptEntries(entries, focusView);
case SharedLibLogEntry:
return this.showSharedLibEntries(entries);
return this.showSharedLibEntries(entries, focusView);
case TimerLogEntry:
case TickLogEntry:
break;
......@@ -245,7 +246,7 @@ class App {
this._view.timelinePanel.timeSelection = {start, end};
}
handleFocusLogEntryl(e) {
handleFocusLogEntry(e) {
e.stopImmediatePropagation();
this.focusLogEntry(e.entry);
}
......@@ -281,11 +282,11 @@ class App {
this._state.map = entry;
this._view.mapTrack.focusedEntry = entry;
this._view.mapPanel.map = entry;
this._view.mapPanel.show();
if (focusSourcePosition) {
this.focusCodeLogEntry(entry.code, false);
this.focusSourcePosition(entry.sourcePosition);
}
this._view.mapPanel.show();
}
focusIcLogEntry(entry) {
......
......@@ -66,6 +66,10 @@ export class CodeLogEntry extends LogEntry {
return this._kindName === 'Builtin';
}
get isBytecodeKind() {
return this._kindName === 'Unopt';
}
get kindName() {
return this._kindName;
}
......
......@@ -10,6 +10,28 @@ export class TickLogEntry extends LogEntry {
super(TickLogEntry.extractType(vmState, processedStack), time);
this.state = vmState;
this.stack = processedStack;
this._endTime = time;
}
end(time) {
if (this.isInitialized) throw new Error('Invalid timer change');
this._endTime = time;
}
get isInitialized() {
return this._endTime !== this._time;
}
get startTime() {
return this._time;
}
get endTime() {
return this._endTime;
}
get duration() {
return this._endTime - this._time;
}
static extractType(vmState, processedStack) {
......
......@@ -59,6 +59,7 @@ export class Processor extends LogReader {
_formatPCRegexp = /(.*):[0-9]+:[0-9]+$/;
_lastTimestamp = 0;
_lastCodeLogEntry;
_lastTickLogEntry;
_chunkRemainder = '';
MAJOR_VERSION = 7;
MINOR_VERSION = 6;
......@@ -248,6 +249,9 @@ export class Processor extends LogReader {
async finalize() {
await this._chunkConsumer.consumeAll();
if (this._profile.warnings.size > 0) {
console.warn('Found profiler warnings:', this._profile.warnings);
}
// TODO(cbruni): print stats;
this._mapTimeline.transitions = new Map();
let id = 0;
......@@ -387,7 +391,12 @@ export class Processor extends LogReader {
const entryStack = this._profile.recordTick(
time_ns, vmState,
this.processStack(pc, tos_or_external_callback, stack));
this._tickTimeline.push(new TickLogEntry(time_ns, vmState, entryStack))
const newEntry = new TickLogEntry(time_ns, vmState, entryStack);
this._tickTimeline.push(newEntry);
if (this._lastTickLogEntry !== undefined) {
this._lastTickLogEntry.end(time_ns);
}
this._lastTickLogEntry = newEntry;
}
processCodeSourceInfo(
......
......@@ -9,17 +9,20 @@ found in the LICENSE file. -->
#sourceCode {
white-space: pre-line;
}
.register {
.reg, .addr {
border-bottom: 1px dashed;
border-radius: 2px;
}
.register:hover {
.reg:hover, .addr:hover {
background-color: var(--border-color);
}
.register.selected {
.reg.selected, .addr.selected {
color: var(--default-color);
background-color: var(--border-color);
}
.addr:hover {
cursor: pointer;
}
</style>
<div class="panel">
......@@ -37,7 +40,5 @@ found in the LICENSE file. -->
<property-link-table id="feedbackVector"></property-link-table>
<h3>Disassembly</h3>
<pre id="disassembly"></pre>
<h3>Source Code</h3>
<pre id="sourceCode"></pre>
</div>
</div>
// 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 {LinuxCppEntriesProvider} from '../../tickprocessor.mjs';
import {SelectRelatedEvent} from './events.mjs';
import {CollapsableElement, DOM, formatBytes, formatMicroSeconds} from './helper.mjs';
const kRegisters = ['rsp', 'rbp', 'rax', 'rbx', 'rcx', 'rdx', 'rsi', 'rdi'];
// Add Interpreter and x64 registers
for (let i = 0; i < 14; i++) {
kRegisters.push(`r${i}`);
}
// Make sure we dont match register on bytecode: Star1 or Star2
const kAvoidBytecodeOps = '(.*?[^a-zA-Z])'
// Look for registers in strings like: movl rbx,[rcx-0x30]
const kRegisterRegexp = `(${kRegisters.join('|')}|r[0-9]+)`
const kRegisterRegexpSplit =
new RegExp(`${kAvoidBytecodeOps}${kRegisterRegexp}`)
const kIsRegisterRegexp = new RegExp(`^${kRegisterRegexp}$`);
const kFullAddressRegexp = /(0x[0-9a-f]{8,})/;
const kRelativeAddressRegexp = /([+-]0x[0-9a-f]+)/;
const kAnyAddressRegexp = /([+-]?0x[0-9a-f]+)/;
DOM.defineCustomElement('view/code-panel',
(templateText) =>
......@@ -23,8 +31,7 @@ DOM.defineCustomElement('view/code-panel',
this._codeSelectNode = this.$('#codeSelect');
this._disassemblyNode = this.$('#disassembly');
this._feedbackVectorNode = this.$('#feedbackVector');
this._sourceNode = this.$('#sourceCode');
this._registerSelector = new RegisterSelector(this._disassemblyNode);
this._selectionHandler = new SelectionHandler(this._disassemblyNode);
this._codeSelectNode.onchange = this._handleSelectCode.bind(this);
this.$('#selectedRelatedButton').onclick =
......@@ -56,7 +63,8 @@ DOM.defineCustomElement('view/code-panel',
script: entry.script,
type: entry.type,
kind: entry.kindName,
variants: entry.variants.length > 1 ? entry.variants : undefined,
variants: entry.variants.length > 1 ? [undefined, ...entry.variants] :
undefined,
};
}
this.requestUpdate();
......@@ -66,7 +74,6 @@ DOM.defineCustomElement('view/code-panel',
this._updateSelect();
this._updateDisassembly();
this._updateFeedbackVector();
this._sourceNode.innerText = this._entry?.source ?? '';
}
_updateFeedbackVector() {
......@@ -81,24 +88,14 @@ DOM.defineCustomElement('view/code-panel',
}
_updateDisassembly() {
if (!this._entry?.code) {
this._disassemblyNode.innerText = '';
return;
}
const rawCode = this._entry?.code;
if (!this._entry?.code) return;
try {
this._disassemblyNode.innerText = rawCode;
let formattedCode = this._disassemblyNode.innerHTML;
for (let register of kRegisters) {
const button = `<span class="register ${register}">${register}</span>`
formattedCode = formattedCode.replaceAll(register, button);
}
// Let's replace the base-address since it doesn't add any value.
// TODO
this._disassemblyNode.innerHTML = formattedCode;
this._disassemblyNode.appendChild(
new AssemblyFormatter(this._entry).fragment);
} catch (e) {
console.error(e);
this._disassemblyNode.innerText = rawCode;
this._disassemblyNode.innerText = this._entry.code;
}
}
......@@ -135,34 +132,133 @@ DOM.defineCustomElement('view/code-panel',
}
});
class RegisterSelector {
_currentRegister;
class AssemblyFormatter {
constructor(codeLogEntry) {
this._fragment = new DocumentFragment();
this._entry = codeLogEntry;
codeLogEntry.code.split('\n').forEach(line => this._addLine(line));
}
get fragment() {
return this._fragment;
}
_addLine(line) {
const parts = line.split(' ');
let lineAddress = 0;
if (kFullAddressRegexp.test(parts[0])) {
lineAddress = parseInt(parts[0]);
}
const content = DOM.span({textContent: parts.join(' ') + '\n'});
let formattedCode = content.innerHTML.split(kRegisterRegexpSplit)
.map(part => this._formatRegisterPart(part))
.join('');
formattedCode = formattedCode.split(kAnyAddressRegexp)
.map(
(part, index) => this._formatAddressPart(
part, index, lineAddress))
.join('');
// Let's replace the base-address since it doesn't add any value.
// TODO
content.innerHTML = formattedCode;
this._fragment.appendChild(content);
}
_formatRegisterPart(part) {
if (!kIsRegisterRegexp.test(part)) return part;
return `<span class="reg ${part}">${part}</span>`
}
_formatAddressPart(part, index, lineAddress) {
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)) {
const targetAddress = (lineAddress + parseInt(part)).toString(16);
return `<span class=addr data-addr="0x${targetAddress}">${part}</span>`;
} else {
return part;
}
}
}
class SelectionHandler {
_currentRegisterHovered;
_currentRegisterClicked;
constructor(node) {
this._node = node;
this._node.onmousemove = this._handleDisassemblyMouseMove.bind(this);
this._node.onmousemove = this._handleMouseMove.bind(this);
this._node.onclick = this._handleClick.bind(this);
}
_handleDisassemblyMouseMove(event) {
$(query) {
return this._node.querySelectorAll(query);
}
_handleClick(event) {
const target = event.target;
if (!target.classList.contains('register')) {
this._clear();
return;
};
this._select(target.innerText);
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);
}
}
_clear() {
if (this._currentRegister == undefined) return;
for (let node of this._node.querySelectorAll('.register')) {
_clearRegisterSelection() {
if (!this._currentRegisterHovered) return;
for (let node of this.$('.reg.selected')) {
node.classList.remove('selected');
}
this._currentRegisterClicked = undefined;
this._currentRegisterHovered = undefined;
}
_select(register) {
if (register == this._currentRegister) return;
this._clear();
this._currentRegister = register;
for (let node of this._node.querySelectorAll(`.register.${register}`)) {
_setRegisterSelection(register) {
if (register == this._currentRegisterHovered) return;
this._clearRegisterSelection();
this._currentRegisterHovered = register;
for (let node of this.$(`.reg.${register}`)) {
node.classList.add('selected');
}
}
......
......@@ -171,7 +171,6 @@ export class CollapsableElement extends V8CustomElement {
this._closer.checked = true;
this._requestUpdateIfVisible();
}
this.scrollIntoView();
}
show() {
......@@ -179,7 +178,7 @@ export class CollapsableElement extends V8CustomElement {
this._closer.checked = false;
this._requestUpdateIfVisible();
}
this.scrollIntoView();
this.scrollIntoView({behavior: 'smooth', block: 'center'});
}
requestUpdate(useAnimation = false) {
......
......@@ -3,17 +3,19 @@
// found in the LICENSE file.
import {App} from '../index.mjs'
import {FocusEvent} from './events.mjs';
import {FocusEvent, SelectRelatedEvent} from './events.mjs';
import {DOM, ExpandableText, V8CustomElement} from './helper.mjs';
DOM.defineCustomElement(
'view/property-link-table',
template => class PropertyLinkTable extends V8CustomElement {
_instance;
DOM.defineCustomElement('view/property-link-table',
template =>
class PropertyLinkTable extends V8CustomElement {
_object;
_propertyDict;
_instanceLinkButtons = false;
_logEntryClickHandler = this._handleLogEntryClick.bind(this);
_logEntryRelatedHandler = this._handleLogEntryRelated.bind(this);
_showHandler = this._handleShow.bind(this);
_showSourcePositionHandler = this._handleShowSourcePosition.bind(this);
_showRelatedHandler = this._handleShowRelated.bind(this);
_arrayValueSelectHandler = this._handleArrayValueSelect.bind(this);
constructor() {
......@@ -57,7 +59,7 @@ DOM.defineCustomElement(
return;
}
if (key == '__this__') {
this._instance = value;
this._object = value;
return;
}
const row = this._table.insertRow();
......@@ -70,7 +72,7 @@ DOM.defineCustomElement(
}
if (App.isClickable(value)) {
cell.className = 'clickable';
cell.onclick = this._logEntryClickHandler;
cell.onclick = this._showHandler;
cell.data = value;
}
new ExpandableText(cell, value.toString());
......@@ -86,7 +88,7 @@ DOM.defineCustomElement(
select.onchange = this._arrayValueSelectHandler;
for (let value of array) {
const option = DOM.element('option');
option.innerText = value.toString();
option.innerText = value === undefined ? '' : value.toString();
option.data = value;
select.add(option);
}
......@@ -100,27 +102,36 @@ DOM.defineCustomElement(
}
_addFooter() {
if (this._instance === undefined) return;
if (this._object === undefined) return;
if (!this._instanceLinkButtons) return;
const td = this._table.createTFoot().insertRow().insertCell();
td.colSpan = 2;
let showButton =
td.appendChild(DOM.button('Show', this._logEntryClickHandler));
showButton.data = this._instance;
let showRelatedButton = td.appendChild(
DOM.button('Show Related', this._logEntryRelatedClickHandler));
showRelatedButton.data = this._instance;
let showButton = td.appendChild(DOM.button('Show', this._showHandler));
showButton.data = this._object;
if (this._object.sourcePosition) {
let showSourcePositionButton = td.appendChild(
DOM.button('Source Position', this._showSourcePositionHandler));
showSourcePositionButton.data = this._object;
}
let showRelatedButton =
td.appendChild(DOM.button('Show Related', this._showRelatedHandler));
showRelatedButton.data = this._object;
}
_handleArrayValueSelect(event) {
const logEntry = event.currentTarget.selectedOptions[0].data;
this.dispatchEvent(new FocusEvent(logEntry));
}
_handleLogEntryClick(event) {
_handleShow(event) {
this.dispatchEvent(new FocusEvent(event.currentTarget.data));
}
_handleLogEntryRelated(event) {
_handleShowSourcePosition(event) {
this.dispatchEvent(new FocusEvent(event.currentTarget.data.sourcePosition));
}
_handleShowRelated(event) {
this.dispatchEvent(new SelectRelatedEvent(event.currentTarget.data));
}
});
});
// 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 {defer, groupBy} from '../helper.mjs';
import {arrayEquals, defer, groupBy} from '../helper.mjs';
import {App} from '../index.mjs'
import {SelectRelatedEvent, ToolTipEvent} from './events.mjs';
import {CollapsableElement, CSSColor, delay, DOM, formatBytes, gradientStopsFromGroups} from './helper.mjs';
import {SelectionEvent, SelectRelatedEvent, ToolTipEvent} from './events.mjs';
import {CollapsableElement, CSSColor, delay, DOM, formatBytes, gradientStopsFromGroups, LazyTable} from './helper.mjs';
// A source mapping proxy for source maps that don't have CORS headers.
// TODO(leszeks): Make this configurable.
......@@ -19,6 +19,8 @@ DOM.defineCustomElement('view/script-panel',
_scripts = [];
_script;
showToolTipEntriesHandler = this.handleShowToolTipEntries.bind(this);
constructor() {
super(templateText);
this.scriptDropdown.addEventListener(
......@@ -40,6 +42,8 @@ DOM.defineCustomElement('view/script-panel',
this._script = script;
script.ensureSourceMapCalculated(sourceMapFetchPrefix);
this._sourcePositionsToMarkNodesPromise = defer();
this._selectedSourcePositions =
this._selectedSourcePositions.filter(each => each.script === script);
this.requestUpdate();
}
......@@ -48,10 +52,14 @@ DOM.defineCustomElement('view/script-panel',
}
set selectedSourcePositions(sourcePositions) {
if (arrayEquals(this._selectedSourcePositions, sourcePositions)) {
this._focusSelectedMarkers(0);
} else {
this._selectedSourcePositions = sourcePositions;
// TODO: highlight multiple scripts
this.script = sourcePositions[0]?.script;
this._focusSelectedMarkers();
this._focusSelectedMarkers(100);
}
}
set scripts(scripts) {
......@@ -106,8 +114,8 @@ DOM.defineCustomElement('view/script-panel',
this.script.replaceChild(scriptNode, oldScriptNode);
}
async _focusSelectedMarkers() {
await delay(100);
async _focusSelectedMarkers(delay_ms) {
if (delay_ms) await delay(delay_ms);
const sourcePositionsToMarkNodes =
await this._sourcePositionsToMarkNodesPromise;
// Remove all marked nodes.
......@@ -127,7 +135,7 @@ DOM.defineCustomElement('view/script-panel',
if (!sourcePosition) return;
const markNode = sourcePositionsToMarkNodes.get(sourcePosition);
markNode.scrollIntoView(
{behavior: 'auto', block: 'center', inline: 'center'});
{behavior: 'smooth', block: 'center', inline: 'center'});
}
_handleSelectScript(e) {
......@@ -141,25 +149,23 @@ DOM.defineCustomElement('view/script-panel',
this.dispatchEvent(new SelectRelatedEvent(this._script));
}
setSelectedSourcePositionInternal(sourcePosition) {
this._selectedSourcePositions = [sourcePosition];
console.assert(sourcePosition.script === this._script);
}
handleSourcePositionClick(e) {
const sourcePosition = e.target.sourcePosition;
this.setSelectedSourcePositionInternal(sourcePosition);
this.dispatchEvent(new SelectRelatedEvent(sourcePosition));
}
handleSourcePositionMouseOver(e) {
const sourcePosition = e.target.sourcePosition;
const entries = sourcePosition.entries;
let text = groupBy(entries, each => each.constructor, true)
.map(group => {
let text = `${group.key.name}: ${group.length}\n`
text += groupBy(group.entries, each => each.type, true)
.map(group => {
return ` - ${group.key}: ${group.length}`;
})
.join('\n');
return text;
})
.join('\n');
const toolTipContent = DOM.div();
toolTipContent.appendChild(
new ToolTipTableBuilder(this, entries).tableNode);
let sourceMapContent;
switch (this._script.sourceMapState) {
......@@ -192,17 +198,50 @@ DOM.defineCustomElement('view/script-panel',
default:
break;
}
const toolTipContent = DOM.div({
children: [
DOM.element('pre', {className: 'textContent', textContent: text}),
sourceMapContent
]
});
toolTipContent.appendChild(sourceMapContent);
this.dispatchEvent(new ToolTipEvent(toolTipContent, e.target));
}
handleShowToolTipEntries(event) {
let entries = event.currentTarget.data;
const sourcePosition = entries[0].sourcePosition;
// Add a source position entry so the current position stays focused.
this.setSelectedSourcePositionInternal(sourcePosition);
entries = entries.concat(this._selectedSourcePositions);
this.dispatchEvent(new SelectionEvent(entries));
}
});
class ToolTipTableBuilder {
constructor(scriptPanel, entries) {
this._scriptPanel = scriptPanel;
this.tableNode = DOM.table();
const tr = DOM.tr();
tr.appendChild(DOM.td('Type'));
tr.appendChild(DOM.td('Subtype'));
tr.appendChild(DOM.td('Count'));
this.tableNode.appendChild(document.createElement('thead')).appendChild(tr);
groupBy(entries, each => each.constructor, true).forEach(group => {
this.addRow(group.key.name, 'all', entries, false)
groupBy(group.entries, each => each.type, true).forEach(group => {
this.addRow('', group.key, group.entries, false)
})
})
}
addRow(name, subtypeName, entries) {
const tr = DOM.tr();
tr.appendChild(DOM.td(name));
tr.appendChild(DOM.td(subtypeName));
tr.appendChild(DOM.td(entries.length));
const button =
DOM.button('Show', this._scriptPanel.showToolTipEntriesHandler);
button.data = entries;
tr.appendChild(DOM.td(button));
this.tableNode.appendChild(tr);
}
}
class SourcePositionIterator {
_entries;
_index = 0;
......
......@@ -27,7 +27,6 @@ export class TimelineTrackBase extends V8CustomElement {
super(templateText);
this._selectionHandler = new SelectionHandler(this);
this._legend = new Legend(this.$('#legendTable'));
this._legend.onFilter = (type) => this._handleFilterTimeline();
this.timelineChunks = this.$('#timelineChunks');
this.timelineSamples = this.$('#timelineSamples');
......@@ -37,14 +36,17 @@ export class TimelineTrackBase extends V8CustomElement {
this.timelineAnnotationsNode = this.$('#timelineAnnotations');
this.timelineMarkersNode = this.$('#timelineMarkers');
this._scalableContentNode = this.$('#scalableContent');
this.isLocked = false;
}
_initEventListeners() {
this._legend.onFilter = (type) => this._handleFilterTimeline();
this.timelineNode.addEventListener(
'scroll', e => this._handleTimelineScroll(e));
this.hitPanelNode.onclick = this._handleClick.bind(this);
this.hitPanelNode.ondblclick = this._handleDoubleClick.bind(this);
this.hitPanelNode.onmousemove = this._handleMouseMove.bind(this);
window.addEventListener('resize', () => this._resetCachedDimensions());
this.isLocked = false;
}
static get observedAttributes() {
......@@ -62,6 +64,8 @@ export class TimelineTrackBase extends V8CustomElement {
}
set data(timeline) {
console.assert(timeline);
if (!this._timeline) this._initEventListeners();
this._timeline = timeline;
this._legend.timeline = timeline;
this.$('.content').style.display = timeline.isEmpty() ? 'none' : 'relative';
......@@ -136,6 +140,11 @@ export class TimelineTrackBase extends V8CustomElement {
}
get chunks() {
if (this._chunks?.length != this.nofChunks) {
this._chunks =
this._timeline.chunks(this.nofChunks, this._legend.filterPredicate);
console.assert(this._chunks.length == this._nofChunks);
}
return this._chunks;
}
......@@ -209,19 +218,13 @@ export class TimelineTrackBase extends V8CustomElement {
_update() {
this._legend.update();
this._drawContent();
this._drawAnnotations(this.selectedEntry);
this._drawContent().then(() => this._drawAnnotations(this.selectedEntry));
this._resetCachedDimensions();
}
async _drawContent() {
await delay(5);
if (this._timeline.isEmpty()) return;
if (this.chunks?.length != this.nofChunks) {
this._chunks =
this._timeline.chunks(this.nofChunks, this._legend.filterPredicate);
console.assert(this._chunks.length == this._nofChunks);
}
await delay(5);
const chunks = this.chunks;
const max = chunks.max(each => each.size());
let buffer = '';
......@@ -558,12 +561,13 @@ class Legend {
tbody.appendChild(this._addTypeRow(group));
missingTypes.delete(group.key);
});
missingTypes.forEach(key => tbody.appendChild(this._row('', key, 0, '0%')));
missingTypes.forEach(
key => tbody.appendChild(this._addRow('', key, 0, '0%')));
if (this._timeline.selection) {
tbody.appendChild(
this._row('', 'Selection', this.selection.length, '100%'));
this._addRow('', 'Selection', this.selection.length, '100%'));
}
tbody.appendChild(this._row('', 'All', this._timeline.length, ''));
tbody.appendChild(this._addRow('', 'All', this._timeline.length, ''));
this._table.tBodies[0].replaceWith(tbody);
}
......@@ -572,11 +576,10 @@ class Legend {
const example = this.selection.at(0);
if (!example || !('duration' in example)) return;
this._enableDuration = true;
this._table.tHead.appendChild(DOM.td('Duration'));
this._table.tHead.appendChild(DOM.td(''));
this._table.tHead.rows[0].appendChild(DOM.td('Duration'));
}
_row(colorNode, type, count, countPercent, duration, durationPercent) {
_addRow(colorNode, type, count, countPercent, duration, durationPercent) {
const row = DOM.tr();
row.appendChild(DOM.td(colorNode));
const typeCell = row.appendChild(DOM.td(type));
......@@ -608,7 +611,7 @@ class Legend {
}
let countPercent =
`${(group.length / this.selection.length * 100).toFixed(1)}%`;
const row = this._row(
const row = this._addRow(
colorDiv, group.key, group.length, countPercent, duration, '');
row.className = 'clickable';
row.onclick = this._typeClickHandler;
......
......@@ -6,7 +6,6 @@ import {delay} from '../../helper.mjs';
import {TickLogEntry} from '../../log/tick.mjs';
import {Timeline} from '../../timeline.mjs';
import {DOM, SVG} from '../helper.mjs';
import {TimelineTrackStackedBase} from './timeline-track-stacked-base.mjs'
class Flame {
......@@ -179,15 +178,15 @@ class Annotations {
if (end > rawFlames.length) end = rawFlames.length;
const logEntry = this._logEntry;
// Also compare against the function, if any.
const func = logEntry.entry?.func;
const func = logEntry.entry?.func ?? -1;
for (let i = start; i < end; i++) {
const flame = rawFlames[i];
if (!flame.entry) continue;
if (flame.entry.logEntry !== logEntry &&
(!func || flame.entry.func !== func)) {
continue;
const flameLogEntry = flame.logEntry;
if (!flameLogEntry) continue;
if (flameLogEntry !== logEntry) {
if (flameLogEntry.entry?.func !== func) continue;
}
this._buffer += this._track.drawFlame(flame, i, true);
this._buffer += this._track._drawItem(flame, i, true);
}
}
......
......@@ -514,6 +514,7 @@ export class TickProcessor extends LogReader {
timedRange,
pairwiseTimedRange);
this.dispatchTable_ = {
__proto__: null,
'shared-library': {
parsers: [parseString, parseInt, parseInt, parseInt],
processor: this.processSharedLibrary
......@@ -575,16 +576,16 @@ export class TickProcessor extends LogReader {
processor: this.advanceDistortion
},
// Ignored events.
'profiler': null,
'function-creation': null,
'function-move': null,
'function-delete': null,
'heap-sample-item': null,
'current-time': null, // Handled specially, not parsed.
'profiler': undefined,
'function-creation': undefined,
'function-move': undefined,
'function-delete': undefined,
'heap-sample-item': undefined,
'current-time': undefined, // Handled specially, not parsed.
// Obsolete row types.
'code-allocate': null,
'begin-code-region': null,
'end-code-region': null
'code-allocate': undefined,
'begin-code-region': undefined,
'end-code-region': undefined
};
this.preprocessJson = preprocessJson;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment