[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/+/3492521 Reviewed-by: Patrick Thier <pthier@chromium.org> Commit-Queue: Camillo Bruni <cbruni@chromium.org> Cr-Commit-Position: refs/heads/main@{#79329}
This commit is contained in:
parent
123c38a5aa
commit
656675313c
@ -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,29 +148,28 @@ export class FileReader extends V8CustomElement {
|
||||
export class DOM {
|
||||
static element(type, options) {
|
||||
const node = document.createElement(type);
|
||||
if (options !== undefined) {
|
||||
if (typeof options === 'string') {
|
||||
// Old behaviour: options = class string
|
||||
node.className = options;
|
||||
} else if (Array.isArray(options)) {
|
||||
// Old behaviour: options = class array
|
||||
DOM.addClasses(node, options);
|
||||
} else {
|
||||
// New behaviour: options = attribute dict
|
||||
for (const [key, value] of Object.entries(options)) {
|
||||
if (key == 'className') {
|
||||
node.className = value;
|
||||
} else if (key == 'classList') {
|
||||
node.classList = value;
|
||||
} else if (key == 'textContent') {
|
||||
node.textContent = value;
|
||||
} else if (key == 'children') {
|
||||
for (const child of value) {
|
||||
node.appendChild(child);
|
||||
}
|
||||
} else {
|
||||
node.setAttribute(key, value);
|
||||
if (options === undefined) return node;
|
||||
if (typeof options === 'string') {
|
||||
// Old behaviour: options = class string
|
||||
node.className = options;
|
||||
} else if (Array.isArray(options)) {
|
||||
// Old behaviour: options = class array
|
||||
DOM.addClasses(node, options);
|
||||
} else {
|
||||
// New behaviour: options = attribute dict
|
||||
for (const [key, value] of Object.entries(options)) {
|
||||
if (key == 'className') {
|
||||
node.className = value;
|
||||
} else if (key == 'classList') {
|
||||
DOM.addClasses(node, value);
|
||||
} else if (key == 'textContent') {
|
||||
node.textContent = value;
|
||||
} else if (key == 'children') {
|
||||
for (const child of value) {
|
||||
node.appendChild(child);
|
||||
}
|
||||
} else {
|
||||
node.setAttribute(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -255,4 +258,4 @@ export class DOM {
|
||||
templateText =>
|
||||
customElements.define(name, generator(templateText)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
@ -298,4 +288,4 @@ class SplayTreeNode {
|
||||
*/
|
||||
this.right = null;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -64,4 +64,13 @@ export function groupBy(array, keyFunction, collect = false) {
|
||||
return groups.sort((a, b) => b.length - a.length);
|
||||
}
|
||||
|
||||
export * from '../js/helper.mjs'
|
||||
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) {
|
||||
@ -34,4 +56,4 @@ export class TickLogEntry extends LogEntry {
|
||||
if (entry?.vmState) return Profile.vmStateString(entry.vmState);
|
||||
return 'Other';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -1,14 +1,22 @@
|
||||
// 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;
|
||||
this._disassemblyNode.innerText = '';
|
||||
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);
|
||||
}
|
||||
|
||||
_clear() {
|
||||
if (this._currentRegister == undefined) return;
|
||||
for (let node of this._node.querySelectorAll('.register')) {
|
||||
node.classList.remove('selected');
|
||||
if (target.classList.contains('addr')) {
|
||||
return this._handleClickAddress(target);
|
||||
} else if (target.classList.contains('reg')) {
|
||||
this._handleClickRegister(target);
|
||||
} else {
|
||||
this._clearRegisterSelection();
|
||||
}
|
||||
}
|
||||
|
||||
_select(register) {
|
||||
if (register == this._currentRegister) return;
|
||||
this._clear();
|
||||
this._currentRegister = register;
|
||||
for (let node of this._node.querySelectorAll(`.register.${register}`)) {
|
||||
_handleClickAddress(target) {
|
||||
let targetAddress = target.getAttribute('data-addr') ?? target.innerText;
|
||||
// Clear any selection
|
||||
for (let addrNode of this.$('.addr.selected')) {
|
||||
addrNode.classList.remove('selected');
|
||||
}
|
||||
// Highlight all matching addresses
|
||||
let lineAddrNode;
|
||||
for (let addrNode of this.$(`.addr[data-addr="${targetAddress}"]`)) {
|
||||
addrNode.classList.add('selected');
|
||||
if (addrNode.classList.contains('line') && lineAddrNode == undefined) {
|
||||
lineAddrNode = addrNode;
|
||||
}
|
||||
}
|
||||
// Jump to potential target address.
|
||||
if (lineAddrNode) {
|
||||
lineAddrNode.scrollIntoView({behavior: 'smooth', block: 'nearest'});
|
||||
}
|
||||
}
|
||||
|
||||
_handleClickRegister(target) {
|
||||
this._setRegisterSelection(target.innerText);
|
||||
this._currentRegisterClicked = this._currentRegisterHovered;
|
||||
}
|
||||
|
||||
_handleMouseMove(event) {
|
||||
if (this._currentRegisterClicked) return;
|
||||
const target = event.target;
|
||||
if (!target.classList.contains('reg')) {
|
||||
this._clearRegisterSelection();
|
||||
} else {
|
||||
this._setRegisterSelection(target.innerText);
|
||||
}
|
||||
}
|
||||
|
||||
_clearRegisterSelection() {
|
||||
if (!this._currentRegisterHovered) return;
|
||||
for (let node of this.$('.reg.selected')) {
|
||||
node.classList.remove('selected');
|
||||
}
|
||||
this._currentRegisterClicked = undefined;
|
||||
this._currentRegisterHovered = undefined;
|
||||
}
|
||||
|
||||
_setRegisterSelection(register) {
|
||||
if (register == this._currentRegisterHovered) return;
|
||||
this._clearRegisterSelection();
|
||||
this._currentRegisterHovered = register;
|
||||
for (let node of this.$(`.reg.${register}`)) {
|
||||
node.classList.add('selected');
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
@ -320,4 +319,4 @@ export function gradientStopsFromGroups(
|
||||
}
|
||||
|
||||
export * from '../helper.mjs';
|
||||
export * from '../../js/web-api-helper.mjs'
|
||||
export * from '../../js/web-api-helper.mjs'
|
||||
|
@ -3,124 +3,135 @@
|
||||
// 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;
|
||||
_propertyDict;
|
||||
_instanceLinkButtons = false;
|
||||
_logEntryClickHandler = this._handleLogEntryClick.bind(this);
|
||||
_logEntryRelatedHandler = this._handleLogEntryRelated.bind(this);
|
||||
_arrayValueSelectHandler = this._handleArrayValueSelect.bind(this);
|
||||
DOM.defineCustomElement('view/property-link-table',
|
||||
template =>
|
||||
class PropertyLinkTable extends V8CustomElement {
|
||||
_object;
|
||||
_propertyDict;
|
||||
_instanceLinkButtons = false;
|
||||
|
||||
constructor() {
|
||||
super(template);
|
||||
}
|
||||
_showHandler = this._handleShow.bind(this);
|
||||
_showSourcePositionHandler = this._handleShowSourcePosition.bind(this);
|
||||
_showRelatedHandler = this._handleShowRelated.bind(this);
|
||||
_arrayValueSelectHandler = this._handleArrayValueSelect.bind(this);
|
||||
|
||||
set instanceLinkButtons(newValue) {
|
||||
this._instanceLinkButtons = newValue;
|
||||
}
|
||||
constructor() {
|
||||
super(template);
|
||||
}
|
||||
|
||||
set propertyDict(propertyDict) {
|
||||
if (this._propertyDict === propertyDict) return;
|
||||
if (typeof propertyDict !== 'object') {
|
||||
throw new Error(
|
||||
`Invalid property dict, expected object: ${propertyDict}`);
|
||||
}
|
||||
this._propertyDict = propertyDict;
|
||||
this.requestUpdate();
|
||||
}
|
||||
set instanceLinkButtons(newValue) {
|
||||
this._instanceLinkButtons = newValue;
|
||||
}
|
||||
|
||||
_update() {
|
||||
this._fragment = new DocumentFragment();
|
||||
this._table = DOM.table('properties');
|
||||
for (let key in this._propertyDict) {
|
||||
const value = this._propertyDict[key];
|
||||
this._addKeyValue(key, value);
|
||||
}
|
||||
this._addFooter();
|
||||
this._fragment.appendChild(this._table);
|
||||
set propertyDict(propertyDict) {
|
||||
if (this._propertyDict === propertyDict) return;
|
||||
if (typeof propertyDict !== 'object') {
|
||||
throw new Error(
|
||||
`Invalid property dict, expected object: ${propertyDict}`);
|
||||
}
|
||||
this._propertyDict = propertyDict;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
const newContent = DOM.div();
|
||||
newContent.appendChild(this._fragment);
|
||||
this.$('#content').replaceWith(newContent);
|
||||
newContent.id = 'content';
|
||||
this._fragment = undefined;
|
||||
}
|
||||
_update() {
|
||||
this._fragment = new DocumentFragment();
|
||||
this._table = DOM.table('properties');
|
||||
for (let key in this._propertyDict) {
|
||||
const value = this._propertyDict[key];
|
||||
this._addKeyValue(key, value);
|
||||
}
|
||||
this._addFooter();
|
||||
this._fragment.appendChild(this._table);
|
||||
|
||||
_addKeyValue(key, value) {
|
||||
if (key == 'title') {
|
||||
this._addTitle(value);
|
||||
return;
|
||||
}
|
||||
if (key == '__this__') {
|
||||
this._instance = value;
|
||||
return;
|
||||
}
|
||||
const row = this._table.insertRow();
|
||||
row.insertCell().innerText = key;
|
||||
const cell = row.insertCell();
|
||||
if (value == undefined) return;
|
||||
if (Array.isArray(value)) {
|
||||
cell.appendChild(this._addArrayValue(value));
|
||||
return;
|
||||
}
|
||||
if (App.isClickable(value)) {
|
||||
cell.className = 'clickable';
|
||||
cell.onclick = this._logEntryClickHandler;
|
||||
cell.data = value;
|
||||
}
|
||||
new ExpandableText(cell, value.toString());
|
||||
}
|
||||
const newContent = DOM.div();
|
||||
newContent.appendChild(this._fragment);
|
||||
this.$('#content').replaceWith(newContent);
|
||||
newContent.id = 'content';
|
||||
this._fragment = undefined;
|
||||
}
|
||||
|
||||
_addArrayValue(array) {
|
||||
if (array.length == 0) {
|
||||
return DOM.text('empty');
|
||||
} else if (array.length > 200) {
|
||||
return DOM.text(`${array.length} items`);
|
||||
}
|
||||
const select = DOM.element('select');
|
||||
select.onchange = this._arrayValueSelectHandler;
|
||||
for (let value of array) {
|
||||
const option = DOM.element('option');
|
||||
option.innerText = value.toString();
|
||||
option.data = value;
|
||||
select.add(option);
|
||||
}
|
||||
return select;
|
||||
}
|
||||
_addKeyValue(key, value) {
|
||||
if (key == 'title') {
|
||||
this._addTitle(value);
|
||||
return;
|
||||
}
|
||||
if (key == '__this__') {
|
||||
this._object = value;
|
||||
return;
|
||||
}
|
||||
const row = this._table.insertRow();
|
||||
row.insertCell().innerText = key;
|
||||
const cell = row.insertCell();
|
||||
if (value == undefined) return;
|
||||
if (Array.isArray(value)) {
|
||||
cell.appendChild(this._addArrayValue(value));
|
||||
return;
|
||||
}
|
||||
if (App.isClickable(value)) {
|
||||
cell.className = 'clickable';
|
||||
cell.onclick = this._showHandler;
|
||||
cell.data = value;
|
||||
}
|
||||
new ExpandableText(cell, value.toString());
|
||||
}
|
||||
|
||||
_addTitle(value) {
|
||||
const title = DOM.element('h3');
|
||||
title.innerText = value;
|
||||
this._fragment.appendChild(title);
|
||||
}
|
||||
_addArrayValue(array) {
|
||||
if (array.length == 0) {
|
||||
return DOM.text('empty');
|
||||
} else if (array.length > 200) {
|
||||
return DOM.text(`${array.length} items`);
|
||||
}
|
||||
const select = DOM.element('select');
|
||||
select.onchange = this._arrayValueSelectHandler;
|
||||
for (let value of array) {
|
||||
const option = DOM.element('option');
|
||||
option.innerText = value === undefined ? '' : value.toString();
|
||||
option.data = value;
|
||||
select.add(option);
|
||||
}
|
||||
return select;
|
||||
}
|
||||
|
||||
_addFooter() {
|
||||
if (this._instance === 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;
|
||||
}
|
||||
_addTitle(value) {
|
||||
const title = DOM.element('h3');
|
||||
title.innerText = value;
|
||||
this._fragment.appendChild(title);
|
||||
}
|
||||
|
||||
_handleArrayValueSelect(event) {
|
||||
const logEntry = event.currentTarget.selectedOptions[0].data;
|
||||
this.dispatchEvent(new FocusEvent(logEntry));
|
||||
}
|
||||
_handleLogEntryClick(event) {
|
||||
this.dispatchEvent(new FocusEvent(event.currentTarget.data));
|
||||
}
|
||||
_addFooter() {
|
||||
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._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;
|
||||
}
|
||||
|
||||
_handleLogEntryRelated(event) {
|
||||
this.dispatchEvent(new SelectRelatedEvent(event.currentTarget.data));
|
||||
}
|
||||
});
|
||||
_handleArrayValueSelect(event) {
|
||||
const logEntry = event.currentTarget.selectedOptions[0].data;
|
||||
this.dispatchEvent(new FocusEvent(logEntry));
|
||||
}
|
||||
|
||||
_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));
|
||||
}
|
||||
});
|
||||
|
@ -1,11 +1,11 @@
|
||||
// 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) {
|
||||
this._selectedSourcePositions = sourcePositions;
|
||||
// TODO: highlight multiple scripts
|
||||
this.script = sourcePositions[0]?.script;
|
||||
this._focusSelectedMarkers();
|
||||
if (arrayEquals(this._selectedSourcePositions, sourcePositions)) {
|
||||
this._focusSelectedMarkers(0);
|
||||
} else {
|
||||
this._selectedSourcePositions = sourcePositions;
|
||||
// TODO: highlight multiple scripts
|
||||
this.script = sourcePositions[0]?.script;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -198,4 +197,4 @@ class Annotations {
|
||||
this._node.appendChild(svg);
|
||||
this._buffer = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user