[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:
Camillo Bruni 2022-04-26 19:18:04 +02:00 committed by V8 LUCI CQ
parent d3341d1102
commit 7a90c32032
22 changed files with 1292 additions and 137 deletions

View File

@ -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 {

View File

@ -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) {

View File

@ -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) {

View File

@ -127,7 +127,7 @@ WebInspector.SourceMap.load = function(sourceMapURL, compiledURL, callback)
WebInspector.SourceMap.prototype = {
/**
* @return {Array.<string>}
* @return {string[]}
*/
sources()
{

View File

@ -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;
}

View File

@ -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);

View File

@ -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>

View File

@ -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!'

View File

@ -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);
}
}

View File

@ -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.');
}
}
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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);
}

View 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);
}
}
}

View File

@ -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));
}

View File

@ -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);
}

View 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>

View 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)
}
});

View File

@ -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>

View 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(' ');
}
}

View File

@ -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;
}

View File

@ -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) {