Upload Kmake

This commit is contained in:
Gorochu
2026-05-26 23:36:42 -07:00
parent ba051b2f74
commit 555ec72358
41615 changed files with 13344630 additions and 1 deletions

View File

@ -0,0 +1,73 @@
<!-- Copyright 2022 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. -->
<head>
<link href="./index.css" rel="stylesheet">
</head>
<style>
:host {
--selection-height: 5px;
--total-height: 50px;
}
#svg {
width: 100%;
height: var(--total-height);
position: sticky;
top: 0px;
}
.marker {
width: 2px;
y: var(--selection-height);
height: calc(var(--total-height) - var(--selection-height));
}
#indicator {
stroke: var(--on-surface-color);
}
.continuousTrack {
transform: translate(0, var(--selection-height));
}
#filler {
height: var(--total-height);
}
#selection {
height: var(--total-height);
}
#selection rect {
height: var(--selection-height);
fill: var(--selection-color);
}
#selection .top {
y: 0px;
}
#selection .bottom {
y: calc(var(--total-height) - var(--selection-height));
}
#selection line {
stroke: var(--on-surface-color);
}
</style>
<svg id="svg" viewBox="0 1 800 50" preserveAspectRatio=none>
<defs>
<pattern id="pattern1" patternUnits="userSpaceOnUse" width="4" height="4">
<path d="M-1,1 l2,-2
M0,4 l4,-4
M3,5 l2,-2" stroke="white"/>
</pattern>
<mask id="mask1">
<rect width="800" height="40" fill="url(#pattern1)" />
</mask>
</defs>
<rect id="filler" width="800" fill-opacity="0"/>
<g id="content"></g>
<svg id="selection">
<line x1="0%" y1="0" x2="0%" y2="50" />
<rect class="top" x="0%" width="100%"></rect>
<rect class="bottom" x="0%" width="100%"></rect>
<line x1="100%" y1="0" x2="100%" y2="50" />
</svg>
<line id="indicator" x1="0" y1="0" x2="0" y2="50" />
</svg>

View File

