[tools][system-analyzer] Add profiler-panel
Add basic profiler support - Moved profiling-related helpers to profiling.mjs - Added bottom-up profiler table - Added mini-timeline overview wit opt/deopt events and usage graph - Added flame-graph, pivoted on the currently selected function Drive-by-fixes: - Added/updated jsdoc type information - Fixed static symbols (builtins, bytecodehandlers) that were both added by the CppEntriesProvider and from code-events in the v8.log - Support platform-specific (linux/macos) dynamic symbol loader by adding a query path ('/v8/info/platform') to lws-middleware.js - added css var --selection-color Bug: v8:10644 Change-Id: I6412bec63eac13140d6d425e7d9cc33316824c73 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3585453 Reviewed-by: Patrick Thier <pthier@chromium.org> Commit-Queue: Camillo Bruni <cbruni@chromium.org> Cr-Commit-Position: refs/heads/main@{#80192}
This commit is contained in:
parent
d3341d1102
commit
7a90c32032
@ -38,8 +38,6 @@ const kPageSize = 1 << kPageAlignment;
|
||||
|
||||
/**
|
||||
* Constructs a mapper that maps addresses into code entries.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
export class CodeMap {
|
||||
/**
|
||||
@ -68,11 +66,33 @@ export class CodeMap {
|
||||
pages_ = new Set();
|
||||
|
||||
|
||||
/**
|
||||
* Adds a code entry that might overlap with static code (e.g. for builtins).
|
||||
*
|
||||
* @param {number} start The starting address.
|
||||
* @param {CodeEntry} codeEntry Code entry object.
|
||||
*/
|
||||
addAnyCode(start, codeEntry) {
|
||||
const pageAddr = (start / kPageSize) | 0;
|
||||
if (!this.pages_.has(pageAddr)) return this.addCode(start, codeEntry);
|
||||
// We might have loaded static code (builtins, bytecode handlers)
|
||||
// and we get more information later in v8.log with code-creation events.
|
||||
// Overwrite the existing entries in this case.
|
||||
let result = this.findInTree_(this.statics_, start);
|
||||
if (result === null) return this.addCode(start, codeEntry);
|
||||
|
||||
const removedNode = this.statics_.remove(start);
|
||||
this.deleteAllCoveredNodes_(
|
||||
this.statics_, start, start + removedNode.value.size);
|
||||
this.statics_.insert(start, codeEntry);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds a dynamic (i.e. moveable and discardable) code entry.
|
||||
*
|
||||
* @param {number} start The starting address.
|
||||
* @param {CodeMap.CodeEntry} codeEntry Code entry object.
|
||||
* @param {CodeEntry} codeEntry Code entry object.
|
||||
*/
|
||||
addCode(start, codeEntry) {
|
||||
this.deleteAllCoveredNodes_(this.dynamics_, start, start + codeEntry.size);
|
||||
@ -106,7 +126,7 @@ export class CodeMap {
|
||||
* Adds a library entry.
|
||||
*
|
||||
* @param {number} start The starting address.
|
||||
* @param {CodeMap.CodeEntry} codeEntry Code entry object.
|
||||
* @param {CodeEntry} codeEntry Code entry object.
|
||||
*/
|
||||
addLibrary(start, codeEntry) {
|
||||
this.markPages_(start, start + codeEntry.size);
|
||||
@ -117,7 +137,7 @@ export class CodeMap {
|
||||
* Adds a static code entry.
|
||||
*
|
||||
* @param {number} start The starting address.
|
||||
* @param {CodeMap.CodeEntry} codeEntry Code entry object.
|
||||
* @param {CodeEntry} codeEntry Code entry object.
|
||||
*/
|
||||
addStaticCode(start, codeEntry) {
|
||||
this.statics_.insert(start, codeEntry);
|
||||
@ -264,21 +284,16 @@ export class CodeMap {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a code entry object.
|
||||
*
|
||||
* @param {number} size Code entry size in bytes.
|
||||
* @param {string} opt_name Code entry name.
|
||||
* @param {string} opt_type Code entry type, e.g. SHARED_LIB, CPP.
|
||||
* @param {object} source Optional source position information
|
||||
* @constructor
|
||||
*/
|
||||
export class CodeEntry {
|
||||
constructor(size, opt_name, opt_type) {
|
||||
/** @type {number} */
|
||||
this.size = size;
|
||||
/** @type {string} */
|
||||
this.name = opt_name || '';
|
||||
/** @type {string} */
|
||||
this.type = opt_type || '';
|
||||
this.nameUpdated_ = false;
|
||||
/** @type {?string} */
|
||||
this.source = undefined;
|
||||
}
|
||||
|
||||
@ -293,6 +308,10 @@ export class CodeEntry {
|
||||
getSourceCode() {
|
||||
return '';
|
||||
}
|
||||
|
||||
get sourcePosition() {
|
||||
return this.logEntry.sourcePosition;
|
||||
}
|
||||
}
|
||||
|
||||
class NameGenerator {
|
||||
|
@ -168,8 +168,8 @@ export class LogReader {
|
||||
*
|
||||
* @param {number} pc Program counter.
|
||||
* @param {number} func JS Function.
|
||||
* @param {Array.<string>} stack String representation of a stack.
|
||||
* @return {Array.<number>} Processed stack.
|
||||
* @param {string[]} stack String representation of a stack.
|
||||
* @return {number[]} Processed stack.
|
||||
*/
|
||||
processStack(pc, func, stack) {
|
||||
const fullStack = func ? [pc, func] : [pc];
|
||||
@ -195,7 +195,7 @@ export class LogReader {
|
||||
/**
|
||||
* Does a dispatch of a log record.
|
||||
*
|
||||
* @param {Array.<string>} fields Log record.
|
||||
* @param {string[]} fields Log record.
|
||||
* @private
|
||||
*/
|
||||
async dispatchLogRow_(fields) {
|
||||
@ -223,7 +223,7 @@ export class LogReader {
|
||||
/**
|
||||
* Processes log lines.
|
||||
*
|
||||
* @param {Array.<string>} lines Log lines.
|
||||
* @param {string[]} lines Log lines.
|
||||
* @private
|
||||
*/
|
||||
async processLog_(lines) {
|
||||
|
@ -477,6 +477,21 @@ export class Profile {
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers dynamic (JIT-compiled) code entry or entries that overlap with
|
||||
* static entries (like builtins).
|
||||
*
|
||||
* @param {string} type Code entry type.
|
||||
* @param {string} name Code entry name.
|
||||
* @param {number} start Starting address.
|
||||
* @param {number} size Code entry size.
|
||||
*/
|
||||
addAnyCode(type, name, timestamp, start, size) {
|
||||
const entry = new DynamicCodeEntry(size, type, name);
|
||||
this.codeMap_.addAnyCode(start, entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers dynamic (JIT-compiled) code entry.
|
||||
*
|
||||
@ -633,7 +648,7 @@ export class Profile {
|
||||
* Records a tick event. Stack must contain a sequence of
|
||||
* addresses starting with the program counter value.
|
||||
*
|
||||
* @param {Array<number>} stack Stack sample.
|
||||
* @param {number[]} stack Stack sample.
|
||||
*/
|
||||
recordTick(time_ns, vmState, stack) {
|
||||
const {nameStack, entryStack} = this.resolveAndFilterFuncs_(stack);
|
||||
@ -647,7 +662,7 @@ export class Profile {
|
||||
* Translates addresses into function names and filters unneeded
|
||||
* functions.
|
||||
*
|
||||
* @param {Array<number>} stack Stack sample.
|
||||
* @param {number[]} stack Stack sample.
|
||||
*/
|
||||
resolveAndFilterFuncs_(stack) {
|
||||
const nameStack = [];
|
||||
@ -937,6 +952,7 @@ class DynamicFuncCodeEntry extends CodeEntry {
|
||||
class FunctionEntry extends CodeEntry {
|
||||
|
||||
// Contains the list of generated code for this function.
|
||||
/** @type {Set<DynamicCodeEntry>} */
|
||||
_codeEntries = new Set();
|
||||
|
||||
constructor(name) {
|
||||
@ -1000,7 +1016,7 @@ class CallTree {
|
||||
/**
|
||||
* Adds the specified call path, constructing nodes as necessary.
|
||||
*
|
||||
* @param {Array<string>} path Call path.
|
||||
* @param {string[]} path Call path.
|
||||
*/
|
||||
addPath(path) {
|
||||
if (path.length == 0) return;
|
||||
@ -1208,7 +1224,7 @@ class CallTreeNode {
|
||||
/**
|
||||
* Tries to find a node with the specified path.
|
||||
*
|
||||
* @param {Array<string>} labels The path.
|
||||
* @param {string[]} labels The path.
|
||||
* @param {function(CallTreeNode)} opt_f Visitor function.
|
||||
*/
|
||||
descendToChild(labels, opt_f) {
|
||||
|
@ -127,7 +127,7 @@ WebInspector.SourceMap.load = function(sourceMapURL, compiledURL, callback)
|
||||
|
||||
WebInspector.SourceMap.prototype = {
|
||||
/**
|
||||
* @return {Array.<string>}
|
||||
* @return {string[]}
|
||||
*/
|
||||
sources()
|
||||
{
|
||||
|
@ -49,11 +49,17 @@ export function groupBy(array, keyFunction, collect = false) {
|
||||
return groups.sort((a, b) => b.length - a.length);
|
||||
}
|
||||
|
||||
export function arrayEquals(left, right) {
|
||||
export function arrayEquals(left, right, compareFn) {
|
||||
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;
|
||||
if (compareFn === undefined) {
|
||||
for (let i = 0; i < left.length; i++) {
|
||||
if (left[i] != right[i]) return false;
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < left.length; i++) {
|
||||
if (!compareFn(left[i], right[i])) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -22,6 +22,7 @@
|
||||
--blue: #6e77dc;
|
||||
--orange: #dc9b6e;
|
||||
--violet: #d26edc;
|
||||
--selection-color: rgba(133, 68, 163, 0.5);
|
||||
--border-color-rgb: 128, 128, 128;
|
||||
--border-color: rgba(var(--border-color-rgb), 0.2);
|
||||
scrollbar-color: rgba(128, 128, 128, 0.5) rgba(0, 0, 0, 0.0);
|
||||
|
@ -91,6 +91,7 @@ found in the LICENSE file. -->
|
||||
<list-panel id="map-list" title="Map Events"></list-panel>
|
||||
<list-panel id="deopt-list" title="Deopt Events"></list-panel>
|
||||
<list-panel id="code-list" title="Code Events"></list-panel>
|
||||
<profiler-panel id="profiler-panel"></profiler-panel>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
@ -42,6 +42,7 @@ class App {
|
||||
|
||||
mapPanel: $('#map-panel'),
|
||||
codePanel: $('#code-panel'),
|
||||
profilerPanel: $('#profiler-panel'),
|
||||
scriptPanel: $('#script-panel'),
|
||||
|
||||
toolTip: $('#tool-tip'),
|
||||
@ -73,11 +74,13 @@ class App {
|
||||
await Promise.all([
|
||||
import('./view/list-panel.mjs'),
|
||||
import('./view/timeline-panel.mjs'),
|
||||
import('./view/timeline/timeline-overview.mjs'),
|
||||
import('./view/map-panel.mjs'),
|
||||
import('./view/script-panel.mjs'),
|
||||
import('./view/code-panel.mjs'),
|
||||
import('./view/property-link-table.mjs'),
|
||||
import('./view/tool-tip.mjs'),
|
||||
import('./view/profiler-panel.mjs'),
|
||||
]);
|
||||
this._addEventListeners();
|
||||
}
|
||||
@ -208,7 +211,11 @@ class App {
|
||||
if (focusView) this._view.codePanel.show();
|
||||
}
|
||||
|
||||
showTickEntries(entries, focusView = true) {}
|
||||
showTickEntries(entries, focusView = true) {
|
||||
this._view.profilerPanel.selectedLogEntries = entries;
|
||||
if (focusView) this._view.profilerPanel.show();
|
||||
}
|
||||
|
||||
showTimerEntries(entries, focusView = true) {}
|
||||
|
||||
showSourcePositions(entries, focusView = true) {
|
||||
@ -372,6 +379,7 @@ class App {
|
||||
this._view.scriptPanel.scripts = processor.scripts;
|
||||
this._view.codePanel.timeline = codeTimeline;
|
||||
this._view.codePanel.timeline = codeTimeline;
|
||||
this._view.profilerPanel.timeline = tickTimeline;
|
||||
this.refreshTimelineTrackView();
|
||||
} catch (e) {
|
||||
this._view.logFileReader.error = 'Log file contains errors!'
|
||||
|
@ -26,12 +26,19 @@ export class DeoptLogEntry extends LogEntry {
|
||||
type, time, entry, deoptReason, deoptLocation, scriptOffset,
|
||||
instructionStart, codeSize, inliningId) {
|
||||
super(type, time);
|
||||
/** @type {CodeEntry} */
|
||||
this._entry = entry;
|
||||
/** @type {string} */
|
||||
this._reason = deoptReason;
|
||||
/** @type {SourcePosition} */
|
||||
this._location = deoptLocation;
|
||||
/** @type {number} */
|
||||
this._scriptOffset = scriptOffset;
|
||||
/** @type {number} */
|
||||
this._instructionStart = instructionStart;
|
||||
/** @type {number} */
|
||||
this._codeSize = codeSize;
|
||||
/** @type {string} */
|
||||
this._inliningId = inliningId;
|
||||
this.fileSourcePosition = undefined;
|
||||
}
|
||||
@ -67,8 +74,10 @@ export class DeoptLogEntry extends LogEntry {
|
||||
class CodeLikeLogEntry extends LogEntry {
|
||||
constructor(type, time, profilerEntry) {
|
||||
super(type, time);
|
||||
/** @type {CodeEntry} */
|
||||
this._entry = profilerEntry;
|
||||
profilerEntry.logEntry = this;
|
||||
/** @type {LogEntry[]} */
|
||||
this._relatedEntries = [];
|
||||
}
|
||||
|
||||
@ -86,11 +95,19 @@ class CodeLikeLogEntry extends LogEntry {
|
||||
}
|
||||
|
||||
export class CodeLogEntry extends CodeLikeLogEntry {
|
||||
constructor(type, time, kindName, kind, profilerEntry) {
|
||||
constructor(type, time, kindName, kind, name, profilerEntry) {
|
||||
super(type, time, profilerEntry);
|
||||
this._kind = kind;
|
||||
/** @type {string} */
|
||||
this._kindName = kindName;
|
||||
/** @type {?FeedbackVectorEntry} */
|
||||
this._feedbackVector = undefined;
|
||||
/** @type {string} */
|
||||
this._name = name;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
get kind() {
|
||||
@ -149,7 +166,7 @@ export class CodeLogEntry extends CodeLikeLogEntry {
|
||||
const dict = super.toolTipDict;
|
||||
dict.size = formatBytes(dict.size);
|
||||
dict.source = new CodeString(dict.source);
|
||||
dict.code = new CodeString(dict.code);
|
||||
if (dict.code) dict.code = new CodeString(dict.code);
|
||||
return dict;
|
||||
}
|
||||
|
||||
@ -215,9 +232,9 @@ export class FeedbackVectorEntry extends LogEntry {
|
||||
}
|
||||
}
|
||||
|
||||
export class SharedLibLogEntry extends CodeLikeLogEntry {
|
||||
constructor(profilerEntry) {
|
||||
super('SHARED_LIB', 0, profilerEntry);
|
||||
export class BaseCPPLogEntry extends CodeLikeLogEntry {
|
||||
constructor(prefix, profilerEntry) {
|
||||
super(prefix, 0, profilerEntry);
|
||||
}
|
||||
|
||||
get name() {
|
||||
@ -232,3 +249,15 @@ export class SharedLibLogEntry extends CodeLikeLogEntry {
|
||||
return ['name'];
|
||||
}
|
||||
}
|
||||
|
||||
export class CPPCodeLogEntry extends BaseCPPLogEntry {
|
||||
constructor(profilerEntry) {
|
||||
super('CPP', profilerEntry);
|
||||
}
|
||||
}
|
||||
|
||||
export class SharedLibLogEntry extends BaseCPPLogEntry {
|
||||
constructor(profilerEntry) {
|
||||
super('SHARED_LIB', profilerEntry);
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,10 @@
|
||||
|
||||
export class LogEntry {
|
||||
constructor(type, time) {
|
||||
/** @type {number} */
|
||||
this._time = time;
|
||||
this._type = type;
|
||||
/** @type {?SourcePosition} */
|
||||
this.sourcePosition = undefined;
|
||||
}
|
||||
|
||||
@ -44,12 +46,14 @@ export class LogEntry {
|
||||
}
|
||||
|
||||
// Returns an Array of all possible #type values.
|
||||
/** @return {string[]} */
|
||||
static get allTypes() {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
|
||||
// Returns an array of public property names.
|
||||
/** @return {string[]} */
|
||||
static get propertyNames() {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,8 +8,11 @@ import {LogEntry} from './log.mjs';
|
||||
export class TickLogEntry extends LogEntry {
|
||||
constructor(time, vmState, processedStack) {
|
||||
super(TickLogEntry.extractType(vmState, processedStack), time);
|
||||
/** @type {string} */
|
||||
this.state = vmState;
|
||||
/** @type {CodeEntry[]} */
|
||||
this.stack = processedStack;
|
||||
/** @type {number} */
|
||||
this._endTime = time;
|
||||
}
|
||||
|
||||
|
@ -23,8 +23,11 @@ class Symbolizer {
|
||||
return async (ctx, next) => {
|
||||
if (ctx.path == '/v8/loadVMSymbols') {
|
||||
await this.parseVMSymbols(ctx)
|
||||
} else if (ctx.path == '/v8/info/platform') {
|
||||
ctx.response.type = 'text';
|
||||
ctx.response.body = process.platform;
|
||||
}
|
||||
await next()
|
||||
await next();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@ import {LogReader, parseString, parseVarArgs} from '../logreader.mjs';
|
||||
import {Profile} from '../profile.mjs';
|
||||
import {RemoteLinuxCppEntriesProvider, RemoteMacOSCppEntriesProvider} from '../tickprocessor.mjs'
|
||||
|
||||
import {CodeLogEntry, DeoptLogEntry, FeedbackVectorEntry, SharedLibLogEntry} from './log/code.mjs';
|
||||
import {CodeLogEntry, CPPCodeLogEntry, DeoptLogEntry, FeedbackVectorEntry, SharedLibLogEntry} from './log/code.mjs';
|
||||
import {IcLogEntry} from './log/ic.mjs';
|
||||
import {Edge, MapLogEntry} from './log/map.mjs';
|
||||
import {TickLogEntry} from './log/tick.mjs';
|
||||
@ -59,6 +59,8 @@ export class Processor extends LogReader {
|
||||
_lastCodeLogEntry;
|
||||
_lastTickLogEntry;
|
||||
|
||||
_cppEntriesProvider;
|
||||
|
||||
_chunkRemainder = '';
|
||||
_lineNumber = 1;
|
||||
|
||||
@ -197,8 +199,6 @@ export class Processor extends LogReader {
|
||||
processor: this.processApiEvent
|
||||
},
|
||||
});
|
||||
// TODO(cbruni): Choose correct cpp entries provider
|
||||
this._cppEntriesProvider = new RemoteLinuxCppEntriesProvider();
|
||||
}
|
||||
|
||||
printError(str) {
|
||||
@ -310,13 +310,37 @@ export class Processor extends LogReader {
|
||||
// Many events rely on having a script around, creating fake entries for
|
||||
// shared libraries.
|
||||
this._profile.addScriptSource(-1, name, '');
|
||||
|
||||
if (this._cppEntriesProvider == undefined) {
|
||||
await this._setupCppEntriesProvider();
|
||||
}
|
||||
|
||||
await this._cppEntriesProvider.parseVmSymbols(
|
||||
name, startAddr, endAddr, aslrSlide, (fName, fStart, fEnd) => {
|
||||
this._profile.addStaticCode(fName, fStart, fEnd);
|
||||
const entry = this._profile.addStaticCode(fName, fStart, fEnd);
|
||||
entry.logEntry = new CPPCodeLogEntry(entry);
|
||||
});
|
||||
}
|
||||
|
||||
processCodeCreation(type, kind, timestamp, start, size, name, maybe_func) {
|
||||
async _setupCppEntriesProvider() {
|
||||
// Probe the local symbol server for the platform:
|
||||
const url = new URL('http://localhost:8000/v8/info/platform')
|
||||
let platform = 'linux'
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
platform = await response.text();
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
if (platform === 'darwin') {
|
||||
this._cppEntriesProvider = new RemoteMacOSCppEntriesProvider();
|
||||
} else {
|
||||
this._cppEntriesProvider = new RemoteLinuxCppEntriesProvider();
|
||||
}
|
||||
}
|
||||
|
||||
processCodeCreation(
|
||||
type, kind, timestamp, start, size, nameAndPosition, maybe_func) {
|
||||
this._lastTimestamp = timestamp;
|
||||
let entry;
|
||||
let stateName = '';
|
||||
@ -325,13 +349,16 @@ export class Processor extends LogReader {
|
||||
stateName = maybe_func[1] ?? '';
|
||||
const state = Profile.parseState(maybe_func[1]);
|
||||
entry = this._profile.addFuncCode(
|
||||
type, name, timestamp, start, size, funcAddr, state);
|
||||
type, nameAndPosition, timestamp, start, size, funcAddr, state);
|
||||
} else {
|
||||
entry = this._profile.addCode(type, name, timestamp, start, size);
|
||||
entry = this._profile.addAnyCode(
|
||||
type, nameAndPosition, timestamp, start, size);
|
||||
}
|
||||
const name = nameAndPosition.slice(0, nameAndPosition.indexOf(' '));
|
||||
this._lastCodeLogEntry = new CodeLogEntry(
|
||||
type + stateName, timestamp,
|
||||
Profile.getKindFromState(Profile.parseState(stateName)), kind, entry);
|
||||
Profile.getKindFromState(Profile.parseState(stateName)), kind, name,
|
||||
entry);
|
||||
this._codeTimeline.push(this._lastCodeLogEntry);
|
||||
}
|
||||
|
||||
|
331
tools/system-analyzer/profiling.mjs
Normal file
331
tools/system-analyzer/profiling.mjs
Normal file
@ -0,0 +1,331 @@
|
||||
// Copyright 2022 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 {TickLogEntry} from './log/tick.mjs';
|
||||
|
||||
const kForward = 1;
|
||||
const kBackwards = -1;
|
||||
|
||||
/**
|
||||
* The StackSorter sorts ticks by recursively grouping the most frequent frames
|
||||
* at each stack-depth to the beginning. This is used to generate flame graphs.
|
||||
*
|
||||
* Example
|
||||
* tick1 = [a1, b1]
|
||||
* tick2 = [a0, b0]
|
||||
* tick3 = [a1, b1, c1]
|
||||
* tick4 = [a1, b0, c0, d0]
|
||||
*
|
||||
* If we sort this recursively from the beginning we get:
|
||||
* tick1 = [a1, b1]
|
||||
* tick3 = [a1, b1, c1]
|
||||
* tick4 = [a1, b0, c0, d0]
|
||||
* tick2 = [a0, b0]
|
||||
*
|
||||
* The idea is to group common stacks together to generate easily readable
|
||||
* graphs where we can quickly discover which function is the highest incoming
|
||||
* or outgoing contributor.
|
||||
*/
|
||||
export class StackSorter {
|
||||
static fromTop(array, maxDepth) {
|
||||
return new this(array, maxDepth, kForward).sorted();
|
||||
}
|
||||
|
||||
static fromBottom(array, maxDepth) {
|
||||
return new this(array, maxDepth, kBackwards).sorted();
|
||||
}
|
||||
|
||||
constructor(array, maxDepth, direction) {
|
||||
this.stacks = array;
|
||||
this.maxDepth = maxDepth;
|
||||
if (direction !== kForward && direction !== kBackwards) {
|
||||
throw new Error('Invalid direction');
|
||||
}
|
||||
this.direction = direction;
|
||||
}
|
||||
|
||||
sorted() {
|
||||
const startLevel = this.direction == kForward ? 0 : this.maxDepth - 1;
|
||||
this._sort(0, this.stacks.length, startLevel);
|
||||
return this.stacks;
|
||||
}
|
||||
|
||||
_sort(start, end, stackIndex) {
|
||||
if (stackIndex >= this.maxDepth || stackIndex < 0) return;
|
||||
const length = end - start;
|
||||
if (length <= 1) return;
|
||||
let counts;
|
||||
|
||||
{
|
||||
const kNoFrame = -1;
|
||||
let bag = new Map();
|
||||
|
||||
for (let i = start; i < end; i++) {
|
||||
let code = this.stacks[i][stackIndex] ?? kNoFrame;
|
||||
const count = bag.get(code) ?? 0;
|
||||
bag.set(code, count + 1);
|
||||
}
|
||||
|
||||
// If all the frames are the same at the current stackIndex, check the
|
||||
// next stackIndex.
|
||||
if (bag.size === 1) {
|
||||
return this._sort(start, end, stackIndex + this.direction);
|
||||
}
|
||||
|
||||
counts = Array.from(bag)
|
||||
counts.sort((a, b) => b[1] - a[1]);
|
||||
|
||||
let currentIndex = start;
|
||||
// Reuse bag since we've copied out the counts;
|
||||
let insertionIndices = bag;
|
||||
for (let i = 0; i < counts.length; i++) {
|
||||
const pair = counts[i];
|
||||
const code = pair[0];
|
||||
const count = pair[1];
|
||||
insertionIndices.set(code, currentIndex);
|
||||
currentIndex += count;
|
||||
}
|
||||
// TODO do copy-less
|
||||
let stacksSegment = this.stacks.slice(start, end);
|
||||
for (let i = 0; i < length; i++) {
|
||||
const stack = stacksSegment[i];
|
||||
const entry = stack[stackIndex] ?? kNoFrame;
|
||||
const insertionIndex = insertionIndices.get(entry);
|
||||
if (!Number.isFinite(insertionIndex)) {
|
||||
throw 'Invalid insertionIndex: ' + insertionIndex;
|
||||
}
|
||||
if (insertionIndex < start || insertionIndex >= end) {
|
||||
throw 'Invalid insertionIndex: ' + insertionIndex;
|
||||
}
|
||||
this.stacks[insertionIndex] = stack;
|
||||
insertionIndices.set(entry, insertionIndex + 1);
|
||||
}
|
||||
// Free up variables before recursing.
|
||||
insertionIndices = bag = stacksSegment = undefined;
|
||||
}
|
||||
|
||||
// Sort sub-segments
|
||||
let segmentStart = start;
|
||||
let segmentEnd = start;
|
||||
for (let i = 0; i < counts.length; i++) {
|
||||
const segmentLength = counts[i][1];
|
||||
segmentEnd = segmentStart + segmentLength - 1;
|
||||
this._sort(segmentStart, segmentEnd, stackIndex + this.direction);
|
||||
segmentStart = segmentEnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ProfileNode {
|
||||
constructor(codeEntry) {
|
||||
this.codeEntry = codeEntry;
|
||||
this.inCodeEntries = [];
|
||||
// [tick0, framePos0, tick1, framePos1, ...]
|
||||
this.ticksAndPosition = [];
|
||||
this.outCodeEntries = [];
|
||||
this._selfDuration = 0;
|
||||
this._totalDuration = 0;
|
||||
}
|
||||
|
||||
stacksOut() {
|
||||
const slicedStacks = [];
|
||||
let maxDepth = 0;
|
||||
for (let i = 0; i < this.ticksAndPosition.length; i += 2) {
|
||||
// tick.stack = [topN, ..., top0, this.codeEntry, bottom0, ..., bottomN];
|
||||
const tick = this.ticksAndPosition[i];
|
||||
const stackIndex = this.ticksAndPosition[i + 1];
|
||||
// slice = [topN, ..., top0]
|
||||
const slice = tick.stack.slice(0, stackIndex);
|
||||
maxDepth = Math.max(maxDepth, slice.length);
|
||||
slicedStacks.push(slice);
|
||||
}
|
||||
// Before:
|
||||
// stack1 = [f4, f3, f2, f1]
|
||||
// stack2 = [f2, f1]
|
||||
// After:
|
||||
// stack1 = [f4, f3, f2, f1]
|
||||
// stack2 = [ , , f2, f1]
|
||||
for (let i = 0; i < slicedStacks.length; i++) {
|
||||
const stack = slicedStacks[i];
|
||||
const length = stack.length;
|
||||
if (length < maxDepth) {
|
||||
// Pad stacks at the beginning
|
||||
stack.splice(maxDepth - length, 0, undefined);
|
||||
}
|
||||
}
|
||||
// Start sorting at the bottom-most frame: top0 => topN / f1 => fN
|
||||
return StackSorter.fromBottom(slicedStacks, maxDepth);
|
||||
}
|
||||
|
||||
stacksIn() {
|
||||
const slicedStacks = [];
|
||||
let maxDepth = 0;
|
||||
for (let i = 0; i < this.ticksAndPosition.length; i += 2) {
|
||||
// tick.stack = [topN, ..., top0, this.codeEntry, bottom0..., bottomN];
|
||||
const tick = this.ticksAndPosition[i];
|
||||
const stackIndex = this.ticksAndPosition[i + 1];
|
||||
// slice = [bottom0, ..., bottomN]
|
||||
const slice = tick.stack.slice(stackIndex + 1);
|
||||
maxDepth = Math.max(maxDepth, slice.length);
|
||||
slicedStacks.push(slice);
|
||||
}
|
||||
// Start storting at the top-most frame: bottom0 => bottomN
|
||||
return StackSorter.fromTop(slicedStacks, maxDepth);
|
||||
}
|
||||
|
||||
startTime() {
|
||||
return this.ticksAndPosition[0].startTime;
|
||||
}
|
||||
|
||||
endTime() {
|
||||
return this.ticksAndPosition[this.ticksAndPosition.length - 2].endTime;
|
||||
}
|
||||
|
||||
duration() {
|
||||
return this.endTime() - this.startTime();
|
||||
}
|
||||
|
||||
selfCount() {
|
||||
return this.totalCount() - this.outCodeEntries.length;
|
||||
}
|
||||
|
||||
totalCount() {
|
||||
return this.ticksAndPosition.length / 2;
|
||||
}
|
||||
|
||||
totalDuration() {
|
||||
let duration = 0;
|
||||
for (let entry of this.ticksAndPosition) duration += entry.duration;
|
||||
return duration;
|
||||
}
|
||||
|
||||
selfDuration() {
|
||||
let duration = this.totalDuration();
|
||||
for (let entry of this.outCodeEntries) duration -= entry.duration;
|
||||
return duration;
|
||||
}
|
||||
}
|
||||
|
||||
export class Flame {
|
||||
constructor(time, logEntry, depth, duration = -1) {
|
||||
this._time = time;
|
||||
this._logEntry = logEntry;
|
||||
this.depth = depth;
|
||||
this._duration = duration;
|
||||
this.parent = undefined;
|
||||
this.children = [];
|
||||
}
|
||||
|
||||
static add(time, logEntry, stack, flames) {
|
||||
const depth = stack.length;
|
||||
const newFlame = new Flame(time, logEntry, depth);
|
||||
if (depth > 0) {
|
||||
const parent = stack[depth - 1];
|
||||
newFlame.parent = parent;
|
||||
parent.children.push(newFlame);
|
||||
}
|
||||
flames.push(newFlame);
|
||||
stack.push(newFlame);
|
||||
}
|
||||
|
||||
stop(time) {
|
||||
if (this._duration !== -1) throw new Error('Already stopped');
|
||||
this._duration = time - this._time
|
||||
}
|
||||
|
||||
get time() {
|
||||
return this._time;
|
||||
}
|
||||
|
||||
get logEntry() {
|
||||
return this._logEntry;
|
||||
}
|
||||
|
||||
get startTime() {
|
||||
return this._time;
|
||||
}
|
||||
|
||||
get endTime() {
|
||||
return this._time + this._duration;
|
||||
}
|
||||
|
||||
get duration() {
|
||||
return this._duration;
|
||||
}
|
||||
|
||||
get type() {
|
||||
return TickLogEntry.extractCodeEntryType(this._logEntry?.entry);
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this._logEntry.name;
|
||||
}
|
||||
}
|
||||
|
||||
export class FlameBuilder {
|
||||
static forTime(ticks, reverseDepth) {
|
||||
return new this(ticks, true, reverseDepth);
|
||||
}
|
||||
|
||||
static forTicks(ticks, reverseDepth = false) {
|
||||
return new this(ticks, false, reverseDepth);
|
||||
}
|
||||
|
||||
constructor(ticks, useTime, reverseDepth) {
|
||||
this.maxDepth = 0;
|
||||
let tempFlames = this.flames = [];
|
||||
this.reverseDepth = reverseDepth;
|
||||
|
||||
if (ticks.length == 0) return;
|
||||
|
||||
if (!(ticks[0] instanceof TickLogEntry) && !Array.isArray(ticks[0])) {
|
||||
throw new Error(
|
||||
'Expected ticks array: `[TickLogEntry, ..]`, or raw stacks: `[[CodeEntry, ...], [...]]`');
|
||||
}
|
||||
// flameStack = [bottom, ..., top];
|
||||
const flameStack = [];
|
||||
let maxDepth = 0;
|
||||
const ticksLength = ticks.length;
|
||||
for (let tickIndex = 0; tickIndex < ticksLength; tickIndex++) {
|
||||
const tick = ticks[tickIndex];
|
||||
// tick is either a Tick log entry, or an Array
|
||||
const tickStack = tick.stack ?? tick;
|
||||
const tickStackLength = tickStack.length;
|
||||
const timeValue = useTime ? tick.time : tickIndex;
|
||||
maxDepth = Math.max(maxDepth, tickStackLength);
|
||||
// tick.stack = [top, .... , bottom];
|
||||
for (let stackIndex = tickStackLength - 1; stackIndex >= 0;
|
||||
stackIndex--) {
|
||||
const codeEntry = tickStack[stackIndex];
|
||||
// Assume that all higher stack entries are undefined as well.
|
||||
if (codeEntry === undefined) break;
|
||||
// codeEntry is either a CodeEntry or a raw pc.
|
||||
const logEntry = codeEntry?.logEntry ?? codeEntry;
|
||||
const flameStackIndex = tickStackLength - stackIndex - 1;
|
||||
if (flameStackIndex < flameStack.length) {
|
||||
if (flameStack[flameStackIndex].logEntry === logEntry) continue;
|
||||
// A lower frame changed, close all higher Flames.
|
||||
for (let k = flameStackIndex; k < flameStack.length; k++) {
|
||||
flameStack[k].stop(timeValue);
|
||||
}
|
||||
flameStack.length = flameStackIndex;
|
||||
}
|
||||
Flame.add(timeValue, logEntry, flameStack, tempFlames);
|
||||
}
|
||||
// Stop all Flames that are deeper nested than the current stack.
|
||||
if (tickStackLength < flameStack.length) {
|
||||
for (let k = tickStackLength; k < flameStack.length; k++) {
|
||||
flameStack[k].stop(timeValue);
|
||||
}
|
||||
flameStack.length = tickStackLength;
|
||||
}
|
||||
}
|
||||
this.maxDepth = maxDepth;
|
||||
|
||||
const lastTime = useTime ? ticks[ticksLength - 1].time : ticksLength - 1;
|
||||
for (let k = 0; k < flameStack.length; k++) {
|
||||
flameStack[k].stop(lastTime);
|
||||
}
|
||||
}
|
||||
}
|
@ -4,19 +4,23 @@
|
||||
|
||||
import {groupBy} from './helper.mjs'
|
||||
|
||||
/** @template T */
|
||||
class Timeline {
|
||||
// Class:
|
||||
/** Class T */
|
||||
_model;
|
||||
// Array of #model instances:
|
||||
/** @type {T[]} */
|
||||
_values;
|
||||
// Current selection, subset of #values:
|
||||
/** @type {?Timeline<T>} */
|
||||
_selection;
|
||||
_breakdown;
|
||||
|
||||
constructor(model, values = [], startTime = null, endTime = null) {
|
||||
this._model = model;
|
||||
this._values = values;
|
||||
/** @type {number} */
|
||||
this.startTime = startTime;
|
||||
/** @type {number} */
|
||||
this.endTime = endTime;
|
||||
if (values.length > 0) {
|
||||
if (startTime === null) this.startTime = values[0].time;
|
||||
@ -39,10 +43,12 @@ class Timeline {
|
||||
return this._selection;
|
||||
}
|
||||
|
||||
/** @returns {Timeline<T>} */
|
||||
get selectionOrSelf() {
|
||||
return this._selection ?? this;
|
||||
}
|
||||
|
||||
/** @param {Timeline<T>} value */
|
||||
set selection(value) {
|
||||
this._selection = value;
|
||||
}
|
||||
@ -60,10 +66,12 @@ class Timeline {
|
||||
return this.chunkSizes(windowSizeMs);
|
||||
}
|
||||
|
||||
/** @returns {T[]} */
|
||||
get values() {
|
||||
return this._values;
|
||||
}
|
||||
|
||||
/** @returns {number} */
|
||||
count(filter) {
|
||||
return this.all.reduce((sum, each) => {
|
||||
return sum + (filter(each) === true ? 1 : 0);
|
||||
@ -74,6 +82,7 @@ class Timeline {
|
||||
return this.all.filter(predicate);
|
||||
}
|
||||
|
||||
/** @param {T} event */
|
||||
push(event) {
|
||||
let time = event.time;
|
||||
if (!this.isEmpty() && this.last().time > time) {
|
||||
@ -94,6 +103,7 @@ class Timeline {
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns {T} */
|
||||
at(index) {
|
||||
return this._values[index];
|
||||
}
|
||||
@ -110,14 +120,17 @@ class Timeline {
|
||||
return this._values.length;
|
||||
}
|
||||
|
||||
/** @returns {T[]} */
|
||||
slice(startIndex, endIndex) {
|
||||
return this._values.slice(startIndex, endIndex);
|
||||
}
|
||||
|
||||
/** @returns {T} */
|
||||
first() {
|
||||
return this._values[0];
|
||||
}
|
||||
|
||||
/** @returns {T} */
|
||||
last() {
|
||||
return this._values[this._values.length - 1];
|
||||
}
|
||||
@ -126,6 +139,7 @@ class Timeline {
|
||||
yield* this._values;
|
||||
}
|
||||
|
||||
/** @returns {number} */
|
||||
duration() {
|
||||
return this.endTime - this.startTime;
|
||||
}
|
||||
@ -145,12 +159,14 @@ class Timeline {
|
||||
fn(index, this._values.length - 1, currentTime, this.endTime);
|
||||
}
|
||||
|
||||
/** @returns {number[]} */
|
||||
chunkSizes(count) {
|
||||
const chunks = [];
|
||||
this.forEachChunkSize(count, (start, end) => chunks.push(end - start));
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/** @returns {Chunk<T>[]} */
|
||||
chunks(count, predicate = undefined) {
|
||||
const chunks = [];
|
||||
this.forEachChunkSize(count, (start, end, startTime, endTime) => {
|
||||
@ -161,7 +177,10 @@ class Timeline {
|
||||
return chunks;
|
||||
}
|
||||
|
||||
// Return all entries in ({startTime}, {endTime}]
|
||||
/**
|
||||
* Return all entries in ({startTime}, {endTime}]
|
||||
* @returns {T[]}
|
||||
**/
|
||||
range(startTime, endTime) {
|
||||
const firstIndex = this.find(startTime);
|
||||
if (firstIndex < 0) return [];
|
||||
@ -234,11 +253,13 @@ class Timeline {
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
/** @template T */
|
||||
class Chunk {
|
||||
constructor(index, start, end, items) {
|
||||
this.index = index;
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
/** @type {T[]} */
|
||||
this.items = items;
|
||||
this.height = 0;
|
||||
}
|
||||
@ -247,14 +268,17 @@ class Chunk {
|
||||
return this.items.length === 0;
|
||||
}
|
||||
|
||||
/** @returns {T} */
|
||||
last() {
|
||||
return this.at(this.size() - 1);
|
||||
}
|
||||
|
||||
/** @returns {T} */
|
||||
first() {
|
||||
return this.at(0);
|
||||
}
|
||||
|
||||
/** @returns {T} */
|
||||
at(index) {
|
||||
return this.items[index];
|
||||
}
|
||||
@ -267,25 +291,30 @@ class Chunk {
|
||||
return this.items.length;
|
||||
}
|
||||
|
||||
/** @param {T} event */
|
||||
yOffset(event) {
|
||||
// items[0] == oldest event, displayed at the top of the chunk
|
||||
// items[n-1] == youngest event, displayed at the bottom of the chunk
|
||||
return ((this.indexOf(event) + 0.5) / this.size()) * this.height;
|
||||
}
|
||||
|
||||
/** @param {T} event */
|
||||
indexOf(event) {
|
||||
return this.items.indexOf(event);
|
||||
}
|
||||
|
||||
/** @param {T} event */
|
||||
has(event) {
|
||||
if (this.isEmpty()) return false;
|
||||
return this.first().time <= event.time && event.time <= this.last().time;
|
||||
}
|
||||
|
||||
/** @param {Chunk<T>[]} chunks */
|
||||
next(chunks) {
|
||||
return this.findChunk(chunks, 1);
|
||||
}
|
||||
|
||||
/** @param {Chunk<T>[]} chunks */
|
||||
prev(chunks) {
|
||||
return this.findChunk(chunks, -1);
|
||||
}
|
||||
@ -304,6 +333,7 @@ class Chunk {
|
||||
return groupBy(this.items, keyFunction);
|
||||
}
|
||||
|
||||
/** @returns {T[]} */
|
||||
filter() {
|
||||
return this.items.filter(map => !map.parent || !this.has(map.parent));
|
||||
}
|
||||
|
@ -140,6 +140,10 @@ export class SVG {
|
||||
return this.element('rect', classes);
|
||||
}
|
||||
|
||||
static path(classes) {
|
||||
return this.element('path', classes);
|
||||
}
|
||||
|
||||
static g(classes) {
|
||||
return this.element('g', classes);
|
||||
}
|
||||
|
145
tools/system-analyzer/view/profiler-panel-template.html
Normal file
145
tools/system-analyzer/view/profiler-panel-template.html
Normal file
@ -0,0 +1,145 @@
|
||||
<!-- Copyright 2022 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. -->
|
||||
|
||||
<head>
|
||||
<link href="./index.css" rel="stylesheet">
|
||||
<style>
|
||||
:host {
|
||||
--flame-category-width: 40px;
|
||||
}
|
||||
.panelBody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#overview {
|
||||
flex: 0 0 25px;
|
||||
}
|
||||
|
||||
.tableContainer {
|
||||
position: relative;
|
||||
overflow-y: scroll;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#flameChart {
|
||||
--height: 400px;
|
||||
flex: 0 0 var(--height);
|
||||
height: var(--height);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
#table thead {
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
background-color: var(--surface-color);
|
||||
}
|
||||
|
||||
#flameChart div {
|
||||
position: absolute;
|
||||
font-size: 8px;
|
||||
line-height: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#flameChartFlames div {
|
||||
height: 10px;
|
||||
border: 1px var(--border-color) solid;
|
||||
font-family: var(--code-font);
|
||||
color: var(--on-primary-color);
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#flameChartFlames div:hover {
|
||||
border: 1px var(--background-color) solid;
|
||||
}
|
||||
|
||||
#flameChart > div {
|
||||
box-sizing: border-box;
|
||||
overflow: visible;
|
||||
color: var(--on-surface-color);
|
||||
padding-right: 5px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#flameChartSelected, #flameChartIn, #flameChartOut {
|
||||
width: var(--flame-category-width);
|
||||
}
|
||||
#flameChartIn {
|
||||
/* bottom-right align the text */
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
#flameChartFlames {
|
||||
top: 0xp;
|
||||
left: var(--flame-category-width);
|
||||
}
|
||||
|
||||
|
||||
#table .r {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* SVG */
|
||||
.fsIn {
|
||||
background-color: bisque;
|
||||
}
|
||||
|
||||
.fsOut {
|
||||
background-color: lightblue;
|
||||
}
|
||||
|
||||
.fsMain {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<div class="panel">
|
||||
<input type="checkbox" id="closer" class="panelCloserInput" checked>
|
||||
<label class="panelCloserLabel" for="closer">▼</label>
|
||||
<h2>Profiler</h2>
|
||||
<div class="selection">
|
||||
<input type="radio" id="show-all" name="selectionType" value="all">
|
||||
<label for="show-all">All</label>
|
||||
<input type="radio" id="show-timerange" name="selectionType" value="timerange">
|
||||
<label for="show-timerange">Time Range</label>
|
||||
<input type="radio" id="show-selection" name="selectionType" value="selection">
|
||||
<label for="show-selection">Last Selection</label>
|
||||
</div>
|
||||
|
||||
<div id="body" class="panelBody">
|
||||
<timeline-overview id="overview"></timeline-overview>
|
||||
|
||||
<div class="tableContainer">
|
||||
<table id="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td colspan="2">Self</td>
|
||||
<td colspan="2">Total</td>
|
||||
<td></td>
|
||||
<td>Type</td>
|
||||
<td>Name</td>
|
||||
<td>SourcePostion</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="flameChart">
|
||||
<div id="flameChartIn">IN↧</div>
|
||||
<div id="flameChartSelected">Pivot</div>
|
||||
<div id="flameChartOut">OUT↧</div>
|
||||
<div id="flameChartFlames"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
273
tools/system-analyzer/view/profiler-panel.mjs
Normal file
273
tools/system-analyzer/view/profiler-panel.mjs
Normal file
@ -0,0 +1,273 @@
|
||||
// Copyright 2022 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 {CodeEntry} from '../../codemap.mjs';
|
||||
import {delay} from '../helper.mjs';
|
||||
import {DeoptLogEntry} from '../log/code.mjs';
|
||||
import {TickLogEntry} from '../log/tick.mjs';
|
||||
import {Flame, FlameBuilder, ProfileNode} from '../profiling.mjs';
|
||||
import {Timeline} from '../timeline.mjs';
|
||||
import {ToolTipEvent} from './events.mjs';
|
||||
import {CollapsableElement, CSSColor, DOM, LazyTable} from './helper.mjs';
|
||||
import {Track} from './timeline/timeline-overview.mjs';
|
||||
|
||||
DOM.defineCustomElement('view/profiler-panel',
|
||||
(templateText) =>
|
||||
class ProfilerPanel extends CollapsableElement {
|
||||
/** @type {Timeline<TickLogEntry>} */
|
||||
_timeline;
|
||||
/** @type {Timeline<TickLogEntry> | TickLogEntry[]} */
|
||||
_displayedLogEntries;
|
||||
/** @type {Timeline<TickLogEntry> | TickLogEntry[]} */
|
||||
_selectedLogEntries;
|
||||
/** @type {ProfileNode[]} */
|
||||
_profileNodes = [];
|
||||
/** @type {Map<CodeEntry, ProfileNode>} */
|
||||
_profileNodeMap;
|
||||
|
||||
constructor() {
|
||||
super(templateText);
|
||||
this._tableNode = this.$('#table');
|
||||
this._tableNode.onclick = this._handleRowClick.bind(this);
|
||||
this._showAllRadio = this.$('#show-all');
|
||||
this._showAllRadio.onclick = _ => this._showEntries(this._timeline);
|
||||
this._showTimeRangeRadio = this.$('#show-timerange');
|
||||
this._showTimeRangeRadio.onclick = _ =>
|
||||
this._showEntries(this._timeline.selectionOrSelf);
|
||||
this._showSelectionRadio = this.$('#show-selection');
|
||||
this._showSelectionRadio.onclick = _ =>
|
||||
this._showEntries(this._selectedLogEntries);
|
||||
/** @type {TimelineOverview<TickLogEntry>} */
|
||||
this._timelineOverview = this.$('#overview');
|
||||
this._timelineOverview.countCallback = (tick, /* trick,*/ track) => {
|
||||
let count = 0;
|
||||
for (let j = 0; j < tick.stack.length; j++) {
|
||||
if (track.hasEntry(tick.stack[j])) count++;
|
||||
}
|
||||
return count;
|
||||
};
|
||||
this._flameChart = this.$('#flameChart');
|
||||
this._flameChart.onmousemove = this._handleFlameChartMouseMove.bind(this);
|
||||
this._flameChart.onclick = this._handleFlameChartClick.bind(this);
|
||||
}
|
||||
|
||||
/** @param {Timeline<TickLogEntry>} timeline */
|
||||
set timeline(timeline) {
|
||||
this._timeline = timeline;
|
||||
this._timelineOverview.timeline = timeline;
|
||||
}
|
||||
|
||||
/** @param {Timeline<TickLogEntry> | TickLogEntry[]} entries */
|
||||
set selectedLogEntries(entries) {
|
||||
if (entries === this._timeline) {
|
||||
this._showAllRadio.click();
|
||||
} else if (entries === this._timeline.selection) {
|
||||
this._showTimeRangeRadio.click();
|
||||
} else {
|
||||
this._selectedLogEntries = entries;
|
||||
this._showSelectionRadio.click();
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {Timeline<TickLogEntry> | TickLogEntry[]} entries */
|
||||
_showEntries(entries) {
|
||||
this._displayedLogEntries = entries;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
_update() {
|
||||
this._profileNodeMap = new Map();
|
||||
const entries = this._displayedLogEntries?.values ?? [];
|
||||
let totalDuration = 0;
|
||||
let totalEntries = 0;
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
/** @type {TickLogEntry} */
|
||||
const tick = entries[i];
|
||||
totalDuration += tick.duration;
|
||||
const stack = tick.stack;
|
||||
let prevCodeEntry;
|
||||
let prevStatsEntry;
|
||||
for (let j = 0; j < stack.length; j++) {
|
||||
const codeEntry = stack[j];
|
||||
totalEntries++;
|
||||
let statsEntry = this._profileNodeMap.get(codeEntry);
|
||||
if (statsEntry === undefined) {
|
||||
statsEntry = new ProfileNode(codeEntry);
|
||||
this._profileNodeMap.set(codeEntry, statsEntry);
|
||||
}
|
||||
statsEntry.ticksAndPosition.push(tick, j);
|
||||
if (prevCodeEntry !== undefined) {
|
||||
statsEntry.inCodeEntries.push(prevCodeEntry);
|
||||
prevStatsEntry.outCodeEntries.push(codeEntry);
|
||||
}
|
||||
prevCodeEntry = codeEntry;
|
||||
prevStatsEntry = statsEntry;
|
||||
}
|
||||
}
|
||||
|
||||
this._profileNodes = Array.from(this._profileNodeMap.values());
|
||||
this._profileNodes.sort((a, b) => b.selfCount() - a.selfCount());
|
||||
|
||||
const body = DOM.tbody();
|
||||
let buffer = [];
|
||||
for (let id = 0; id < this._profileNodes.length; id++) {
|
||||
/** @type {ProfileNode} */
|
||||
const node = this._profileNodes[id];
|
||||
/** @type {CodeEntry} */
|
||||
const codeEntry = node.codeEntry;
|
||||
buffer.push(`<tr data-id=${id} class=clickable >`);
|
||||
buffer.push(`<td class=r >${node.selfCount()}</td>`);
|
||||
const selfPercent = (node.selfCount() / entries.length * 100).toFixed(1);
|
||||
buffer.push(`<td class=r >${selfPercent}%</td>`);
|
||||
buffer.push(`<td class=r >${node.totalCount()}</td>`);
|
||||
const totalPercent = (node.totalCount() / totalEntries * 100).toFixed(1);
|
||||
buffer.push(`<td class=r >${totalPercent}%</td>`);
|
||||
buffer.push('<td></td>');
|
||||
if (typeof codeEntry === 'number') {
|
||||
buffer.push('<td></td>');
|
||||
buffer.push(`<td>${codeEntry}</td>`);
|
||||
buffer.push('<td></td>');
|
||||
} else {
|
||||
const logEntry = codeEntry.logEntry;
|
||||
buffer.push(`<td>${logEntry.type}</td>`);
|
||||
buffer.push(`<td>${logEntry.name}</td>`);
|
||||
buffer.push(`<td>${logEntry.sourcePosition?.toString() ?? ''}</td>`);
|
||||
}
|
||||
buffer.push('</tr>');
|
||||
}
|
||||
body.innerHTML = buffer.join('');
|
||||
this._tableNode.replaceChild(body, this._tableNode.tBodies[0]);
|
||||
|
||||
this._updateOverview(this._profileNodes[0])
|
||||
}
|
||||
|
||||
_handleRowClick(e) {
|
||||
let node = e.target;
|
||||
let dataId = null;
|
||||
try {
|
||||
while (dataId === null) {
|
||||
dataId = node.getAttribute('data-id');
|
||||
node = node.parentNode;
|
||||
if (!node) return;
|
||||
}
|
||||
} catch (e) {
|
||||
// getAttribute can throw, this is the lazy way out if we click on the
|
||||
// title (or anywhere that doesn't have a data-it on any parent).
|
||||
return;
|
||||
}
|
||||
const profileNode = this._profileNodes[dataId];
|
||||
this._updateOverview(profileNode);
|
||||
this._updateFlameChart(profileNode);
|
||||
}
|
||||
|
||||
_updateOverview(profileNode) {
|
||||
if (profileNode === undefined) {
|
||||
this._timelineOverview.tracks = [];
|
||||
return;
|
||||
}
|
||||
const mainCode = profileNode.codeEntry;
|
||||
const secondaryCodeEntries = [];
|
||||
const deopts = [];
|
||||
const codeCreation = [mainCode.logEntry];
|
||||
if (mainCode.func?.codeEntries.size > 1) {
|
||||
for (let dynamicCode of mainCode.func.codeEntries) {
|
||||
for (let related of dynamicCode.logEntry.relatedEntries()) {
|
||||
if (related instanceof DeoptLogEntry) deopts.push(related);
|
||||
}
|
||||
if (dynamicCode === profileNode.codeEntry) continue;
|
||||
codeCreation.push(dynamicCode.logEntry);
|
||||
secondaryCodeEntries.push(dynamicCode);
|
||||
}
|
||||
}
|
||||
this._timelineOverview.tracks = [
|
||||
Track.continuous([mainCode], CSSColor.primaryColor),
|
||||
Track.continuous(secondaryCodeEntries, CSSColor.secondaryColor),
|
||||
Track.discrete(deopts, CSSColor.red),
|
||||
Track.discrete(codeCreation, CSSColor.green),
|
||||
];
|
||||
}
|
||||
|
||||
async _updateFlameChart(profileNode) {
|
||||
await delay(100);
|
||||
const codeEntry = profileNode.codeEntry;
|
||||
const stacksIn = profileNode.stacksIn();
|
||||
// Reverse the stack so the FlameBuilder groups the top-most frame
|
||||
for (let i = 0; i < stacksIn.length; i++) {
|
||||
stacksIn[i].reverse();
|
||||
}
|
||||
const stacksOut = profileNode.stacksOut();
|
||||
|
||||
const flameBuilderIn = FlameBuilder.forTicks(stacksIn);
|
||||
const flameBuilderOut = FlameBuilder.forTicks(stacksOut);
|
||||
|
||||
let fragment = new DocumentFragment();
|
||||
const kItemHeight = 12;
|
||||
// One empty line at the beginning
|
||||
const maxInDepth = Math.max(2, flameBuilderIn.maxDepth + 1);
|
||||
let centerDepth = maxInDepth;
|
||||
for (const flame of flameBuilderIn.flames) {
|
||||
// Ignore padded frames.
|
||||
if (flame.logEntry === undefined) continue;
|
||||
const codeEntry = flame.logEntry.entry;
|
||||
const flameProfileNode = this._profileNodeMap.get(codeEntry);
|
||||
const y = (centerDepth - flame.depth - 1) * kItemHeight;
|
||||
fragment.appendChild(
|
||||
this._createFlame(flame, flameProfileNode, y, 'fsIn'));
|
||||
}
|
||||
|
||||
// Add spacing:
|
||||
centerDepth++;
|
||||
const y = centerDepth * kItemHeight;
|
||||
// Create fake Flame for the main entry;
|
||||
const centerFlame =
|
||||
new Flame(0, codeEntry.logEntry, 0, profileNode.totalCount());
|
||||
fragment.appendChild(
|
||||
this._createFlame(centerFlame, profileNode, y, 'fsMain'));
|
||||
|
||||
// Add spacing:
|
||||
centerDepth += 2;
|
||||
|
||||
for (const flame of flameBuilderOut.flames) {
|
||||
if (flame.logEntry === undefined) continue;
|
||||
const codeEntry = flame.logEntry.entry;
|
||||
const flameProfileNode = this._profileNodeMap.get(codeEntry);
|
||||
const y = (flame.depth + centerDepth) * kItemHeight;
|
||||
fragment.appendChild(
|
||||
this._createFlame(flame, flameProfileNode, y, 'fsOut'));
|
||||
}
|
||||
this.$('#flameChartFlames').replaceChildren(fragment);
|
||||
|
||||
this.$('#flameChartIn').style.height = (maxInDepth * kItemHeight) + 'px';
|
||||
this.$('#flameChartSelected').style.top =
|
||||
((maxInDepth + 1) * kItemHeight) + 'px';
|
||||
this.$('#flameChartOut').style.top = (centerDepth * kItemHeight) + 'px';
|
||||
this.$('#flameChartOut').style.height =
|
||||
(flameBuilderOut.maxDepth * kItemHeight) + 'px';
|
||||
}
|
||||
|
||||
_createFlame(flame, profileNode, y, className) {
|
||||
const ticksToPixel = 4;
|
||||
const x = flame.time * ticksToPixel;
|
||||
const width = flame.duration * ticksToPixel;
|
||||
const div = DOM.div(className);
|
||||
div.style = `left:${x}px;top:${y}px;width:${width}px`;
|
||||
div.innerText = flame.name;
|
||||
div.data = profileNode;
|
||||
return div;
|
||||
}
|
||||
|
||||
_handleFlameChartMouseMove(e) {
|
||||
const profileNode = e.target.data;
|
||||
if (!profileNode) return;
|
||||
const logEntry = profileNode.codeEntry.logEntry;
|
||||
this.dispatchEvent(new ToolTipEvent(logEntry, e.target));
|
||||
}
|
||||
|
||||
_handleFlameChartClick(e) {
|
||||
const profileNode = e.target.data;
|
||||
if (!profileNode) return;
|
||||
this._updateOverview(profileNode);
|
||||
this._updateFlameChart(profileNode)
|
||||
}
|
||||
});
|
@ -0,0 +1,73 @@
|
||||
<!-- Copyright 2022 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. -->
|
||||
|
||||
<head>
|
||||
<link href="./index.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
|
||||
<style>
|
||||
:host {
|
||||
--selection-height: 5px;
|
||||
--total-height: 30px;
|
||||
}
|
||||
#svg {
|
||||
width: 100%;
|
||||
height: var(--total-height);
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
}
|
||||
.marker {
|
||||
width: 1px;
|
||||
y: var(--selection-height);
|
||||
height: calc(var(--total-height) - var(--selection-height));
|
||||
}
|
||||
#indicator {
|
||||
stroke: var(--on-surface-color);
|
||||
}
|
||||
.continuousTrack {
|
||||
transform: translate(0, var(--selection-height));
|
||||
}
|
||||
#filler {
|
||||
height: var(--total-height);
|
||||
}
|
||||
#selection {
|
||||
height: var(--total-height);
|
||||
}
|
||||
#selection rect {
|
||||
height: var(--selection-height);
|
||||
fill: var(--selection-color);
|
||||
}
|
||||
#selection .top {
|
||||
y: 0px;
|
||||
}
|
||||
#selection .bottom {
|
||||
y: calc(var(--total-height) - var(--selection-height));
|
||||
}
|
||||
#selection line {
|
||||
stroke: var(--on-surface-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<svg id="svg" viewBox="0 1 800 30" preserveAspectRatio=none>
|
||||
<defs>
|
||||
<pattern id="pattern1" patternUnits="userSpaceOnUse" width="4" height="4">
|
||||
<path d="M-1,1 l2,-2
|
||||
M0,4 l4,-4
|
||||
M3,5 l2,-2" stroke="white"/>
|
||||
</pattern>
|
||||
<mask id="mask1">
|
||||
<rect width="800" height="20" fill="url(#pattern1)" />
|
||||
</mask>
|
||||
</defs>
|
||||
<rect id="filler" width="800" fill-opacity="0"/>
|
||||
<g id="content"></g>
|
||||
<svg id="selection">
|
||||
<line x1="0%" y1="0" x2="0%" y2="30" />
|
||||
<rect class="top" x="0%" width="100%"></rect>
|
||||
<rect class="bottom" x="0%" width="100%"></rect>
|
||||
<line x1="100%" y1="0" x2="100%" y2="30" />
|
||||
</svg>
|
||||
<line id="indicator" x1="0" y1="0" x2="0" y2="30" />
|
||||
</svg>
|
269
tools/system-analyzer/view/timeline/timeline-overview.mjs
Normal file
269
tools/system-analyzer/view/timeline/timeline-overview.mjs
Normal file
@ -0,0 +1,269 @@
|
||||
// Copyright 2022 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 {Timeline} from '../../timeline.mjs';
|
||||
import {ToolTipEvent} from '../events.mjs';
|
||||
import {arrayEquals, CSSColor, DOM, formatDurationMicros, SVG, V8CustomElement} from '../helper.mjs';
|
||||
|
||||
/** @template T */
|
||||
export class Track {
|
||||
static continuous(array, color) {
|
||||
return new this(array, color, true);
|
||||
}
|
||||
|
||||
static discrete(array, color) {
|
||||
return new this(array, color, false);
|
||||
}
|
||||
|
||||
/** @param {T[]} logEntries */
|
||||
constructor(logEntries, color, isContinuous) {
|
||||
/** @type {Set<T>} */
|
||||
this.logEntries = new Set(logEntries);
|
||||
this.color = color;
|
||||
/** @type {bool} */
|
||||
this.isContinuous = isContinuous;
|
||||
}
|
||||
|
||||
isEmpty() {
|
||||
return this.logEntries.size == 0;
|
||||
}
|
||||
|
||||
/** @param {T} logEntry */
|
||||
hasEntry(logEntry) {
|
||||
return this.logEntries.has(logEntry);
|
||||
}
|
||||
|
||||
static compare(left, right) {
|
||||
return left.equals(right);
|
||||
}
|
||||
|
||||
equals(other) {
|
||||
if (!arrayEquals(
|
||||
Array.from(this.logEntries), Array.from(other.logEntries))) {
|
||||
return false;
|
||||
}
|
||||
if (this.color != other.color) return false;
|
||||
if (this.isContinuous != other.isContinuous) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const kHorizontalPixels = 800;
|
||||
const kMarginHeight = 5;
|
||||
const kHeight = 20;
|
||||
|
||||
DOM.defineCustomElement('view/timeline/timeline-overview',
|
||||
(templateText) =>
|
||||
/** @template T */
|
||||
class TimelineOverview extends V8CustomElement {
|
||||
/** @type {Timeline<T>} */
|
||||
_timeline;
|
||||
/** @type {Track[]} */
|
||||
_tracks = [];
|
||||
_timeToPixel = 1;
|
||||
/** @type {{entry:T, track:Track} => number} */
|
||||
_countCallback = (entry, track) => track.hasEntry(entry);
|
||||
|
||||
constructor() {
|
||||
super(templateText);
|
||||
this._indicatorNode = this.$('#indicator');
|
||||
this._selectionNode = this.$('#selection');
|
||||
this._contentNode = this.$('#content');
|
||||
this._svgNode = this.$('#svg');
|
||||
this._svgNode.onmousemove = this._handleMouseMove.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Timeline<T>} timeline
|
||||
*/
|
||||
set timeline(timeline) {
|
||||
this._timeline = timeline;
|
||||
this._timeToPixel = kHorizontalPixels / this._timeline.duration();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Track[]} tracks
|
||||
*/
|
||||
set tracks(tracks) {
|
||||
// TODO(cbruni): Allow updating the selection time-range independently from
|
||||
// the data.
|
||||
// if (arrayEquals(this._tracks, tracks, Track.compare)) return;
|
||||
this._tracks = tracks;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
/** @param {{entry:T, track:Track} => number} callback*/
|
||||
set countCallback(callback) {
|
||||
this._countCallback = callback;
|
||||
}
|
||||
|
||||
_handleMouseMove(e) {
|
||||
const externalPixelToTime =
|
||||
this._timeline.duration() / this._svgNode.getBoundingClientRect().width;
|
||||
const timeToInternalPixel = kHorizontalPixels / this._timeline.duration();
|
||||
const xPos = e.offsetX;
|
||||
const timeMicros = xPos * externalPixelToTime;
|
||||
const maxTimeDistance = 2 * externalPixelToTime;
|
||||
this._setIndicatorPosition(timeMicros * timeToInternalPixel);
|
||||
let toolTipContent = this._findLogEntryAtTime(timeMicros, maxTimeDistance);
|
||||
if (!toolTipContent) {
|
||||
toolTipContent = `Time ${formatDurationMicros(timeMicros)}`;
|
||||
}
|
||||
this.dispatchEvent(new ToolTipEvent(toolTipContent, this._indicatorNode));
|
||||
}
|
||||
|
||||
_findLogEntryAtTime(time, maxTimeDistance) {
|
||||
const minTime = time - maxTimeDistance;
|
||||
const maxTime = time + maxTimeDistance;
|
||||
for (let track of this._tracks) {
|
||||
for (let entry of track.logEntries) {
|
||||
if (minTime <= entry.time && entry.time <= maxTime) return entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_setIndicatorPosition(x) {
|
||||
this._indicatorNode.setAttribute('x1', x);
|
||||
this._indicatorNode.setAttribute('x2', x);
|
||||
}
|
||||
|
||||
_update() {
|
||||
const fragment = new DocumentFragment();
|
||||
this._tracks.forEach((track, index) => {
|
||||
if (!track.isEmpty()) {
|
||||
fragment.appendChild(this._renderTrack(track, index));
|
||||
}
|
||||
});
|
||||
DOM.removeAllChildren(this._contentNode);
|
||||
this._contentNode.appendChild(fragment);
|
||||
this._setIndicatorPosition(-10);
|
||||
this._updateSelection();
|
||||
}
|
||||
|
||||
_renderTrack(track, index) {
|
||||
if (track.isContinuous) return this._renderContinuousTrack(track, index);
|
||||
return this._renderDiscreteTrack(track, index);
|
||||
}
|
||||
|
||||
_renderContinuousTrack(track, index) {
|
||||
const freq = new Frequency(this._timeline);
|
||||
freq.collect(track, this._countCallback);
|
||||
const path = SVG.path('continuousTrack');
|
||||
let vScale = kHeight / freq.max();
|
||||
path.setAttribute('d', freq.toSVG(vScale));
|
||||
path.setAttribute('fill', track.color);
|
||||
if (index != 0) path.setAttribute('mask', `url(#mask${index})`)
|
||||
return path;
|
||||
}
|
||||
|
||||
_renderDiscreteTrack(track) {
|
||||
const group = SVG.g();
|
||||
for (let entry of track.logEntries) {
|
||||
const x = entry.time * this._timeToPixel;
|
||||
const rect = SVG.rect('marker');
|
||||
rect.setAttribute('x', x);
|
||||
rect.setAttribute('fill', track.color);
|
||||
rect.data = entry;
|
||||
group.appendChild(rect);
|
||||
}
|
||||
return group;
|
||||
}
|
||||
|
||||
_updateSelection() {
|
||||
let startTime = -10;
|
||||
let duration = 0;
|
||||
if (this._timeline) {
|
||||
startTime = this._timeline.selectionOrSelf.startTime;
|
||||
duration = this._timeline.selectionOrSelf.duration();
|
||||
}
|
||||
this._selectionNode.setAttribute('x', startTime * this._timeToPixel);
|
||||
this._selectionNode.setAttribute('width', duration * this._timeToPixel);
|
||||
}
|
||||
});
|
||||
|
||||
const kernel = smoothingKernel(50);
|
||||
function smoothingKernel(size) {
|
||||
const kernel = new Float32Array(size);
|
||||
const mid = (size - 1) / 2;
|
||||
const stddev = size / 10;
|
||||
for (let i = mid; i < size; i++) {
|
||||
const x = i - (mid | 0);
|
||||
const value =
|
||||
Math.exp(-(x ** 2 / 2 / stddev)) / (stddev * Math.sqrt(2 * Math.PI));
|
||||
kernel[Math.ceil(x + mid)] = value;
|
||||
kernel[Math.floor(mid - x)] = value;
|
||||
}
|
||||
return kernel;
|
||||
}
|
||||
|
||||
class Frequency {
|
||||
_smoothenedData;
|
||||
|
||||
constructor(timeline) {
|
||||
this._size = kHorizontalPixels;
|
||||
this._timeline = timeline;
|
||||
this._data = new Int16Array(this._size + kernel.length);
|
||||
this._max = 0;
|
||||
}
|
||||
|
||||
collect(track, sumFn) {
|
||||
const kernelRadius = (kernel.length / 2) | 0;
|
||||
let count = 0;
|
||||
let dataIndex = kernelRadius;
|
||||
const timeDelta = this._timeline.duration() / this._size;
|
||||
let nextTime = this._timeline.startTime + timeDelta;
|
||||
const values = this._timeline.values;
|
||||
const length = values.length;
|
||||
let i = 0;
|
||||
while (i < length && dataIndex < this._data.length) {
|
||||
const tick = values[i];
|
||||
if (tick.time < nextTime) {
|
||||
count += sumFn(tick, track);
|
||||
i++;
|
||||
} else {
|
||||
this._data[dataIndex] = count;
|
||||
nextTime += timeDelta;
|
||||
dataIndex++;
|
||||
count = 0;
|
||||
}
|
||||
}
|
||||
this._data[dataIndex] = count;
|
||||
}
|
||||
|
||||
max() {
|
||||
let max = 0;
|
||||
this._smoothenedData = new Float32Array(this._size);
|
||||
for (let start = 0; start < this._size; start++) {
|
||||
let value = 0
|
||||
for (let i = 0; i < kernel.length; i++) {
|
||||
value += this._data[start + i] * kernel[i];
|
||||
}
|
||||
this._smoothenedData[start] = value;
|
||||
max = Math.max(max, value);
|
||||
}
|
||||
this._max = max;
|
||||
return this._max;
|
||||
}
|
||||
|
||||
toSVG(vScale = 1) {
|
||||
const buffer = ['M 0 0'];
|
||||
let prevY = 0;
|
||||
let usedPrevY = false;
|
||||
for (let i = 0; i < this._size; i++) {
|
||||
const y = (this._smoothenedData[i] * vScale) | 0;
|
||||
if (y == prevY) {
|
||||
usedPrevY = false;
|
||||
continue;
|
||||
}
|
||||
if (!usedPrevY) buffer.push('L', i - 1, prevY);
|
||||
buffer.push('L', i, y);
|
||||
prevY = y;
|
||||
usedPrevY = true;
|
||||
}
|
||||
if (!usedPrevY) buffer.push('L', this._size - 1, prevY);
|
||||
buffer.push('L', this._size - 1, 0);
|
||||
buffer.push('Z');
|
||||
return buffer.join(' ');
|
||||
}
|
||||
}
|
@ -148,7 +148,7 @@ found in the LICENSE file. -->
|
||||
cursor: grabbing;
|
||||
}
|
||||
#selectionBackground {
|
||||
background-color: rgba(133, 68, 163, 0.5);
|
||||
background-color: var(--selection-color);
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
@ -2,64 +2,12 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import {TickLogEntry} from '../../log/tick.mjs';
|
||||
import {Flame, FlameBuilder} from '../../profiling.mjs';
|
||||
import {Timeline} from '../../timeline.mjs';
|
||||
import {delay, DOM, SVG} from '../helper.mjs';
|
||||
|
||||
import {TimelineTrackStackedBase} from './timeline-track-stacked-base.mjs'
|
||||
|
||||
class Flame {
|
||||
constructor(time, logEntry, depth) {
|
||||
this._time = time;
|
||||
this._logEntry = logEntry;
|
||||
this.depth = depth;
|
||||
this._duration = -1;
|
||||
this.parent = undefined;
|
||||
this.children = [];
|
||||
}
|
||||
|
||||
static add(time, logEntry, stack, flames) {
|
||||
const depth = stack.length;
|
||||
const newFlame = new Flame(time, logEntry, depth)
|
||||
if (depth > 0) {
|
||||
const parent = stack[depth - 1];
|
||||
newFlame.parent = parent;
|
||||
parent.children.push(newFlame);
|
||||
}
|
||||
flames.push(newFlame);
|
||||
stack.push(newFlame);
|
||||
}
|
||||
|
||||
stop(time) {
|
||||
if (this._duration !== -1) throw new Error('Already stopped');
|
||||
this._duration = time - this._time
|
||||
}
|
||||
|
||||
get time() {
|
||||
return this._time;
|
||||
}
|
||||
|
||||
get logEntry() {
|
||||
return this._logEntry;
|
||||
}
|
||||
|
||||
get startTime() {
|
||||
return this._time;
|
||||
}
|
||||
|
||||
get endTime() {
|
||||
return this._time + this._duration;
|
||||
}
|
||||
|
||||
get duration() {
|
||||
return this._duration;
|
||||
}
|
||||
|
||||
get type() {
|
||||
return TickLogEntry.extractCodeEntryType(this._logEntry?.entry);
|
||||
}
|
||||
}
|
||||
|
||||
DOM.defineCustomElement(
|
||||
'view/timeline/timeline-track', 'timeline-track-tick',
|
||||
(templateText) => class TimelineTrackTick extends TimelineTrackStackedBase {
|
||||
@ -69,45 +17,10 @@ DOM.defineCustomElement(
|
||||
}
|
||||
|
||||
_prepareDrawableItems() {
|
||||
const tmpFlames = [];
|
||||
// flameStack = [bottom, ..., top];
|
||||
const flameStack = [];
|
||||
const ticks = this._timeline.values;
|
||||
let maxDepth = 0;
|
||||
for (let tickIndex = 0; tickIndex < ticks.length; tickIndex++) {
|
||||
const tick = ticks[tickIndex];
|
||||
const tickStack = tick.stack;
|
||||
maxDepth = Math.max(maxDepth, tickStack.length);
|
||||
// tick.stack = [top, .... , bottom];
|
||||
for (let stackIndex = tickStack.length - 1; stackIndex >= 0;
|
||||
stackIndex--) {
|
||||
const codeEntry = tickStack[stackIndex];
|
||||
// codeEntry is either a CodeEntry or a raw pc.
|
||||
const logEntry = codeEntry?.logEntry;
|
||||
const flameStackIndex = tickStack.length - stackIndex - 1;
|
||||
if (flameStackIndex < flameStack.length) {
|
||||
if (flameStack[flameStackIndex].logEntry === logEntry) continue;
|
||||
for (let k = flameStackIndex; k < flameStack.length; k++) {
|
||||
flameStack[k].stop(tick.time);
|
||||
}
|
||||
flameStack.length = flameStackIndex;
|
||||
}
|
||||
Flame.add(tick.time, logEntry, flameStack, tmpFlames);
|
||||
}
|
||||
if (tickStack.length < flameStack.length) {
|
||||
for (let k = tickStack.length; k < flameStack.length; k++) {
|
||||
flameStack[k].stop(tick.time);
|
||||
}
|
||||
flameStack.length = tickStack.length;
|
||||
}
|
||||
}
|
||||
const lastTime = ticks[ticks.length - 1].time;
|
||||
for (let k = 0; k < flameStack.length; k++) {
|
||||
flameStack[k].stop(lastTime);
|
||||
}
|
||||
this._drawableItems = new Timeline(Flame, tmpFlames);
|
||||
const flameBuilder = FlameBuilder.forTime(this._timeline.values, true);
|
||||
this._drawableItems = new Timeline(Flame, flameBuilder.flames);
|
||||
this._annotations.flames = this._drawableItems;
|
||||
this._adjustStackDepth(maxDepth);
|
||||
this._adjustStackDepth(flameBuilder.maxDepth);
|
||||
}
|
||||
|
||||
_drawAnnotations(logEntry, time) {
|
||||
|
Loading…
Reference in New Issue
Block a user