8b1cfdf682
- Debounce creating tooltips to declutter the UI - CTRL-mouse move causes immediate tooltips - Use icons and help text on tooltip buttons - Recreate tooltip target nodes in timeline views to avoid moving the existing tooltip if the update is debounced Change-Id: I65a885827ebfeafc09c1c08e2cfe9c2dd448edca Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/4012720 Commit-Queue: Camillo Bruni <cbruni@chromium.org> Reviewed-by: Marja Hölttä <marja@chromium.org> Cr-Commit-Position: refs/heads/main@{#84120}
458 lines
14 KiB
JavaScript
458 lines
14 KiB
JavaScript
// Copyright 2020 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 {Script, SourcePosition} from '../profile.mjs';
|
|
|
|
import {State} from './app-model.mjs';
|
|
import {CodeLogEntry, DeoptLogEntry, SharedLibLogEntry} from './log/code.mjs';
|
|
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 {FocusEvent, SelectionEvent, SelectRelatedEvent, SelectTimeEvent, ToolTipEvent,} from './view/events.mjs';
|
|
import {$, groupBy} from './view/helper.mjs';
|
|
|
|
class App {
|
|
_state;
|
|
_view;
|
|
_navigation;
|
|
_startupPromise;
|
|
constructor() {
|
|
this._view = {
|
|
__proto__: null,
|
|
logFileReader: $('#log-file-reader'),
|
|
|
|
timelinePanel: $('#timeline-panel'),
|
|
tickTrack: $('#tick-track'),
|
|
mapTrack: $('#map-track'),
|
|
icTrack: $('#ic-track'),
|
|
deoptTrack: $('#deopt-track'),
|
|
codeTrack: $('#code-track'),
|
|
timerTrack: $('#timer-track'),
|
|
|
|
icList: $('#ic-list'),
|
|
mapList: $('#map-list'),
|
|
codeList: $('#code-list'),
|
|
deoptList: $('#deopt-list'),
|
|
|
|
mapPanel: $('#map-panel'),
|
|
codePanel: $('#code-panel'),
|
|
profilerPanel: $('#profiler-panel'),
|
|
scriptPanel: $('#script-panel'),
|
|
|
|
toolTip: $('#tool-tip'),
|
|
};
|
|
this._view.logFileReader.addEventListener(
|
|
'fileuploadstart', this.handleFileUploadStart.bind(this));
|
|
this._view.logFileReader.addEventListener(
|
|
'fileuploadchunk', this.handleFileUploadChunk.bind(this));
|
|
this._view.logFileReader.addEventListener(
|
|
'fileuploadend', this.handleFileUploadEnd.bind(this));
|
|
this._startupPromise = this._loadCustomElements();
|
|
this._view.codeTrack.svg = true;
|
|
}
|
|
|
|
static get allEventTypes() {
|
|
return new Set([
|
|
SourcePosition,
|
|
MapLogEntry,
|
|
IcLogEntry,
|
|
CodeLogEntry,
|
|
DeoptLogEntry,
|
|
SharedLibLogEntry,
|
|
TickLogEntry,
|
|
TimerLogEntry,
|
|
]);
|
|
}
|
|
|
|
async _loadCustomElements() {
|
|
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();
|
|
}
|
|
|
|
_addEventListeners() {
|
|
document.addEventListener(
|
|
'keydown', e => this._navigation?.handleKeyDown(e));
|
|
document.addEventListener(
|
|
SelectRelatedEvent.name, this.handleSelectRelatedEntries.bind(this));
|
|
document.addEventListener(
|
|
SelectionEvent.name, this.handleSelectEntries.bind(this))
|
|
document.addEventListener(
|
|
FocusEvent.name, this.handleFocusLogEntry.bind(this));
|
|
document.addEventListener(
|
|
SelectTimeEvent.name, this.handleTimeRangeSelect.bind(this));
|
|
document.addEventListener(ToolTipEvent.name, this.handleToolTip.bind(this));
|
|
}
|
|
|
|
handleSelectRelatedEntries(e) {
|
|
e.stopImmediatePropagation();
|
|
this.selectRelatedEntries(e.entry);
|
|
}
|
|
|
|
selectRelatedEntries(entry) {
|
|
let entries = [entry];
|
|
switch (entry.constructor) {
|
|
case SourcePosition:
|
|
entries = entries.concat(entry.entries);
|
|
break;
|
|
case MapLogEntry:
|
|
entries = this._state.icTimeline.filter(each => each.map === entry);
|
|
break;
|
|
case IcLogEntry:
|
|
if (entry.map) entries.push(entry.map);
|
|
break;
|
|
case DeoptLogEntry:
|
|
// TODO select map + code entries
|
|
if (entry.fileSourcePosition) entries.push(entry.fileSourcePosition);
|
|
break;
|
|
case Script:
|
|
entries = entry.entries.concat(entry.sourcePositions);
|
|
break;
|
|
case TimerLogEntry:
|
|
case CodeLogEntry:
|
|
case TickLogEntry:
|
|
case SharedLibLogEntry:
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown selection type: ${entry.constructor?.name}`);
|
|
}
|
|
if (entry.sourcePosition) {
|
|
entries.push(entry.sourcePosition);
|
|
// TODO: find the matching Code log entries.
|
|
}
|
|
this.selectEntries(entries);
|
|
}
|
|
|
|
static isClickable(object) {
|
|
if (typeof object !== 'object') return false;
|
|
if (object instanceof LogEntry) return true;
|
|
if (object instanceof SourcePosition) return true;
|
|
if (object instanceof Script) return true;
|
|
return false;
|
|
}
|
|
|
|
handleSelectEntries(e) {
|
|
e.stopImmediatePropagation();
|
|
this.selectEntries(e.entries);
|
|
}
|
|
|
|
selectEntries(entries) {
|
|
const missingTypes = App.allEventTypes;
|
|
groupBy(entries, each => each.constructor, true).forEach(group => {
|
|
this.selectEntriesOfSingleType(group.entries);
|
|
missingTypes.delete(group.key);
|
|
});
|
|
missingTypes.forEach(
|
|
type => this.selectEntriesOfSingleType([], type, false));
|
|
}
|
|
|
|
selectEntriesOfSingleType(entries, type, focusView = true) {
|
|
const entryType = entries[0]?.constructor ?? type;
|
|
switch (entryType) {
|
|
case Script:
|
|
entries = entries.flatMap(script => script.sourcePositions);
|
|
return this.showSourcePositions(entries, focusView);
|
|
case SourcePosition:
|
|
return this.showSourcePositions(entries, focusView);
|
|
case MapLogEntry:
|
|
return this.showMapEntries(entries, focusView);
|
|
case IcLogEntry:
|
|
return this.showIcEntries(entries, focusView);
|
|
case CodeLogEntry:
|
|
return this.showCodeEntries(entries, focusView);
|
|
case DeoptLogEntry:
|
|
return this.showDeoptEntries(entries, focusView);
|
|
case SharedLibLogEntry:
|
|
return this.showSharedLibEntries(entries, focusView);
|
|
case TimerLogEntry:
|
|
case TickLogEntry:
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown selection type: ${entryType?.name}`);
|
|
}
|
|
}
|
|
|
|
showMapEntries(entries, focusView = true) {
|
|
this._view.mapPanel.selectedLogEntries = entries;
|
|
this._view.mapList.selectedLogEntries = entries;
|
|
if (focusView) this._view.mapPanel.show();
|
|
}
|
|
|
|
showIcEntries(entries, focusView = true) {
|
|
this._view.icList.selectedLogEntries = entries;
|
|
if (focusView) this._view.icList.show();
|
|
}
|
|
|
|
showDeoptEntries(entries, focusView = true) {
|
|
this._view.deoptList.selectedLogEntries = entries;
|
|
if (focusView) this._view.deoptList.show();
|
|
}
|
|
|
|
showSharedLibEntries(entries, focusView = true) {}
|
|
|
|
showCodeEntries(entries, focusView = true) {
|
|
this._view.codePanel.selectedEntries = entries;
|
|
this._view.codeList.selectedLogEntries = entries;
|
|
if (focusView) this._view.codePanel.show();
|
|
}
|
|
|
|
showTickEntries(entries, focusView = true) {
|
|
this._view.profilerPanel.selectedLogEntries = entries;
|
|
if (focusView) this._view.profilerPanel.show();
|
|
}
|
|
|
|
showTimerEntries(entries, focusView = true) {}
|
|
|
|
showSourcePositions(entries, focusView = true) {
|
|
this._view.scriptPanel.selectedSourcePositions = entries
|
|
if (focusView) this._view.scriptPanel.show();
|
|
}
|
|
|
|
handleTimeRangeSelect(e) {
|
|
e.stopImmediatePropagation();
|
|
this.selectTimeRange(e.start, e.end, e.focus, e.zoom);
|
|
}
|
|
|
|
selectTimeRange(start, end, focus = false, zoom = false) {
|
|
this._state.selectTimeRange(start, end);
|
|
this.showMapEntries(this._state.mapTimeline.selectionOrSelf, false);
|
|
this.showIcEntries(this._state.icTimeline.selectionOrSelf, false);
|
|
this.showDeoptEntries(this._state.deoptTimeline.selectionOrSelf, false);
|
|
this.showCodeEntries(this._state.codeTimeline.selectionOrSelf, false);
|
|
this.showTickEntries(this._state.tickTimeline.selectionOrSelf, false);
|
|
this.showTimerEntries(this._state.timerTimeline.selectionOrSelf, false);
|
|
this._view.timelinePanel.timeSelection = {start, end, focus, zoom};
|
|
}
|
|
|
|
handleFocusLogEntry(e) {
|
|
e.stopImmediatePropagation();
|
|
this.focusLogEntry(e.entry);
|
|
}
|
|
|
|
focusLogEntry(entry) {
|
|
switch (entry.constructor) {
|
|
case Script:
|
|
return this.focusSourcePosition(entry.sourcePositions[0]);
|
|
case SourcePosition:
|
|
return this.focusSourcePosition(entry);
|
|
case MapLogEntry:
|
|
return this.focusMapLogEntry(entry);
|
|
case IcLogEntry:
|
|
return this.focusIcLogEntry(entry);
|
|
case CodeLogEntry:
|
|
return this.focusCodeLogEntry(entry);
|
|
case DeoptLogEntry:
|
|
return this.focusDeoptLogEntry(entry);
|
|
case SharedLibLogEntry:
|
|
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}`);
|
|
}
|
|
}
|
|
|
|
focusMapLogEntry(entry, focusSourcePosition = true) {
|
|
this._state.map = entry;
|
|
this._view.mapTrack.focusedEntry = entry;
|
|
this._view.mapPanel.map = entry;
|
|
if (focusSourcePosition) {
|
|
this.focusCodeLogEntry(entry.code, false);
|
|
this.focusSourcePosition(entry.sourcePosition);
|
|
}
|
|
this._view.mapPanel.show();
|
|
}
|
|
|
|
focusIcLogEntry(entry) {
|
|
this._state.ic = entry;
|
|
this.focusMapLogEntry(entry.map, false);
|
|
this.focusCodeLogEntry(entry.code, false);
|
|
this.focusSourcePosition(entry.sourcePosition);
|
|
}
|
|
|
|
focusCodeLogEntry(entry, focusSourcePosition = true) {
|
|
this._state.code = entry;
|
|
this._view.codePanel.entry = entry;
|
|
if (focusSourcePosition) this.focusSourcePosition(entry.sourcePosition);
|
|
this._view.codePanel.show();
|
|
}
|
|
|
|
focusDeoptLogEntry(entry) {
|
|
this._state.deoptLogEntry = entry;
|
|
this.focusCodeLogEntry(entry.code, false);
|
|
this.focusSourcePosition(entry.sourcePosition);
|
|
}
|
|
|
|
focusSharedLibLogEntry(entry) {
|
|
// no-op.
|
|
}
|
|
|
|
focusTickLogEntry(entry) {
|
|
this._state.tickLogEntry = entry;
|
|
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];
|
|
this._view.scriptPanel.show();
|
|
}
|
|
|
|
handleToolTip(event) {
|
|
let content = event.content;
|
|
if (typeof content !== 'string' &&
|
|
!(content?.nodeType && content?.nodeName)) {
|
|
content = content?.toolTipDict;
|
|
}
|
|
if (!content) {
|
|
throw new Error(
|
|
`Unknown tooltip content type: ${content.constructor?.name}`);
|
|
}
|
|
this._view.toolTip.data = {
|
|
content: content,
|
|
positionOrTargetNode: event.positionOrTargetNode,
|
|
immediate: event.immediate,
|
|
};
|
|
}
|
|
|
|
restartApp() {
|
|
this._state = new State();
|
|
this._navigation = new Navigation(this._state, this._view);
|
|
}
|
|
|
|
handleFileUploadStart(e) {
|
|
this.restartApp();
|
|
$('#container').className = 'initial';
|
|
this._processor = new Processor();
|
|
this._processor.setProgressCallback(
|
|
e.detail.totalSize, e.detail.progressCallback);
|
|
}
|
|
|
|
async handleFileUploadChunk(e) {
|
|
this._processor.processChunk(e.detail);
|
|
}
|
|
|
|
async handleFileUploadEnd(e) {
|
|
try {
|
|
const processor = this._processor;
|
|
await processor.finalize();
|
|
await this._startupPromise;
|
|
|
|
this._state.profile = processor.profile;
|
|
const mapTimeline = processor.mapTimeline;
|
|
const icTimeline = processor.icTimeline;
|
|
const deoptTimeline = processor.deoptTimeline;
|
|
const codeTimeline = processor.codeTimeline;
|
|
const tickTimeline = processor.tickTimeline;
|
|
const timerTimeline = processor.timerTimeline;
|
|
this._state.setTimelines(
|
|
mapTimeline, icTimeline, deoptTimeline, codeTimeline, tickTimeline,
|
|
timerTimeline);
|
|
this._view.mapPanel.timeline = mapTimeline;
|
|
this._view.icList.timeline = icTimeline;
|
|
this._view.mapList.timeline = mapTimeline;
|
|
this._view.deoptList.timeline = deoptTimeline;
|
|
this._view.codeList.timeline = codeTimeline;
|
|
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!'
|
|
throw (e);
|
|
} finally {
|
|
$('#container').className = 'loaded';
|
|
this.fileLoaded = true;
|
|
}
|
|
}
|
|
|
|
refreshTimelineTrackView() {
|
|
this._view.mapTrack.data = this._state.mapTimeline;
|
|
this._view.icTrack.data = this._state.icTimeline;
|
|
this._view.deoptTrack.data = this._state.deoptTimeline;
|
|
this._view.codeTrack.data = this._state.codeTimeline;
|
|
this._view.tickTrack.data = this._state.tickTimeline;
|
|
this._view.timerTrack.data = this._state.timerTimeline;
|
|
}
|
|
}
|
|
|
|
class Navigation {
|
|
_view;
|
|
constructor(state, view) {
|
|
this.state = state;
|
|
this._view = view;
|
|
}
|
|
|
|
get map() {
|
|
return this.state.map
|
|
}
|
|
|
|
set map(value) {
|
|
this.state.map = value
|
|
}
|
|
|
|
get chunks() {
|
|
return this.state.mapTimeline.chunks;
|
|
}
|
|
|
|
increaseTimelineResolution() {
|
|
this._view.timelinePanel.nofChunks *= 1.5;
|
|
this.state.nofChunks *= 1.5;
|
|
}
|
|
|
|
decreaseTimelineResolution() {
|
|
this._view.timelinePanel.nofChunks /= 1.5;
|
|
this.state.nofChunks /= 1.5;
|
|
}
|
|
|
|
updateUrl() {
|
|
let entries = this.state.entries;
|
|
let params = new URLSearchParams(entries);
|
|
window.history.pushState(entries, '', '?' + params.toString());
|
|
}
|
|
|
|
scrollLeft() {}
|
|
|
|
scrollRight() {}
|
|
|
|
handleKeyDown(event) {
|
|
switch (event.key) {
|
|
case 'd':
|
|
this.scrollLeft();
|
|
return false;
|
|
case 'a':
|
|
this.scrollRight();
|
|
return false;
|
|
case '+':
|
|
this.increaseTimelineResolution();
|
|
return false;
|
|
case '-':
|
|
this.decreaseTimelineResolution();
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
export {App};
|