@ -0,0 +1,279 @@
// Copyright 2022 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 {Timeline} from '../../timeline.mjs';
import {ToolTipEvent} from '../events.mjs';
import {arrayEquals, CSSColor, DOM, formatDurationMicros, SVG, V8CustomElement} from '../helper.mjs';
/** @template T */
export class Track {
static continuous(array, color) {
return new this(array, color, true);
}
static discrete(array, color) {
return new this(array, color, false);
}
/** @param {T[]} logEntries */
constructor(logEntries, color, isContinuous) {
/** @type {Set<T>} */
this.logEntries = new Set(logEntries);
this.color = color;
/** @type {bool} */
this.isContinuous = isContinuous;
}
isEmpty() {
return this.logEntries.size == 0;
}
/** @param {T} logEntry */
hasEntry(logEntry) {
return this.logEntries.has(logEntry);
}
static compare(left, right) {
return left.equals(right);
}
equals(other) {
if (!arrayEquals(
Array.from(this.logEntries), Array.from(other.logEntries))) {
return false;
}
if (this.color != other.color) return false;
if (this.isContinuous != other.isContinuous) return false;
return true;
}
}
const kHorizontalPixels = 800;
const kMarginHeight = 5;
const kHeight = 40;
DOM.defineCustomElement(
'view/timeline/timeline-overview',
(templateText) =>
/** @template T */
class TimelineOverview extends V8CustomElement {
/** @type {Timeline<T>} */
_timeline;
/** @type {Track[]} */
_tracks = [];
_timeToPixel = 1;
/** @type {{entry:T, track:Track} => number} */
_countCallback = (entry, track) => track.hasEntry(entry);
constructor() {
super(templateText);
this._indicatorNode = this.$('#indicator');
this._selectionNode = this.$('#selection');
this._contentNode = this.$('#content');
this._svgNode = this.$('#svg');
this._svgNode.onmousemove = this._handleMouseMove.bind(this);
}
/**
* @param {Timeline<T>} timeline
*/
set timeline(timeline) {
this._timeline = timeline;
this._timeToPixel = kHorizontalPixels / this._timeline.duration();
}
/**
* @param {Track[]} tracks
*/
set tracks(tracks) {
// TODO(cbruni): Allow updating the selection time-range independently
// from the data. if (arrayEquals(this._tracks, tracks, Track.compare))
// return;
this._tracks = tracks;
this.requestUpdate();
}
/** @param {{entry:T, track:Track} => number} callback*/
set countCallback(callback) {
this._countCallback = callback;
}
_handleMouseMove(e) {
const externalPixelToTime = this._timeline.duration() /
this._svgNode.getBoundingClientRect().width;
const timeToInternalPixel =
kHorizontalPixels / this._timeline.duration();
const xPos = e.offsetX;
const timeMicros = xPos * externalPixelToTime;
const maxTimeDistance = 2 * externalPixelToTime;
this._setIndicatorPosition(timeMicros * timeToInternalPixel);
let toolTipContent =
this._findLogEntryAtTime(timeMicros, maxTimeDistance);
if (!toolTipContent) {
toolTipContent = `Time ${formatDurationMicros(timeMicros)}`;
}
this.dispatchEvent(
new ToolTipEvent(toolTipContent, this._indicatorNode, e.ctrlKey));
}
_findLogEntryAtTime(time, maxTimeDistance) {
const minTime = time - maxTimeDistance;
const maxTime = time + maxTimeDistance;
for (let track of this._tracks) {
for (let entry of track.logEntries) {
if (minTime <= entry.time && entry.time <= maxTime) return entry;
}
}
}
_setIndicatorPosition(x) {
this._indicatorNode.setAttribute('x1', x);
this._indicatorNode.setAttribute('x2', x);
}
_update() {
const fragment = new DocumentFragment();
this._tracks.forEach((track, index) => {
if (!track.isEmpty()) {
fragment.appendChild(this._renderTrack(track, index));
}
});
DOM.removeAllChildren(this._contentNode);
this._contentNode.appendChild(fragment);
this._setIndicatorPosition(-10);
this._updateSelection();
}
_renderTrack(track, index) {
if (track.isContinuous)
return this._renderContinuousTrack(track, index);
return this._renderDiscreteTrack(track, index);
}
_renderContinuousTrack(track, index) {
const freq = new Frequency(this._timeline);
freq.collect(track, this._countCallback);
const path = SVG.path('continuousTrack');
path.setAttribute('d', freq.toSVG(kHeight));
path.setAttribute('fill', track.color);
if (index != 0) path.setAttribute('mask', `url(#mask${index})`)
return path;
}
_renderDiscreteTrack(track) {
const group = SVG.g();
for (let entry of track.logEntries) {
const x = entry.time * this._timeToPixel;
const kWidth = 2;
const rect = SVG.rect('marker');
rect.setAttribute('x', x - (kWidth / 2));
rect.setAttribute('fill', track.color);
rect.data = entry;
group.appendChild(rect);
}
return group;
}
_updateSelection() {
let startTime = -10;
let duration = 0;
if (this._timeline) {
startTime = this._timeline.selectionOrSelf.startTime;
duration = this._timeline.selectionOrSelf.duration();
}
this._selectionNode.setAttribute('x', startTime * this._timeToPixel);
this._selectionNode.setAttribute('width', duration * this._timeToPixel);
}
});
const kernel = smoothingKernel(50);
function smoothingKernel(size) {
const kernel = new Float32Array(size);
const mid = (size - 1) / 2;
const stddev = size / 10;
for (let i = mid; i < size; i++) {
const x = i - (mid | 0);
const value =
Math.exp(-(x ** 2 / 2 / stddev)) / (stddev * Math.sqrt(2 * Math.PI));
kernel[Math.ceil(x + mid)] = value;
kernel[Math.floor(mid - x)] = value;
}
return kernel;
}
class Frequency {
constructor(timeline) {
this._size = kHorizontalPixels;
this._timeline = timeline;
this._data = new Int16Array(this._size + kernel.length);
this._max = undefined;
this._smoothenedData = undefined;
}
collect(track, sumFn) {
const kernelRadius = (kernel.length / 2) | 0;
let count = 0;
let dataIndex = kernelRadius;
const timeDelta = this._timeline.duration() / this._size;
let nextTime = this._timeline.startTime + timeDelta;
const values = this._timeline.values;
const length = values.length;
let i = 0;
while (i < length && dataIndex < this._data.length) {
const tick = values[i];
if (tick.time < nextTime) {
count += sumFn(tick, track);
i++;
} else {
this._data[dataIndex] = count;
nextTime += timeDelta;
dataIndex++;
count = 0;
}
}
this._data[dataIndex] = count;
}
max() {
if (this._max !== undefined) return this._max;
let max = 0;
this._smoothenedData = new Float32Array(this._size);
for (let start = 0; start < this._size; start++) {
let value = 0;
for (let i = 0; i < kernel.length; i++) {
value += this._data[start + i] * kernel[i];
}
this._smoothenedData[start] = value;
max = Math.max(max, value);
}
this._max = max;
return this._max;
}
toSVG(height) {
const vScale = height / this.max();
const initialY = height;
const buffer = [
'M 0',
initialY,
];
let prevY = initialY;
let usedPrevY = false;
for (let i = 0; i < this._size; i++) {
const y = height - (this._smoothenedData[i] * vScale) | 0;
if (y == prevY) {
usedPrevY = false;
continue;
}
if (!usedPrevY) buffer.push('L', i - 1, prevY);
buffer.push('L', i, y);
prevY = y;
usedPrevY = true;
}
if (!usedPrevY) buffer.push('L', this._size - 1, prevY);
buffer.push('L', this._size - 1, initialY);
buffer.push('Z');
return buffer.join(' ');
}
}

View File

