[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:
parent
a303317a0d
commit
abf874416c
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 = '●';
|
||||
}
|
||||
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 = '●';
|
||||
}
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -13,6 +13,7 @@ DOM.defineCustomElement('source-panel',
|
||||
_sourcePositionsToMarkNodes;
|
||||
_scripts = [];
|
||||
_script;
|
||||
|
||||
constructor() {
|
||||
super(templateText);
|
||||
this.scriptDropdown.addEventListener(
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user