Commit 72eb1ca1 authored by Camillo Bruni's avatar Camillo Bruni Committed by V8 LUCI CQ

[tools][system-analyzer] Switch to SVG rendering + various improvements

- Introduce proper TickLogEntry and use a separate Timeline object
- Update the main rendering to use SVG for speed
- Separate custom-elements: timeline-track-map and timeline-track-tick
- Revamp flame-chart drawing
- Enable map-transitions overlay
- Use mouse position to infer current log-entry instead of individual
  event handlers
- Fix first timelineLegend column header
- Fixing scrollbar-color for FireFox

Change-Id: I7c53c13366b3e4614b1c5592dfaa69d0654a3b5f
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2944430
Commit-Queue: Camillo Bruni <cbruni@chromium.org>
Reviewed-by: 's avatarPatrick Thier <pthier@chromium.org>
Cr-Commit-Position: refs/heads/master@{#74987}
parent b308c41a
...@@ -159,14 +159,6 @@ class SourceInfo { ...@@ -159,14 +159,6 @@ class SourceInfo {
} }
} }
class Tick {
constructor(time_ns, vmState, processedStack) {
this.time = time_ns;
this.state = vmState;
this.stack = processedStack;
}
}
/** /**
* Creates a profile object for processing profiling-related events * Creates a profile object for processing profiling-related events
* and calculating function execution times. * and calculating function execution times.
...@@ -178,7 +170,6 @@ export class Profile { ...@@ -178,7 +170,6 @@ export class Profile {
topDownTree_ = new CallTree(); topDownTree_ = new CallTree();
bottomUpTree_ = new CallTree(); bottomUpTree_ = new CallTree();
c_entries_ = {}; c_entries_ = {};
ticks_ = [];
scripts_ = []; scripts_ = [];
urlToScript_ = new Map(); urlToScript_ = new Map();
...@@ -491,7 +482,7 @@ export class Profile { ...@@ -491,7 +482,7 @@ export class Profile {
this.bottomUpTree_.addPath(nameStack); this.bottomUpTree_.addPath(nameStack);
nameStack.reverse(); nameStack.reverse();
this.topDownTree_.addPath(nameStack); this.topDownTree_.addPath(nameStack);
this.ticks_.push(new Tick(time_ns, vmState, entryStack)); return entryStack;
} }
/** /**
......
...@@ -18,6 +18,7 @@ class State { ...@@ -18,6 +18,7 @@ class State {
_deoptTimeline; _deoptTimeline;
_codeTimeline; _codeTimeline;
_apiTimeline; _apiTimeline;
_tickTimeline;
_minStartTime = Number.POSITIVE_INFINITY; _minStartTime = Number.POSITIVE_INFINITY;
_maxEndTime = Number.NEGATIVE_INFINITY; _maxEndTime = Number.NEGATIVE_INFINITY;
...@@ -40,12 +41,14 @@ class State { ...@@ -40,12 +41,14 @@ class State {
} }
setTimelines( setTimelines(
mapTimeline, icTimeline, deoptTimeline, codeTimeline, apiTimeline) { mapTimeline, icTimeline, deoptTimeline, codeTimeline, apiTimeline,
tickTimeline) {
this._mapTimeline = mapTimeline; this._mapTimeline = mapTimeline;
this._icTimeline = icTimeline; this._icTimeline = icTimeline;
this._deoptTimeline = deoptTimeline; this._deoptTimeline = deoptTimeline;
this._codeTimeline = codeTimeline; this._codeTimeline = codeTimeline;
this._apiTimeline = apiTimeline; this._apiTimeline = apiTimeline;
this._tickTimeline = tickTimeline;
for (let timeline of arguments) { for (let timeline of arguments) {
if (timeline === undefined) return; if (timeline === undefined) return;
this._minStartTime = Math.min(this._minStartTime, timeline.startTime); this._minStartTime = Math.min(this._minStartTime, timeline.startTime);
...@@ -77,10 +80,14 @@ class State { ...@@ -77,10 +80,14 @@ class State {
return this._apiTimeline; return this._apiTimeline;
} }
get tickTimeline() {
return this._tickTimeline;
}
get timelines() { get timelines() {
return [ return [
this._mapTimeline, this._icTimeline, this._deoptTimeline, this._mapTimeline, this._icTimeline, this._deoptTimeline,
this._codeTimeline, this._apiTimeline this._codeTimeline, this._apiTimeline, this._tickTimeline
]; ];
} }
......
...@@ -20,18 +20,7 @@ ...@@ -20,18 +20,7 @@
--violet: #d26edc; --violet: #d26edc;
--border-color-rgb: 128, 128, 128; --border-color-rgb: 128, 128, 128;
--border-color: rgba(var(--border-color-rgb), 0.2); --border-color: rgba(var(--border-color-rgb), 0.2);
} scrollbar-color: rgba(128, 128, 128, 0.5) rgba(0, 0, 0, 0.0);
body {
font-family: sans-serif;
font-size: 14px;
color: var(--on-background-color);
margin: 10px 10px 0 10px;
background-color: var(--background-color);
}
section {
margin-bottom: 10px;
} }
::-webkit-scrollbar, ::-webkit-scrollbar-track, ::-webkit-scrollbar-corner { ::-webkit-scrollbar, ::-webkit-scrollbar-track, ::-webkit-scrollbar-corner {
...@@ -50,6 +39,18 @@ section { ...@@ -50,6 +39,18 @@ section {
background-color: rgba(128, 128, 128, 0.8); background-color: rgba(128, 128, 128, 0.8);
} }
body {
font-family: sans-serif;
font-size: 14px;
color: var(--on-background-color);
margin: 10px 10px 0 10px;
background-color: var(--background-color);
}
section {
margin-bottom: 10px;
}
kbd { kbd {
color: var(--on-primary-color); color: var(--on-primary-color);
background-color: var(--primary-color); background-color: var(--primary-color);
......
...@@ -55,8 +55,8 @@ found in the LICENSE file. --> ...@@ -55,8 +55,8 @@ found in the LICENSE file. -->
<section id="container" class="initial"> <section id="container" class="initial">
<timeline-panel id="timeline-panel"> <timeline-panel id="timeline-panel">
<timeline-track id="sample-track" title="Samples"></timeline-track> <timeline-track-tick id="tick-track" title="Samples"></timeline-track-tick>
<timeline-track id="map-track" title="Map"></timeline-track> <timeline-track-map id="map-track" title="Map"></timeline-track-map>
<timeline-track id="ic-track" title="IC"></timeline-track> <timeline-track id="ic-track" title="IC"></timeline-track>
<timeline-track id="deopt-track" title="Deopt"></timeline-track> <timeline-track id="deopt-track" title="Deopt"></timeline-track>
<timeline-track id="code-track" title="Code"></timeline-track> <timeline-track id="code-track" title="Code"></timeline-track>
......
...@@ -6,11 +6,12 @@ import {Script, SourcePosition} from '../profile.mjs'; ...@@ -6,11 +6,12 @@ import {Script, SourcePosition} from '../profile.mjs';
import {State} from './app-model.mjs'; import {State} from './app-model.mjs';
import {ApiLogEntry} from './log/api.mjs'; import {ApiLogEntry} from './log/api.mjs';
import {DeoptLogEntry} from './log/code.mjs';
import {CodeLogEntry} from './log/code.mjs'; import {CodeLogEntry} from './log/code.mjs';
import {DeoptLogEntry} from './log/code.mjs';
import {IcLogEntry} from './log/ic.mjs'; import {IcLogEntry} from './log/ic.mjs';
import {LogEntry} from './log/log.mjs'; import {LogEntry} from './log/log.mjs';
import {MapLogEntry} from './log/map.mjs'; import {MapLogEntry} from './log/map.mjs';
import {TickLogEntry} from './log/tick.mjs';
import {Processor} from './processor.mjs'; import {Processor} from './processor.mjs';
import {Timeline} from './timeline.mjs' import {Timeline} from './timeline.mjs'
import {FocusEvent, SelectionEvent, SelectRelatedEvent, SelectTimeEvent, ToolTipEvent,} from './view/events.mjs'; import {FocusEvent, SelectionEvent, SelectRelatedEvent, SelectTimeEvent, ToolTipEvent,} from './view/events.mjs';
...@@ -27,7 +28,7 @@ class App { ...@@ -27,7 +28,7 @@ class App {
logFileReader: $('#log-file-reader'), logFileReader: $('#log-file-reader'),
timelinePanel: $('#timeline-panel'), timelinePanel: $('#timeline-panel'),
sampleTrack: $('#sample-track'), tickTrack: $('#tick-track'),
mapTrack: $('#map-track'), mapTrack: $('#map-track'),
icTrack: $('#ic-track'), icTrack: $('#ic-track'),
deoptTrack: $('#deopt-track'), deoptTrack: $('#deopt-track'),
...@@ -51,12 +52,13 @@ class App { ...@@ -51,12 +52,13 @@ class App {
this._view.logFileReader.addEventListener( this._view.logFileReader.addEventListener(
'fileuploadend', (e) => this.handleFileUploadEnd(e)); 'fileuploadend', (e) => this.handleFileUploadEnd(e));
this._startupPromise = this.runAsyncInitialize(); this._startupPromise = this.runAsyncInitialize();
this._view.codeTrack.svg = true;
} }
static get allEventTypes() { static get allEventTypes() {
return new Set([ return new Set([
SourcePosition, MapLogEntry, IcLogEntry, ApiLogEntry, CodeLogEntry, SourcePosition, MapLogEntry, IcLogEntry, ApiLogEntry, CodeLogEntry,
DeoptLogEntry DeoptLogEntry, TickLogEntry
]); ]);
} }
...@@ -103,6 +105,8 @@ class App { ...@@ -103,6 +105,8 @@ class App {
break; break;
case CodeLogEntry: case CodeLogEntry:
break; break;
case TickLogEntry:
break;
case DeoptLogEntry: case DeoptLogEntry:
// TODO select map + code entries // TODO select map + code entries
if (entry.fileSourcePosition) entries.push(entry.fileSourcePosition); if (entry.fileSourcePosition) entries.push(entry.fileSourcePosition);
...@@ -160,38 +164,37 @@ class App { ...@@ -160,38 +164,37 @@ class App {
return this.showCodeEntries(entries); return this.showCodeEntries(entries);
case DeoptLogEntry: case DeoptLogEntry:
return this.showDeoptEntries(entries); return this.showDeoptEntries(entries);
case TickLogEntry:
break;
default: default:
throw new Error(`Unknown selection type: ${entryType?.name}`); throw new Error(`Unknown selection type: ${entryType?.name}`);
} }
} }
showMapEntries(entries) { showMapEntries(entries) {
this._state.selectedMapLogEntries = entries;
this._view.mapPanel.selectedLogEntries = entries; this._view.mapPanel.selectedLogEntries = entries;
this._view.mapList.selectedLogEntries = entries; this._view.mapList.selectedLogEntries = entries;
} }
showIcEntries(entries) { showIcEntries(entries) {
this._state.selectedIcLogEntries = entries;
this._view.icList.selectedLogEntries = entries; this._view.icList.selectedLogEntries = entries;
} }
showDeoptEntries(entries) { showDeoptEntries(entries) {
this._state.selectedDeoptLogEntries = entries;
this._view.deoptList.selectedLogEntries = entries; this._view.deoptList.selectedLogEntries = entries;
} }
showCodeEntries(entries) { showCodeEntries(entries) {
this._state.selectedCodeLogEntries = entries;
this._view.codePanel.selectedEntries = entries; this._view.codePanel.selectedEntries = entries;
this._view.codeList.selectedLogEntries = entries; this._view.codeList.selectedLogEntries = entries;
} }
showApiEntries(entries) { showApiEntries(entries) {
this._state.selectedApiLogEntries = entries;
this._view.apiList.selectedLogEntries = entries; this._view.apiList.selectedLogEntries = entries;
} }
showTickEntries(entries) {}
showSourcePositions(entries) { showSourcePositions(entries) {
this._view.scriptPanel.selectedSourcePositions = entries this._view.scriptPanel.selectedSourcePositions = entries
} }
...@@ -208,6 +211,7 @@ class App { ...@@ -208,6 +211,7 @@ class App {
this.showDeoptEntries(this._state.deoptTimeline.selectionOrSelf); this.showDeoptEntries(this._state.deoptTimeline.selectionOrSelf);
this.showCodeEntries(this._state.codeTimeline.selectionOrSelf); this.showCodeEntries(this._state.codeTimeline.selectionOrSelf);
this.showApiEntries(this._state.apiTimeline.selectionOrSelf); this.showApiEntries(this._state.apiTimeline.selectionOrSelf);
this.showTickEntries(this._state.tickTimeline.selectionOrSelf);
this._view.timelinePanel.timeSelection = {start, end}; this._view.timelinePanel.timeSelection = {start, end};
} }
...@@ -232,6 +236,8 @@ class App { ...@@ -232,6 +236,8 @@ class App {
return this.focusCodeLogEntry(entry); return this.focusCodeLogEntry(entry);
case DeoptLogEntry: case DeoptLogEntry:
return this.focusDeoptLogEntry(entry); return this.focusDeoptLogEntry(entry);
case TickLogEntry:
return this.focusTickLogEntry(entry);
default: default:
throw new Error(`Unknown selection type: ${entry.constructor?.name}`); throw new Error(`Unknown selection type: ${entry.constructor?.name}`);
} }
...@@ -255,7 +261,7 @@ class App { ...@@ -255,7 +261,7 @@ class App {
} }
focusDeoptLogEntry(entry) { focusDeoptLogEntry(entry) {
this._state.DeoptLogEntry = entry; this._state.deoptLogEntry = entry;
} }
focusApiLogEntry(entry) { focusApiLogEntry(entry) {
...@@ -263,6 +269,11 @@ class App { ...@@ -263,6 +269,11 @@ class App {
this._view.apiTrack.focusedEntry = entry; this._view.apiTrack.focusedEntry = entry;
} }
focusTickLogEntry(entry) {
this._state.tickLogEntry = entry;
this._view.tickTrack.focusedEntry = entry;
}
focusSourcePosition(sourcePosition) { focusSourcePosition(sourcePosition) {
if (!sourcePosition) return; if (!sourcePosition) return;
this._view.scriptPanel.focusedSourcePositions = [sourcePosition]; this._view.scriptPanel.focusedSourcePositions = [sourcePosition];
...@@ -281,6 +292,7 @@ class App { ...@@ -281,6 +292,7 @@ class App {
case ApiLogEntry: case ApiLogEntry:
case CodeLogEntry: case CodeLogEntry:
case DeoptLogEntry: case DeoptLogEntry:
case TickLogEntry:
content = content.toolTipDict; content = content.toolTipDict;
break; break;
default: default:
...@@ -315,8 +327,10 @@ class App { ...@@ -315,8 +327,10 @@ class App {
const deoptTimeline = processor.deoptTimeline; const deoptTimeline = processor.deoptTimeline;
const codeTimeline = processor.codeTimeline; const codeTimeline = processor.codeTimeline;
const apiTimeline = processor.apiTimeline; const apiTimeline = processor.apiTimeline;
const tickTimeline = processor.tickTimeline;
this._state.setTimelines( this._state.setTimelines(
mapTimeline, icTimeline, deoptTimeline, codeTimeline, apiTimeline); mapTimeline, icTimeline, deoptTimeline, codeTimeline, apiTimeline,
tickTimeline);
this._view.mapPanel.timeline = mapTimeline; this._view.mapPanel.timeline = mapTimeline;
this._view.icList.timeline = icTimeline; this._view.icList.timeline = icTimeline;
this._view.mapList.timeline = mapTimeline; this._view.mapList.timeline = mapTimeline;
...@@ -336,17 +350,12 @@ class App { ...@@ -336,17 +350,12 @@ class App {
} }
refreshTimelineTrackView() { refreshTimelineTrackView() {
const ticks = this._state.profile.ticks_;
if (ticks.length > 0) {
this._view.sampleTrack.data = new Timeline(
Object, ticks, ticks[0].time, ticks[ticks.length - 1].time);
this._view.sampleTrack.ticks = ticks;
}
this._view.mapTrack.data = this._state.mapTimeline; this._view.mapTrack.data = this._state.mapTimeline;
this._view.icTrack.data = this._state.icTimeline; this._view.icTrack.data = this._state.icTimeline;
this._view.deoptTrack.data = this._state.deoptTimeline; this._view.deoptTrack.data = this._state.deoptTimeline;
this._view.codeTrack.data = this._state.codeTimeline; this._view.codeTrack.data = this._state.codeTimeline;
this._view.apiTrack.data = this._state.apiTimeline; this._view.apiTrack.data = this._state.apiTimeline;
this._view.tickTrack.data = this._state.tickTimeline;
} }
} }
......
...@@ -121,8 +121,8 @@ class MapLogEntry extends LogEntry { ...@@ -121,8 +121,8 @@ class MapLogEntry extends LogEntry {
position(chunks) { position(chunks) {
const index = this.chunkIndex(chunks); const index = this.chunkIndex(chunks);
if (index === -1) return [0, 0]; if (index === -1) return [0, 0];
const xFrom = (index + 1.5) * kChunkWidth; const xFrom = (index + 0.5) * kChunkWidth | 0;
const yFrom = kChunkHeight - chunks[index].yOffset(this); const yFrom = kChunkHeight - chunks[index].yOffset(this) | 0;
return [xFrom, yFrom]; return [xFrom, yFrom];
} }
......
// 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 {Profile} from '../../profile.mjs'
import {LogEntry} from './log.mjs';
export class TickLogEntry extends LogEntry {
constructor(time, vmState, processedStack) {
super(TickLogEntry.extractType(processedStack), time);
this.state = vmState;
this.stack = processedStack;
}
static extractType(processedStack) {
if (processedStack.length == 0) return 'idle';
const topOfStack = processedStack[processedStack.length - 1];
if (topOfStack?.state) {
return Profile.getKindFromState(topOfStack.state);
}
return 'native';
}
}
\ No newline at end of file
...@@ -9,17 +9,19 @@ import {ApiLogEntry} from './log/api.mjs'; ...@@ -9,17 +9,19 @@ import {ApiLogEntry} from './log/api.mjs';
import {CodeLogEntry, DeoptLogEntry} from './log/code.mjs'; import {CodeLogEntry, DeoptLogEntry} from './log/code.mjs';
import {IcLogEntry} from './log/ic.mjs'; import {IcLogEntry} from './log/ic.mjs';
import {Edge, MapLogEntry} from './log/map.mjs'; import {Edge, MapLogEntry} from './log/map.mjs';
import {TickLogEntry} from './log/tick.mjs';
import {Timeline} from './timeline.mjs'; import {Timeline} from './timeline.mjs';
// =========================================================================== // ===========================================================================
export class Processor extends LogReader { export class Processor extends LogReader {
_profile = new Profile(); _profile = new Profile();
_mapTimeline = new Timeline();
_icTimeline = new Timeline();
_deoptTimeline = new Timeline();
_codeTimeline = new Timeline();
_apiTimeline = new Timeline(); _apiTimeline = new Timeline();
_codeTimeline = new Timeline();
_deoptTimeline = new Timeline();
_icTimeline = new Timeline();
_mapTimeline = new Timeline();
_tickTimeline = new Timeline();
_formatPCRegexp = /(.*):[0-9]+:[0-9]+$/; _formatPCRegexp = /(.*):[0-9]+:[0-9]+$/;
_lastTimestamp = 0; _lastTimestamp = 0;
_lastCodeLogEntry; _lastCodeLogEntry;
...@@ -275,8 +277,8 @@ export class Processor extends LogReader { ...@@ -275,8 +277,8 @@ export class Processor extends LogReader {
} }
processTick( processTick(
pc, ns_since_start, is_external_callback, tos_or_external_callback, pc, time_ns, is_external_callback, tos_or_external_callback, vmState,
vmState, stack) { stack) {
if (is_external_callback) { if (is_external_callback) {
// Don't use PC when in external callback code, as it can point // Don't use PC when in external callback code, as it can point
// inside callback's code, and we will erroneously report // inside callback's code, and we will erroneously report
...@@ -292,9 +294,10 @@ export class Processor extends LogReader { ...@@ -292,9 +294,10 @@ export class Processor extends LogReader {
tos_or_external_callback = 0; tos_or_external_callback = 0;
} }
} }
this._profile.recordTick( const entryStack = this._profile.recordTick(
ns_since_start, vmState, time_ns, vmState,
this.processStack(pc, tos_or_external_callback, stack)); this.processStack(pc, tos_or_external_callback, stack));
this._tickTimeline.push(new TickLogEntry(time_ns, vmState, entryStack))
} }
processCodeSourceInfo( processCodeSourceInfo(
...@@ -476,6 +479,10 @@ export class Processor extends LogReader { ...@@ -476,6 +479,10 @@ export class Processor extends LogReader {
return this._apiTimeline; return this._apiTimeline;
} }
get tickTimeline() {
return this._tickTimeline;
}
get scripts() { get scripts() {
return this._profile.scripts_.filter(script => script !== undefined); return this._profile.scripts_.filter(script => script !== undefined);
} }
......
...@@ -120,8 +120,7 @@ class Timeline { ...@@ -120,8 +120,7 @@ class Timeline {
} }
duration() { duration() {
if (this.isEmpty()) return 0; return this.endTime - this.startTime;
return this.last().time - this.first().time;
} }
forEachChunkSize(count, fn) { forEachChunkSize(count, fn) {
...@@ -240,7 +239,7 @@ class Chunk { ...@@ -240,7 +239,7 @@ class Chunk {
yOffset(event) { yOffset(event) {
// items[0] == oldest event, displayed at the top of the chunk // items[0] == oldest event, displayed at the top of the chunk
// items[n-1] == youngest event, displayed at the bottom of the chunk // items[n-1] == youngest event, displayed at the bottom of the chunk
return (1 - (this.indexOf(event) + 0.5) / this.size()) * this.height; return ((this.indexOf(event) + 0.5) / this.size()) * this.height;
} }
indexOf(event) { indexOf(event) {
......
...@@ -125,11 +125,24 @@ export class CSSColor { ...@@ -125,11 +125,24 @@ export class CSSColor {
export class DOM { export class DOM {
static element(type, classes) { static element(type, classes) {
const node = document.createElement(type); const node = document.createElement(type);
if (classes === undefined) return node; if (classes !== undefined) {
if (typeof classes === 'string') {
node.className = classes;
} else {
DOM.addClasses(node, classes);
}
}
return node;
}
static addClasses(node, classes) {
const classList = node.classList;
if (typeof classes === 'string') { if (typeof classes === 'string') {
node.className = classes; classList.add(classes);
} else { } else {
classes.forEach(cls => node.classList.add(cls)); for (let i = 0; i < classes.length; i++) {
classList.add(classes[i]);
}
} }
return node; return node;
} }
...@@ -182,8 +195,17 @@ export class DOM { ...@@ -182,8 +195,17 @@ export class DOM {
range.deleteContents(); range.deleteContents();
} }
static defineCustomElement(path, generator) { static defineCustomElement(
let name = path.substring(path.lastIndexOf('/') + 1, path.length); path, nameOrGenerator, maybeGenerator = undefined) {
let generator = nameOrGenerator;
let name = nameOrGenerator;
if (typeof nameOrGenerator == 'function') {
console.assert(maybeGenerator === undefined);
name = path.substring(path.lastIndexOf('/') + 1, path.length);
} else {
console.assert(typeof nameOrGenerator == 'string');
generator = maybeGenerator;
}
path = path + '-template.html'; path = path + '-template.html';
fetch(path) fetch(path)
.then(stream => stream.text()) .then(stream => stream.text())
...@@ -193,6 +215,27 @@ export class DOM { ...@@ -193,6 +215,27 @@ export class DOM {
} }
} }
const SVGNamespace = 'http://www.w3.org/2000/svg';
export class SVG {
static element(type, classes) {
const node = document.createElementNS(SVGNamespace, type);
if (classes !== undefined) DOM.addClasses(node, classes);
return node;
}
static svg() {
return this.element('svg');
}
static rect(classes) {
return this.element('rect', classes);
}
static g() {
return this.element('g');
}
}
export function $(id) { export function $(id) {
return document.querySelector(id) return document.querySelector(id)
} }
...@@ -394,7 +437,7 @@ export function gradientStopsFromGroups( ...@@ -394,7 +437,7 @@ export function gradientStopsFromGroups(
for (let group of groups) { for (let group of groups) {
const color = colorFn(group.key); const color = colorFn(group.key);
increment += group.count; increment += group.count;
let height = (increment / totalLength * kMaxHeight) | 0; const height = (increment / totalLength * kMaxHeight) | 0;
stops.push(`${color} ${lastHeight}${kUnit} ${height}${kUnit}`) stops.push(`${color} ${lastHeight}${kUnit} ${height}${kUnit}`)
lastHeight = height; lastHeight = height;
} }
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
// found in the LICENSE file. // found in the LICENSE file.
import './timeline/timeline-track.mjs'; import './timeline/timeline-track.mjs';
import './timeline/timeline-track-map.mjs';
import './timeline/timeline-track-tick.mjs';
import {SynchronizeSelectionEvent} from './events.mjs'; import {SynchronizeSelectionEvent} from './events.mjs';
import {DOM, V8CustomElement} from './helper.mjs'; import {DOM, V8CustomElement} from './helper.mjs';
...@@ -19,8 +21,10 @@ DOM.defineCustomElement( ...@@ -19,8 +21,10 @@ DOM.defineCustomElement(
} }
set nofChunks(count) { set nofChunks(count) {
const time = this.currentTime
for (const track of this.timelineTracks) { for (const track of this.timelineTracks) {
track.nofChunks = count; track.nofChunks = count;
track.currentTime = time;
} }
} }
...@@ -28,6 +32,10 @@ DOM.defineCustomElement( ...@@ -28,6 +32,10 @@ DOM.defineCustomElement(
return this.timelineTracks[0].nofChunks; return this.timelineTracks[0].nofChunks;
} }
get currentTime() {
return this.timelineTracks[0].currentTime;
}
get timelineTracks() { get timelineTracks() {
return this.$('slot').assignedNodes().filter( return this.$('slot').assignedNodes().filter(
node => node.nodeType === Node.ELEMENT_NODE); node => node.nodeType === Node.ELEMENT_NODE);
......
This diff is collapsed.
// 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 {MapLogEntry} from '../../log/map.mjs';
import {CSSColor, DOM, SVG, V8CustomElement} from '../helper.mjs';
import {TimelineTrackBase} from './timeline-track-base.mjs'
DOM.defineCustomElement('view/timeline/timeline-track', 'timeline-track-map',
(templateText) =>
class TimelineTrackMap extends TimelineTrackBase {
constructor() {
super(templateText);
}
getMapStyle(map) {
return map.edge && map.edge.from ? CSSColor.onBackgroundColor :
CSSColor.onPrimaryColor;
}
markMap(map) {
const [x, y] = map.position(this.chunks);
const strokeColor = this.getMapStyle(map);
return `<circle cx=${x} cy=${y} r=${2} stroke=${
strokeColor} class=annotationPoint />`
}
markSelectedMap(map) {
const [x, y] = map.position(this.chunks);
const strokeColor = this.getMapStyle(map);
return `<circle cx=${x} cy=${y} r=${3} stroke=${
strokeColor} class=annotationPoint />`
}
_drawAnnotations(logEntry) {
if (!(logEntry instanceof MapLogEntry)) return;
if (!logEntry.edge) {
this.timelineAnnotationsNode.innerHTML = '';
return;
}
// Draw the trace of maps in reverse order to make sure the outgoing
// transitions of previous maps aren't drawn over.
const kOpaque = 1.0;
let stack = [];
let current = logEntry;
while (current !== undefined) {
stack.push(current);
current = current.parent();
}
// Draw outgoing refs as fuzzy background. Skip the last map entry.
let buffer = '';
let nofEdges = 0;
const kMaxOutgoingEdges = 100;
for (let i = stack.length - 2; i >= 0; i--) {
const map = stack[i].parent();
nofEdges += map.children.length;
if (nofEdges > kMaxOutgoingEdges) break;
buffer += this.drawOutgoingEdges(map, 0.4, 1);
}
// Draw main connection.
let labelOffset = 15;
let xPrev = 0;
for (let i = stack.length - 1; i >= 0; i--) {
let map = stack[i];
if (map.edge) {
const [xTo, data] = this.drawEdge(map.edge, kOpaque, labelOffset);
buffer += data;
if (xTo == xPrev) {
labelOffset += 10;
} else {
labelOffset = 15
}
xPrev = xTo;
}
buffer += this.markMap(map);
}
buffer += this.drawOutgoingEdges(logEntry, 0.9, 3);
// Mark selected map
buffer += this.markSelectedMap(logEntry);
this.timelineAnnotationsNode.innerHTML = buffer;
}
drawEdge(edge, opacity, labelOffset = 20) {
let buffer = '';
if (!edge.from || !edge.to) return [-1, buffer];
const [xFrom, yFrom] = edge.from.position(this.chunks);
const [xTo, yTo] = edge.to.position(this.chunks);
const sameChunk = xTo == xFrom;
if (sameChunk) labelOffset += 10;
const color = this._legend.colorForType(edge.type);
const offsetX = 20;
const midX = xFrom + (xTo - xFrom) / 2;
const midY = (yFrom + yTo) / 2 - 100;
if (!sameChunk) {
if (opacity == 1.0) {
buffer += `<path d="M ${xFrom} ${yFrom} Q ${midX} ${midY}, ${xTo} ${
yTo}" class=strokeBG />`
}
buffer += `<path d="M ${xFrom} ${yFrom} Q ${midX} ${midY}, ${xTo} ${
yTo}" stroke=${color} fill=none opacity=${opacity} />`
} else {
if (opacity == 1.0) {
buffer += `<line x1=${xFrom} x2=${xTo} y1=${yFrom} y2=${
yTo} class=strokeBG />`;
}
buffer += `<line x1=${xFrom} x2=${xTo} y1=${yFrom} y2=${yTo} stroke=${
color} fill=none opacity=${opacity} />`;
}
if (opacity == 1.0) {
const centerX = sameChunk ? xTo : ((xFrom / 2 + midX + xTo / 2) / 2) | 0;
const centerY = sameChunk ? yTo : ((yFrom / 2 + midY + yTo / 2) / 2) | 0;
const centerYTo = centerY - labelOffset;
buffer += `<line x1=${centerX} x2=${centerX + offsetX} y1=${centerY} y2=${
centerYTo} stroke=${color} fill=none opacity=${opacity} />`;
buffer += `<text x=${centerX + offsetX + 2} y=${
centerYTo} class=annotationLabel opacity=${opacity} >${
edge.toString()}</text>`;
}
return [xTo, buffer];
}
drawOutgoingEdges(map, opacity = 1.0, max = 10, depth = 0) {
let buffer = '';
if (!map || depth >= max) return buffer;
const limit = Math.min(map.children.length, 100)
for (let i = 0; i < limit; i++) {
const edge = map.children[i];
const [xTo, data] = this.drawEdge(edge, opacity);
buffer += data;
buffer += this.drawOutgoingEdges(edge.to, opacity * 0.5, max, depth + 1);
}
return buffer;
}
})
\ No newline at end of file
...@@ -25,11 +25,16 @@ found in the LICENSE file. --> ...@@ -25,11 +25,16 @@ found in the LICENSE file. -->
opacity: 0.5; opacity: 0.5;
} }
#timelineSamples, #timelineChunks { #timelineSamples, #timelineChunks,
#timelineMarkers, #timelineAnnotations {
top: 0px;
height: 200px; height: 200px;
position: absolute; position: absolute;
margin-right: 100px; margin-right: 100px;
} }
#timelineMarkers, #timelineAnnotations {
pointer-events: none;
}
#timelineCanvas { #timelineCanvas {
height: 200px; height: 200px;
...@@ -46,6 +51,7 @@ found in the LICENSE file. --> ...@@ -46,6 +51,7 @@ found in the LICENSE file. -->
bottom: 0px; bottom: 0px;
background-color: var(--on-surface-color); background-color: var(--on-surface-color);
cursor: pointer; cursor: pointer;
content-visibility: auto;
} }
.chunk:hover { .chunk:hover {
border-radius: 2px 2px 0 0; border-radius: 2px 2px 0 0;
...@@ -106,10 +112,12 @@ found in the LICENSE file. --> ...@@ -106,10 +112,12 @@ found in the LICENSE file. -->
padding: 1px 3px 2px 3px; padding: 1px 3px 2px 3px;
} }
#legendTable td {
padding-top: 3px;
}
/* Center colors */ /* Center colors */
#legendTable td:nth-of-type(4n+1) { #legendTable td:nth-of-type(4n+1) {
text-align: center; text-align: center;
padding-top: 3px;
} }
/* Left align text*/ /* Left align text*/
#legendTable td:nth-of-type(4n+2) { #legendTable td:nth-of-type(4n+2) {
...@@ -177,6 +185,7 @@ found in the LICENSE file. --> ...@@ -177,6 +185,7 @@ found in the LICENSE file. -->
height: 10px; height: 10px;
position: absolute; position: absolute;
font-size: 8px; font-size: 8px;
content-visibility: auto;
} }
.flame.Opt{ .flame.Opt{
background-color: red; background-color: red;
...@@ -187,6 +196,22 @@ found in the LICENSE file. --> ...@@ -187,6 +196,22 @@ found in the LICENSE file. -->
.flame.default { .flame.default {
background-color: black; background-color: black;
} }
.txt {
font: 8px monospace;
}
.annotationLabel {
fill: var(--on-surface-color);
font-size: 9px;
}
.annotationPoint {
fill: var(--on-background-color);
stroke-width: 1;
}
.strokeBG {
stroke: var(--on-background-color);
stroke-width: 2;
fill: none;
}
</style> </style>
<div class="content"> <div class="content">
...@@ -201,8 +226,9 @@ found in the LICENSE file. --> ...@@ -201,8 +226,9 @@ found in the LICENSE file. -->
<div id="rightHandle"></div> <div id="rightHandle"></div>
</div> </div>
<div id="timelineLabel">Frequency</div> <div id="timelineLabel">Frequency</div>
<div id="timelineChunks"></div> <svg id="timelineChunks" xmlns="http://www.w3.org/2000/svg"></svg>
<div id="timelineSamples"></div> <svg id="timelineAnnotations" xmlns="http://www.w3.org/2000/svg"></svg>
<div id="timelineMarkers"></div>
<canvas id="timelineCanvas"></canvas> <canvas id="timelineCanvas"></canvas>
</div> </div>
...@@ -210,6 +236,7 @@ found in the LICENSE file. --> ...@@ -210,6 +236,7 @@ found in the LICENSE file. -->
<table id="legendTable"> <table id="legendTable">
<thead> <thead>
<tr> <tr>
<td></td>
<td>Type</td> <td>Type</td>
<td>Count</td> <td>Count</td>
<td>Percent</td> <td>Percent</td>
......
// 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 {Profile} from '../../../profile.mjs'
import {delay} from '../../helper.mjs';
import {CSSColor, DOM, SVG, V8CustomElement} from '../helper.mjs';
import {TimelineTrackBase} from './timeline-track-base.mjs'
class Flame {
constructor(time, entry) {
this.start = time;
this.end = this.start;
this.entry = entry;
}
stop(time) {
this.end = time;
this.duration = time - this.start
}
}
DOM.defineCustomElement('view/timeline/timeline-track', 'timeline-track-tick',
(templateText) =>
class TimelineTrackTick extends TimelineTrackBase {
constructor() {
super(templateText);
}
async _drawContent() {
this.timelineChunks.innerHTML = '';
const stack = [];
let buffer = '';
const kMinPixelWidth = 1
const kMinTimeDelta = kMinPixelWidth / this._timeToPixel;
let lastTime = 0;
let flameCount = 0;
const ticks = this._timeline.values;
for (let tickIndex = 0; tickIndex < ticks.length; tickIndex++) {
const tick = ticks[tickIndex];
// Skip ticks beyond visible resolution.
if ((tick.time - lastTime) < kMinTimeDelta) continue;
lastTime = tick.time;
if (flameCount > 1000) {
const svg = SVG.svg();
svg.innerHTML = buffer;
this.timelineChunks.appendChild(svg);
buffer = '';
flameCount = 0;
await delay(15);
}
for (let stackIndex = 0; stackIndex < tick.stack.length; stackIndex++) {
const entry = tick.stack[stackIndex];
if (stack.length <= stackIndex) {
stack.push(new Flame(tick.time, entry));
} else {
const flame = stack[stackIndex];
if (flame.entry !== entry) {
for (let k = stackIndex; k < stack.length; k++) {
stack[k].stop(tick.time);
buffer += this.drawFlame(stack[k], k);
flameCount++
}
stack.length = stackIndex;
stack[stackIndex] = new Flame(tick.time, entry);
}
}
}
}
const svg = SVG.svg();
svg.innerHTML = buffer;
this.timelineChunks.appendChild(svg);
}
drawFlame(flame, depth) {
let type = 'native';
if (flame.entry?.state) {
type = Profile.getKindFromState(flame.entry.state);
}
const kHeight = 9;
const x = this.timeToPosition(flame.start);
const y = depth * (kHeight + 1);
const width = (flame.duration * this._timeToPixel - 0.5);
const color = this._legend.colorForType(type);
let buffer =
`<rect x=${x} y=${y} width=${width} height=${kHeight} fill=${color} />`;
if (width < 15 || type == 'native') return buffer;
const rawName = flame.entry.getRawName();
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;
}
})
\ No newline at end of file
...@@ -50,10 +50,11 @@ DOM.defineCustomElement( ...@@ -50,10 +50,11 @@ DOM.defineCustomElement(
set targetNode(targetNode) { set targetNode(targetNode) {
this._intersectionObserver.disconnect(); this._intersectionObserver.disconnect();
this._targetNode = targetNode; this._targetNode = targetNode;
if (targetNode) { if (targetNode === undefined) return;
if (!(targetNode instanceof SVGElement)) {
this._intersectionObserver.observe(targetNode); this._intersectionObserver.observe(targetNode);
this.requestUpdate(true);
} }
this.requestUpdate(true);
} }
set position(position) { set position(position) {
......
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