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