[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:
parent
115b866443
commit
4a0921704a
@ -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) {
|
||||
|
@ -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
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -48,6 +48,10 @@ body {
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-block-end: 0.3em;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
56
tools/system-analyzer/log/timer.mjs
Normal file
56
tools/system-analyzer/log/timer.mjs
Normal 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',
|
||||
];
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
@ -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">
|
||||
|
@ -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));
|
||||
}
|
||||
});
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
41
tools/system-analyzer/view/timeline/timeline-track-timer.mjs
Normal file
41
tools/system-analyzer/view/timeline/timeline-track-timer.mjs
Normal 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++);
|
||||
}
|
||||
});
|
Loading…
Reference in New Issue
Block a user