[tools] Speed up system-analyzer

- Reuse DOM nodes if possible
- Delay slow DOM node removal to not block the UI
- Fix global time range syncing when adding timelines to the state
- Use a Proxy to cache CSS colors instead of querying CSS properties
  on every access
- Set className on newly create DOM nodes instead of adding to the
  classList
- Use bound functions for event handlers that are added multiple times
- Speed up Chunk.getBreackdown
- Use CSS gradient for timeline-track chunk backgrounds, which is an
  order of magnitude faster than the serialised canvas approach

Bug: v8:10644
Change-Id: Ie2d6d5b404f18e920c10c0a6460669fd4d0b20e8
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2539947
Commit-Queue: Camillo Bruni <cbruni@chromium.org>
Reviewed-by: Sathya Gunasekaran  <gsathya@chromium.org>
Cr-Commit-Position: refs/heads/master@{#71207}
This commit is contained in:
Camillo Bruni 2020-11-16 14:19:25 +01:00 committed by Commit Bot
parent a303317a0d
commit abf874416c
8 changed files with 350 additions and 293 deletions

View File

@ -32,33 +32,44 @@ class State {
this._deoptTimeline.selectTimeRange(start, end);
}
_updateTimeRange(timeline) {
this._minStartTime = Math.min(this._minStartTime, timeline.startTime);
this._maxEndTime = Math.max(this._maxEndTime, timeline.endTime);
timeline.startTime = this._minStartTime;
timeline.endTime = this._maxEndTime;
_updateTimeRange() {
for (let timeline of this.timelines) {
if (timeline === undefined) return;
this._minStartTime = Math.min(this._minStartTime, timeline.startTime);
this._maxEndTime = Math.max(this._maxEndTime, timeline.endTime);
}
for (let timeline of this.timelines) {
timeline.startTime = this._minStartTime;
timeline.endTime = this._maxEndTime;
}
}
get mapTimeline() {
return this._mapTimeline;
}
set mapTimeline(timeline) {
this._updateTimeRange(timeline);
this._mapTimeline = timeline;
this._updateTimeRange();
}
get icTimeline() {
return this._icTimeline;
}
set icTimeline(timeline) {
this._updateTimeRange(timeline);
this._icTimeline = timeline;
this._updateTimeRange();
}
get deoptTimeline() {
return this._deoptTimeline;
}
set deoptTimeline(timeline) {
this._updateTimeRange(timeline);
this._deoptTimeline = timeline;
this._updateTimeRange();
}
get timelines() {
return [this.mapTimeline, this.icTimeline, this.deoptTimeline];
}
set chunks(value) {
// TODO(zcankara) split up between maps and ics, and every timeline track
this._chunks = value;

View File

@ -23,63 +23,76 @@ function formatSeconds(millis) {
}
class CSSColor {
static getColor(name) {
static _cache = new Map();
static get(name) {
let color = this._cache.get(name);
if (color !== undefined) return color;
const style = getComputedStyle(document.body);
return style.getPropertyValue(`--${name}`);
color = style.getPropertyValue(`--${name}`);
if (color === undefined) {
throw new Error(`CSS color does not exist: ${name}`);
}
this._cache.set(name, color);
return color;
}
static reset() {
this._cache.clear();
}
static get backgroundColor() {
return CSSColor.getColor('backgroud-color');
return this.get('background-color');
}
static get surfaceColor() {
return CSSColor.getColor('surface-color');
return this.get('surface-color');
}
static get primaryColor() {
return CSSColor.getColor('primary-color');
return this.get('primary-color');
}
static get secondaryColor() {
return CSSColor.getColor('secondary-color');
return this.get('secondary-color');
}
static get onSurfaceColor() {
return CSSColor.getColor('on-surface-color');
return this.get('on-surface-color');
}
static get onBackgroundColor() {
return CSSColor.getColor('on-background-color');
return this.get('on-background-color');
}
static get onPrimaryColor() {
return CSSColor.getColor('on-primary-color');
return this.get('on-primary-color');
}
static get onSecondaryColor() {
return CSSColor.getColor('on-secondary-color');
return this.get('on-secondary-color');
}
static get defaultColor() {
return CSSColor.getColor('default-color');
return this.get('default-color');
}
static get errorColor() {
return CSSColor.getColor('error-color');
return this.get('error-color');
}
static get mapBackgroundColor() {
return CSSColor.getColor('map-background-color');
return this.get('map-background-color');
}
static get timelineBackgroundColor() {
return CSSColor.getColor('timeline-background-color');
return this.get('timeline-background-color');
}
static get red() {
return CSSColor.getColor('red');
return this.get('red');
}
static get green() {
return CSSColor.getColor('green');
return this.get('green');
}
static get yellow() {
return CSSColor.getColor('yellow');
return this.get('yellow');
}
static get blue() {
return CSSColor.getColor('blue');
return this.get('blue');
}
static get orange() {
return CSSColor.getColor('orange');
return this.get('orange');
}
static get violet() {
return CSSColor.getColor('violet');
return this.get('violet');
}
}
@ -120,7 +133,7 @@ class DOM {
const node = document.createElement('div');
if (classes !== void 0) {
if (typeof classes === 'string') {
node.classList.add(classes);
node.className = classes;
} else {
classes.forEach(cls => node.classList.add(cls));
}
@ -130,7 +143,7 @@ class DOM {
static table(className) {
const node = document.createElement('table');
if (className) node.classList.add(className);
if (className) node.className = className;
return node;
}
@ -141,13 +154,13 @@ class DOM {
} else if (textOrNode) {
node.innerText = textOrNode;
}
if (className) node.classList.add(className);
if (className) node.className = className;
return node;
}
static tr(className) {
const node = document.createElement('tr');
if (className) node.classList.add(className);
if (className) node.className = className;
return node;
}
@ -184,6 +197,7 @@ class V8CustomElement extends HTMLElement {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = templateText;
this._updateCallback = this._update.bind(this);
}
$(id) {

View File

@ -12,10 +12,15 @@ DOM.defineCustomElement(
'ic-panel', (templateText) => class ICPanel extends V8CustomElement {
_selectedLogEntries;
_timeline;
_detailsClickHandler = this.handleDetailsClick.bind(this);
_mapClickHandler = this.handleMapClick.bind(this);
_fileClickHandler = this.handleFilePositionClick.bind(this);
constructor() {
super(templateText);
this.initGroupKeySelect();
this.groupKey.addEventListener('change', e => this.updateTable(e));
this.groupKey.addEventListener('change', e => this.update());
}
set timeline(value) {
console.assert(value !== undefined, 'timeline undefined!');
@ -104,24 +109,21 @@ DOM.defineCustomElement(
_render(groups, parent) {
const fragment = document.createDocumentFragment();
const max = Math.min(1000, groups.length)
const detailsClickHandler = this.handleDetailsClick.bind(this);
const mapClickHandler = this.handleMapClick.bind(this);
const fileClickHandler = this.handleFilePositionClick.bind(this);
for (let i = 0; i < max; i++) {
const group = groups[i];
const tr = DOM.tr();
tr.group = group;
const details = tr.appendChild(DOM.td('', 'toggle'));
details.onclick = detailsClickHandler;
details.onclick = this._detailsClickHandler;
tr.appendChild(DOM.td(group.percentage + '%', 'percentage'));
tr.appendChild(DOM.td(group.count, 'count'));
const valueTd = tr.appendChild(DOM.td(group.key, 'key'));
if (group.property === 'map') {
valueTd.onclick = mapClickHandler;
valueTd.onclick = this._mapClickHandler;
valueTd.classList.add('clickable');
} else if (group.property == 'filePosition') {
valueTd.classList.add('clickable');
valueTd.onclick = fileClickHandler;
valueTd.onclick = this._fileClickHandler;
}
fragment.appendChild(tr);
}

View File

@ -6,6 +6,7 @@ import {SourcePosition} from '../profile.mjs';
import {State} from './app-model.mjs';
import {FocusEvent, SelectionEvent, SelectTimeEvent} from './events.mjs';
import {CSSColor} from './helper.mjs';
import {$} from './helper.mjs';
import {IcLogEntry} from './log/ic.mjs';
import {MapLogEntry} from './log/map.mjs';
@ -174,9 +175,9 @@ class App {
switchTheme(event) {
document.documentElement.dataset.theme =
event.target.checked ? 'light' : 'dark';
if (this.fileLoaded) {
this.refreshTimelineTrackView();
}
CSSColor.reset();
if (!this.fileLoaded) return;
this.refreshTimelineTrackView();
}
}

View File

@ -4,181 +4,189 @@
import {FocusEvent, SelectionEvent} from '../events.mjs';
import {DOM, typeToColor, V8CustomElement} from '../helper.mjs';
DOM.defineCustomElement('./map-panel/map-transitions',
(templateText) =>
class MapTransitions extends V8CustomElement {
_map;
_selectedMapLogEntries;
_displayedMapsInTree;
DOM.defineCustomElement(
'./map-panel/map-transitions',
(templateText) => class MapTransitions extends V8CustomElement {
_map;
_selectedMapLogEntries;
_displayedMapsInTree;
currentMap = undefined;
_toggleSubtreeHandler = this._handleToggleSubtree.bind(this);
_selectMapHandler = this._handleSelectMap.bind(this);
constructor() {
super(templateText);
this.transitionView.addEventListener(
'mousemove', (e) => this.handleTransitionViewChange(e));
this.currentNode = this.transitionView;
this.currentMap = undefined;
}
get transitionView() {
return this.$('#transitionView');
}
get tooltip() {
return this.$('#tooltip');
}
get tooltipContents() {
return this.$('#tooltipContents');
}
set map(value) {
this._map = value;
this.showMap();
}
handleTransitionViewChange(e) {
this.tooltip.style.left = e.pageX + 'px';
this.tooltip.style.top = e.pageY + 'px';
const map = e.target.map;
if (map) {
this.tooltipContents.innerText = map.description;
}
}
_selectMap(map) {
this.dispatchEvent(new SelectionEvent([map]));
}
showMap() {
if (this.currentMap === this._map) return;
this.currentMap = this._map;
this.selectedMapLogEntries = [this._map];
this.update();
}
_update() {
this.transitionView.style.display = 'none';
DOM.removeAllChildren(this.transitionView);
this._displayedMapsInTree = new Set();
// Limit view to 200 maps for performance reasons.
this.selectedMapLogEntries.slice(0, 200).forEach(
(map) => this.addMapAndParentTransitions(map));
this._displayedMapsInTree = undefined;
this.transitionView.style.display = '';
}
set selectedMapLogEntries(list) {
this._selectedMapLogEntries = list;
this.update();
}
get selectedMapLogEntries() {
return this._selectedMapLogEntries;
}
addMapAndParentTransitions(map) {
if (map === void 0) return;
if (this._displayedMapsInTree.has(map)) return;
this._displayedMapsInTree.add(map);
this.currentNode = this.transitionView;
let parents = map.getParents();
if (parents.length > 0) {
this.addTransitionTo(parents.pop());
parents.reverse().forEach((each) => this.addTransitionTo(each));
}
let mapNode = this.addSubtransitions(map);
// Mark and show the selected map.
mapNode.classList.add('selected');
if (this.selectedMap == map) {
setTimeout(
() => mapNode.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest',
}),
1);
}
}
addSubtransitions(map) {
let mapNode = this.addTransitionTo(map);
// Draw outgoing linear transition line.
let current = map;
while (current.children.length == 1) {
current = current.children[0].to;
this.addTransitionTo(current);
}
return mapNode;
}
addTransitionEdge(map) {
let classes = ['transitionEdge'];
let edge = DOM.div(classes);
edge.style.backgroundColor = typeToColor(map.edge);
let labelNode = DOM.div('transitionLabel');
labelNode.innerText = map.edge.toString();
edge.appendChild(labelNode);
return edge;
}
addTransitionTo(map) {
// transition[ transitions[ transition[...], transition[...], ...]];
this._displayedMapsInTree?.add(map);
let transition = DOM.div('transition');
if (map.isDeprecated()) transition.classList.add('deprecated');
if (map.edge) {
transition.appendChild(this.addTransitionEdge(map));
}
let mapNode = this.addMapNode(map);
transition.appendChild(mapNode);
let subtree = DOM.div('transitions');
transition.appendChild(subtree);
this.currentNode.appendChild(transition);
this.currentNode = subtree;
return mapNode;
}
addMapNode(map) {
let node = DOM.div('map');
if (map.edge) node.style.backgroundColor = typeToColor(map.edge);
node.map = map;
node.addEventListener('click', () => this._selectMap(map));
if (map.children.length > 1) {
node.innerText = map.children.length;
let showSubtree = DOM.div('showSubtransitions');
showSubtree.addEventListener('click', (e) => this.toggleSubtree(e, node));
node.appendChild(showSubtree);
} else if (map.children.length == 0) {
node.innerHTML = '&#x25CF;';
}
this.currentNode.appendChild(node);
return node;
}
toggleSubtree(event, node) {
let map = node.map;
event.target.classList.toggle('opened');
let transitionsNode = node.parentElement.querySelector('.transitions');
let subtransitionNodes = transitionsNode.children;
if (subtransitionNodes.length <= 1) {
// Add subtransitions excepth the one that's already shown.
let visibleTransitionMap = subtransitionNodes.length == 1 ?
transitionsNode.querySelector('.map').map :
void 0;
map.children.forEach((edge) => {
if (edge.to != visibleTransitionMap) {
this.currentNode = transitionsNode;
this.addSubtransitions(edge.to);
}
});
} else {
// remove all but the first (currently selected) subtransition
for (let i = subtransitionNodes.length - 1; i > 0; i--) {
transitionsNode.removeChild(subtransitionNodes[i]);
constructor() {
super(templateText);
this.transitionView.addEventListener(
'mousemove', (e) => this._handleTransitionViewChange(e));
this.currentNode = this.transitionView;
}
}
}
});
get transitionView() {
return this.$('#transitionView');
}
get tooltip() {
return this.$('#tooltip');
}
get tooltipContents() {
return this.$('#tooltipContents');
}
set map(value) {
this._map = value;
this._showMap();
}
_handleTransitionViewChange(e) {
this.tooltip.style.left = e.pageX + 'px';
this.tooltip.style.top = e.pageY + 'px';
const map = e.target.map;
if (map) {
this.tooltipContents.innerText = map.description;
}
}
_selectMap(map) {
this.dispatchEvent(new SelectionEvent([map]));
}
_showMap() {
if (this.currentMap === this._map) return;
this.currentMap = this._map;
this.selectedMapLogEntries = [this._map];
this.update();
}
_update() {
this.transitionView.style.display = 'none';
DOM.removeAllChildren(this.transitionView);
this._displayedMapsInTree = new Set();
// Limit view to 200 maps for performance reasons.
this.selectedMapLogEntries.slice(0, 200).forEach(
(map) => this._addMapAndParentTransitions(map));
this._displayedMapsInTree = undefined;
this.transitionView.style.display = '';
}
set selectedMapLogEntries(list) {
this._selectedMapLogEntries = list;
this.update();
}
get selectedMapLogEntries() {
return this._selectedMapLogEntries;
}
_addMapAndParentTransitions(map) {
if (map === void 0) return;
if (this._displayedMapsInTree.has(map)) return;
this._displayedMapsInTree.add(map);
this.currentNode = this.transitionView;
let parents = map.getParents();
if (parents.length > 0) {
this._addTransitionTo(parents.pop());
parents.reverse().forEach((each) => this._addTransitionTo(each));
}
let mapNode = this._addSubtransitions(map);
// Mark and show the selected map.
mapNode.classList.add('selected');
if (this.selectedMap == map) {
setTimeout(
() => mapNode.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest',
}),
1);
}
}
_addSubtransitions(map) {
let mapNode = this._addTransitionTo(map);
// Draw outgoing linear transition line.
let current = map;
while (current.children.length == 1) {
current = current.children[0].to;
this._addTransitionTo(current);
}
return mapNode;
}
_addTransitionEdge(map) {
let classes = ['transitionEdge'];
let edge = DOM.div(classes);
edge.style.backgroundColor = typeToColor(map.edge);
let labelNode = DOM.div('transitionLabel');
labelNode.innerText = map.edge.toString();
edge.appendChild(labelNode);
return edge;
}
_addTransitionTo(map) {
// transition[ transitions[ transition[...], transition[...], ...]];
this._displayedMapsInTree?.add(map);
let transition = DOM.div('transition');
if (map.isDeprecated()) transition.classList.add('deprecated');
if (map.edge) {
transition.appendChild(this._addTransitionEdge(map));
}
let mapNode = this._addMapNode(map);
transition.appendChild(mapNode);
let subtree = DOM.div('transitions');
transition.appendChild(subtree);
this.currentNode.appendChild(transition);
this.currentNode = subtree;
return mapNode;
}
_addMapNode(map) {
let node = DOM.div('map');
if (map.edge) node.style.backgroundColor = typeToColor(map.edge);
node.map = map;
node.onclick = this._selectMapHandler
if (map.children.length > 1) {
node.innerText = map.children.length;
const showSubtree = DOM.div('showSubtransitions');
showSubtree.onclick = this._toggleSubtreeHandler
node.appendChild(showSubtree);
}
else if (map.children.length == 0) {
node.innerHTML = '&#x25CF;';
}
this.currentNode.appendChild(node);
return node;
}
_handleSelectMap(event) {
this._selectMap(event.currentTarget.map)
}
_handleToggleSubtree(event) {
const node = event.currentTarget.parentElement;
let map = node.map;
event.target.classList.toggle('opened');
let transitionsNode = node.parentElement.querySelector('.transitions');
let subtransitionNodes = transitionsNode.children;
if (subtransitionNodes.length <= 1) {
// Add subtransitions except the one that's already shown.
let visibleTransitionMap = subtransitionNodes.length == 1 ?
transitionsNode.querySelector('.map').map :
void 0;
map.children.forEach((edge) => {
if (edge.to != visibleTransitionMap) {
this.currentNode = transitionsNode;
this._addSubtransitions(edge.to);
}
});
} else {
// remove all but the first (currently selected) subtransition
for (let i = subtransitionNodes.length - 1; i > 0; i--) {
transitionsNode.removeChild(subtransitionNodes[i]);
}
}
}
});

View File

@ -13,6 +13,7 @@ DOM.defineCustomElement('source-panel',
_sourcePositionsToMarkNodes;
_scripts = [];
_script;
constructor() {
super(templateText);
this.scriptDropdown.addEventListener(

View File

@ -248,17 +248,27 @@ class Chunk {
return chunk;
}
getBreakdown(event_fn) {
if (event_fn === void 0) {
event_fn = each => each;
getBreakdown(keyFunction) {
if (this.items.length === 0) return [];
if (keyFunction === void 0) {
keyFunction = each => each;
}
let breakdown = {__proto__: null};
this.items.forEach(each => {
const type = event_fn(each);
const v = breakdown[type];
breakdown[type] = (v | 0) + 1;
});
return Object.entries(breakdown).sort((a, b) => a[1] - b[1]);
const typeToindex = new Map();
const breakdown = [];
// This is performance critical, resorting to for-loop
for (let i = 0; i < this.items.length; i++) {
const each = this.items[i];
const type = keyFunction(each);
const index = typeToindex.get(type);
if (index === void 0) {
typeToindex.set(type, breakdown.length);
breakdown.push([type, 0]);
} else {
breakdown[index][1]++;
}
}
// Sort by count
return breakdown.sort((a, b) => a[1] - b[1]);
}
filter() {

View File

@ -5,6 +5,7 @@
import {FocusEvent, SelectionEvent, SelectTimeEvent, SynchronizeSelectionEvent} from '../events.mjs';
import {CSSColor, delay, DOM, V8CustomElement} from '../helper.mjs';
import {kChunkHeight, kChunkWidth} from '../log/map.mjs';
import {MapLogEntry} from '../log/map.mjs';
const kColors = [
CSSColor.green,
@ -34,6 +35,12 @@ DOM.defineCustomElement('./timeline/timeline-track',
_timeStartOffset;
_selectionOriginTime;
_typeToColor;
_entryTypeDoubleClickHandler = this.handleEntryTypeDoubleClick.bind(this);
_chunkMouseMoveHandler = this.handleChunkMouseMove.bind(this);
_chunkClickHandler = this.handleChunkClick.bind(this);
_chunkDoubleClickHandler = this.handleChunkDoubleClick.bind(this);
constructor() {
super(templateText);
this.timeline.addEventListener('scroll', e => this.handleTimelineScroll(e));
@ -43,7 +50,6 @@ DOM.defineCustomElement('./timeline/timeline-track',
'mouseup', e => this.handleTimeSelectionMouseUp(e));
this.timeline.addEventListener(
'mousemove', e => this.handleTimeSelectionMouseMove(e));
this.backgroundCanvas = document.createElement('canvas');
this.isLocked = false;
}
@ -167,13 +173,14 @@ DOM.defineCustomElement('./timeline/timeline-track',
set data(value) {
this._timeline = value;
this._resetTypeToColorCache();
// Only update legend if the timeline data has changed.
this._updateLegend();
this._updateChunks();
this.update();
}
_update() {
this._updateChunks();
this._updateTimeline();
this._renderLegend();
}
_resetTypeToColorCache() {
@ -190,6 +197,7 @@ DOM.defineCustomElement('./timeline/timeline-track',
set nofChunks(count) {
this._nofChunks = count;
this._updateChunks();
this.update();
}
@ -222,13 +230,13 @@ DOM.defineCustomElement('./timeline/timeline-track',
return this._typeToColor.get(type);
}
_renderLegend() {
_updateLegend() {
let timelineLegendContent = this.timelineLegendContent;
DOM.removeAllChildren(timelineLegendContent);
this._timeline.uniqueTypes.forEach((entries, type) => {
let row = DOM.tr('clickable');
row.entries = entries;
row.addEventListener('dblclick', e => this.handleEntryTypeDblClick(e));
row.ondblclick = this.entryTypeDoubleClickHandler_;
let color = this.typeToColor(type);
if (color !== null) {
let div = DOM.div('colorbox');
@ -254,7 +262,7 @@ DOM.defineCustomElement('./timeline/timeline-track',
this.timelineLegend.appendChild(timelineLegendContent);
}
handleEntryTypeDblClick(e) {
handleEntryTypeDoubleClick(e) {
this.dispatchEvent(new SelectionEvent(e.target.parentNode.entries));
}
@ -268,52 +276,28 @@ DOM.defineCustomElement('./timeline/timeline-track',
'scrolltrack', {bubbles: true, composed: true, detail: horizontal}));
}
async setChunkBackgrounds(backgroundTodo) {
const kMaxDuration = 50;
let lastTime = 0;
for (let [chunk, node] of backgroundTodo) {
const current = performance.now();
if (current - lastTime > kMaxDuration) {
await delay(25);
lastTime = current;
}
this.setChunkBackground(chunk, node);
}
}
setChunkBackground(chunk, node) {
_createBackgroundImage(chunk) {
// Render the types of transitions as bar charts
const kHeight = chunk.height;
const kWidth = 1;
this.backgroundCanvas.width = kWidth;
this.backgroundCanvas.height = kHeight;
let ctx = this.backgroundCanvas.getContext('2d');
ctx.clearRect(0, 0, kWidth, kHeight);
let y = 0;
let total = chunk.size();
let type, count;
if (true) {
chunk.getBreakdown(map => map.type).forEach(([type, count]) => {
ctx.fillStyle = this.typeToColor(type);
let height = count / total * kHeight;
ctx.fillRect(0, y, kWidth, y + height);
y += height;
});
} else {
chunk.items.forEach(map => {
ctx.fillStyle = this.typeToColor(map.type);
let y = chunk.yOffset(map);
ctx.fillRect(0, y, kWidth, y + 1);
});
const total = chunk.size();
let increment = 0;
let lastHeight = 0.0;
const stops = [];
const breakDown = chunk.getBreakdown(map => map.type);
for (let i = 0; i < breakDown.length; i++) {
let [type, count] = breakDown[i];
const color = this.typeToColor(type);
increment += count;
let height = (increment / total * kHeight) | 0;
stops.push(`${color} ${lastHeight}px ${height}px`)
lastHeight = height;
}
let imageData = this.backgroundCanvas.toDataURL('image/webp', 0.2);
node.style.backgroundImage = `url(${imageData})`;
return `linear-gradient(0deg,${stops.join(',')})`;
}
_updateTimeline() {
let chunksNode = this.timelineChunks;
DOM.removeAllChildren(chunksNode);
const reusableNodes = Array.from(this.timelineChunks.childNodes).reverse();
let fragment = new DocumentFragment();
let chunks = this.chunks;
let max = chunks.max(each => each.size());
let start = this.data.startTime;
@ -321,30 +305,29 @@ DOM.defineCustomElement('./timeline/timeline-track',
let duration = end - start;
this._timeToPixel = chunks.length * kChunkWidth / duration;
this._timeStartOffset = start * this._timeToPixel;
let addTimestamp = (time, name) => {
let timeNode = DOM.div('timestamp');
timeNode.innerText = name;
timeNode.style.left = ((time - start) * this._timeToPixel) + 'px';
chunksNode.appendChild(timeNode);
};
let backgroundTodo = [];
for (let i = 0; i < chunks.length; i++) {
let chunk = chunks[i];
let height = (chunk.size() / max * kChunkHeight);
chunk.height = height;
if (chunk.isEmpty()) continue;
let node = DOM.div();
node.className = 'chunk';
node.style.left = ((chunks[i].start - start) * this._timeToPixel) + 'px';
node.style.height = height + 'px';
let node = reusableNodes[reusableNodes.length - 1];
let reusedNode = false;
if (node?.className == 'chunk') {
reusableNodes.pop();
reusedNode = true;
} else {
node = DOM.div('chunk');
node.onmousemove = this._chunkMouseMoveHandler;
node.onclick = this._chunkClickHandler;
node.ondblclick = this.chunkDoubleClickHandler;
}
const style = node.style;
style.left = `${((chunk.start - start) * this._timeToPixel) | 0}px`;
style.height = `${height | 0}px`;
style.backgroundImage = this._createBackgroundImage(chunk);
node.chunk = chunk;
node.addEventListener('mousemove', e => this.handleChunkMouseMove(e));
node.addEventListener('click', e => this.handleChunkClick(e));
node.addEventListener('dblclick', e => this.handleChunkDoubleClick(e));
backgroundTodo.push([chunk, node])
chunksNode.appendChild(node);
if (!reusedNode) fragment.appendChild(node);
}
this.setChunkBackgrounds(backgroundTodo);
// Put a time marker roughly every 20 chunks.
let expected = duration / chunks.length * 20;
@ -355,9 +338,29 @@ DOM.defineCustomElement('./timeline/timeline-track',
let time = start;
while (time < end) {
addTimestamp(time, ((time - start) / 1000) + ' ms');
let timeNode = DOM.div('timestamp');
timeNode.innerText = `${((time - start) / 1000) | 0} ms`;
timeNode.style.left = `${((time - start) * this._timeToPixel) | 0}px`;
fragment.appendChild(timeNode);
time += interval;
}
// Remove superfluos nodes lazily, for Chrome this is a very expensive
// operation.
if (reusableNodes.length > 0) {
for (const node of reusableNodes) {
node.style.display = 'none';
}
setTimeout(() => {
const range = document.createRange();
const first = reusableNodes[reusableNodes.length - 1];
const last = reusableNodes[0];
range.setStartBefore(first);
range.setEndAfter(last);
range.deleteContents();
}, 100);
}
this.timelineChunks.appendChild(fragment);
this.redraw();
}
@ -384,8 +387,15 @@ DOM.defineCustomElement('./timeline/timeline-track',
}
redraw() {
window.requestAnimationFrame(() => this._redraw());
}
_redraw() {
if (!(this._timeline.at(0) instanceof MapLogEntry)) return;
let canvas = this.timelineCanvas;
canvas.width = (this.chunks.length + 1) * kChunkWidth;
let width = (this.chunks.length + 1) * kChunkWidth;
if (width > 32767) width = 32767;
canvas.width = width;
canvas.height = kChunkHeight;
let ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, kChunkHeight);