Commit 7a90c320 authored by Camillo Bruni's avatar Camillo Bruni Committed by V8 LUCI CQ

[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/+/3585453Reviewed-by: 's avatarPatrick Thier <pthier@chromium.org>
Commit-Queue: Camillo Bruni <cbruni@chromium.org>
Cr-Commit-Position: refs/heads/main@{#80192}
parent d3341d11
......@@ -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,12 +49,18 @@ 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;
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,11 +46,13 @@ 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);
}
......
This diff is collapsed.
......@@ -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);
}
......
<!-- 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>
This diff is collapsed.
<!-- 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>
// 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) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment