From 656675313cb1f028ecb255c67a3302d37e0707e7 Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Wed, 2 Mar 2022 10:56:03 +0100 Subject: [PATCH] [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 Commit-Queue: Camillo Bruni Cr-Commit-Position: refs/heads/main@{#79329} --- tools/codemap.mjs | 51 ++-- tools/csvparser.mjs | 7 +- tools/dumpcpp.mjs | 1 + tools/js/web-api-helper.mjs | 49 ++-- tools/logreader.mjs | 20 +- tools/profile.mjs | 76 +++--- tools/splaytree.mjs | 34 +-- tools/system-analyzer/helper.mjs | 11 +- tools/system-analyzer/index.mjs | 29 +-- tools/system-analyzer/log/code.mjs | 4 + tools/system-analyzer/log/tick.mjs | 24 +- tools/system-analyzer/processor.mjs | 11 +- .../view/code-panel-template.html | 11 +- tools/system-analyzer/view/code-panel.mjs | 182 ++++++++++---- tools/system-analyzer/view/helper.mjs | 5 +- .../view/property-link-table.mjs | 227 +++++++++--------- tools/system-analyzer/view/script-panel.mjs | 95 +++++--- .../view/timeline/timeline-track-base.mjs | 37 +-- .../view/timeline/timeline-track-tick.mjs | 15 +- tools/tickprocessor.mjs | 19 +- 20 files changed, 541 insertions(+), 367 deletions(-) diff --git a/tools/codemap.mjs b/tools/codemap.mjs index 55327b6982..8d1e00c9e9 100644 --- a/tools/codemap.mjs +++ b/tools/codemap.mjs @@ -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; } /** diff --git a/tools/csvparser.mjs b/tools/csvparser.mjs index c43ee4c4fc..273bf89776 100644 --- a/tools/csvparser.mjs +++ b/tools/csvparser.mjs @@ -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); } diff --git a/tools/dumpcpp.mjs b/tools/dumpcpp.mjs index 9459deda15..e92ee9ab5a 100644 --- a/tools/dumpcpp.mjs +++ b/tools/dumpcpp.mjs @@ -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 } diff --git a/tools/js/web-api-helper.mjs b/tools/js/web-api-helper.mjs index 17c1c0b1be..15a23e1070 100644 --- a/tools/js/web-api-helper.mjs +++ b/tools/js/web-api-helper.mjs @@ -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))); } -} \ No newline at end of file +} diff --git a/tools/logreader.mjs b/tools/logreader.mjs index ecd7b573a2..26a6106a01 100644 --- a/tools/logreader.mjs +++ b/tools/logreader.mjs @@ -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') { diff --git a/tools/profile.mjs b/tools/profile.mjs index 3f11bff139..c62ebcf177 100644 --- a/tools/profile.mjs +++ b/tools/profile.mjs @@ -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} 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); } } diff --git a/tools/splaytree.mjs b/tools/splaytree.mjs index d942d1f463..ac25cf0668 100644 --- a/tools/splaytree.mjs +++ b/tools/splaytree.mjs @@ -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; } -}; \ No newline at end of file +}; diff --git a/tools/system-analyzer/helper.mjs b/tools/system-analyzer/helper.mjs index c444aea944..a50e06d3be 100644 --- a/tools/system-analyzer/helper.mjs +++ b/tools/system-analyzer/helper.mjs @@ -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' \ No newline at end of file +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' diff --git a/tools/system-analyzer/index.mjs b/tools/system-analyzer/index.mjs index 2cae0d3b6d..41463d9484 100644 --- a/tools/system-analyzer/index.mjs +++ b/tools/system-analyzer/index.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) { diff --git a/tools/system-analyzer/log/code.mjs b/tools/system-analyzer/log/code.mjs index feee95361e..4e8ca40f5e 100644 --- a/tools/system-analyzer/log/code.mjs +++ b/tools/system-analyzer/log/code.mjs @@ -66,6 +66,10 @@ export class CodeLogEntry extends LogEntry { return this._kindName === 'Builtin'; } + get isBytecodeKind() { + return this._kindName === 'Unopt'; + } + get kindName() { return this._kindName; } diff --git a/tools/system-analyzer/log/tick.mjs b/tools/system-analyzer/log/tick.mjs index 64dbeb3780..e8093df93f 100644 --- a/tools/system-analyzer/log/tick.mjs +++ b/tools/system-analyzer/log/tick.mjs @@ -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'; } -} \ No newline at end of file +} diff --git a/tools/system-analyzer/processor.mjs b/tools/system-analyzer/processor.mjs index 4f192ba96f..38f3a46b9e 100644 --- a/tools/system-analyzer/processor.mjs +++ b/tools/system-analyzer/processor.mjs @@ -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( diff --git a/tools/system-analyzer/view/code-panel-template.html b/tools/system-analyzer/view/code-panel-template.html index 105e6b980c..d237ac3a51 100644 --- a/tools/system-analyzer/view/code-panel-template.html +++ b/tools/system-analyzer/view/code-panel-template.html @@ -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; + }
@@ -37,7 +40,5 @@ found in the LICENSE file. -->

Disassembly


-    

Source Code

-

   
diff --git a/tools/system-analyzer/view/code-panel.mjs b/tools/system-analyzer/view/code-panel.mjs index 084c8fb2d3..42fe7b3d4c 100644 --- a/tools/system-analyzer/view/code-panel.mjs +++ b/tools/system-analyzer/view/code-panel.mjs @@ -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 = `${register}` - 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 `${part}` + } + + _formatAddressPart(part, index, lineAddress) { + if (kFullAddressRegexp.test(part)) { + // The first or second address must be the line address + if (index <= 1) { + return `${part}`; + } + return `${part}`; + } else if (kRelativeAddressRegexp.test(part)) { + const targetAddress = (lineAddress + parseInt(part)).toString(16); + return `${part}`; + } 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'); } } diff --git a/tools/system-analyzer/view/helper.mjs b/tools/system-analyzer/view/helper.mjs index f9a1272573..93823e1106 100644 --- a/tools/system-analyzer/view/helper.mjs +++ b/tools/system-analyzer/view/helper.mjs @@ -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' \ No newline at end of file +export * from '../../js/web-api-helper.mjs' diff --git a/tools/system-analyzer/view/property-link-table.mjs b/tools/system-analyzer/view/property-link-table.mjs index 17cecc58ed..2c81bc6536 100644 --- a/tools/system-analyzer/view/property-link-table.mjs +++ b/tools/system-analyzer/view/property-link-table.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)); + } +}); diff --git a/tools/system-analyzer/view/script-panel.mjs b/tools/system-analyzer/view/script-panel.mjs index 75720534ca..f6b24733be 100644 --- a/tools/system-analyzer/view/script-panel.mjs +++ b/tools/system-analyzer/view/script-panel.mjs @@ -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; diff --git a/tools/system-analyzer/view/timeline/timeline-track-base.mjs b/tools/system-analyzer/view/timeline/timeline-track-base.mjs index 1ef8347088..678817399d 100644 --- a/tools/system-analyzer/view/timeline/timeline-track-base.mjs +++ b/tools/system-analyzer/view/timeline/timeline-track-base.mjs @@ -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; diff --git a/tools/system-analyzer/view/timeline/timeline-track-tick.mjs b/tools/system-analyzer/view/timeline/timeline-track-tick.mjs index 502504beb4..0f376ea355 100644 --- a/tools/system-analyzer/view/timeline/timeline-track-tick.mjs +++ b/tools/system-analyzer/view/timeline/timeline-track-tick.mjs @@ -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 = ''; } -} \ No newline at end of file +} diff --git a/tools/tickprocessor.mjs b/tools/tickprocessor.mjs index 1929c3069d..071bf35ca4 100644 --- a/tools/tickprocessor.mjs +++ b/tools/tickprocessor.mjs @@ -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;