@ -0,0 +1,735 @@
// 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 {kChunkHeight, kChunkVisualWidth, kChunkWidth} from '../../log/map.mjs';
import {SelectionEvent, SelectTimeEvent, SynchronizeSelectionEvent, ToolTipEvent,} from '../events.mjs';
import {CSSColor, delay, DOM, formatDurationMicros, V8CustomElement} from '../helper.mjs';
export const kTimelineHeight = 200;
export class TimelineTrackBase extends V8CustomElement {
_timeline;
_nofChunks = 500;
_chunks = [];
_selectedEntry;
_focusedEntry;
_timeToPixel;
_timeStartPixelOffset;
_legend;
_lastContentWidth = 0;
_cachedTimelineBoundingClientRect;
_cachedTimelineScrollLeft;
constructor(templateText) {
super(templateText);
this._selectionHandler = new SelectionHandler(this);
this._legend = new Legend(this.$('#legendTable'));
this.timelineChunks = this.$('#timelineChunks');
this.timelineSamples = this.$('#timelineSamples');
this.timelineNode = this.$('#timeline');
this._toolTipTargetNode = undefined;
this.hitPanelNode = this.$('#hitPanel');
this.timelineAnnotationsNode = this.$('#timelineAnnotations');
this.timelineMarkersNode = this.$('#timelineMarkers');
this._scalableContentNode = this.$('#scalableContent');
this.isLocked = false;
this.setAttribute('tabindex', 0);
}
_initEventListeners() {
this._legend.onFilter = this._handleFilterTimeline.bind(this);
this.timelineNode.addEventListener(
'scroll', this._handleTimelineScroll.bind(this));
this.hitPanelNode.onclick = this._handleClick.bind(this);
this.hitPanelNode.ondblclick = this._handleDoubleClick.bind(this);
this.hitPanelNode.onmousemove = this._handleMouseMove.bind(this);
this.$('#selectionForeground')
.addEventListener('mousemove', this._handleMouseMove.bind(this));
window.addEventListener('resize', () => this._resetCachedDimensions());
}
static get observedAttributes() {
return ['title'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name == 'title') {
this.$('#title').innerHTML = newValue;
}
}
_handleFilterTimeline(type) {
this._updateChunks();
this._legend.update(true);
}
set data(timeline) {
console.assert(timeline);
if (!this._timeline) this._initEventListeners();
this._timeline = timeline;
this._legend.timeline = timeline;
this.$('.content').style.display = timeline.isEmpty() ? 'none' : 'relative';
this._updateChunks();
}
set timeSelection({start, end, focus = false, zoom = false}) {
this._selectionHandler.timeSelection = {start, end};
this.updateSelection();
if (focus || zoom) {
if (!Number.isFinite(start) || !Number.isFinite(end)) {
throw new Error('Invalid number ranges');
}
if (focus) {
this.currentTime = (start + end) / 2;
}
if (zoom) {
const margin = 0.2;
const newVisibleTime = (end - start) * (1 + 2 * margin);
const currentVisibleTime =
this._cachedTimelineBoundingClientRect.width / this._timeToPixel;
this.nofChunks = this.nofChunks * (currentVisibleTime / newVisibleTime);
}
}
}
updateSelection() {
this._selectionHandler.update();
this._legend.update();
}
get _timelineBoundingClientRect() {
if (this._cachedTimelineBoundingClientRect === undefined) {
this._cachedTimelineBoundingClientRect =
this.timelineNode.getBoundingClientRect();
}
return this._cachedTimelineBoundingClientRect;
}
get _timelineScrollLeft() {
if (this._cachedTimelineScrollLeft === undefined) {
this._cachedTimelineScrollLeft = this.timelineNode.scrollLeft;
}
return this._cachedTimelineScrollLeft;
}
_resetCachedDimensions() {
this._cachedTimelineBoundingClientRect = undefined;
this._cachedTimelineScrollLeft = undefined;
}
// Maps the clicked x position to the x position on timeline
positionOnTimeline(pagePosX) {
let rect = this._timelineBoundingClientRect;
let posClickedX = pagePosX - rect.left + this._timelineScrollLeft;
return posClickedX;
}
positionToTime(pagePosX) {
return this.relativePositionToTime(this.positionOnTimeline(pagePosX));
}
relativePositionToTime(timelineRelativeX) {
const timelineAbsoluteX = timelineRelativeX + this._timeStartPixelOffset;
return (timelineAbsoluteX / this._timeToPixel) | 0;
}
timeToPosition(time) {
let relativePosX = time * this._timeToPixel;
relativePosX -= this._timeStartPixelOffset;
return relativePosX;
}
set nofChunks(count) {
const centerTime = this.currentTime;
const kMinNofChunks = 100;
if (count < kMinNofChunks) count = kMinNofChunks;
const kMaxNofChunks = 10 * 1000;
if (count > kMaxNofChunks) count = kMaxNofChunks;
this._nofChunks = count | 0;
this._updateChunks();
this.currentTime = centerTime;
}
get nofChunks() {
return this._nofChunks;
}
_updateChunks() {
this._chunks = undefined;
this._updateDimensions();
this.requestUpdate();
}
get chunks() {
if (this._chunks?.length != this.nofChunks) {
this._chunks =
this._timeline.chunks(this.nofChunks, this._legend.filterPredicate);
console.assert(this._chunks.length == this._nofChunks);
}
return this._chunks;
}
set selectedEntry(value) {
this._selectedEntry = value;
}
get selectedEntry() {
return this._selectedEntry;
}
get focusedEntry() {
return this._focusedEntry;
}
set focusedEntry(entry) {
this._focusedEntry = entry;
if (entry) this._drawAnnotations(entry);
}
set scrollLeft(offset) {
this.timelineNode.scrollLeft = offset;
this._cachedTimelineScrollLeft = offset;
}
get scrollLeft() {
return this._cachedTimelineScrollLeft;
}
set currentTime(time) {
const position = this.timeToPosition(time);
const centerOffset = this._timelineBoundingClientRect.width / 2;
this.scrollLeft = Math.max(0, position - centerOffset);
}
get currentTime() {
const centerOffset =
this._timelineBoundingClientRect.width / 2 + this.scrollLeft;
return this.relativePositionToTime(centerOffset);
}
handleEntryTypeDoubleClick(e) {
this.dispatchEvent(new SelectionEvent(e.target.parentNode.entries));
}
timelineIndicatorMove(offset) {
this.timelineNode.scrollLeft += offset;
this._cachedTimelineScrollLeft = undefined;
}
_handleTimelineScroll(e) {
let scrollLeft = e.currentTarget.scrollLeft;
this._cachedTimelineScrollLeft = scrollLeft;
this.dispatchEvent(new CustomEvent(
'scrolltrack', {bubbles: true, composed: true, detail: scrollLeft}));
}
_updateDimensions() {
// No data in this timeline, no need to resize
if (!this._timeline) return;
const centerOffset = this._timelineBoundingClientRect.width / 2;
const time =
this.relativePositionToTime(this._timelineScrollLeft + centerOffset);
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.$('#cropper').style.width = `${width}px`;
this.timelineMarkersNode.style.width = `${width}px`;
this.timelineAnnotationsNode.style.width = `${width}px`;
this.hitPanelNode.style.width = `${width}px`;
const ratio = this._scaleContent(width) || 1;
this.timelineChunks.style.width = `${width / Math.min(1, ratio)}px`;
this._drawMarkers();
this._selectionHandler.update();
this._cachedTimelineScrollLeft = this.timelineNode.scrollLeft =
this.timeToPosition(time) - centerOffset;
}
_scaleContent(currentWidth) {
if (!this._lastContentWidth) return 1;
const ratio = currentWidth / this._lastContentWidth;
this._scalableContentNode.style.transform = `scale(${ratio}, 1)`;
return ratio;
}
_adjustHeight(height) {
const dataHeight = Math.max(height, 200);
const viewHeight = Math.min(dataHeight, 400);
this.style.setProperty('--data-height', dataHeight + 'px');
this.style.setProperty('--view-height', viewHeight + 'px');
this.timelineNode.style.overflowY =
(height > kTimelineHeight) ? 'scroll' : 'hidden';
}
_update() {
this._legend.update();
this._drawContent().then(() => this._drawAnnotations(this.selectedEntry));
this._resetCachedDimensions();
}
async _drawContent() {
if (this._timeline.isEmpty()) return;
await delay(5);
const chunks = this.chunks;
const max = chunks.max(each => each.size());
let buffer = '';
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
const height = (chunk.size() / max * kChunkHeight);
chunk.height = height;
if (chunk.isEmpty()) continue;
buffer += '<g>';
buffer += this._drawChunk(i, chunk);
buffer += '</g>'
}
this._scalableContentNode.innerHTML = buffer;
this._scalableContentNode.style.transform = 'scale(1, 1)';
}
_drawChunk(chunkIndex, chunk) {
const groups = chunk.getBreakdown(event => event.type);
let buffer = '';
const kHeight = chunk.height;
let lastHeight = kTimelineHeight;
for (let i = 0; i < groups.length; i++) {
const group = groups[i];
if (group.length == 0) break;
const height = (group.length / chunk.size() * kHeight) | 0;
lastHeight -= height;
const color = this._legend.colorForType(group.key);
buffer += `<rect x=${chunkIndex * kChunkWidth} y=${lastHeight} height=${
height} width=${kChunkVisualWidth} fill=${color} />`
}
return buffer;
}
_drawMarkers() {
// Put a time marker roughly every 20 chunks.
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;
interval *= correction;
const start = this._timeline.startTime;
let time = start;
let buffer = '';
while (time < this._timeline.endTime) {
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;
}
this.timelineMarkersNode.innerHTML = buffer;
}
_drawAnnotations(logEntry, time) {
if (!this._focusedEntry) return;
this._drawEntryMark(this._focusedEntry);
}
_drawEntryMark(entry) {
const [x, y] = this._positionForEntry(entry);
const color = this._legend.colorForType(entry.type);
const mark =
`<circle cx=${x} cy=${y} r=3 stroke=${color} class=annotationPoint />`;
this.timelineAnnotationsNode.innerHTML = mark;
}
_handleUnlockedMouseEvent(event) {
this._focusedEntry = this._getEntryForEvent(event);
if (!this._focusedEntry) return false;
this._updateToolTip(event);
const time = this.positionToTime(event.pageX);
this._drawAnnotations(this._focusedEntry, time);
}
_updateToolTip(event) {
if (!this._focusedEntry) return false;
this.dispatchEvent(new ToolTipEvent(
this._focusedEntry, this._toolTipTargetNode, event.ctrlKey));
event.stopImmediatePropagation();
}
_handleClick(event) {
if (event.button !== 0) return;
if (event.target === this.timelineChunks) return;
this.isLocked = !this.isLocked;
// Do this unconditionally since we want the tooltip to be update to the
// latest locked state.
this._handleUnlockedMouseEvent(event);
return false;
}
_handleDoubleClick(event) {
if (event.button !== 0) return;
this._selectionHandler.clearSelection();
const time = this.positionToTime(event.pageX);
const chunk = this._getChunkForEvent(event)
if (!chunk) return;
event.stopImmediatePropagation();
this.dispatchEvent(new SelectTimeEvent(chunk.start, chunk.end));
return false;
}
_handleMouseMove(event) {
if (event.button !== 0) return;
if (this._selectionHandler.isSelecting) return false;
if (this.isLocked && this._focusedEntry) {
this._updateToolTip(event);
return false;
}
this._handleUnlockedMouseEvent(event);
}
_getChunkForEvent(event) {
const time = this.positionToTime(event.pageX);
return this._chunkForTime(time);
}
_chunkForTime(time) {
const chunkIndex = ((time - this._timeline.startTime) /
this._timeline.duration() * this._nofChunks) |
0;
return this.chunks[chunkIndex];
}
_positionForEntry(entry) {
const chunk = this._chunkForTime(entry.time);
if (chunk === undefined) return [-1, -1];
const xFrom = (chunk.index * kChunkWidth + kChunkVisualWidth / 2) | 0;
const yFrom = kTimelineHeight - chunk.yOffset(entry) | 0;
return [xFrom, yFrom];
}
_getEntryForEvent(event) {
const chunk = this._getChunkForEvent(event);
if (chunk?.isEmpty() ?? true) return false;
const relativeIndex = Math.round(
(kTimelineHeight - event.layerY) / chunk.height * (chunk.size() - 1));
if (relativeIndex > chunk.size()) return false;
const logEntry = chunk.at(relativeIndex);
const node = this.getToolTipTargetNode(logEntry);
if (!node) return logEntry;
const style = node.style;
style.left = `${chunk.index * kChunkWidth}px`;
style.top = `${kTimelineHeight - chunk.height}px`;
style.height = `${chunk.height}px`;
style.width = `${kChunkVisualWidth}px`;
return logEntry;
}
getToolTipTargetNode(logEntry) {
let node = this._toolTipTargetNode;
if (node) {
if (node.logEntry === logEntry) return undefined;
node.parentNode.removeChild(node);
}
node = this._toolTipTargetNode = DOM.div('toolTipTarget');
node.logEntry = logEntry;
this.$('#cropper').appendChild(node);
return node;
}
};
class SelectionHandler {
// TODO turn into static field once Safari supports it.
static get SELECTION_OFFSET() {
return 10
};
_timeSelection = {start: -1, end: Infinity};
_selectionOriginTime = -1;
constructor(timeline) {
this._timeline = timeline;
this._timelineNode = this._timeline.$('#timeline');
this._timelineNode.addEventListener(
'mousedown', this._handleMouseDown.bind(this));
this._timelineNode.addEventListener(
'mouseup', this._handleMouseUp.bind(this));
this._timelineNode.addEventListener(
'mousemove', this._handleMouseMove.bind(this));
this._selectionNode = this._timeline.$('#selection');
this._selectionForegroundNode = this._timeline.$('#selectionForeground');
this._selectionForegroundNode.addEventListener(
'dblclick', this._handleDoubleClick.bind(this));
this._selectionBackgroundNode = this._timeline.$('#selectionBackground');
this._leftHandleNode = this._timeline.$('#leftHandle');
this._rightHandleNode = this._timeline.$('#rightHandle');
}
update() {
if (!this.hasSelection) {
this._selectionNode.style.display = 'none';
return;
}
this._selectionNode.style.display = 'inherit';
const startPosition = this.timeToPosition(this._timeSelection.start);
const endPosition = this.timeToPosition(this._timeSelection.end);
this._leftHandleNode.style.left = startPosition + 'px';
this._rightHandleNode.style.left = endPosition + 'px';
const delta = endPosition - startPosition;
this._selectionForegroundNode.style.left = startPosition + 'px';
this._selectionForegroundNode.style.width = delta + 'px';
this._selectionBackgroundNode.style.left = startPosition + 'px';
this._selectionBackgroundNode.style.width = delta + 'px';
}
set timeSelection(selection) {
this._timeSelection.start = selection.start;
this._timeSelection.end = selection.end;
}
clearSelection() {
this._timeline.dispatchEvent(new SelectTimeEvent());
}
timeToPosition(posX) {
return this._timeline.timeToPosition(posX);
}
positionToTime(posX) {
return this._timeline.positionToTime(posX);
}
get isSelecting() {
return this._selectionOriginTime >= 0;
}
get hasSelection() {
return this._timeSelection.start >= 0 &&
this._timeSelection.end != Infinity;
}
get _leftHandlePosX() {
return this._leftHandleNode.getBoundingClientRect().x;
}
get _rightHandlePosX() {
return this._rightHandleNode.getBoundingClientRect().x;
}
_isOnLeftHandle(posX) {
return Math.abs(this._leftHandlePosX - posX) <=
SelectionHandler.SELECTION_OFFSET;
}
_isOnRightHandle(posX) {
return Math.abs(this._rightHandlePosX - posX) <=
SelectionHandler.SELECTION_OFFSET;
}
_handleMouseDown(event) {
if (event.button !== 0) return;
let xPosition = event.clientX
// Update origin time in case we click on a handle.
if (this._isOnLeftHandle(xPosition)) {
xPosition = this._rightHandlePosX;
}
else if (this._isOnRightHandle(xPosition)) {
xPosition = this._leftHandlePosX;
}
this._selectionOriginTime = this.positionToTime(xPosition);
}
_handleMouseMove(event) {
if (event.button !== 0) return;
if (!this.isSelecting) return;
const currentTime = this.positionToTime(event.clientX);
this._timeline.dispatchEvent(new SynchronizeSelectionEvent(
Math.min(this._selectionOriginTime, currentTime),
Math.max(this._selectionOriginTime, currentTime)));
}
_handleMouseUp(event) {
if (event.button !== 0) return;
this._selectionOriginTime = -1;
if (this._timeSelection.start === -1) return;
const delta = this._timeSelection.end - this._timeSelection.start;
if (delta <= 1 || isNaN(delta)) return;
this._timeline.dispatchEvent(new SelectTimeEvent(
this._timeSelection.start, this._timeSelection.end));
}
_handleDoubleClick(event) {
if (!this.hasSelection) return;
// Focus and zoom to the current selection.
this._timeline.dispatchEvent(new SelectTimeEvent(
this._timeSelection.start, this._timeSelection.end, true, true));
}
}
class Legend {
_timeline;
_lastSelection;
_typesFilters = new Map();
_typeClickHandler = this._handleTypeClick.bind(this);
_filterPredicate = this.filter.bind(this);
onFilter = () => {};
constructor(table) {
this._table = table;
this._enableDuration = false;
}
set timeline(timeline) {
this._timeline = timeline;
const groups = timeline.getBreakdown();
this._typesFilters = new Map(groups.map(each => [each.key, true]));
this._colors =
new Map(groups.map(each => [each.key, CSSColor.at(each.id)]));
}
get selection() {
return this._timeline.selectionOrSelf;
}
get filterPredicate() {
for (let visible of this._typesFilters.values()) {
if (!visible) return this._filterPredicate;
}
return undefined;
}
colorForType(type) {
let color = this._colors.get(type);
if (color === undefined) {
color = CSSColor.at(this._colors.size);
this._colors.set(type, color);
}
return color;
}
filter(logEntry) {
return this._typesFilters.get(logEntry.type);
}
update(force = false) {
if (!force && this._lastSelection === this.selection) return;
this._lastSelection = this.selection;
const tbody = DOM.tbody();
const missingTypes = new Set(this._typesFilters.keys());
this._checkDurationField();
let selectionDuration = 0;
const breakdown =
this.selection.getBreakdown(undefined, this._enableDuration);
if (this._enableDuration) {
if (this.selection.cachedDuration === undefined) {
this.selection.cachedDuration = this._breakdownTotalDuration(breakdown);
}
selectionDuration = this.selection.cachedDuration;
}
breakdown.forEach(group => {
tbody.appendChild(this._addTypeRow(group, selectionDuration));
missingTypes.delete(group.key);
});
missingTypes.forEach(key => {
const emptyGroup = {key, length: 0, duration: 0};
tbody.appendChild(this._addTypeRow(emptyGroup, selectionDuration));
});
if (this._timeline.selection) {
tbody.appendChild(this._addRow(
'', 'Selection', this.selection.length, '100%', selectionDuration,
'100%'));
}
// Showing 100% for 'All' and for 'Selection' would be confusing.
const allPercent = this._timeline.selection ? '' : '100%';
tbody.appendChild(this._addRow(
'', 'All', this._timeline.length, allPercent,
this._timeline.cachedDuration, allPercent));
this._table.tBodies[0].replaceWith(tbody);
}
_checkDurationField() {
if (this._enableDuration) return;
const example = this.selection.at(0);
if (!example || !('duration' in example)) return;
this._enableDuration = true;
this._table.tHead.rows[0].appendChild(DOM.td('Duration'));
}
_addRow(colorNode, type, count, countPercent, duration, durationPercent) {
const row = DOM.tr();
const colorCell = row.appendChild(DOM.td(colorNode, 'color'));
colorCell.setAttribute('title', `Toggle '${type}' entries.`);
const typeCell = row.appendChild(DOM.td(type, 'text'));
typeCell.setAttribute('title', type);
row.appendChild(DOM.td(count.toString()));
row.appendChild(DOM.td(countPercent));
if (this._enableDuration) {
row.appendChild(DOM.td(formatDurationMicros(duration ?? 0)));
row.appendChild(DOM.td(durationPercent ?? '0%'));
}
return row
}
_addTypeRow(group, selectionDuration) {
const color = this.colorForType(group.key);
const classes = ['colorbox'];
if (group.length == 0) classes.push('empty');
const colorDiv = DOM.div(classes);
colorDiv.style.borderColor = color;
if (this._typesFilters.get(group.key)) {
colorDiv.style.backgroundColor = color;
} else {
colorDiv.style.backgroundColor = CSSColor.backgroundImage;
}
let duration = 0;
let durationPercent = '';
if (this._enableDuration) {
// group.duration was added in _breakdownTotalDuration.
duration = group.duration;
durationPercent = selectionDuration == 0 ?
'0%' :
this._formatPercent(duration / selectionDuration);
}
const countPercent =
this._formatPercent(group.length / this.selection.length);
const row = this._addRow(
colorDiv, group.key, group.length, countPercent, duration,
durationPercent);
row.className = 'clickable';
row.onclick = this._typeClickHandler;
row.data = group.key;
return row;
}
_handleTypeClick(e) {
const type = e.currentTarget.data;
this._typesFilters.set(type, !this._typesFilters.get(type));
this.onFilter(type);
}
_breakdownTotalDuration(breakdown) {
let duration = 0;
breakdown.forEach(group => {
group.duration = this._groupDuration(group);
duration += group.duration;
})
return duration;
}
_groupDuration(group) {
let duration = 0;
const entries = group.entries;
for (let i = 0; i < entries.length; i++) {
duration += entries[i].duration;
}
return duration;
}
_formatPercent(ratio) {
return `${(ratio * 100).toFixed(1)}%`;
}
}

