[tools][system-analyzer] Display timer events

Add common TimelineTrackStackedBase base class for TimelineTrackTick
and TimelineTrackTimer for visualising stacked time ranges that only
need rescaling when zooming in.

Additional changes:
- Highlight matching registers in disassembly
- Simplify CodeLogEntry summary for script code
- Show event for array items in the property-link-table


Bug: v8:10644
Change-Id: I0b37274e12ba55f1c6251b90d39d996ffae7f37e
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2992716
Reviewed-by: Victor Gomes <victorgomes@chromium.org>
Commit-Queue: Camillo Bruni <cbruni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#75437}
This commit is contained in:
Camillo Bruni 2021-06-28 21:46:31 +02:00 committed by V8 LUCI CQ
parent 115b866443
commit 4a0921704a
18 changed files with 546 additions and 227 deletions

View File

@ -546,8 +546,8 @@ export class Profile {
addDisassemble(start, kind, disassemble) {
const entry = this.codeMap_.findDynamicEntryByStartAddress(start);
if (!entry) return;
this.getOrCreateSourceInfo(entry).setDisassemble(disassemble);
if (entry) this.getOrCreateSourceInfo(entry).setDisassemble(disassemble);
return entry;
}
getOrCreateSourceInfo(entry) {

View File

@ -19,6 +19,7 @@ class State {
_codeTimeline;
_apiTimeline;
_tickTimeline;
_timerTimeline;
_minStartTime = Number.POSITIVE_INFINITY;
_maxEndTime = Number.NEGATIVE_INFINITY;
@ -42,13 +43,14 @@ class State {
setTimelines(
mapTimeline, icTimeline, deoptTimeline, codeTimeline, apiTimeline,
tickTimeline) {
tickTimeline, timerTimeline) {
this._mapTimeline = mapTimeline;
this._icTimeline = icTimeline;
this._deoptTimeline = deoptTimeline;
this._codeTimeline = codeTimeline;
this._apiTimeline = apiTimeline;
this._tickTimeline = tickTimeline;
this._timerTimeline = timerTimeline;
for (let timeline of arguments) {
if (timeline === undefined) return;
this._minStartTime = Math.min(this._minStartTime, timeline.startTime);
@ -84,10 +86,15 @@ class State {
return this._tickTimeline;
}
get timerTimeline() {
return this._timerTimeline;
}
get timelines() {
return [
this._mapTimeline, this._icTimeline, this._deoptTimeline,
this._codeTimeline, this._apiTimeline, this._tickTimeline
this._codeTimeline, this._apiTimeline, this._tickTimeline,
this._timerTimeline
];
}

View File

@ -18,8 +18,30 @@ export function formatBytes(bytes) {
return bytes.toFixed(2) + units[index];
}
export function formatMicroSeconds(millis) {
return (millis * kMicro2Milli).toFixed(1) + 'ms';
export function formatMicroSeconds(micro) {
return (micro * kMicro2Milli).toFixed(1) + 'ms';
}
export function formatDurationMicros(micros, secondsDigits = 3) {
return formatDurationMillis(micros * kMicro2Milli, secondsDigits);
}
export function formatDurationMillis(millis, secondsDigits = 3) {
if (millis < 1000) {
if (millis < 1) {
return (millis / kMicro2Milli).toFixed(1) + 'ns';
}
return millis.toFixed(2) + 'ms';
}
let seconds = millis / 1000;
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
seconds = seconds % 60;
let buffer = ''
if (hours > 0) buffer += hours + 'h ';
if (hours > 0 || minutes > 0) buffer += minutes + 'm ';
buffer += seconds.toFixed(secondsDigits) + 's'
return buffer;
}
export function delay(time) {

View File

@ -48,6 +48,10 @@ body {
background-color: var(--background-color);
}
h3 {
margin-block-end: 0.3em;
}
section {
margin-bottom: 10px;
}

View File

@ -56,6 +56,7 @@ found in the LICENSE file. -->
<section id="container" class="initial">
<timeline-panel id="timeline-panel">
<timeline-track-tick id="tick-track" title="Samples"></timeline-track-tick>
<timeline-track-timer id="timer-track" title="Timers"></timeline-track-timer>
<timeline-track-map id="map-track" title="Map"></timeline-track-map>
<timeline-track id="ic-track" title="IC"></timeline-track>
<timeline-track id="deopt-track" title="Deopt"></timeline-track>

View File

@ -13,6 +13,7 @@ import {IcLogEntry} from './log/ic.mjs';
import {LogEntry} from './log/log.mjs';
import {MapLogEntry} from './log/map.mjs';
import {TickLogEntry} from './log/tick.mjs';
import {TimerLogEntry} from './log/timer.mjs';
import {Processor} from './processor.mjs';
import {Timeline} from './timeline.mjs'
import {FocusEvent, SelectionEvent, SelectRelatedEvent, SelectTimeEvent, ToolTipEvent,} from './view/events.mjs';
@ -35,6 +36,7 @@ class App {
deoptTrack: $('#deopt-track'),
codeTrack: $('#code-track'),
apiTrack: $('#api-track'),
timerTrack: $('#timer-track'),
icList: $('#ic-list'),
mapList: $('#map-list'),
@ -60,8 +62,15 @@ class App {
static get allEventTypes() {
return new Set([
SourcePosition, MapLogEntry, IcLogEntry, ApiLogEntry, CodeLogEntry,
DeoptLogEntry, SharedLibLogEntry, TickLogEntry
SourcePosition,
MapLogEntry,
IcLogEntry,
ApiLogEntry,
CodeLogEntry,
DeoptLogEntry,
SharedLibLogEntry,
TickLogEntry,
TimerLogEntry,
]);
}
@ -112,6 +121,7 @@ class App {
case Script:
entries = entry.entries.concat(entry.sourcePositions);
break;
case TimerLogEntry:
case ApiLogEntry:
case CodeLogEntry:
case TickLogEntry:
@ -169,6 +179,7 @@ class App {
return this.showDeoptEntries(entries);
case SharedLibLogEntry:
return this.showSharedLibEntries(entries);
case TimerLogEntry:
case TickLogEntry:
break;
default:
@ -206,6 +217,7 @@ class App {
}
showTickEntries(entries, focusView = true) {}
showTimerEntries(entries, focusView = true) {}
showSourcePositions(entries, focusView = true) {
this._view.scriptPanel.selectedSourcePositions = entries
@ -225,6 +237,7 @@ class App {
this.showCodeEntries(this._state.codeTimeline.selectionOrSelf, false);
this.showApiEntries(this._state.apiTimeline.selectionOrSelf, false);
this.showTickEntries(this._state.tickTimeline.selectionOrSelf, false);
this.showTimerEntries(this._state.timerTimeline.selectionOrSelf, false);
this._view.timelinePanel.timeSelection = {start, end};
}
@ -253,6 +266,8 @@ class App {
return this.focusDeoptLogEntry(entry);
case TickLogEntry:
return this.focusTickLogEntry(entry);
case TimerLogEntry:
return this.focusTimerLogEntry(entry);
default:
throw new Error(`Unknown selection type: ${entry.constructor?.name}`);
}
@ -304,6 +319,11 @@ class App {
this._view.tickTrack.focusedEntry = entry;
}
focusTimerLogEntry(entry) {
this._state.timerLogEntry = entry;
this._view.timerTrack.focusedEntry = entry;
}
focusSourcePosition(sourcePosition) {
if (!sourcePosition) return;
this._view.scriptPanel.focusedSourcePositions = [sourcePosition];
@ -357,9 +377,10 @@ class App {
const codeTimeline = processor.codeTimeline;
const apiTimeline = processor.apiTimeline;
const tickTimeline = processor.tickTimeline;
const timerTimeline = processor.timerTimeline;
this._state.setTimelines(
mapTimeline, icTimeline, deoptTimeline, codeTimeline, apiTimeline,
tickTimeline);
tickTimeline, timerTimeline);
this._view.mapPanel.timeline = mapTimeline;
this._view.icList.timeline = icTimeline;
this._view.mapList.timeline = mapTimeline;
@ -368,6 +389,7 @@ class App {
this._view.apiList.timeline = apiTimeline;
this._view.scriptPanel.scripts = processor.scripts;
this._view.codePanel.timeline = codeTimeline;
this._view.codePanel.timeline = codeTimeline;
this.refreshTimelineTrackView();
} catch (e) {
this._view.logFileReader.error = 'Log file contains errors!'
@ -385,6 +407,7 @@ class App {
this._view.codeTrack.data = this._state.codeTimeline;
this._view.apiTrack.data = this._state.apiTimeline;
this._view.tickTrack.data = this._state.tickTimeline;
this._view.timerTrack.data = this._state.timerTimeline;
}
}

View File

@ -61,6 +61,10 @@ export class CodeLogEntry extends LogEntry {
return this._kind;
}
get isBuiltinKind() {
return this._kindName === 'Builtin';
}
get kindName() {
return this._kindName;
}
@ -70,7 +74,7 @@ export class CodeLogEntry extends LogEntry {
}
get functionName() {
return this._entry.functionName;
return this._entry.functionName ?? this._entry.getRawName();
}
get size() {

View File

@ -0,0 +1,56 @@
// Copyright 2021 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 {formatDurationMicros} from '../helper.mjs';
import {LogEntry} from './log.mjs';
export class TimerLogEntry extends LogEntry {
constructor(type, startTime, endTime = -1) {
super(type, startTime);
this._endTime = endTime;
this.depth = 0;
}
end(time) {
if (this.isInitialized) throw new Error('Invalid timer change');
this._endTime = time;
}
get isInitialized() {
return this._endTime !== -1;
}
get startTime() {
return this._time;
}
get endTime() {
return this._endTime;
}
get duration() {
return this._endTime - this._time;
}
covers(time) {
return this._time <= time && time <= this._endTime;
}
get toolTipDict() {
const dict = super.toolTipDict;
dict.startTime = formatDurationMicros(dict.startTime);
dict.endTime = formatDurationMicros(dict.endTime);
dict.duration = formatDurationMicros(dict.duration);
return dict;
}
static get propertyNames() {
return [
'type',
'startTime',
'endTime',
'duration',
];
}
}

View File

@ -10,6 +10,7 @@ import {CodeLogEntry, DeoptLogEntry, SharedLibLogEntry} from './log/code.mjs';
import {IcLogEntry} from './log/ic.mjs';
import {Edge, MapLogEntry} from './log/map.mjs';
import {TickLogEntry} from './log/tick.mjs';
import {TimerLogEntry} from './log/timer.mjs';
import {Timeline} from './timeline.mjs';
// ===========================================================================
@ -22,6 +23,7 @@ export class Processor extends LogReader {
_icTimeline = new Timeline();
_mapTimeline = new Timeline();
_tickTimeline = new Timeline();
_timerTimeline = new Timeline();
_formatPCRegexp = /(.*):[0-9]+:[0-9]+$/;
_lastTimestamp = 0;
_lastCodeLogEntry;
@ -93,8 +95,14 @@ export class Processor extends LogReader {
'active-runtime-timer': undefined,
'heap-sample-begin': undefined,
'heap-sample-end': undefined,
'timer-event-start': undefined,
'timer-event-end': undefined,
'timer-event-start': {
parsers: [parseString, parseInt],
processor: this.processTimerEventStart
},
'timer-event-end': {
parsers: [parseString, parseInt],
processor: this.processTimerEventEnd
},
'map-create':
{parsers: [parseInt, parseString], processor: this.processMapCreate},
'map': {
@ -479,6 +487,24 @@ export class Processor extends LogReader {
new ApiLogEntry(type, this._lastTimestamp, name, arg1));
}
processTimerEventStart(type, time) {
const entry = new TimerLogEntry(type, time);
this._timerTimeline.push(entry);
}
processTimerEventEnd(type, time) {
// Timer-events are infrequent, and not deeply nested, doing a linear walk
// is usually good enough.
for (let i = this._timerTimeline.length - 1; i >= 0; i--) {
const timer = this._timerTimeline.at(i);
if (timer.type == type && !timer.isInitialized) {
timer.end(time);
return;
}
}
console.error('Couldn\'t find matching timer event start', {type, time});
}
get icTimeline() {
return this._icTimeline;
}
@ -503,6 +529,10 @@ export class Processor extends LogReader {
return this._tickTimeline;
}
get timerTimeline() {
return this._timerTimeline;
}
get scripts() {
return this._profile.scripts_.filter(script => script !== undefined);
}

View File

@ -4,11 +4,24 @@ found in the LICENSE file. -->
<head>
<link href="./index.css" rel="stylesheet">
</head>
<style>
#sourceCode {
white-space: pre-line;
}
.register {
border-bottom: 1px dashed;
border-radius: 2px;
}
.register:hover {
background-color: var(--border-color);
}
.register.selected {
color: var(--default-color);
background-color: var(--border-color);
}
</style>
<div class="panel">
<input type="checkbox" id="closer" class="panelCloserInput" checked>
<label class="panelCloserLabel" for="closer"></label>

View File

@ -4,6 +4,12 @@
import {SelectRelatedEvent} from './events.mjs';
import {CollapsableElement, DOM, formatBytes, formatMicroSeconds} from './helper.mjs';
const kRegisters = ['rsp', 'rbp', 'rax', 'rbx', 'rcx', 'rdx', 'rsi', 'rdi'];
// Add Interpreter and x64 registers
for (let i = 0; i < 14; i++) {
kRegisters.push(`r${i}`);
}
DOM.defineCustomElement('view/code-panel',
(templateText) =>
class CodePanel extends CollapsableElement {
@ -13,6 +19,11 @@ DOM.defineCustomElement('view/code-panel',
constructor() {
super(templateText);
this._codeSelectNode = this.$('#codeSelect');
this._disassemblyNode = this.$('#disassembly');
this._sourceNode = this.$('#sourceCode');
this._registerSelector = new RegisterSelector(this._disassemblyNode);
this._codeSelectNode.onchange = this._handleSelectCode.bind(this);
this.$('#selectedRelatedButton').onclick =
this._handleSelectRelated.bind(this)
@ -41,7 +52,7 @@ DOM.defineCustomElement('view/code-panel',
script: entry.script,
type: entry.type,
kind: entry.kindName,
variants: entry.variants,
variants: entry.variants.length > 1 ? entry.variants : undefined,
};
} else {
this.$('#properties').propertyDict = {};
@ -49,24 +60,34 @@ DOM.defineCustomElement('view/code-panel',
this.requestUpdate();
}
get _disassemblyNode() {
return this.$('#disassembly');
}
get _sourceNode() {
return this.$('#sourceCode');
}
get _codeSelectNode() {
return this.$('#codeSelect');
}
_update() {
this._updateSelect();
this._disassemblyNode.innerText = this._entry?.code ?? '';
this._formatDisassembly();
this._sourceNode.innerText = this._entry?.source ?? '';
}
_formatDisassembly() {
if (!this._entry?.code) {
this._disassemblyNode.innerText = '';
return;
}
const rawCode = this._entry?.code;
try {
this._disassemblyNode.innerText = rawCode;
let formattedCode = this._disassemblyNode.innerHTML;
for (let register of kRegisters) {
const button = `<span class="register ${register}">${register}</span>`
formattedCode = formattedCode.replaceAll(register, button);
}
// Let's replace the base-address since it doesn't add any value.
// TODO
this._disassemblyNode.innerHTML = formattedCode;
} catch (e) {
console.error(e);
this._disassemblyNode.innerText = rawCode;
}
}
_updateSelect() {
const select = this._codeSelectNode;
if (select.data === this._selectedEntries) return;
@ -76,13 +97,19 @@ DOM.defineCustomElement('view/code-panel',
this._selectedEntries.slice().sort((a, b) => a.time - b.time);
for (const code of this._selectedEntries) {
const option = DOM.element('option');
option.text =
`${code.functionName}(...) t=${formatMicroSeconds(code.time)} size=${
formatBytes(code.size)} script=${code.script?.toString()}`;
option.text = this._entrySummary(code);
option.data = code;
select.add(option);
}
}
_entrySummary(code) {
if (code.isBuiltinKind) {
return `${code.functionName}(...) t=${
formatMicroSeconds(code.time)} size=${formatBytes(code.size)}`;
}
return `${code.functionName}(...) t=${formatMicroSeconds(code.time)} size=${
formatBytes(code.size)} script=${code.script?.toString()}`;
}
_handleSelectCode() {
this.entry = this._codeSelectNode.selectedOptions[0].data;
@ -92,4 +119,37 @@ DOM.defineCustomElement('view/code-panel',
if (!this._entry) return;
this.dispatchEvent(new SelectRelatedEvent(this._entry));
}
});
});
class RegisterSelector {
_currentRegister;
constructor(node) {
this._node = node;
this._node.onmousemove = this._handleDisassemblyMouseMove.bind(this);
}
_handleDisassemblyMouseMove(event) {
const target = event.target;
if (!target.classList.contains('register')) {
this._clear();
return;
};
this._select(target.innerText);
}
_clear() {
if (this._currentRegister == undefined) return;
for (let node of this._node.querySelectorAll('.register')) {
node.classList.remove('selected');
}
}
_select(register) {
if (register == this._currentRegister) return;
this._clear();
this._currentRegister = register;
for (let node of this._node.querySelectorAll(`.register.${register}`)) {
node.classList.add('selected');
}
}
}

View File

@ -11,11 +11,27 @@ found in the LICENSE file. -->
}
.properties > tbody > tr > td:nth-child(2n+1):after {
content: ':';
}
.properties > tbody > tr > td:nth-child(2n+1) {
padding-right: 3px;
}
.properties > tbody > tr > td:nth-child(2n+2) {
width: 100%;
}
.properties > tfoot {
text-align: right;
}
.properties {
min-width: 350px;
border-collapse: collapse;
}
h3 {
margin-block-start: 0em;
}
</style>
<div id="body">

View File

@ -14,6 +14,7 @@ DOM.defineCustomElement(
_instanceLinkButtons = false;
_logEntryClickHandler = this._handleLogEntryClick.bind(this);
_logEntryRelatedHandler = this._handleLogEntryRelated.bind(this);
_arrayValueSelectHandler = this._handleArrayValueSelect.bind(this);
constructor() {
super(template);
@ -78,6 +79,7 @@ DOM.defineCustomElement(
return DOM.text(`${array.length} items`);
}
const select = DOM.element('select');
select.onchange = this._arrayValueSelectHandler;
for (let value of array) {
const option = DOM.element('option');
option.innerText = value.toString();
@ -106,11 +108,15 @@ DOM.defineCustomElement(
showRelatedButton.data = this._instance;
}
_handleLogEntryClick(e) {
this.dispatchEvent(new FocusEvent(e.currentTarget.data));
_handleArrayValueSelect(event) {
const logEntry = event.currentTarget.selectedOptions[0].data;
this.dispatchEvent(new FocusEvent(logEntry));
}
_handleLogEntryClick(event) {
this.dispatchEvent(new FocusEvent(event.currentTarget.data));
}
_handleLogEntryRelated(e) {
this.dispatchEvent(new SelectRelatedEvent(e.currentTarget.data));
_handleLogEntryRelated(event) {
this.dispatchEvent(new SelectRelatedEvent(event.currentTarget.data));
}
});

View File

@ -5,6 +5,7 @@
import './timeline/timeline-track.mjs';
import './timeline/timeline-track-map.mjs';
import './timeline/timeline-track-tick.mjs';
import './timeline/timeline-track-timer.mjs';
import {SynchronizeSelectionEvent} from './events.mjs';
import {DOM, V8CustomElement} from './helper.mjs';

View File

@ -326,7 +326,7 @@ export class TimelineTrackBase extends V8CustomElement {
_handleMouseMove(event) {
if (event.button !== 0) return;
if (this._selectionHandler.isSelecting) return false;
if (this.isLocked) {
if (this.isLocked && this._focusedEntry) {
this._updateToolTip(event);
return false;
}

View File

@ -0,0 +1,144 @@
// Copyright 2021 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 {delay} from '../../helper.mjs';
import {Timeline} from '../../timeline.mjs';
import {SelectTimeEvent} from '../events.mjs';
import {CSSColor, DOM, SVG} from '../helper.mjs';
import {TimelineTrackBase} from './timeline-track-base.mjs'
const kItemHeight = 8;
export class TimelineTrackStackedBase extends TimelineTrackBase {
_originalContentWidth = 0;
_drawableItems = new Timeline();
_updateChunks() {
// We don't need to update the chunks here.
this._updateDimensions();
this.requestUpdate();
}
set data(timeline) {
super.data = timeline;
this._contentWidth = 0;
this._prepareDrawableItems();
}
_handleDoubleClick(event) {
if (event.button !== 0) return;
this._selectionHandler.clearSelection();
const item = this._getDrawableItemForEvent(event);
if (item === undefined) return;
event.stopImmediatePropagation();
this.dispatchEvent(new SelectTimeEvent(item.startTime, item.endTime));
return false;
}
_getStackDepthForEvent(event) {
return Math.floor(event.layerY / kItemHeight) - 1;
}
_getDrawableItemForEvent(event) {
const depth = this._getStackDepthForEvent(event);
const time = this.positionToTime(event.pageX);
const index = this._drawableItems.find(time);
for (let i = index - 1; i > 0; i--) {
const item = this._drawableItems.at(i);
if (item.depth != depth) continue;
if (item.endTime < time) continue;
return item;
}
return undefined;
}
_drawableItemToLogEntry(item) {
return item;
}
_getEntryForEvent(event) {
const item = this._getDrawableItemForEvent(event);
const logEntry = this._drawableItemToLogEntry(item);
if (item === undefined) return undefined;
const style = this.toolTipTargetNode.style;
style.left = `${event.layerX}px`;
style.top = `${(item.depth + 1) * kItemHeight}px`;
style.height = `${kItemHeight}px`
return logEntry;
}
_prepareDrawableItems() {
// Subclass responsibility.
}
_adjustStackDepth(maxDepth) {
// Account for empty top line
maxDepth++;
this._adjustHeight(maxDepth * kItemHeight);
}
_scaleContent(currentWidth) {
if (this._originalContentWidth == 0) return;
// Instead of repainting just scale the content.
const ratio = currentWidth / this._originalContentWidth;
this._scalableContentNode.style.transform = `scale(${ratio}, 1)`;
this.style.setProperty('--txt-scale', `scale(${1 / ratio}, 1)`);
}
async _drawContent() {
if (this._originalContentWidth > 0) return;
this._originalContentWidth = parseInt(this.timelineMarkersNode.style.width);
this._scalableContentNode.innerHTML = '';
let buffer = '';
const add = async () => {
const svg = SVG.svg();
svg.innerHTML = buffer;
this._scalableContentNode.appendChild(svg);
buffer = '';
await delay(50);
};
const items = this._drawableItems.values;
for (let i = 0; i < items.length; i++) {
if ((i % 3000) == 0) await add();
buffer += this._drawItem(items[i], i);
}
add();
}
_drawItem(item, i, outline = false) {
const x = this.timeToPosition(item.time);
const y = (item.depth + 1) * kItemHeight;
let width = item.duration * this._timeToPixel;
if (outline) {
return `<rect x=${x} y=${y} width=${width} height=${
kItemHeight - 1} class=flameSelected />`;
}
let color = this._legend.colorForType(item.type);
if (i % 2 == 1) {
color = CSSColor.darken(color, 20);
}
return `<rect x=${x} y=${y} width=${width} height=${kItemHeight - 1} fill=${
color} class=flame />`;
}
_drawItemText(item) {
const type = item.type;
const kHeight = 9;
const x = this.timeToPosition(item.time);
const y = item.depth * (kHeight + 1);
let width = item.duration * this._timeToPixel;
width -= width * 0.1;
let buffer = '';
if (width < 15 || type == 'Other') return buffer;
const rawName = item.entry.getName();
if (rawName.length == 0) return buffer;
const kChartWidth = 5;
const maxChars = Math.floor(width / kChartWidth)
const text = rawName.substr(0, maxChars);
buffer += `<text x=${x + 1} y=${y - 3} class=txt>${text}</text>`
return buffer;
}
}

View File

@ -5,28 +5,23 @@
import {delay} from '../../helper.mjs';
import {TickLogEntry} from '../../log/tick.mjs';
import {Timeline} from '../../timeline.mjs';
import {SelectTimeEvent} from '../events.mjs';
import {CSSColor, DOM, SVG} from '../helper.mjs';
import {DOM, SVG} from '../helper.mjs';
import {TimelineTrackBase} from './timeline-track-base.mjs'
const kFlameHeight = 8;
import {TimelineTrackStackedBase} from './timeline-track-stacked-base.mjs'
class Flame {
constructor(time, entry, depth, id) {
this.time = time;
this.entry = entry;
constructor(time, logEntry, depth) {
this._time = time;
this._logEntry = logEntry;
this.depth = depth;
this.id = id;
this.duration = -1;
this._duration = -1;
this.parent = undefined;
this.children = [];
}
static add(time, entry, stack, flames) {
static add(time, logEntry, stack, flames) {
const depth = stack.length;
const id = flames.length;
const newFlame = new Flame(time, entry, depth, id)
const newFlame = new Flame(time, logEntry, depth)
if (depth > 0) {
const parent = stack[depth - 1];
newFlame.parent = parent;
@ -37,204 +32,100 @@ class Flame {
}
stop(time) {
this.duration = time - this.time
if (this._duration !== -1) throw new Error('Already stopped');
this._duration = time - this._time
}
get start() {
return this.time;
get time() {
return this._time;
}
get end() {
return this.time + this.duration;
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.entry);
return TickLogEntry.extractCodeEntryType(this._logEntry?.entry);
}
}
DOM.defineCustomElement('view/timeline/timeline-track', 'timeline-track-tick',
(templateText) =>
class TimelineTrackTick extends TimelineTrackBase {
_flames = new Timeline();
_originalContentWidth = 0;
DOM.defineCustomElement(
'view/timeline/timeline-track', 'timeline-track-tick',
(templateText) => class TimelineTrackTick extends TimelineTrackStackedBase {
constructor() {
super(templateText);
this._annotations = new Annotations(this);
}
constructor() {
super(templateText);
this._annotations = new Annotations(this);
}
_prepareDrawableItems() {
const tmpFlames = [];
// flameStack = [bottom, ..., top];
const flameStack = [];
const ticks = this._timeline.values;
let maxDepth = 0;
_updateChunks() {
// We don't need to update the chunks here.
this._updateDimensions();
this.requestUpdate();
}
set data(timeline) {
super.data = timeline;
this._contentWidth = 0;
this._updateFlames();
}
_handleDoubleClick(event) {
if (event.button !== 0) return;
this._selectionHandler.clearSelection();
const flame = this._getFlameForEvent(event);
if (flame === undefined) return;
event.stopImmediatePropagation();
this.dispatchEvent(new SelectTimeEvent(flame.start, flame.end));
return false;
}
_getFlameDepthForEvent(event) {
return Math.floor(event.layerY / kFlameHeight) - 1;
}
_getFlameForEvent(event) {
const depth = this._getFlameDepthForEvent(event);
const time = this.positionToTime(event.pageX);
const index = this._flames.find(time);
for (let i = index - 1; i > 0; i--) {
const flame = this._flames.at(i);
if (flame.depth != depth) continue;
if (flame.end < time) continue;
return flame;
}
return undefined;
}
_getEntryForEvent(event) {
const depth = this._getFlameDepthForEvent(event);
const time = this.positionToTime(event.pageX);
const index = this._timeline.find(time);
const tick = this._timeline.at(index);
let stack = tick.stack;
if (index > 0 && tick.time > time) {
stack = this._timeline.at(index - 1).stack;
}
// tick.stack = [top, ...., bottom];
const logEntry = stack[stack.length - depth - 1]?.logEntry ?? false;
// Filter out raw pc entries.
if (typeof logEntry == 'number' || logEntry === false) return false;
this.toolTipTargetNode.style.left = `${event.layerX}px`;
this.toolTipTargetNode.style.top = `${(depth + 2) * kFlameHeight}px`;
return logEntry;
}
_updateFlames() {
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];
maxDepth = Math.max(maxDepth, tick.stack.length);
// tick.stack = [top, .... , bottom];
for (let stackIndex = tick.stack.length - 1; stackIndex >= 0;
stackIndex--) {
const entry = tick.stack[stackIndex];
const flameStackIndex = tick.stack.length - stackIndex - 1;
if (flameStackIndex < flameStack.length) {
if (flameStack[flameStackIndex].entry === entry) continue;
for (let k = flameStackIndex; k < flameStack.length; k++) {
flameStack[k].stop(tick.time);
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;
}
flameStack.length = flameStackIndex;
}
Flame.add(tick.time, entry, flameStack, tmpFlames);
}
if (tick.stack.length < flameStack.length) {
for (let k = tick.stack.length; k < flameStack.length; k++) {
flameStack[k].stop(tick.time);
const lastTime = ticks[ticks.length - 1].time;
for (let k = 0; k < flameStack.length; k++) {
flameStack[k].stop(lastTime);
}
flameStack.length = tick.stack.length;
this._drawableItems = new Timeline(Flame, tmpFlames);
this._annotations.flames = this._drawableItems;
this._adjustStackDepth(maxDepth);
}
}
const lastTime = ticks[ticks.length - 1].time;
for (let k = 0; k < flameStack.length; k++) {
flameStack[k].stop(lastTime);
}
this._flames = new Timeline(Flame, tmpFlames);
this._annotations.flames = this._flames;
// Account for empty top line
maxDepth++;
this._adjustHeight(maxDepth * kFlameHeight);
}
_scaleContent(currentWidth) {
if (this._originalContentWidth == 0) return;
// Instead of repainting just scale the flames
const ratio = currentWidth / this._originalContentWidth;
this._scalableContentNode.style.transform = `scale(${ratio}, 1)`;
this.style.setProperty('--txt-scale', `scale(${1 / ratio}, 1)`);
}
async _drawContent() {
if (this._originalContentWidth > 0) return;
this._originalContentWidth = parseInt(this.timelineMarkersNode.style.width);
this._scalableContentNode.innerHTML = '';
let buffer = '';
const add = () => {
const svg = SVG.svg();
svg.innerHTML = buffer;
this._scalableContentNode.appendChild(svg);
buffer = '';
};
const rawFlames = this._flames.values;
for (let i = 0; i < rawFlames.length; i++) {
if ((i % 3000) == 0) {
add();
await delay(50);
_drawAnnotations(logEntry, time) {
if (time === undefined) {
time = this.relativePositionToTime(this._timelineScrollLeft);
}
this._annotations.update(logEntry, time);
}
buffer += this.drawFlame(rawFlames[i], i);
}
add();
}
drawFlame(flame, i, outline = false) {
const x = this.timeToPosition(flame.time);
const y = (flame.depth + 1) * kFlameHeight;
let width = flame.duration * this._timeToPixel;
if (outline) {
return `<rect x=${x} y=${y} width=${width} height=${
kFlameHeight - 1} class=flameSelected />`;
}
let color = this._legend.colorForType(flame.type);
if (i % 2 == 1) {
color = CSSColor.darken(color, 20);
}
return `<rect x=${x} y=${y} width=${width} height=${
kFlameHeight - 1} fill=${color} class=flame />`;
}
drawFlameText(flame) {
let type = flame.type;
const kHeight = 9;
const x = this.timeToPosition(flame.time);
const y = flame.depth * (kHeight + 1);
let width = flame.duration * this._timeToPixel;
width -= width * 0.1;
let buffer = '';
if (width < 15 || type == 'Other') return buffer;
const rawName = flame.entry.getName();
if (rawName.length == 0) return buffer;
const kChartWidth = 5;
const maxChars = Math.floor(width / kChartWidth)
const text = rawName.substr(0, maxChars);
buffer += `<text x=${x + 1} y=${y - 3} class=txt>${text}</text>`
return buffer;
}
_drawAnnotations(logEntry, time) {
if (time === undefined) {
time = this.relativePositionToTime(this._timelineScrollLeft);
}
this._annotations.update(logEntry, time);
}
})
_drawableItemToLogEntry(flame) {
const logEntry = flame?.logEntry;
if (logEntry === undefined || typeof logEntry == 'number')
return undefined;
return logEntry;
}
})
class Annotations {
_flames;

View File

@ -0,0 +1,41 @@
// Copyright 2021 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 {CSSColor, DOM, SVG, V8CustomElement} from '../helper.mjs';
import {TimelineTrackBase} from './timeline-track-base.mjs'
import {TimelineTrackStackedBase} from './timeline-track-stacked-base.mjs'
DOM.defineCustomElement(
'view/timeline/timeline-track', 'timeline-track-timer',
(templateText) =>
class TimelineTrackTimer extends TimelineTrackStackedBase {
constructor() {
super(templateText);
}
_prepareDrawableItems() {
const stack = [];
let maxDepth = 0;
for (let i = 0; i < this._timeline.length; i++) {
const timer = this._timeline.at(i);
let insertDepth = -1;
for (let depth = 0; depth < stack.length; depth++) {
const pendingTimer = stack[depth];
if (pendingTimer === undefined) {
if (insertDepth === -1) insertDepth = depth;
} else if (pendingTimer.endTime <= timer.startTime) {
stack[depth] == undefined;
if (insertDepth === -1) insertDepth = depth;
}
}
if (insertDepth === -1) insertDepth = stack.length;
stack[insertDepth] = timer;
timer.depth = insertDepth;
maxDepth = Math.max(maxDepth, insertDepth);
}
this._drawableItems = this._timeline;
this._adjustStackDepth(maxDepth++);
}
});