Commit 425f4ed8 authored by Camillo Bruni's avatar Camillo Bruni Committed by V8 LUCI CQ

[tools][system-analyzer] Improve flamechart

- Scale svg flamechart directly instead of rerendering
- Convert markers to SVG as well
- Fix scroll position after zooming
- Support tooltips for flamechart

Change-Id: I01c966d2705989cf45a91c64aa4302a8de035414
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2944894
Commit-Queue: Camillo Bruni <cbruni@chromium.org>
Reviewed-by: 's avatarPatrick Thier <pthier@chromium.org>
Cr-Commit-Position: refs/heads/master@{#75008}
parent 8803cc14
......@@ -18,6 +18,10 @@ class Timeline {
this._values = values;
this.startTime = startTime;
this.endTime = endTime;
if (values.length > 0) {
if (startTime === 0) this.startTime = values[0].time;
if (endTime === 0) this.endTime = values[values.length - 1].time;
}
}
get model() {
......
......@@ -223,16 +223,16 @@ export class SVG {
return node;
}
static svg() {
return this.element('svg');
static svg(classes) {
return this.element('svg', classes);
}
static rect(classes) {
return this.element('rect', classes);
}
static g() {
return this.element('g');
static g(classes) {
return this.element('g', classes);
}
}
......
......@@ -42,7 +42,6 @@ DOM.defineCustomElement(
}
handleTrackScroll(event) {
// TODO(zcankara) add forEachTrack helper method
for (const track of this.timelineTracks) {
track.scrollLeft = event.detail;
}
......
......@@ -13,13 +13,9 @@ export class TimelineTrackBase extends V8CustomElement {
_chunks;
_selectedEntry;
_timeToPixel;
_timeStartOffset;
_timeStartPixelOffset;
_legend;
_chunkMouseMoveHandler = this._handleChunkMouseMove.bind(this);
_chunkClickHandler = this._handleChunkClick.bind(this);
_chunkDoubleClickHandler = this._handleChunkDoubleClick.bind(this);
_flameMouseOverHandler = this._handleFlameMouseOver.bind(this);
_lastContentWidth = 0;
constructor(templateText) {
super(templateText);
......@@ -28,9 +24,9 @@ export class TimelineTrackBase extends V8CustomElement {
this._legend.onFilter = (type) => this._handleFilterTimeline();
this.timelineNode.addEventListener(
'scroll', e => this._handleTimelineScroll(e));
this.timelineNode.ondblclick = (e) =>
this._selectionHandler.clearSelection();
this.timelineChunks.onmousemove = this._chunkMouseMoveHandler;
this.timelineNode.onclick = (e) => this._handleClick(e);
this.timelineNode.ondblclick = (e) => this._handleDoubleClick(e);
this.timelineChunks.onmousemove = (e) => this._handleMouseMove(e);
this.isLocked = false;
}
......@@ -73,27 +69,20 @@ export class TimelineTrackBase extends V8CustomElement {
}
positionToTime(pagePosX) {
let posTimelineX =
this.positionOnTimeline(pagePosX) + this._timeStartOffset;
return posTimelineX / this._timeToPixel;
return this.relativePositionToTime(this.positionOnTimeline(pagePosX));
}
relativePositionToTime(timelineRelativeX) {
const timelineAbsoluteX = timelineRelativeX + this._timeStartPixelOffset;
return timelineAbsoluteX / this._timeToPixel;
}
timeToPosition(time) {
let relativePosX = time * this._timeToPixel;
relativePosX -= this._timeStartOffset;
relativePosX -= this._timeStartPixelOffset;
return relativePosX;
}
get currentTime() {
const centerOffset = this.timelineNode.getBoundingClientRect().width / 2;
return this.positionToTime(this.timelineNode.scrollLeft + centerOffset);
}
set currentTime(time) {
const centerOffset = this.timelineNode.getBoundingClientRect().width / 2;
this.timelineNode.scrollLeft = this.timeToPosition(time) - centerOffset;
}
get timelineCanvas() {
return this.$('#timelineCanvas');
}
......@@ -127,18 +116,12 @@ export class TimelineTrackBase extends V8CustomElement {
return this.$('#timelineMarkers');
}
_update() {
this._updateTimeline();
this._legend.update();
}
_handleFlameMouseOver(event) {
const codeEntry = event.target.data;
this.dispatchEvent(new ToolTipEvent(codeEntry.logEntry, event.target));
get _scalableContentNode() {
return this.$('#scalableContent');
}
set nofChunks(count) {
this._nofChunks = count;
this._nofChunks = count | 0;
this._updateChunks();
}
......@@ -147,8 +130,8 @@ export class TimelineTrackBase extends V8CustomElement {
}
_updateChunks() {
this._chunks =
this._timeline.chunks(this.nofChunks, this._legend.filterPredicate);
this._chunks = undefined;
this._updateDimensions();
this.requestUpdate();
}
......@@ -183,29 +166,44 @@ export class TimelineTrackBase extends V8CustomElement {
'scrolltrack', {bubbles: true, composed: true, detail: horizontal}));
}
_updateTimeline() {
const chunks = this.chunks;
const start = this._timeline.startTime;
const end = this._timeline.endTime;
const duration = end - start;
const width = chunks.length * kChunkWidth;
let oldWidth = width;
if (this.timelineChunks.style.width) {
oldWidth = parseInt(this.timelineChunks.style.width);
}
_updateDimensions() {
const centerOffset = this.timelineNode.getBoundingClientRect().width / 2;
const time = this.relativePositionToTime(
this.timelineNode.scrollLeft + centerOffset);
this._timeToPixel = width / duration;
this._timeStartOffset = start * this._timeToPixel;
const start = this._timeline.startTime;
const width = this._nofChunks * kChunkWidth;
this._lastContentWidth = parseInt(this.timelineMarkersNode.style.width);
this._timeToPixel = width / this._timeline.duration();
this._timeStartPixelOffset = start * this._timeToPixel;
this.timelineChunks.style.width = `${width}px`;
this.timelineMarkersNode.style.width = `${width}px`;
this.timelineAnnotationsNode.style.width = `${width}px`;
this._drawMarkers();
this._selectionHandler.update();
this._scaleContent(width);
this.timelineNode.scrollLeft = this.timeToPosition(time) - centerOffset;
}
_scaleContent(currentWidth) {
if (!this._lastContentWidth) return;
const ratio = currentWidth / this._lastContentWidth;
this._scalableContentNode.style.transform = `scale(${ratio}, 1)`;
}
_update() {
this._legend.update();
this._drawContent();
this._drawAnnotations(this.selectedEntry);
}
async _drawContent() {
await delay(5);
if (this.chunks?.length != this.nofChunks) {
this._chunks =
this._timeline.chunks(this.nofChunks, this._legend.filterPredicate);
console.assert(this._chunks.length == this._nofChunks);
}
const chunks = this.chunks;
const max = chunks.max(each => each.size());
let buffer = '';
......@@ -218,7 +216,8 @@ export class TimelineTrackBase extends V8CustomElement {
buffer += this._drawChunk(i, chunk);
buffer += '</g>'
}
this.timelineChunks.innerHTML = buffer;
this._scalableContentNode.innerHTML = buffer;
this._scalableContentNode.style.transform = 'scale(1, 1)';
}
_drawChunk(chunkIndex, chunk) {
......@@ -240,9 +239,8 @@ export class TimelineTrackBase extends V8CustomElement {
}
_drawMarkers() {
const fragment = new DocumentFragment();
// Put a time marker roughly every 20 chunks.
const expected = this._timeline.duration() / this.chunks.length * 20;
const expected = this._timeline.duration() / this._nofChunks * 20;
let interval = (10 ** Math.floor(Math.log10(expected)));
let correction = Math.log10(expected / interval);
correction = (correction < 0.33) ? 1 : (correction < 0.75) ? 2.5 : 5;
......@@ -250,49 +248,64 @@ export class TimelineTrackBase extends V8CustomElement {
const start = this._timeline.startTime;
let time = start;
let buffer = '';
while (time < this._timeline.endTime) {
const timeNode = DOM.div('timestamp');
timeNode.innerText = `${((time - start) / 1000) | 0} ms`;
timeNode.style.left = `${((time - start) * this._timeToPixel) | 0}px`;
fragment.appendChild(timeNode);
const delta = time - start;
const text = `${(delta / 1000) | 0} ms`;
const x = (delta * this._timeToPixel) | 0;
buffer += `<text x=${x - 2} y=0 class=markerText >${text}</text>`
buffer +=
`<line x1=${x} x2=${x} y1=12 y2=2000 dy=100% class=markerLine />`
time += interval;
}
DOM.removeAllChildren(this.timelineMarkersNode);
this.timelineMarkersNode.appendChild(fragment);
}
_handleChunkMouseMove(event) {
if (this.isLocked) return false;
if (this._selectionHandler.isSelecting) return false;
let target = event.target;
if (target === this.timelineChunks) return false;
target = target.parentNode;
const time = this.positionToTime(event.pageX);
const chunkIndex = (time - this._timeline.startTime) /
this._timeline.duration() * this._nofChunks;
const chunk = this.chunks[chunkIndex | 0];
if (!chunk || chunk.isEmpty()) return;
const relativeIndex =
Math.round((200 - event.layerY) / chunk.height * (chunk.size() - 1));
if (relativeIndex > chunk.size()) return;
const logEntry = chunk.at(relativeIndex);
this.dispatchEvent(new ToolTipEvent(logEntry, target));
this._drawAnnotations(logEntry);
this.timelineMarkersNode.innerHTML = buffer;
}
_drawAnnotations(logEntry) {
// Subclass responsibility.
}
_handleChunkClick(event) {
_handleClick(event) {
if (event.target === this.timelineChunks) return;
this.isLocked = !this.isLocked;
event.stopImmediatePropagation();
event.stopPropagation();
return false;
}
_handleChunkDoubleClick(event) {
_handleDoubleClick(event) {
this._selectionHandler.clearSelection();
const chunk = event.target.chunk;
if (!chunk) return;
event.stopPropagation();
event.stopImmediatePropagation();
this.dispatchEvent(new SelectTimeEvent(chunk.start, chunk.end));
return false;
}
_handleMouseMove(event) {
if (this.isLocked) return false;
if (this._selectionHandler.isSelecting) return false;
const {logEntry, target} = this._getEntryForEvent(event);
if (!logEntry) return false;
this.dispatchEvent(new ToolTipEvent(logEntry, target));
this._drawAnnotations(logEntry);
}
_getEntryForEvent(event) {
let target = event.target;
let logEntry = false;
if (target === this.timelineChunks) return {logEntry, target};
target = target.parentNode;
const time = this.positionToTime(event.pageX);
const chunkIndex = (time - this._timeline.startTime) /
this._timeline.duration() * this._nofChunks;
const chunk = this.chunks[chunkIndex | 0];
if (!chunk?.isEmpty()) {
const relativeIndex =
Math.round((200 - event.layerY) / chunk.height * (chunk.size() - 1));
if (relativeIndex < chunk.size()) logEntry = chunk.at(relativeIndex);
}
return {logEntry, target};
}
};
......
......@@ -212,6 +212,17 @@ found in the LICENSE file. -->
stroke-width: 2;
fill: none;
}
.markerLine {
stroke: var(--on-background-color);
stroke-dasharray: 2 2;
}
.markerText {
fill: var(--on-surface-color);
dominant-baseline: hanging;
font-size: 9px;
}
#scalableContent {
}
</style>
<div class="content">
......@@ -226,9 +237,11 @@ found in the LICENSE file. -->
<div id="rightHandle"></div>
</div>
<div id="timelineLabel">Frequency</div>
<svg id="timelineChunks" xmlns="http://www.w3.org/2000/svg"></svg>
<svg id="timelineChunks" xmlns="http://www.w3.org/2000/svg">
<g id="scalableContent"></g>
</svg>
<svg id="timelineAnnotations" xmlns="http://www.w3.org/2000/svg"></svg>
<div id="timelineMarkers"></div>
<svg id="timelineMarkers" xmlns="http://www.w3.org/2000/svg"></svg>
<canvas id="timelineCanvas"></canvas>
</div>
......
......@@ -4,15 +4,18 @@
import {Profile} from '../../../profile.mjs'
import {delay} from '../../helper.mjs';
import {Timeline} from '../../timeline.mjs';
import {CSSColor, DOM, SVG, V8CustomElement} from '../helper.mjs';
import {TimelineTrackBase} from './timeline-track-base.mjs'
class Flame {
constructor(time, entry) {
constructor(time, entry, depth, id) {
this.start = time;
this.end = this.start;
this.entry = entry;
this.depth = depth;
this.id = id;
}
stop(time) {
this.end = time;
......@@ -23,68 +26,131 @@ class Flame {
DOM.defineCustomElement('view/timeline/timeline-track', 'timeline-track-tick',
(templateText) =>
class TimelineTrackTick extends TimelineTrackBase {
_flames = new Timeline();
_originalContentWidth = 0;
constructor() {
super(templateText);
}
async _drawContent() {
this.timelineChunks.innerHTML = '';
_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();
}
_getEntryForEvent(event) {
let logEntry = false;
const target = event.target;
const id = event.target.getAttribute('data-id');
if (id) {
const codeEntry = this._flames.at(id).entry;
if (codeEntry.logEntry) {
logEntry = codeEntry.logEntry;
}
}
return {logEntry, target};
}
_updateFlames() {
const tmpFlames = [];
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));
const newFlame =
new Flame(tick.time, entry, stackIndex, tmpFlames.length);
tmpFlames.push(newFlame);
stack.push(newFlame);
} else {
const flame = stack[stackIndex];
if (flame.entry !== entry) {
if (stack[stackIndex].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 replacementFlame =
new Flame(tick.time, entry, stackIndex, tmpFlames.length);
tmpFlames.push(replacementFlame);
stack[stackIndex] = replacementFlame;
}
}
}
}
const svg = SVG.svg();
svg.innerHTML = buffer;
this.timelineChunks.appendChild(svg);
const lastTime = ticks[ticks.length - 1].time;
for (let stackIndex = 0; stackIndex < stack.length; stackIndex++) {
stack[stackIndex].stop(lastTime);
}
this._flames = new Timeline(Flame, tmpFlames);
}
_scaleContent(currentWidth) {
if (this._originalContentWidth > 0) {
// Instead of repainting just scale the flames
const ratio = currentWidth / this._originalContentWidth;
this._scalableContentNode.style.transform = `scale(${ratio}, 1)`;
}
}
drawFlame(flame, depth) {
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);
}
buffer += this.drawFlame(rawFlames[i]);
}
add();
}
drawFlame(flame) {
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 y = flame.depth * (kHeight + 1);
let width = flame.duration * this._timeToPixel;
width -= width * 0.1;
const color = this._legend.colorForType(type);
let buffer =
`<rect x=${x} y=${y} width=${width} height=${kHeight} fill=${color} />`;
return `<rect x=${x} y=${y} width=${width} height=${kHeight} fill=${
color} data-id=${flame.id} />`;
}
drawFlameText(flame) {
let type = 'native';
if (flame.entry?.state) {
type = Profile.getKindFromState(flame.entry.state);
}
const kHeight = 9;
const x = this.timeToPosition(flame.start);
const y = flame.depth * (kHeight + 1);
let width = flame.duration * this._timeToPixel;
width -= width * 0.1;
let buffer = '';
if (width < 15 || type == 'native') return buffer;
const rawName = flame.entry.getRawName();
if (rawName.length == 0) return buffer;
......
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