View File

@ -0,0 +1,259 @@
// 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 {kChunkVisualWidth, MapLogEntry} from '../../log/map.mjs';
import {FocusEvent} from '../events.mjs';
import {CSSColor, DOM} 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);
this.navigation = new Navigation(this)
}
_handleKeyDown(event) {}
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, time) {
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;
}
})
class Navigation {
constructor(track) {
this._track = track;
this._track.addEventListener('keydown', this._handleKeyDown.bind(this));
this._map = undefined;
}
_handleKeyDown(event) {
if (!this._track.isFocused) return;
let handled = false;
switch (event.key) {
case 'ArrowDown':
handled = true;
if (event.shiftKey) {
this.selectPrevEdge();
} else {
this.moveInChunk(-1);
}
break;
case 'ArrowUp':
handled = true;
if (event.shiftKey) {
this.selectNextEdge();
} else {
this.moveInChunk(1);
}
break;
case 'ArrowLeft':
handled = true;
this.moveInChunks(false);
break;
case 'ArrowRight':
handled = true;
this.moveInChunks(true);
break;
case 'Enter':
handled = true;
this.selectMap();
break
}
if (handled) {
event.stopPropagation();
event.preventDefault();
return false;
}
}
get map() {
return this._track.focusedEntry;
}
set map(map) {
this._track.focusedEntry = map;
}
get chunks() {
return this._track.chunks;
}
selectMap() {
if (!this.map) return;
this._track.dispatchEvent(new FocusEvent(this.map))
}
selectNextEdge() {
if (!this.map) return;
if (this.map.children.length != 1) return;
this.show(this.map.children[0].to);
}
selectPrevEdge() {
if (!this.map) return;
if (!this.map.parent) return;
this.map = this.map.parent;
}
selectDefaultMap() {
this.map = this.chunks[0].at(0);
}
moveInChunks(next) {
if (!this.map) return this.selectDefaultMap();
let chunkIndex = this.map.chunkIndex(this.chunks);
let currentChunk = this.chunks[chunkIndex];
let currentIndex = currentChunk.indexOf(this.map);
let newChunk;
if (next) {
newChunk = chunk.next(this.chunks);
} else {
newChunk = chunk.prev(this.chunks);
}
if (!newChunk) return;
let newIndex = Math.min(currentIndex, newChunk.size() - 1);
this.map = newChunk.at(newIndex);
}
moveInChunk(delta) {
if (!this.map) return this.selectDefaultMap();
let chunkIndex = this.map.chunkIndex(this.chunks)
let chunk = this.chunks[chunkIndex];
let index = chunk.indexOf(this.map) + delta;
let map;
if (index < 0) {
map = chunk.prev(this.chunks).last();
} else if (index >= chunk.size()) {
map = chunk.next(this.chunks).first()
} else {
map = chunk.at(index);
}
this.map = map;
}
}

