[tools][system-analyzer] Create timeline track component
This CL creates a timeline track component to make the timeline view extensible as different data sources added. The timeline track component will take data source and display it with respect to time axis of timeline overview. Bug: v8:10644, v8:10735 Change-Id: I1c88dd2dc967be68e6235e517dcf8554a891eee4 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2302053 Commit-Queue: Zeynep Cankara <zcankara@google.com> Reviewed-by: Camillo Bruni <cbruni@chromium.org> Cr-Commit-Position: refs/heads/master@{#69102}
This commit is contained in:
parent
385382097a
commit
ff4833f14c
@ -2,80 +2,88 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import {$} from './helper.mjs';
|
||||
import { V8Map } from './map-processor.mjs';
|
||||
|
||||
class State {
|
||||
#mapTimeline;
|
||||
#icTimeline;
|
||||
#timeline;
|
||||
#transitions;
|
||||
constructor(mapPanelId, timelinePanelId) {
|
||||
this.mapPanel_ = $(mapPanelId);
|
||||
this.timelinePanel_ = $(timelinePanelId);
|
||||
this._navigation = new Navigation(this);
|
||||
this.timelinePanel_.addEventListener(
|
||||
#mapPanel;
|
||||
#timelinePanel;
|
||||
#mapTrack;
|
||||
#icTrack;
|
||||
#map;
|
||||
#ic;
|
||||
#navigation;
|
||||
constructor(mapPanel, timelinePanel, mapTrack, icTrack) {
|
||||
this.#mapPanel = mapPanel;
|
||||
this.#timelinePanel = timelinePanel;
|
||||
this.#mapTrack = mapTrack;
|
||||
this.#icTrack = icTrack;
|
||||
this.#navigation = new Navigation(this);
|
||||
this.#timelinePanel.addEventListener(
|
||||
'mapchange', e => this.handleMapChange(e));
|
||||
this.timelinePanel_.addEventListener(
|
||||
this.#timelinePanel.addEventListener(
|
||||
'showmaps', e => this.handleShowMaps(e));
|
||||
this.mapPanel_.addEventListener(
|
||||
'statemapchange', e => this.handleStateMapChange(e));
|
||||
this.mapPanel_.addEventListener(
|
||||
this.#mapPanel.addEventListener(
|
||||
'mapchange', e => this.handleMapChange(e));
|
||||
this.#mapPanel.addEventListener(
|
||||
'selectmapdblclick', e => this.handleDblClickSelectMap(e));
|
||||
this.mapPanel_.addEventListener(
|
||||
this.#mapPanel.addEventListener(
|
||||
'sourcepositionsclick', e => this.handleClickSourcePositions(e));
|
||||
}
|
||||
get chunks(){
|
||||
//TODO(zcankara) for timeline dependency
|
||||
return this.#mapTrack.chunks;
|
||||
}
|
||||
handleMapChange(e){
|
||||
if (!(e.detail instanceof V8Map)) return;
|
||||
this.map = e.detail;
|
||||
}
|
||||
handleStateMapChange(e){
|
||||
this.map = e.detail;
|
||||
}
|
||||
handleShowMaps(e){
|
||||
this.mapPanel_.mapEntries = e.detail.filter();
|
||||
}
|
||||
get icTimeline() {
|
||||
return this.#icTimeline;
|
||||
}
|
||||
set icTimeline(value) {
|
||||
this.#icTimeline = value;
|
||||
if (!(e.detail instanceof V8Map)) return;
|
||||
this.#mapPanel.mapEntries = e.detail.filter();
|
||||
}
|
||||
set transitions(value) {
|
||||
this.mapPanel_.transitions = value;
|
||||
this.#mapPanel.transitions = value;
|
||||
}
|
||||
get timeline() {
|
||||
return this.#timeline;
|
||||
}
|
||||
set timeline(value) {
|
||||
this.#timeline = value;
|
||||
this.timelinePanel.timelineEntries = value;
|
||||
this.timelinePanel.updateTimeline(this.map);
|
||||
this.mapPanel_.timeline = this.timeline;
|
||||
}
|
||||
get chunks() {
|
||||
return this.timelinePanel.chunks;
|
||||
set mapTimeline(value) {
|
||||
this.#mapPanel.timeline = value;
|
||||
}
|
||||
get nofChunks() {
|
||||
return this.timelinePanel.nofChunks;
|
||||
}
|
||||
set nofChunks(count) {
|
||||
this.timelinePanel.nofChunks = count;
|
||||
this.timelinePanel.updateTimeline(this.map);
|
||||
}
|
||||
get mapPanel() {
|
||||
return this.mapPanel_;
|
||||
return this.#mapPanel;
|
||||
}
|
||||
get timelinePanel() {
|
||||
return this.timelinePanel_;
|
||||
return this.#timelinePanel;
|
||||
}
|
||||
get navigation() {
|
||||
return this._navigation
|
||||
return this.#navigation
|
||||
}
|
||||
|
||||
get map() {
|
||||
return this.timelinePanel.map;
|
||||
return this.#map;
|
||||
}
|
||||
set map(value) {
|
||||
if(!value) return;
|
||||
this.timelinePanel.map = value;
|
||||
this._navigation.updateUrl();
|
||||
this.#map = value;
|
||||
this.#mapTrack.selectedEntry = value;
|
||||
this.#navigation.updateUrl();
|
||||
this.mapPanel.map = value;
|
||||
}
|
||||
get ic() {
|
||||
return this.#ic;
|
||||
}
|
||||
set ic(value) {
|
||||
if(!value) return;
|
||||
this.#ic = value;
|
||||
}
|
||||
|
||||
get entries() {
|
||||
if (!this.map) return {};
|
||||
return {
|
||||
@ -92,11 +100,6 @@ class State {
|
||||
//TODO(zcankara) Handle double clicked map
|
||||
console.log("double clicked map: ", e.detail);
|
||||
}
|
||||
|
||||
handleStateMapChange(e){
|
||||
this.map = e.detail;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Navigation {
|
||||
@ -174,5 +177,4 @@ class Navigation {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export { State };
|
||||
export { State };
|
||||
|
@ -6,6 +6,7 @@ class Event {
|
||||
#time;
|
||||
#type;
|
||||
constructor(type, time) {
|
||||
//TODO(zcankara) remove type and add empty getters to override
|
||||
this.#time = time;
|
||||
this.#type = type;
|
||||
}
|
||||
|
@ -118,11 +118,22 @@ function transitionTypeToColor(type) {
|
||||
return CSSColor.primaryColor;
|
||||
case 'ReplaceDescriptors':
|
||||
return CSSColor.red;
|
||||
case 'LoadGlobalIC':
|
||||
return CSSColor.green;
|
||||
case 'StoreInArrayLiteralIC':
|
||||
return CSSColor.violet;
|
||||
case 'StoreIC':
|
||||
return CSSColor.orange;
|
||||
case 'KeyedLoadIC':
|
||||
return CSSColor.red;
|
||||
case 'KeyedStoreIC':
|
||||
return CSSColor.primaryColor;
|
||||
}
|
||||
return CSSColor.primaryColor;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function div(classes) {
|
||||
let node = document.createElement('div');
|
||||
if (classes !== void 0) {
|
||||
|
@ -261,4 +261,4 @@ class Entry extends Event {
|
||||
}
|
||||
}
|
||||
|
||||
export { CustomIcProcessor as default };
|
||||
export { CustomIcProcessor as default };
|
||||
|
@ -93,8 +93,8 @@ found in the LICENSE file. -->
|
||||
<script type="module" >
|
||||
import {App} from './index.mjs';
|
||||
|
||||
globalThis.app = new App("#log-file-reader", "#map-panel",
|
||||
"#timeline-panel", "#ic-panel");
|
||||
globalThis.app = new App("#log-file-reader", "#map-panel", "#timeline-panel",
|
||||
"#ic-panel", "#map-track", "#ic-track");
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
@ -105,7 +105,10 @@ globalThis.app = new App("#log-file-reader", "#map-panel",
|
||||
<br></br>
|
||||
</section>
|
||||
<div id="container" style="display: none;">
|
||||
<timeline-panel id="timeline-panel"></timeline-panel>
|
||||
<timeline-panel id="timeline-panel">
|
||||
<timeline-track id="map-track"></timeline-track>
|
||||
<timeline-track id="ic-track"></timeline-track>
|
||||
</timeline-panel>
|
||||
<map-panel id="map-panel" onclick="app.handleMapAddressSearch(event)" onchange="app.handleShowMaps(event)"></map-panel>
|
||||
<ic-panel id="ic-panel" onchange="app.handleSelectIc(event)"></ic-panel>
|
||||
</div>
|
||||
|
@ -11,23 +11,31 @@ import './map-panel.mjs';
|
||||
import './log-file-reader.mjs';
|
||||
class App {
|
||||
#timeSelection = {start: 0, end: Infinity};
|
||||
constructor(fileReaderId, mapPanelId, timelinePanelId, icPanelId) {
|
||||
this.mapPanelId_ = mapPanelId;
|
||||
this.timelinePanelId_ = timelinePanelId;
|
||||
this.icPanelId_ = icPanelId;
|
||||
this.icPanel_ = this.$(icPanelId);
|
||||
this.fileLoaded = false;
|
||||
this.logFileReader_ = this.$(fileReaderId);
|
||||
this.logFileReader_.addEventListener('fileuploadstart',
|
||||
#mapPanel;
|
||||
#timelinePanel;
|
||||
#icPanel;
|
||||
#mapTrack;
|
||||
#icTrack;
|
||||
#logFileReader;
|
||||
constructor(fileReaderId, mapPanelId, timelinePanelId,
|
||||
icPanelId, mapTrackId, icTrackId) {
|
||||
this.#logFileReader = this.$(fileReaderId);
|
||||
this.#mapPanel = this.$(mapPanelId);
|
||||
this.#timelinePanel = this.$(timelinePanelId);
|
||||
this.#icPanel = this.$(icPanelId);
|
||||
this.#mapTrack = this.$(mapTrackId);
|
||||
this.#icTrack = this.$(icTrackId);
|
||||
|
||||
this.#logFileReader.addEventListener('fileuploadstart',
|
||||
e => this.handleFileUpload(e));
|
||||
this.logFileReader_.addEventListener('fileuploadend',
|
||||
this.#logFileReader.addEventListener('fileuploadend',
|
||||
e => this.handleDataUpload(e));
|
||||
document.addEventListener('keydown', e => this.handleKeyDown(e));
|
||||
this.icPanel_.addEventListener(
|
||||
this.#icPanel.addEventListener(
|
||||
'ictimefilter', e => this.handleICTimeFilter(e));
|
||||
this.icPanel_.addEventListener(
|
||||
this.#icPanel.addEventListener(
|
||||
'mapclick', e => this.handleMapClick(e));
|
||||
this.icPanel_.addEventListener(
|
||||
this.#icPanel.addEventListener(
|
||||
'filepositionclick', e => this.handleFilePositionClick(e));
|
||||
this.toggleSwitch = this.$('.theme-switch input[type="checkbox"]');
|
||||
this.toggleSwitch.addEventListener('change', e => this.switchTheme(e));
|
||||
@ -48,9 +56,9 @@ class App {
|
||||
handleICTimeFilter(event) {
|
||||
this.#timeSelection.start = event.detail.startTime;
|
||||
this.#timeSelection.end = event.detail.endTime;
|
||||
document.state.icTimeline.selectTimeRange(this.#timeSelection.start,
|
||||
this.#icTrack.data.selectTimeRange(this.#timeSelection.start,
|
||||
this.#timeSelection.end);
|
||||
this.icPanel_.filteredEntries = document.state.icTimeline.selection;
|
||||
this.#icPanel.filteredEntries = this.#icTrack.data.selection;
|
||||
}
|
||||
|
||||
|
||||
@ -102,13 +110,13 @@ class App {
|
||||
reader.onload = (evt) => {
|
||||
let icProcessor = new CustomIcProcessor();
|
||||
//TODO(zcankara) Assign timeline directly to the ic panel
|
||||
//TODO(zcankara) Exe: this.icPanel_.timeline = document.state.icTimeline
|
||||
document.state.icTimeline = icProcessor.processString(fileData.chunk);
|
||||
this.icPanel_.filteredEntries = document.state.icTimeline.all;
|
||||
this.icPanel_.count.innerHTML = document.state.icTimeline.all.length;
|
||||
//TODO(zcankara) Exe: this.#icPanel.timeline = document.state.icTimeline
|
||||
this.#icTrack.data = icProcessor.processString(fileData.chunk);
|
||||
this.#icPanel.filteredEntries = this.#icTrack.data.all;
|
||||
this.#icPanel.count.innerHTML = this.#icTrack.data.all.length;
|
||||
}
|
||||
reader.readAsText(fileData.file);
|
||||
this.icPanel_.initGroupKeySelect();
|
||||
this.#icPanel.initGroupKeySelect();
|
||||
}
|
||||
|
||||
// call when a new file uploaded
|
||||
@ -117,12 +125,14 @@ class App {
|
||||
this.$('#container').style.display = 'block';
|
||||
// instantiate the app logic
|
||||
let fileData = e.detail;
|
||||
document.state = new State(this.mapPanelId_, this.timelinePanelId_);
|
||||
document.state = new State(this.#mapPanel, this.#timelinePanel,
|
||||
this.#mapTrack, this.#icTrack);
|
||||
try {
|
||||
const timeline = this.handleLoadTextMapProcessor(fileData.chunk);
|
||||
// Transitions must be set before timeline for stats panel.
|
||||
document.state.transitions= timeline.transitions;
|
||||
document.state.timeline = timeline;
|
||||
this.#mapTrack.data = timeline;
|
||||
document.state.mapTimeline = timeline;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ defineCustomElement('./map-panel/map-transitions', (templateText) =>
|
||||
this.dispatchEvent(new CustomEvent(
|
||||
'mapdetailsupdate', {bubbles: true, composed: true, detail: map}));
|
||||
this.dispatchEvent(new CustomEvent(
|
||||
'statemapchange', {bubbles: true, composed: true, detail: map}));
|
||||
'mapchange', {bubbles: true, composed: true, detail: map}));
|
||||
}
|
||||
|
||||
dblClickSelectMap(map) {
|
||||
|
@ -247,11 +247,12 @@ class V8Map extends Event {
|
||||
leftId= 0;
|
||||
rightId = 0;
|
||||
filePosition = '';
|
||||
|
||||
constructor(id, time = -1) {
|
||||
if (time <= 0) throw new Error('Invalid time');
|
||||
id = -1;
|
||||
constructor(id, time) {
|
||||
if (!time) throw new Error('Invalid time');
|
||||
super(id, time);
|
||||
V8Map.set(id, this);
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
finalizeRootMap(id) {
|
||||
@ -333,7 +334,7 @@ class V8Map extends Event {
|
||||
return transitions;
|
||||
}
|
||||
|
||||
getType() {
|
||||
get type() {
|
||||
return this.edge === void 0 ? 'new' : this.edge.type;
|
||||
}
|
||||
|
||||
@ -516,4 +517,4 @@ class ArgumentsProcessor extends BaseArgumentsProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
export { MapProcessor, V8Map, kChunkWidth, kChunkHeight};
|
||||
export { MapProcessor, V8Map, kChunkWidth, kChunkHeight};
|
||||
|
@ -4,55 +4,6 @@ found in the LICENSE file. -->
|
||||
|
||||
<style>
|
||||
@import "./index.css";
|
||||
#timeline {
|
||||
position: relative;
|
||||
height: 300px;
|
||||
overflow-y: hidden;
|
||||
overflow-x: scroll;
|
||||
user-select: none;
|
||||
background-color: var(--timeline-background-color);
|
||||
box-shadow: 0 19px 38px rgba(0,0,0,0.30), 0 15px 12px rgba(0,0,0,0.22);
|
||||
}
|
||||
#timelineLabel {
|
||||
transform: rotate(90deg);
|
||||
transform-origin: left bottom 0;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 250px;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
#timelineChunks {
|
||||
height: 250px;
|
||||
position: absolute;
|
||||
margin-right: 100px;
|
||||
}
|
||||
#timelineCanvas {
|
||||
height: 250px;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
}
|
||||
.chunk {
|
||||
width: 6px;
|
||||
border: 0px var(--timeline-background-color) solid;
|
||||
border-width: 0 2px 0 2px;
|
||||
position: absolute;
|
||||
background-size: 100% 100%;
|
||||
image-rendering: pixelated;
|
||||
bottom: 0px;
|
||||
}
|
||||
.timestamp {
|
||||
height: 250px;
|
||||
width: 100px;
|
||||
border-left: 1px var(--surface-color) dashed;
|
||||
padding-left: 4px;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
font-size: 10px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
#timelineOverview {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
@ -89,10 +40,8 @@ found in the LICENSE file. -->
|
||||
<div class="panel">
|
||||
<h2>Timeline Panel</h2>
|
||||
<h3>Timeline</h3>
|
||||
<div id="timeline">
|
||||
<div id="timelineLabel">Frequency</div>
|
||||
<div id="timelineChunks"></div>
|
||||
<canvas id="timelineCanvas"></canvas>
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div id="timelineOverview">
|
||||
<div id="timelineOverviewIndicator">
|
||||
|
@ -1,9 +1,9 @@
|
||||
// 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 {defineCustomElement, V8CustomElement,
|
||||
transitionTypeToColor, CSSColor} from './helper.mjs';
|
||||
import {kChunkWidth, kChunkHeight} from './map-processor.mjs';
|
||||
|
||||
import {defineCustomElement, V8CustomElement} from './helper.mjs';
|
||||
import './timeline/timeline-track.mjs';
|
||||
|
||||
defineCustomElement('timeline-panel', (templateText) =>
|
||||
class TimelinePanel extends V8CustomElement {
|
||||
@ -11,14 +11,11 @@ defineCustomElement('timeline-panel', (templateText) =>
|
||||
super(templateText);
|
||||
this.timelineOverview.addEventListener(
|
||||
'mousemove', e => this.handleTimelineIndicatorMove(e));
|
||||
setInterval(this.updateOverviewWindow(), 50);
|
||||
this.addEventListener(
|
||||
'overviewupdate', e => this.handleOverviewBackgroundUpdate(e));
|
||||
this.backgroundCanvas = document.createElement('canvas');
|
||||
this.isLocked = false;
|
||||
}
|
||||
#timelineEntries;
|
||||
#nofChunks = 400;
|
||||
#chunks;
|
||||
#map;
|
||||
|
||||
get timelineOverview() {
|
||||
return this.$('#timelineOverview');
|
||||
@ -29,50 +26,34 @@ defineCustomElement('timeline-panel', (templateText) =>
|
||||
}
|
||||
|
||||
get timelineCanvas() {
|
||||
return this.$('#timelineCanvas');
|
||||
}
|
||||
|
||||
get timelineChunks() {
|
||||
return this.$('#timelineChunks');
|
||||
//TODO Don't access the timeline canvas from outside timeline track
|
||||
if(!this.timelineTracks || !this.timelineTracks.length) return;
|
||||
return this.timelineTracks[0].timelineCanvas;
|
||||
}
|
||||
|
||||
get timeline() {
|
||||
return this.$('#timeline');
|
||||
//TODO(zcankara) Don't access the timeline from outside timeline track
|
||||
if(!this.timelineTracks || !this.timelineTracks.length) return;
|
||||
return this.timelineTracks[0].timeline;
|
||||
}
|
||||
|
||||
set timelineEntries(value){
|
||||
this.#timelineEntries = value;
|
||||
this.updateChunks();
|
||||
}
|
||||
get timelineEntries(){
|
||||
return this.#timelineEntries;
|
||||
}
|
||||
set nofChunks(count){
|
||||
this.#nofChunks = count;
|
||||
this.updateChunks();
|
||||
for (const track of this.timelineTracks) {
|
||||
track.nofChunks = count;
|
||||
}
|
||||
}
|
||||
get nofChunks(){
|
||||
return this.#nofChunks;
|
||||
}
|
||||
updateChunks() {
|
||||
this.#chunks = this.timelineEntries.chunks(this.nofChunks);
|
||||
}
|
||||
get chunks(){
|
||||
return this.#chunks;
|
||||
}
|
||||
set map(value){
|
||||
this.#map = value;
|
||||
this.redraw(this.map);
|
||||
}
|
||||
get map(){
|
||||
return this.#map;
|
||||
get timelineTracks(){
|
||||
return this.$("slot").assignedNodes().filter(
|
||||
track => track.nodeType === Node.ELEMENT_NODE);
|
||||
}
|
||||
|
||||
handleTimelineIndicatorMove(event) {
|
||||
if (event.buttons == 0) return;
|
||||
let timelineTotalWidth = this.timelineCanvas.offsetWidth;
|
||||
let factor = this.timelineOverview.offsetWidth / timelineTotalWidth;
|
||||
this.timeline.scrollLeft += event.movementX / factor;
|
||||
for (const track of this.timelineTracks) {
|
||||
track.handleTimelineIndicatorMove(event.movementX / factor);
|
||||
}
|
||||
}
|
||||
|
||||
updateOverviewWindow() {
|
||||
@ -80,8 +61,7 @@ defineCustomElement('timeline-panel', (templateText) =>
|
||||
let totalIndicatorWidth =
|
||||
this.timelineOverview.offsetWidth;
|
||||
let div = this.timeline;
|
||||
let timelineTotalWidth =
|
||||
this.timelineCanvas.offsetWidth;
|
||||
let timelineTotalWidth = this.timelineCanvas.offsetWidth;
|
||||
let factor = totalIndicatorWidth / timelineTotalWidth;
|
||||
let width = div.offsetWidth * factor;
|
||||
let left = div.scrollLeft * factor;
|
||||
@ -89,289 +69,9 @@ defineCustomElement('timeline-panel', (templateText) =>
|
||||
indicator.style.left = left + 'px';
|
||||
}
|
||||
|
||||
asyncSetTimelineChunkBackground(backgroundTodo) {
|
||||
const kIncrement = 100;
|
||||
let start = 0;
|
||||
let delay = 1;
|
||||
while (start < backgroundTodo.length) {
|
||||
let end = Math.min(start + kIncrement, backgroundTodo.length);
|
||||
setTimeout((from, to) => {
|
||||
for (let i = from; i < to; i++) {
|
||||
let [chunk, node] = backgroundTodo[i];
|
||||
this.setTimelineChunkBackground(chunk, node);
|
||||
}
|
||||
}, delay++, start, end);
|
||||
start = end;
|
||||
}
|
||||
}
|
||||
|
||||
setTimelineChunkBackground(chunk, node) {
|
||||
// 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.getType()).forEach(([type, count]) => {
|
||||
ctx.fillStyle = transitionTypeToColor(type);
|
||||
let height = count / total * kHeight;
|
||||
ctx.fillRect(0, y, kWidth, y + height);
|
||||
y += height;
|
||||
});
|
||||
} else {
|
||||
chunk.items.forEach(map => {
|
||||
ctx.fillStyle = transitionTypeToColor(map.getType());
|
||||
let y = chunk.yOffset(map);
|
||||
ctx.fillRect(0, y, kWidth, y + 1);
|
||||
});
|
||||
}
|
||||
|
||||
let imageData = this.backgroundCanvas.toDataURL('image/webp', 0.2);
|
||||
node.style.backgroundImage = 'url(' + imageData + ')';
|
||||
}
|
||||
|
||||
// TODO(zcankara) Timeline colors
|
||||
updateTimeline(isMapSelected) {
|
||||
let chunksNode = this.timelineChunks;
|
||||
this.removeAllChildren(chunksNode);
|
||||
let chunks = this.chunks;
|
||||
let max = chunks.max(each => each.size());
|
||||
let start = this.timelineEntries.startTime;
|
||||
let end = this.timelineEntries.endTime;
|
||||
let duration = end - start;
|
||||
const timeToPixel = chunks.length * kChunkWidth / duration;
|
||||
let addTimestamp = (time, name) => {
|
||||
let timeNode = this.div('timestamp');
|
||||
timeNode.innerText = name;
|
||||
timeNode.style.left = ((time - start) * 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 = this.div();
|
||||
node.className = 'chunk';
|
||||
node.style.left = (i * kChunkWidth) + 'px';
|
||||
node.style.height = height + 'px';
|
||||
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);
|
||||
}
|
||||
this.asyncSetTimelineChunkBackground(backgroundTodo)
|
||||
|
||||
// Put a time marker roughly every 20 chunks.
|
||||
let expected = duration / chunks.length * 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;
|
||||
|
||||
let time = start;
|
||||
while (time < end) {
|
||||
addTimestamp(time, ((time - start) / 1000) + ' ms');
|
||||
time += interval;
|
||||
}
|
||||
this.drawOverview();
|
||||
this.redraw(isMapSelected);
|
||||
}
|
||||
|
||||
handleChunkMouseMove(event) {
|
||||
if (this.isLocked) return false;
|
||||
let chunk = event.target.chunk;
|
||||
if (!chunk) return;
|
||||
// topmost map (at chunk.height) == map #0.
|
||||
let relativeIndex =
|
||||
Math.round(event.layerY / event.target.offsetHeight * chunk.size());
|
||||
let map = chunk.at(relativeIndex);
|
||||
this.dispatchEvent(new CustomEvent(
|
||||
'mapchange', {bubbles: true, composed: true, detail: map}));
|
||||
}
|
||||
|
||||
handleChunkClick(event) {
|
||||
this.isLocked = !this.isLocked;
|
||||
}
|
||||
|
||||
handleChunkDoubleClick(event) {
|
||||
this.isLocked = true;
|
||||
let chunk = event.target.chunk;
|
||||
if (!chunk) return;
|
||||
this.dispatchEvent(new CustomEvent(
|
||||
'showmaps', {bubbles: true, composed: true, detail: chunk}));
|
||||
}
|
||||
|
||||
drawOverview() {
|
||||
const height = 50;
|
||||
const kFactor = 2;
|
||||
let canvas = this.backgroundCanvas;
|
||||
canvas.height = height;
|
||||
canvas.width = window.innerWidth;
|
||||
let ctx = canvas.getContext('2d');
|
||||
let chunks = this.timelineEntries.chunkSizes(canvas.width * kFactor);
|
||||
let max = chunks.max();
|
||||
ctx.clearRect(0, 0, canvas.width, height);
|
||||
ctx.fillStyle = CSSColor.onBackgroundColor;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, height);
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
ctx.lineTo(i / kFactor, height - chunks[i] / max * height);
|
||||
}
|
||||
ctx.lineTo(chunks.length, height);
|
||||
ctx.strokeStyle = CSSColor.onBackgroundColor;
|
||||
ctx.stroke();
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
let imageData = canvas.toDataURL('image/webp', 0.2);
|
||||
handleOverviewBackgroundUpdate(e){
|
||||
this.timelineOverview.style.backgroundImage =
|
||||
'url(' + imageData + ')';
|
||||
}
|
||||
|
||||
redraw(isMapSelected) {
|
||||
let canvas = this.timelineCanvas;
|
||||
canvas.width = (this.chunks.length + 1) * kChunkWidth;
|
||||
canvas.height = kChunkHeight;
|
||||
let ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, kChunkHeight);
|
||||
if (!isMapSelected) return;
|
||||
this.drawEdges(ctx);
|
||||
}
|
||||
|
||||
|
||||
setMapStyle(map, ctx) {
|
||||
ctx.fillStyle = map.edge && map.edge.from
|
||||
? CSSColor.onBackgroundColor : CSSColor.onPrimaryColor;
|
||||
}
|
||||
|
||||
setEdgeStyle(edge, ctx) {
|
||||
const color = transitionTypeToColor(edge.type);
|
||||
ctx.strokeStyle = color;
|
||||
ctx.fillStyle = color;
|
||||
}
|
||||
|
||||
markMap(ctx, map) {
|
||||
let [x, y] = map.position(this.chunks);
|
||||
ctx.beginPath();
|
||||
this.setMapStyle(map, ctx);
|
||||
ctx.arc(x, y, 3, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = CSSColor.onBackgroundColor;
|
||||
ctx.arc(x, y, 2, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
markSelectedMap(ctx, map) {
|
||||
let [x, y] = map.position(this.chunks);
|
||||
ctx.beginPath();
|
||||
this.setMapStyle(map, ctx);
|
||||
ctx.arc(x, y, 6, 0, 2 * Math.PI);
|
||||
ctx.strokeStyle = CSSColor.onBackgroundColor;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
drawEdges(ctx) {
|
||||
// Draw the trace of maps in reverse order to make sure the outgoing
|
||||
// transitions of previous maps aren't drawn over.
|
||||
const kMaxOutgoingEdges = 100;
|
||||
let nofEdges = 0;
|
||||
let stack = [];
|
||||
let current = this.map;
|
||||
while (current && nofEdges < kMaxOutgoingEdges) {
|
||||
nofEdges += current.children.length;
|
||||
stack.push(current);
|
||||
current = current.parent();
|
||||
}
|
||||
ctx.save();
|
||||
this.drawOutgoingEdges(ctx, this.map, 3);
|
||||
ctx.restore();
|
||||
|
||||
let labelOffset = 15;
|
||||
let xPrev = 0;
|
||||
while (current = stack.pop()) {
|
||||
if (current.edge) {
|
||||
this.setEdgeStyle(current.edge, ctx);
|
||||
let [xTo, yTo] = this.drawEdge(ctx, current.edge, true, labelOffset);
|
||||
if (xTo == xPrev) {
|
||||
labelOffset += 8;
|
||||
} else {
|
||||
labelOffset = 15
|
||||
}
|
||||
xPrev = xTo;
|
||||
}
|
||||
this.markMap(ctx, current);
|
||||
current = current.parent();
|
||||
ctx.save();
|
||||
// this.drawOutgoingEdges(ctx, current, 1);
|
||||
ctx.restore();
|
||||
}
|
||||
// Mark selected map
|
||||
this.markSelectedMap(ctx, this.map);
|
||||
}
|
||||
|
||||
drawEdge(ctx, edge, showLabel = true, labelOffset = 20) {
|
||||
if (!edge.from || !edge.to) return [-1, -1];
|
||||
let [xFrom, yFrom] = edge.from.position(this.chunks);
|
||||
let [xTo, yTo] = edge.to.position(this.chunks);
|
||||
let sameChunk = xTo == xFrom;
|
||||
if (sameChunk) labelOffset += 8;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(xFrom, yFrom);
|
||||
let offsetX = 20;
|
||||
let offsetY = 20;
|
||||
let midX = xFrom + (xTo - xFrom) / 2;
|
||||
let midY = (yFrom + yTo) / 2 - 100;
|
||||
if (!sameChunk) {
|
||||
ctx.quadraticCurveTo(midX, midY, xTo, yTo);
|
||||
} else {
|
||||
ctx.lineTo(xTo, yTo);
|
||||
}
|
||||
if (!showLabel) {
|
||||
ctx.strokeStyle = CSSColor.onBackgroundColor;
|
||||
ctx.stroke();
|
||||
} else {
|
||||
let centerX, centerY;
|
||||
if (!sameChunk) {
|
||||
centerX = (xFrom / 2 + midX + xTo / 2) / 2;
|
||||
centerY = (yFrom / 2 + midY + yTo / 2) / 2;
|
||||
} else {
|
||||
centerX = xTo;
|
||||
centerY = yTo;
|
||||
}
|
||||
ctx.strokeStyle = CSSColor.onBackgroundColor;
|
||||
ctx.moveTo(centerX, centerY);
|
||||
ctx.lineTo(centerX + offsetX, centerY - labelOffset);
|
||||
ctx.stroke();
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillStyle = CSSColor.onBackgroundColor;
|
||||
ctx.fillText(
|
||||
edge.toString(), centerX + offsetX + 2, centerY - labelOffset)
|
||||
}
|
||||
return [xTo, yTo];
|
||||
}
|
||||
|
||||
drawOutgoingEdges(ctx, map, max = 10, depth = 0) {
|
||||
if (!map) return;
|
||||
if (depth >= max) return;
|
||||
ctx.globalAlpha = 0.5 - depth * (0.3 / max);
|
||||
ctx.strokeStyle = CSSColor.timelineBackgroundColor;
|
||||
|
||||
const limit = Math.min(map.children.length, 100)
|
||||
for (let i = 0; i < limit; i++) {
|
||||
let edge = map.children[i];
|
||||
this.drawEdge(ctx, edge, true);
|
||||
this.drawOutgoingEdges(ctx, edge.to, max, depth + 1);
|
||||
}
|
||||
'url(' + e.detail + ')';
|
||||
}
|
||||
|
||||
});
|
||||
|
78
tools/system-analyzer/timeline/timeline-track-template.html
Normal file
78
tools/system-analyzer/timeline/timeline-track-template.html
Normal file
@ -0,0 +1,78 @@
|
||||
<!-- 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>
|
||||
#timeline {
|
||||
position: relative;
|
||||
height: 300px;
|
||||
overflow-y: hidden;
|
||||
overflow-x: scroll;
|
||||
user-select: none;
|
||||
background-color: var(--timeline-background-color);
|
||||
box-shadow: 0 19px 38px rgba(0,0,0,0.30), 0 15px 12px rgba(0,0,0,0.22);
|
||||
}
|
||||
#timelineLabel {
|
||||
transform: rotate(90deg);
|
||||
transform-origin: left bottom 0;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 250px;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
#timelineChunks {
|
||||
height: 250px;
|
||||
position: absolute;
|
||||
margin-right: 100px;
|
||||
}
|
||||
#timelineCanvas {
|
||||
height: 250px;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
}
|
||||
.chunk {
|
||||
width: 6px;
|
||||
border: 0px var(--timeline-background-color) solid;
|
||||
border-width: 0 2px 0 2px;
|
||||
position: absolute;
|
||||
background-size: 100% 100%;
|
||||
image-rendering: pixelated;
|
||||
bottom: 0px;
|
||||
}
|
||||
.timestamp {
|
||||
height: 250px;
|
||||
width: 100px;
|
||||
border-left: 1px var(--surface-color) dashed;
|
||||
padding-left: 4px;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
font-size: 10px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
#timelineLegend {
|
||||
position: relative;
|
||||
float: right;
|
||||
text-align: center;
|
||||
}
|
||||
.timeline {
|
||||
background-color: var(--timeline-background-color);
|
||||
}
|
||||
</style>
|
||||
<div class="timeline">
|
||||
<div id="timelineLegend">
|
||||
<p>Category</p>
|
||||
<dl id="timelineLegendContent" style="float:right; padding:20px">
|
||||
</dl>
|
||||
</div>
|
||||
<div id="timeline">
|
||||
<div id="timelineLabel">Frequency</div>
|
||||
<div id="timelineChunks"></div>
|
||||
<canvas id="timelineCanvas"></canvas>
|
||||
</div>
|
||||
</div>
|
392
tools/system-analyzer/timeline/timeline-track.mjs
Normal file
392
tools/system-analyzer/timeline/timeline-track.mjs
Normal file
@ -0,0 +1,392 @@
|
||||
// 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 {defineCustomElement, V8CustomElement,
|
||||
transitionTypeToColor, CSSColor} from '../helper.mjs';
|
||||
import {kChunkWidth, kChunkHeight} from '../map-processor.mjs';
|
||||
|
||||
defineCustomElement('./timeline/timeline-track', (templateText) =>
|
||||
class TimelineTrack extends V8CustomElement {
|
||||
#timeline;
|
||||
#nofChunks = 400;
|
||||
#chunks;
|
||||
#selectedEntry;
|
||||
|
||||
constructor() {
|
||||
super(templateText);
|
||||
this.backgroundCanvas = document.createElement('canvas');
|
||||
this.isLocked = false;
|
||||
}
|
||||
|
||||
get timelineCanvas() {
|
||||
return this.$('#timelineCanvas');
|
||||
}
|
||||
|
||||
get timelineChunks() {
|
||||
return this.$('#timelineChunks');
|
||||
}
|
||||
|
||||
get timeline() {
|
||||
return this.$('#timeline');
|
||||
}
|
||||
|
||||
get timelineLegendContent() {
|
||||
return this.$('#timelineLegendContent');
|
||||
}
|
||||
|
||||
set data(value) {
|
||||
this.#timeline = value;
|
||||
this.updateChunks();
|
||||
this.updateTimeline();
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
get data() {
|
||||
return this.#timeline;
|
||||
}
|
||||
|
||||
set nofChunks(count){
|
||||
this.#nofChunks = count;
|
||||
this.updateChunks();
|
||||
this.updateTimeline();
|
||||
}
|
||||
get nofChunks(){
|
||||
return this.#nofChunks;
|
||||
}
|
||||
updateChunks() {
|
||||
this.#chunks = this.data.chunks(this.nofChunks);
|
||||
}
|
||||
get chunks(){
|
||||
return this.#chunks;
|
||||
}
|
||||
set selectedEntry(value){
|
||||
this.#selectedEntry = value;
|
||||
if(value.edge) this.redraw();
|
||||
}
|
||||
get selectedEntry(){
|
||||
return this.#selectedEntry;
|
||||
}
|
||||
|
||||
updateStats(){
|
||||
let unique = new Map();
|
||||
for (const entry of this.data.all) {
|
||||
if(!unique.has(entry.type)) {
|
||||
unique.set(entry.type, 1);
|
||||
} else {
|
||||
unique.set(entry.type, unique.get(entry.type) + 1);
|
||||
}
|
||||
}
|
||||
console.log(unique);
|
||||
//TODO(zcankara) Update it to make it work without relying on hex colors
|
||||
this.renderStatsWindow(unique);
|
||||
}
|
||||
|
||||
renderStatsWindow(unique){
|
||||
//TODO(zcankara) Update legend with colors and entries
|
||||
let timelineLegendContent = this.timelineLegendContent;
|
||||
this.removeAllChildren(timelineLegendContent);
|
||||
let fragment = document.createDocumentFragment();
|
||||
let colorIterator = 0;
|
||||
unique.forEach((key, val) => {
|
||||
let dt = document.createElement("dt");
|
||||
dt.innerHTML = key;
|
||||
dt.style.backgroundColor = transitionTypeToColor(val);
|
||||
dt.style.color = CSSColor.surfaceColor;
|
||||
fragment.appendChild(dt);
|
||||
let dd = document.createElement("dd");
|
||||
dd.innerHTML = val;
|
||||
fragment.appendChild(dd);
|
||||
colorIterator += 1;
|
||||
});
|
||||
timelineLegendContent.appendChild(fragment);
|
||||
}
|
||||
|
||||
// TODO(zcankara) Emit event and handle data in timeline track
|
||||
handleTimelineIndicatorMove(offset) {
|
||||
this.timeline.scrollLeft += offset;
|
||||
}
|
||||
|
||||
asyncSetTimelineChunkBackground(backgroundTodo) {
|
||||
const kIncrement = 100;
|
||||
let start = 0;
|
||||
let delay = 1;
|
||||
while (start < backgroundTodo.length) {
|
||||
let end = Math.min(start + kIncrement, backgroundTodo.length);
|
||||
setTimeout((from, to) => {
|
||||
for (let i = from; i < to; i++) {
|
||||
let [chunk, node] = backgroundTodo[i];
|
||||
this.setTimelineChunkBackground(chunk, node);
|
||||
}
|
||||
}, delay++, start, end);
|
||||
start = end;
|
||||
}
|
||||
}
|
||||
|
||||
setTimelineChunkBackground(chunk, node) {
|
||||
// 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 = transitionTypeToColor(type);
|
||||
let height = count / total * kHeight;
|
||||
ctx.fillRect(0, y, kWidth, y + height);
|
||||
y += height;
|
||||
});
|
||||
} else {
|
||||
chunk.items.forEach(map => {
|
||||
ctx.fillStyle = transitionTypeToColor(map.type);
|
||||
let y = chunk.yOffset(map);
|
||||
ctx.fillRect(0, y, kWidth, y + 1);
|
||||
});
|
||||
}
|
||||
|
||||
let imageData = this.backgroundCanvas.toDataURL('image/webp', 0.2);
|
||||
node.style.backgroundImage = 'url(' + imageData + ')';
|
||||
}
|
||||
|
||||
updateTimeline() {
|
||||
let chunksNode = this.timelineChunks;
|
||||
this.removeAllChildren(chunksNode);
|
||||
let chunks = this.chunks;
|
||||
let max = chunks.max(each => each.size());
|
||||
let start = this.data.startTime;
|
||||
let end = this.data.endTime;
|
||||
let duration = end - start;
|
||||
const timeToPixel = chunks.length * kChunkWidth / duration;
|
||||
let addTimestamp = (time, name) => {
|
||||
let timeNode = this.div('timestamp');
|
||||
timeNode.innerText = name;
|
||||
timeNode.style.left = ((time - start) * 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 = this.div();
|
||||
node.className = 'chunk';
|
||||
node.style.left = (i * kChunkWidth) + 'px';
|
||||
node.style.height = height + 'px';
|
||||
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);
|
||||
}
|
||||
this.asyncSetTimelineChunkBackground(backgroundTodo)
|
||||
|
||||
// Put a time marker roughly every 20 chunks.
|
||||
let expected = duration / chunks.length * 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;
|
||||
|
||||
let time = start;
|
||||
while (time < end) {
|
||||
addTimestamp(time, ((time - start) / 1000) + ' ms');
|
||||
time += interval;
|
||||
}
|
||||
this.drawOverview();
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
handleChunkMouseMove(event) {
|
||||
if (this.isLocked) return false;
|
||||
let chunk = event.target.chunk;
|
||||
if (!chunk) return;
|
||||
// topmost map (at chunk.height) == map #0.
|
||||
let relativeIndex =
|
||||
Math.round(event.layerY / event.target.offsetHeight * chunk.size());
|
||||
let map = chunk.at(relativeIndex);
|
||||
this.dispatchEvent(new CustomEvent(
|
||||
'mapchange', {bubbles: true, composed: true, detail: map}));
|
||||
}
|
||||
|
||||
handleChunkClick(event) {
|
||||
this.isLocked = !this.isLocked;
|
||||
}
|
||||
|
||||
handleChunkDoubleClick(event) {
|
||||
this.isLocked = true;
|
||||
let chunk = event.target.chunk;
|
||||
if (!chunk) return;
|
||||
this.dispatchEvent(new CustomEvent(
|
||||
'showmaps', {bubbles: true, composed: true, detail: chunk}));
|
||||
}
|
||||
|
||||
drawOverview() {
|
||||
const height = 50;
|
||||
const kFactor = 2;
|
||||
let canvas = this.backgroundCanvas;
|
||||
canvas.height = height;
|
||||
canvas.width = window.innerWidth;
|
||||
let ctx = canvas.getContext('2d');
|
||||
let chunks = this.data.chunkSizes(canvas.width * kFactor);
|
||||
let max = chunks.max();
|
||||
ctx.clearRect(0, 0, canvas.width, height);
|
||||
ctx.fillStyle = CSSColor.onBackgroundColor;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, height);
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
ctx.lineTo(i / kFactor, height - chunks[i] / max * height);
|
||||
}
|
||||
ctx.lineTo(chunks.length, height);
|
||||
ctx.strokeStyle = CSSColor.onBackgroundColor;
|
||||
ctx.stroke();
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
let imageData = canvas.toDataURL('image/webp', 0.2);
|
||||
this.dispatchEvent(new CustomEvent(
|
||||
'overviewupdate', {bubbles: true, composed: true, detail: imageData}));
|
||||
}
|
||||
|
||||
redraw() {
|
||||
let canvas = this.timelineCanvas;
|
||||
canvas.width = (this.chunks.length + 1) * kChunkWidth;
|
||||
canvas.height = kChunkHeight;
|
||||
let ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, kChunkHeight);
|
||||
if (!this.selectedEntry || !this.selectedEntry.edge) return;
|
||||
this.drawEdges(ctx);
|
||||
}
|
||||
setMapStyle(map, ctx) {
|
||||
ctx.fillStyle = map.edge && map.edge.from ?
|
||||
CSSColor.onBackgroundColor : CSSColor.onPrimaryColor;
|
||||
}
|
||||
|
||||
setEdgeStyle(edge, ctx) {
|
||||
let color = transitionTypeToColor(edge.type);
|
||||
ctx.strokeStyle = color;
|
||||
ctx.fillStyle = color;
|
||||
}
|
||||
|
||||
markMap(ctx, map) {
|
||||
let [x, y] = map.position(this.chunks);
|
||||
ctx.beginPath();
|
||||
this.setMapStyle(map, ctx);
|
||||
ctx.arc(x, y, 3, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = CSSColor.onBackgroundColor;
|
||||
ctx.arc(x, y, 2, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
markSelectedMap(ctx, map) {
|
||||
let [x, y] = map.position(this.chunks);
|
||||
ctx.beginPath();
|
||||
this.setMapStyle(map, ctx);
|
||||
ctx.arc(x, y, 6, 0, 2 * Math.PI);
|
||||
ctx.strokeStyle = CSSColor.onBackgroundColor;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
drawEdges(ctx) {
|
||||
// Draw the trace of maps in reverse order to make sure the outgoing
|
||||
// transitions of previous maps aren't drawn over.
|
||||
const kMaxOutgoingEdges = 100;
|
||||
let nofEdges = 0;
|
||||
let stack = [];
|
||||
let current = this.selectedEntry;
|
||||
while (current && nofEdges < kMaxOutgoingEdges) {
|
||||
nofEdges += current.children.length;
|
||||
stack.push(current);
|
||||
current = current.parent();
|
||||
}
|
||||
ctx.save();
|
||||
this.drawOutgoingEdges(ctx, this.selectedEntry, 3);
|
||||
ctx.restore();
|
||||
|
||||
let labelOffset = 15;
|
||||
let xPrev = 0;
|
||||
while (current = stack.pop()) {
|
||||
if (current.edge) {
|
||||
this.setEdgeStyle(current.edge, ctx);
|
||||
let [xTo, yTo] = this.drawEdge(ctx, current.edge, true, labelOffset);
|
||||
if (xTo == xPrev) {
|
||||
labelOffset += 8;
|
||||
} else {
|
||||
labelOffset = 15
|
||||
}
|
||||
xPrev = xTo;
|
||||
}
|
||||
this.markMap(ctx, current);
|
||||
current = current.parent();
|
||||
ctx.save();
|
||||
// this.drawOutgoingEdges(ctx, current, 1);
|
||||
ctx.restore();
|
||||
}
|
||||
// Mark selected map
|
||||
this.markSelectedMap(ctx, this.selectedEntry);
|
||||
}
|
||||
|
||||
drawEdge(ctx, edge, showLabel = true, labelOffset = 20) {
|
||||
if (!edge.from || !edge.to) return [-1, -1];
|
||||
let [xFrom, yFrom] = edge.from.position(this.chunks);
|
||||
let [xTo, yTo] = edge.to.position(this.chunks);
|
||||
let sameChunk = xTo == xFrom;
|
||||
if (sameChunk) labelOffset += 8;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(xFrom, yFrom);
|
||||
let offsetX = 20;
|
||||
let offsetY = 20;
|
||||
let midX = xFrom + (xTo - xFrom) / 2;
|
||||
let midY = (yFrom + yTo) / 2 - 100;
|
||||
if (!sameChunk) {
|
||||
ctx.quadraticCurveTo(midX, midY, xTo, yTo);
|
||||
} else {
|
||||
ctx.lineTo(xTo, yTo);
|
||||
}
|
||||
if (!showLabel) {
|
||||
ctx.strokeStyle = CSSColor.onBackgroundColor;
|
||||
ctx.stroke();
|
||||
} else {
|
||||
let centerX, centerY;
|
||||
if (!sameChunk) {
|
||||
centerX = (xFrom / 2 + midX + xTo / 2) / 2;
|
||||
centerY = (yFrom / 2 + midY + yTo / 2) / 2;
|
||||
} else {
|
||||
centerX = xTo;
|
||||
centerY = yTo;
|
||||
}
|
||||
ctx.strokeStyle = CSSColor.onBackgroundColor;
|
||||
ctx.moveTo(centerX, centerY);
|
||||
ctx.lineTo(centerX + offsetX, centerY - labelOffset);
|
||||
ctx.stroke();
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillStyle = CSSColor.onBackgroundColor;
|
||||
ctx.fillText(
|
||||
edge.toString(), centerX + offsetX + 2, centerY - labelOffset);
|
||||
}
|
||||
return [xTo, yTo];
|
||||
}
|
||||
|
||||
drawOutgoingEdges(ctx, map, max = 10, depth = 0) {
|
||||
if (!map) return;
|
||||
if (depth >= max) return;
|
||||
ctx.globalAlpha = 0.5 - depth * (0.3 / max);
|
||||
ctx.strokeStyle = CSSColor.timelineBackgroundColor;
|
||||
|
||||
const limit = Math.min(map.children.length, 100)
|
||||
for (let i = 0; i < limit; i++) {
|
||||
let edge = map.children[i];
|
||||
this.drawEdge(ctx, edge, true);
|
||||
this.drawOutgoingEdges(ctx, edge.to, max, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
Loading…
Reference in New Issue
Block a user