View File

@ -0,0 +1,150 @@
// 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 {Timeline} from '../../timeline.mjs';
import {SelectTimeEvent} from '../events.mjs';
import {CSSColor, delay, 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;
if (timeline.values.length > 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 node = this.getToolTipTargetNode(logEntry);
if (!node) return logEntry;
const style = node.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)`);
return ratio;
}
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 = roundTo3Digits(this.timeToPosition(item.time));
const y = (item.depth + 1) * kItemHeight;
let width = roundTo3Digits(item.duration * this._timeToPixel);
if (outline) {
return `<rect x=${x} y=${y} width=${width} height=${
kItemHeight - 1} class=fs />`;
}
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=f />`;
}
_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;
}
}
function roundTo3Digits(value) {
return ((value * 1000) | 0) / 1000;
}

View File

@ -0,0 +1,258 @@
<!-- 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. -->
<head>
<link href="./index.css" rel="stylesheet">
</head>
<style>
:host {
--view-height: 200px;
--data-height: 200px;
--text-scale: scale(1, 1);
}
#timeline {
position: relative;
height: calc(var(--view-height) + 12px);
overflow-y: hidden;
overflow-x: scroll;
user-select: none;
}
#cropper {
overflow: hidden;
position: relative;
}
.dataSized {
min-height: var(--data-height);
}
#timelineSamples, #timelineChunks,
#timelineMarkers, #timelineAnnotations, #hitPanel {
top: 0px;
position: absolute;
margin-right: 100px;
}
#timelineMarkers, #timelineAnnotations,
.noPointerEvents, .noPointerEvents * {
pointer-events: none;
}
.toolTipTarget {
position: absolute;
}
.title {
position: relative;
float: left;
width: 20px;
writing-mode: vertical-rl;
text-orientation: mixed;
margin: 20px 0 0 -10px;
padding: 5px 5px 0px 5px;
overflow: hidden;
border-radius: 7px;
font-weight: 400;
}
.panelCloserInput:checked ~ h3 {
display: inherit;
flex: 1;
writing-mode: unset;
text-orientation: unset;
background-color: var(--border-color);
border-radius: 0px;
padding: 5px;
margin: 0 -10px 0 20px;
}
.timelineLegend {
position: relative;
float: right;
height: calc(var(--view-height) + 12px);
overflow-y: scroll;
margin-right: -10px;
padding: 0 2px 0 2px;
width: 400px;
border-left: 1px solid var(--border-color);
}
#legendTable {
width: 100%;
border-collapse: collapse;
}
thead {
border-top: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
}
th,
td {
padding: 1px 3px 2px 3px;
}
#legendTable td {
padding-top: 3px;
text-align: right;
}
/* Center colors */
#legendTable .color {
text-align: center;
}
/* Left align text*/
#legendTable .text {
text-align: left;
width: 100%;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
}
.timeline {
background-color: var(--timeline-background-color);
}
#selection {
display: none;
top: 0px;
left: 0px;
position: absolute;
}
#rightHandle,
#leftHandle {
background-color: rgba(200, 200, 200, 0.5);
height: 100%;
width: 5px;
position: absolute;
z-index: 3;
cursor: col-resize;
}
#leftHandle {
border-left: 1px solid var(--on-surface-color);
}
#rightHandle {
border-right: 1px solid var(--on-surface-color);
margin-left: -5px;
}
#selectionForeground{
z-index: 2;
cursor: grab;
height: 100%;
position: absolute;
}
#selectionForeground:active {
cursor: grabbing;
}
#selectionBackground {
background-color: var(--selection-color);
height: 100%;
position: absolute;
}
.content {
display: flex;
position: relative;
}
.panelCloserLabel {
position: absolute;
top: 5px;
left: 0px;
}
.title {
flex: initial;
}
#timeline {
flex: 1;
}
.legend {
flex: initial;
}
.colorbox.empty {
opacity: 0.5;
}
</style>
<style>
/* SVG styles */
.txt {
font: 8px var(--code-font);
transform: var(--txt-scale);
}
.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;
}
.markerLine {
stroke: var(--on-background-color);
stroke-dasharray: 2 2;
}
.markerText {
fill: var(--on-surface-color);
dominant-baseline: hanging;
font-size: 9px;
}
.flame, .f {
stroke-width: 0;
}
.flameSelected, .fs {
fill: var(--on-background-color);
fill-opacity: 0.1;
stroke: var(--on-background-color);
stroke-opacity: 0.8;
stroke-width: 1;
vector-effect: non-scaling-stroke;
}
svg {
shape-rendering: optimizeSpeed;
}
</style>
<div class="content">
<input type="checkbox" id="closer" class="panelCloserInput">
<label class="panelCloserLabel" for="closer">▼</label>
<h3 class="title" id="title"></h3>
<div id="timeline">
<div id="selection" class="dataSized">
<div id="leftHandle"></div>
<div id="selectionForeground"></div>
<div id="selectionBackground"></div>
<div id="rightHandle"></div>
</div>
<div id="cropper" class="dataSized">
<svg id="timelineChunks" xmlns="http://www.w3.org/2000/svg" class="dataSized">
<g id="scalableContent"></g>
</svg>
<svg id="timelineAnnotations" xmlns="http://www.w3.org/2000/svg" class="dataSized noPointerEvents"></svg>
<svg id="timelineMarkers" xmlns="http://www.w3.org/2000/svg" class="dataSized noPointerEvents"></svg>
</div>
<!-- Use a div element covering all complex items to prevent slow hit test-->
<div id="hitPanel" class="dataSized"></div>
</div>
<div class="timelineLegend">
<table id="legendTable">
<thead>
<tr>
<td></td>
<td class="text">Type</td>
<td>Count</td>
<td></td>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,112 @@
// 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 {Flame, FlameBuilder} from '../../profiling.mjs';
import {Timeline} from '../../timeline.mjs';
import {delay, DOM, SVG} from '../helper.mjs';
import {TimelineTrackStackedBase} from './timeline-track-stacked-base.mjs'
DOM.defineCustomElement(
'view/timeline/timeline-track', 'timeline-track-tick',
(templateText) => class TimelineTrackTick extends TimelineTrackStackedBase {
constructor() {
super(templateText);
this._annotations = new Annotations(this);
}
_prepareDrawableItems() {
const flameBuilder = FlameBuilder.forTime(this._timeline.values, true);
this._drawableItems = new Timeline(Flame, flameBuilder.flames);
this._annotations.flames = this._drawableItems;
this._adjustStackDepth(flameBuilder.maxDepth);
}
_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;
_logEntry;
_buffer;
constructor(track) {
this._track = track;
}
set flames(flames) {
this._flames = flames;
}
get _node() {
return this._track.timelineAnnotationsNode;
}
async update(logEntry, time) {
if (this._logEntry == logEntry) return;
this._logEntry = logEntry;
this._node.innerHTML = '';
if (logEntry === undefined) return;
this._buffer = '';
const start = this._flames.find(time);
let offset = 0;
// Draw annotations gradually outwards starting form the given time.
let deadline = performance.now() + 100;
for (let range = 0; range < this._flames.length; range += 10000) {
this._markFlames(start - range, start - offset);
this._markFlames(start + offset, start + range);
offset = range;
if ((navigator?.scheduling?.isInputPending({includeContinuous: true}) ??
false) ||
performance.now() >= deadline) {
// Yield if we have to handle an input event, or we're out of time.
await delay(50);
// Abort if we started another update asynchronously.
if (this._logEntry != logEntry) return;
deadline = performance.now() + 100;
}
this._drawBuffer();
}
this._drawBuffer();
}
_markFlames(start, end) {
const rawFlames = this._flames.values;
if (start < 0) start = 0;
if (end > rawFlames.length) end = rawFlames.length;
const logEntry = this._logEntry;
// Also compare against the SFI , if any.
const sfi = logEntry.entry?.sfi ?? -1;
for (let i = start; i < end; i++) {
const flame = rawFlames[i];
const flameLogEntry = flame.logEntry;
if (!flameLogEntry) continue;
if (flameLogEntry !== logEntry) {
if (flameLogEntry.entry?.sfi !== sfi) continue;
}
this._buffer += this._track._drawItem(flame, i, true);
}
}
_drawBuffer() {
if (this._buffer.length == 0) return;
const svg = SVG.svg();
svg.innerHTML = this._buffer;
this._node.appendChild(svg);
this._buffer = '';
}
}

View File

@ -0,0 +1,41 @@
// Copyright 2021 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {CSSColor, DOM, SVG, V8CustomElement} from '../helper.mjs';
import {TimelineTrackBase} from './timeline-track-base.mjs'
import {TimelineTrackStackedBase} from './timeline-track-stacked-base.mjs'
DOM.defineCustomElement(
'view/timeline/timeline-track', 'timeline-track-timer',
(templateText) =>
class TimelineTrackTimer extends TimelineTrackStackedBase {
constructor() {
super(templateText);
}
_prepareDrawableItems() {
const stack = [];
let maxDepth = 0;
for (let i = 0; i < this._timeline.length; i++) {
const timer = this._timeline.at(i);
let insertDepth = -1;
for (let depth = 0; depth < stack.length; depth++) {
const pendingTimer = stack[depth];
if (pendingTimer === undefined) {
if (insertDepth === -1) insertDepth = depth;
} else if (pendingTimer.endTime <= timer.startTime) {
stack[depth] == undefined;
if (insertDepth === -1) insertDepth = depth;
}
}
if (insertDepth === -1) insertDepth = stack.length;
stack[insertDepth] = timer;
timer.depth = insertDepth;
maxDepth = Math.max(maxDepth, insertDepth);
}
this._drawableItems = this._timeline;
this._adjustStackDepth(maxDepth++);
}
});

View File

@ -0,0 +1,15 @@
// 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'
DOM.defineCustomElement(
'view/timeline/timeline-track',
(templateText) => class TimelineTrack extends TimelineTrackBase {
constructor() {
super(templateText);
}
})