Commit 9c8ebcbb authored by Zeynep Cankara's avatar Zeynep Cankara Committed by Commit Bot

[tools][system-analyzer] Timeline-track filter by time event

This CL adds the functionality to filter log events
falling into the time range specified by the user via
mouse events on timeline tracks. The log event selections
on panels updated based on the selected time range.

Bug: v8:10644

Change-Id: Iaf53896fd5c43cefea6d4c40bab5fcb136494b5f
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2351670
Commit-Queue: Zeynep Cankara <zcankara@google.com>
Reviewed-by: 's avatarCamillo Bruni <cbruni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#69375}
parent 833662c7
...@@ -3,48 +3,50 @@ ...@@ -3,48 +3,50 @@
// found in the LICENSE file. // found in the LICENSE file.
class State { class State {
#timeSelection = {start: 0, end: Infinity}; #timeSelection = { start: 0, end: Infinity };
#map; #map;
#ic; #ic;
#selectedMapLogEvents;
#selectedIcLogEvents;
#nofChunks; #nofChunks;
#chunks; #chunks;
#icTimeline; #icTimeline;
#mapTimeline; #mapTimeline;
#minStartTime = Number.POSITIVE_INFINITY; #minStartTime = Number.POSITIVE_INFINITY;
#maxEndTime = Number.NEGATIVE_INFINITY; #maxEndTime = Number.NEGATIVE_INFINITY;
get minStartTime(){ get minStartTime() {
return this.#minStartTime; return this.#minStartTime;
} }
get maxEndTime(){ get maxEndTime() {
return this.#maxEndTime; return this.#maxEndTime;
} }
#updateTimeRange(timeline){ #updateTimeRange(timeline) {
this.#minStartTime = Math.min(this.#minStartTime, timeline.startTime); this.#minStartTime = Math.min(this.#minStartTime, timeline.startTime);
this.#maxEndTime = Math.max(this.#maxEndTime, timeline.endTime); this.#maxEndTime = Math.max(this.#maxEndTime, timeline.endTime);
} }
get mapTimeline(){ get mapTimeline() {
return this.#mapTimeline; return this.#mapTimeline;
} }
set mapTimeline(timeline){ set mapTimeline(timeline) {
this.#updateTimeRange(timeline); this.#updateTimeRange(timeline);
timeline.startTime = this.#minStartTime; timeline.startTime = this.#minStartTime;
timeline.endTime = this.#maxEndTime; timeline.endTime = this.#maxEndTime;
this.#mapTimeline = timeline; this.#mapTimeline = timeline;
} }
set icTimeline(timeline){ set icTimeline(timeline) {
this.#updateTimeRange(timeline); this.#updateTimeRange(timeline);
timeline.startTime = this.#minStartTime; timeline.startTime = this.#minStartTime;
timeline.endTime = this.#maxEndTime; timeline.endTime = this.#maxEndTime;
this.#icTimeline = timeline; this.#icTimeline = timeline;
} }
get icTimeline(){ get icTimeline() {
return this.#icTimeline; return this.#icTimeline;
} }
set chunks(value){ set chunks(value) {
//TODO(zcankara) split up between maps and ics, and every timeline track //TODO(zcankara) split up between maps and ics, and every timeline track
this.#chunks = value; this.#chunks = value;
} }
get chunks(){ get chunks() {
//TODO(zcankara) split up between maps and ics, and every timeline track //TODO(zcankara) split up between maps and ics, and every timeline track
return this.#chunks; return this.#chunks;
} }
...@@ -60,7 +62,7 @@ class State { ...@@ -60,7 +62,7 @@ class State {
} }
set map(value) { set map(value) {
//TODO(zcankara) rename as selectedMapEvents, array of selected events //TODO(zcankara) rename as selectedMapEvents, array of selected events
if(!value) return; if (!value) return;
this.#map = value; this.#map = value;
} }
get ic() { get ic() {
...@@ -69,9 +71,23 @@ class State { ...@@ -69,9 +71,23 @@ class State {
} }
set ic(value) { set ic(value) {
//TODO(zcankara) rename selectedIcEvents, array of selected events //TODO(zcankara) rename selectedIcEvents, array of selected events
if(!value) return; if (!value) return;
this.#ic = value; this.#ic = value;
} }
get selectedMapLogEvents() {
return this.#selectedMapLogEvents;
}
set selectedMapLogEvents(value) {
if (!value) return;
this.#selectedMapLogEvents = value;
}
get selectedIcLogEvents() {
return this.#selectedIcLogEvents;
}
set selectedIcLogEvents(value) {
if (!value) return;
this.#selectedIcLogEvents = value;
}
get timeSelection() { get timeSelection() {
return this.#timeSelection; return this.#timeSelection;
} }
......
...@@ -4,9 +4,9 @@ ...@@ -4,9 +4,9 @@
// found in the LICENSE file. // found in the LICENSE file.
class SelectionEvent extends CustomEvent { class SelectionEvent extends CustomEvent {
constructor(entries){ constructor(entries) {
super('showentries', {bubbles: true, composed: true}); super('showentries', { bubbles: true, composed: true });
if(!Array.isArray(entries) || entries.length == 0){ if (!Array.isArray(entries) || entries.length == 0) {
throw new Error('No valid entries selected!') throw new Error('No valid entries selected!')
} }
this.entries = entries; this.entries = entries;
...@@ -14,10 +14,19 @@ class SelectionEvent extends CustomEvent { ...@@ -14,10 +14,19 @@ class SelectionEvent extends CustomEvent {
} }
class SelectEvent extends CustomEvent { class SelectEvent extends CustomEvent {
constructor(entry){ constructor(entry) {
super('showentrydetail', {bubbles: true, composed: true}); super('showentrydetail', { bubbles: true, composed: true });
this.entry = entry; this.entry = entry;
} }
} }
export {SelectionEvent, SelectEvent}; class SelectTimeEvent extends CustomEvent {
\ No newline at end of file static name = 'timerangeselect';
constructor(start, end) {
super(SelectTimeEvent.name, { bubbles: true, composed: true });
this.start = start;
this.end = end;
}
}
export { SelectionEvent, SelectEvent, SelectTimeEvent };
...@@ -2,199 +2,197 @@ ...@@ -2,199 +2,197 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import {Group} from './ic-model.mjs'; import { Group } from './ic-model.mjs';
import CustomIcProcessor from "./ic-processor.mjs"; import CustomIcProcessor from "./ic-processor.mjs";
import {SelectEvent} from './events.mjs'; import { SelectEvent, SelectTimeEvent } from './events.mjs';
import {defineCustomElement, V8CustomElement} from './helper.mjs'; import { defineCustomElement, V8CustomElement } from './helper.mjs';
defineCustomElement('ic-panel', (templateText) => defineCustomElement('ic-panel', (templateText) =>
class ICPanel extends V8CustomElement { class ICPanel extends V8CustomElement {
//TODO(zcankara) Entries never set //TODO(zcankara) Entries never set
#entries; #entries;
#filteredEntries; #filteredEntries;
constructor() { constructor() {
super(templateText); super(templateText);
this.groupKey.addEventListener( this.groupKey.addEventListener(
'change', e => this.updateTable(e)); 'change', e => this.updateTable(e));
this.$('#filterICTimeBtn').addEventListener( this.$('#filterICTimeBtn').addEventListener(
'click', e => this.handleICTimeFilter(e)); 'click', e => this.handleICTimeFilter(e));
} }
get entries(){ get entries() {
return this.#entries; return this.#entries;
} }
get groupKey() { get groupKey() {
return this.$('#group-key'); return this.$('#group-key');
} }
get table() { get table() {
return this.$('#table'); return this.$('#table');
} }
get tableBody() { get tableBody() {
return this.$('#table-body'); return this.$('#table-body');
} }
get count() { get count() {
return this.$('#count'); return this.$('#count');
} }
get spanSelectAll(){ get spanSelectAll() {
return this.querySelectorAll("span"); return this.querySelectorAll("span");
} }
set filteredEntries(value){ set filteredEntries(value) {
this.#filteredEntries = value; this.#filteredEntries = value;
this.updateTable(); this.updateTable();
} }
get filteredEntries(){ get filteredEntries() {
return this.#filteredEntries; return this.#filteredEntries;
} }
updateTable(event) { updateTable(event) {
let select = this.groupKey; let select = this.groupKey;
let key = select.options[select.selectedIndex].text; let key = select.options[select.selectedIndex].text;
let tableBody = this.tableBody; let tableBody = this.tableBody;
this.removeAllChildren(tableBody); this.removeAllChildren(tableBody);
let groups = Group.groupBy(this.filteredEntries, key, true); let groups = Group.groupBy(this.filteredEntries, key, true);
this.render(groups, tableBody); this.render(groups, tableBody);
} }
escapeHtml(unsafe) { escapeHtml(unsafe) {
if (!unsafe) return ""; if (!unsafe) return "";
return unsafe.toString() return unsafe.toString()
.replace(/&/g, "&amp;") .replace(/&/g, "&amp;")
.replace(/</g, "&lt;") .replace(/</g, "&lt;")
.replace(/>/g, "&gt;") .replace(/>/g, "&gt;")
.replace(/"/g, "&quot;") .replace(/"/g, "&quot;")
.replace(/'/g, "&#039;"); .replace(/'/g, "&#039;");
} }
processValue(unsafe) { processValue(unsafe) {
if (!unsafe) return ""; if (!unsafe) return "";
if (!unsafe.startsWith("http")) return this.escapeHtml(unsafe); if (!unsafe.startsWith("http")) return this.escapeHtml(unsafe);
let a = document.createElement("a"); let a = document.createElement("a");
a.href = unsafe; a.href = unsafe;
a.textContent = unsafe; a.textContent = unsafe;
return a; return a;
} }
td(tr, content, className) { td(tr, content, className) {
let node = document.createElement("td"); let node = document.createElement("td");
if (typeof content == "object") { if (typeof content == "object") {
node.appendChild(content); node.appendChild(content);
} else { } else {
node.innerHTML = content; node.innerHTML = content;
}
node.className = className;
tr.appendChild(node);
return node
}
handleMapClick(e){
this.dispatchEvent(new SelectEvent(e.target.parentNode.entry));
}
handleFilePositionClick(e){
this.dispatchEvent(new SelectEvent(e.target.parentNode.entry.key));
}
render(entries, parent) {
let fragment = document.createDocumentFragment();
let max = Math.min(1000, entries.length)
for (let i = 0; i < max; i++) {
let entry = entries[i];
let tr = document.createElement("tr");
tr.entry = entry;
//TODO(zcankara) Create one bound method and use it everywhere
if (entry.property === "map") {
tr.addEventListener('click', e => this.handleMapClick(e));
} else if (entry.property == "filePosition") {
tr.addEventListener('click',
e => this.handleFilePositionClick(e));
} }
let details = this.td(tr,'<span>&#8505;</a>', 'details'); node.className = className;
//TODO(zcankara) don't keep the whole function context alive tr.appendChild(node);
details.onclick = _ => this.toggleDetails(details); return node
this.td(tr, entry.percentage + "%", 'percentage'); }
this.td(tr, entry.count, 'count');
this.td(tr, this.processValue(entry.key), 'key'); handleMapClick(e) {
fragment.appendChild(tr); this.dispatchEvent(new SelectEvent(e.target.parentNode.entry));
} }
let omitted = entries.length - max;
if (omitted > 0) { handleFilePositionClick(e) {
let tr = document.createElement("tr"); this.dispatchEvent(new SelectEvent(e.target.parentNode.entry.key));
let tdNode = this.td(tr, 'Omitted ' + omitted + " entries."); }
tdNode.colSpan = 4;
fragment.appendChild(tr); render(entries, parent) {
} let fragment = document.createDocumentFragment();
parent.appendChild(fragment); let max = Math.min(1000, entries.length)
} for (let i = 0; i < max; i++) {
let entry = entries[i];
let tr = document.createElement("tr");
renderDrilldown(entry, previousSibling) { tr.entry = entry;
let tr = document.createElement('tr'); //TODO(zcankara) Create one bound method and use it everywhere
tr.className = "entry-details"; if (entry.property === "map") {
tr.style.display = "none"; tr.addEventListener('click', e => this.handleMapClick(e));
// indent by one td. } else if (entry.property == "filePosition") {
tr.appendChild(document.createElement("td")); tr.addEventListener('click',
let td = document.createElement("td"); e => this.handleFilePositionClick(e));
td.colSpan = 3; }
for (let key in entry.groups) { let details = this.td(tr, '<span>&#8505;</a>', 'details');
td.appendChild(this.renderDrilldownGroup(entry, key)); //TODO(zcankara) don't keep the whole function context alive
} details.onclick = _ => this.toggleDetails(details);
tr.appendChild(td); this.td(tr, entry.percentage + "%", 'percentage');
// Append the new TR after previousSibling. this.td(tr, entry.count, 'count');
previousSibling.parentNode.insertBefore(tr, previousSibling.nextSibling) this.td(tr, this.processValue(entry.key), 'key');
} fragment.appendChild(tr);
}
renderDrilldownGroup(entry, key) { let omitted = entries.length - max;
let max = 20; if (omitted > 0) {
let group = entry.groups[key]; let tr = document.createElement("tr");
let div = document.createElement("div") let tdNode = this.td(tr, 'Omitted ' + omitted + " entries.");
div.className = 'drilldown-group-title' tdNode.colSpan = 4;
div.textContent = key + ' [top ' + max + ' out of ' + group.length + ']'; fragment.appendChild(tr);
let table = document.createElement("table"); }
this.render(group.slice(0, max), table, false) parent.appendChild(fragment);
div.appendChild(table); }
return div;
}
renderDrilldown(entry, previousSibling) {
toggleDetails(node) { let tr = document.createElement('tr');
let tr = node.parentNode; tr.className = "entry-details";
let entry = tr.entry; tr.style.display = "none";
// Create subgroup in-place if the don't exist yet. // indent by one td.
if (entry.groups === undefined) { tr.appendChild(document.createElement("td"));
entry.createSubGroups(); let td = document.createElement("td");
this.renderDrilldown(entry, tr); td.colSpan = 3;
} for (let key in entry.groups) {
let details = tr.nextSibling; td.appendChild(this.renderDrilldownGroup(entry, key));
let display = details.style.display; }
if (display != "none") { tr.appendChild(td);
display = "none"; // Append the new TR after previousSibling.
} else { previousSibling.parentNode.insertBefore(tr, previousSibling.nextSibling)
display = "table-row" }
};
details.style.display = display; renderDrilldownGroup(entry, key) {
} let max = 20;
let group = entry.groups[key];
initGroupKeySelect() { let div = document.createElement("div")
let select = this.groupKey; div.className = 'drilldown-group-title'
select.options.length = 0; div.textContent = key + ' [top ' + max + ' out of ' + group.length + ']';
for (let i in CustomIcProcessor.kProperties) { let table = document.createElement("table");
let option = document.createElement("option"); this.render(group.slice(0, max), table, false)
option.text = CustomIcProcessor.kProperties[i]; div.appendChild(table);
select.add(option); return div;
} }
}
toggleDetails(node) {
handleICTimeFilter(e) { let tr = node.parentNode;
const startTime = parseInt(this.$('#filter-time-start').value); let entry = tr.entry;
const endTime = parseInt(this.$('#filter-time-end').value); // Create subgroup in-place if the don't exist yet.
const dataModel = {startTime, endTime}; if (entry.groups === undefined) {
this.dispatchEvent(new CustomEvent( entry.createSubGroups();
'ictimefilter', {bubbles: true, composed: true, detail: dataModel})); this.renderDrilldown(entry, tr);
} }
let details = tr.nextSibling;
}); let display = details.style.display;
if (display != "none") {
display = "none";
} else {
display = "table-row"
};
details.style.display = display;
}
initGroupKeySelect() {
let select = this.groupKey;
select.options.length = 0;
for (let i in CustomIcProcessor.kProperties) {
let option = document.createElement("option");
option.text = CustomIcProcessor.kProperties[i];
select.add(option);
}
}
handleICTimeFilter(e) {
this.dispatchEvent(new SelectTimeEvent(
parseInt(this.$('#filter-time-start').value),
parseInt(this.$('#filter-time-end').value)));
}
});
...@@ -6,6 +6,7 @@ import CustomIcProcessor from "./ic-processor.mjs"; ...@@ -6,6 +6,7 @@ import CustomIcProcessor from "./ic-processor.mjs";
import { Entry } from "./ic-processor.mjs"; import { Entry } from "./ic-processor.mjs";
import { State } from "./app-model.mjs"; import { State } from "./app-model.mjs";
import { MapProcessor, V8Map } from "./map-processor.mjs"; import { MapProcessor, V8Map } from "./map-processor.mjs";
import { SelectTimeEvent } from "./events.mjs";
import { $ } from "./helper.mjs"; import { $ } from "./helper.mjs";
import "./ic-panel.mjs"; import "./ic-panel.mjs";
import "./timeline-panel.mjs"; import "./timeline-panel.mjs";
...@@ -38,26 +39,28 @@ class App { ...@@ -38,26 +39,28 @@ class App {
this.handleDataUpload(e) this.handleDataUpload(e)
); );
Object.entries(this.#view).forEach(([_, value]) => { Object.entries(this.#view).forEach(([_, value]) => {
value.addEventListener("showentries", (e) => this.handleShowEntries(e)); value.addEventListener('showentries',
value.addEventListener("showentrydetail", (e) => e => this.handleShowEntries(e));
this.handleShowEntryDetail(e) value.addEventListener('showentrydetail',
); e => this.handleShowEntryDetail(e));
value.addEventListener(SelectTimeEvent.name,
e => this.handleTimeRangeSelect(e));
}); });
this.#view.icPanel.addEventListener("ictimefilter", (e) =>
this.handleICTimeFilter(e)
);
} }
handleShowEntries(e) { handleShowEntries(e) {
if (e.entries[0] instanceof V8Map) { if (e.entries[0] instanceof V8Map) {
this.#view.mapPanel.mapEntries = e.entries; this.showMapEntries(e.entries);
} }
} }
handleTimeRangeSelect(e) {
this.selectTimeRange(e.start, e.end);
}
handleShowEntryDetail(e) { handleShowEntryDetail(e) {
if (e.entry instanceof V8Map) { if (e.entry instanceof V8Map) {
this.selectMapLogEvent(e.entry); this.selectMapLogEvent(e.entry);
} else if (e.entry instanceof Entry) { } else if (e.entry instanceof Entry) {
this.selectICLogEvent(e.entry); this.selectICLogEvent(e.entry);
} else if (typeof e.entry === "string") { } else if (typeof e.entry === 'string') {
this.selectSourcePositionEvent(e.entry); this.selectSourcePositionEvent(e.entry);
} else { } else {
console.log("undefined"); console.log("undefined");
...@@ -67,6 +70,18 @@ class App { ...@@ -67,6 +70,18 @@ class App {
//TODO(zcankara) Handle source position //TODO(zcankara) Handle source position
console.log("Entry containing source position: ", e.entries); console.log("Entry containing source position: ", e.entries);
} }
selectTimeRange(start, end) {
this.#state.timeSelection.start = start;
this.#state.timeSelection.end = end;
this.#state.icTimeline.selectTimeRange(start, end);
this.#state.mapTimeline.selectTimeRange(start, end);
this.#view.mapPanel.selectedMapLogEvents = this.#state.mapTimeline.selection;
this.#view.icPanel.filteredEntries = this.#state.icTimeline.selection;
}
showMapEntries(entries) {
this.#state.selectedMapLogEvents = entries;
this.#view.mapPanel.selectedMapLogEvents = this.#state.selectedMapLogEvents;
}
selectMapLogEvent(entry) { selectMapLogEvent(entry) {
this.#state.map = entry; this.#state.map = entry;
this.#view.mapTrack.selectedEntry = entry; this.#view.mapTrack.selectedEntry = entry;
...@@ -78,15 +93,6 @@ class App { ...@@ -78,15 +93,6 @@ class App {
selectSourcePositionEvent(sourcePositions) { selectSourcePositionEvent(sourcePositions) {
console.log("source positions: ", sourcePositions); console.log("source positions: ", sourcePositions);
} }
handleICTimeFilter(event) {
this.#state.timeSelection.start = event.detail.startTime;
this.#state.timeSelection.end = event.detail.endTime;
this.#view.icTrack.data.selectTimeRange(
this.#state.timeSelection.start,
this.#state.timeSelection.end
);
this.#view.icPanel.filteredEntries = this.#view.icTrack.data.selection;
}
handleFileUpload(e) { handleFileUpload(e) {
$("#container").className = "initial"; $("#container").className = "initial";
} }
......
...@@ -4,88 +4,88 @@ ...@@ -4,88 +4,88 @@
import "./stats-panel.mjs"; import "./stats-panel.mjs";
import "./map-panel/map-details.mjs"; import "./map-panel/map-details.mjs";
import "./map-panel/map-transitions.mjs"; import "./map-panel/map-transitions.mjs";
import {SelectEvent} from './events.mjs'; import { SelectEvent } from './events.mjs';
import {V8Map} from "./map-processor.mjs"; import { V8Map } from "./map-processor.mjs";
import {defineCustomElement, V8CustomElement} from './helper.mjs'; import { defineCustomElement, V8CustomElement } from './helper.mjs';
defineCustomElement('map-panel', (templateText) => defineCustomElement('map-panel', (templateText) =>
class MapPanel extends V8CustomElement { class MapPanel extends V8CustomElement {
#map; #map;
constructor() { constructor() {
super(templateText); super(templateText);
this.searchBarBtn.addEventListener( this.searchBarBtn.addEventListener(
'click', e => this.handleSearchBar(e)); 'click', e => this.handleSearchBar(e));
this.addEventListener( this.addEventListener(
'mapdetailsupdate', e => this.handleUpdateMapDetails(e)); 'mapdetailsupdate', e => this.handleUpdateMapDetails(e));
} }
handleUpdateMapDetails(e){ handleUpdateMapDetails(e) {
this.mapDetailsPanel.mapDetails = e.detail; this.mapDetailsPanel.mapDetails = e.detail;
} }
get statsPanel() { get statsPanel() {
return this.$('#stats-panel'); return this.$('#stats-panel');
} }
get mapTransitionsPanel() { get mapTransitionsPanel() {
return this.$('#map-transitions'); return this.$('#map-transitions');
} }
get mapDetailsPanel() { get mapDetailsPanel() {
return this.$('#map-details'); return this.$('#map-details');
} }
get searchBarBtn() { get searchBarBtn() {
return this.$('#searchBarBtn'); return this.$('#searchBarBtn');
} }
get searchBar() { get searchBar() {
return this.$('#searchBar'); return this.$('#searchBar');
} }
get mapDetails() { get mapDetails() {
return this.mapDetailsPanel.mapDetails; return this.mapDetailsPanel.mapDetails;
} }
// send a timeline to the stats-panel // send a timeline to the stats-panel
get timeline() { get timeline() {
return this.statsPanel.timeline; return this.statsPanel.timeline;
} }
set timeline(value) { set timeline(value) {
console.assert(value !== undefined, "timeline undefined!"); console.assert(value !== undefined, "timeline undefined!");
this.statsPanel.timeline = value; this.statsPanel.timeline = value;
this.statsPanel.update(); this.statsPanel.update();
} }
get transitions() { get transitions() {
return this.statsPanel.transitions; return this.statsPanel.transitions;
} }
set transitions(value) { set transitions(value) {
this.statsPanel.transitions = value; this.statsPanel.transitions = value;
} }
set map(value) { set map(value) {
this.#map = value; this.#map = value;
this.mapTransitionsPanel.map = this.#map; this.mapTransitionsPanel.map = this.#map;
} }
handleSearchBar(e){ handleSearchBar(e) {
let searchBar = this.$('#searchBarInput'); let searchBar = this.$('#searchBarInput');
let searchBarInput = searchBar.value; let searchBarInput = searchBar.value;
//access the map from model cache //access the map from model cache
let selectedMap = V8Map.get(searchBarInput); let selectedMap = V8Map.get(searchBarInput);
if(selectedMap){ if (selectedMap) {
searchBar.className = "success"; searchBar.className = "success";
} else { } else {
searchBar.className = "failure"; searchBar.className = "failure";
} }
this.dispatchEvent(new SelectEvent(selectedMap)); this.dispatchEvent(new SelectEvent(selectedMap));
} }
set mapEntries(list){ set selectedMapLogEvents(list) {
this.mapTransitionsPanel.mapEntries = list; this.mapTransitionsPanel.selectedMapLogEvents = list;
} }
get mapEntries(){ get selectedMapLogEvents() {
return this.mapTransitionsPanel.mapEntries; return this.mapTransitionsPanel.selectedMapLogEvents;
} }
}); });
// Copyright 2020 the V8 project authors. All rights reserved. // Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import {V8CustomElement, defineCustomElement} from '../helper.mjs'; import { V8CustomElement, defineCustomElement } from '../helper.mjs';
import {SelectEvent} from '../events.mjs'; import { SelectEvent } from '../events.mjs';
defineCustomElement('./map-panel/map-transitions', (templateText) => defineCustomElement('./map-panel/map-transitions', (templateText) =>
class MapTransitions extends V8CustomElement { class MapTransitions extends V8CustomElement {
#map; #map;
#mapEntries; #selectedMapLogEvents;
constructor() { constructor() {
super(templateText); super(templateText);
this.transitionView.addEventListener( this.transitionView.addEventListener(
'mousemove', e => this.handleTransitionViewChange(e)); 'mousemove', e => this.handleTransitionViewChange(e));
this.currentNode = this.transitionView; this.currentNode = this.transitionView;
this.currentMap = undefined; this.currentMap = undefined;
} }
get transitionView() { get transitionView() {
return this.$('#transitionView'); return this.$('#transitionView');
} }
get tooltip() { get tooltip() {
return this.$('#tooltip'); return this.$('#tooltip');
} }
get tooltipContents() { get tooltipContents() {
return this.$('#tooltipContents'); return this.$('#tooltipContents');
} }
set map(value) { set map(value) {
this.#map = value; this.#map = value;
this.showMap(); this.showMap();
} }
handleTransitionViewChange(e){ handleTransitionViewChange(e) {
this.tooltip.style.left = e.pageX + "px"; this.tooltip.style.left = e.pageX + "px";
this.tooltip.style.top = e.pageY + "px"; this.tooltip.style.top = e.pageY + "px";
let map = e.target.map; let map = e.target.map;
if (map) { if (map) {
this.tooltipContents.innerText = map.description; this.tooltipContents.innerText = map.description;
}
}
selectMap(map) {
this.currentMap = map;
this.dispatchEvent(new CustomEvent(
'mapdetailsupdate', { bubbles: true, composed: true, detail: map }));
this.dispatchEvent(new SelectEvent(map));
}
dblClickSelectMap(map) {
this.dispatchEvent(new SelectEvent(map));
}
showMap() {
// Called when a map selected
let selected = this.#map;
if (this.currentMap === this.#map) return;
this.currentMap = this.#map;
this.selectedMapLogEvents = [this.#map];
this.dispatchEvent(new CustomEvent(
'mapdetailsupdate', { bubbles: true, composed: true, detail: selected }));
}
showMaps() {
// Timeline dbl click to show map transitions of selected maps
this.transitionView.style.display = 'none';
this.removeAllChildren(this.transitionView);
this.selectedMapLogEvents.forEach(map => this.addMapAndParentTransitions(map));
this.transitionView.style.display = '';
}
set selectedMapLogEvents(list) {
this.#selectedMapLogEvents = list;
this.showMaps();
} }
}
get selectedMapLogEvents() {
selectMap(map) { return this.#selectedMapLogEvents;
this.currentMap = map; }
this.dispatchEvent(new CustomEvent(
'mapdetailsupdate', {bubbles: true, composed: true, detail: map})); addMapAndParentTransitions(map) {
this.dispatchEvent(new SelectEvent(map)); if (map === void 0) return;
} this.currentNode = this.transitionView;
let parents = map.getParents();
dblClickSelectMap(map) { if (parents.length > 0) {
this.dispatchEvent(new SelectEvent(map)); this.addTransitionTo(parents.pop());
} parents.reverse().forEach(each => this.addTransitionTo(each));
}
showMap() { let mapNode = this.addSubtransitions(map);
// Called when a map selected // Mark and show the selected map.
let selected = this.#map; mapNode.classList.add('selected');
if (this.currentMap === this.#map) return; if (this.selectedMap == map) {
this.currentMap = this.#map; setTimeout(
this.mapEntries = [this.#map];
this.dispatchEvent(new CustomEvent(
'mapdetailsupdate', {bubbles: true, composed: true, detail: selected}));
}
showMaps() {
// Timeline dbl click to show map transitions of selected maps
this.transitionView.style.display = 'none';
this.removeAllChildren(this.transitionView);
this.mapEntries.forEach(map => this.addMapAndParentTransitions(map));
this.transitionView.style.display = '';
}
set mapEntries(list){
this.#mapEntries = list;
this.showMaps();
}
get mapEntries(){
return this.#mapEntries;
}
addMapAndParentTransitions(map) {
if (map === void 0) return;
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( () => mapNode.scrollIntoView(
{behavior: 'smooth', block: 'nearest', inline: 'nearest'}), { behavior: 'smooth', block: 'nearest', inline: 'nearest' }),
1); 1);
}
}
addMapNode(map) {
let node = this.div('map');
if (map.edge) node.style.backgroundColor = map.edge.getColor();
node.map = map;
node.addEventListener('click', () => this.selectMap(map));
node.addEventListener('dblclick', () => this.dblClickSelectMap(map));
if (map.children.length > 1) {
node.innerText = map.children.length;
let showSubtree = this.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;
}
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) {
addMapNode(map) { let classes = ['transitionEdge'];
let node = this.div('map'); let edge = this.div(classes);
if (map.edge) node.style.backgroundColor = map.edge.getColor(); edge.style.backgroundColor = map.edge.getColor();
node.map = map; let labelNode = this.div('transitionLabel');
node.addEventListener('click', () => this.selectMap(map)); labelNode.innerText = map.edge.toString();
node.addEventListener('dblclick', () => this.dblClickSelectMap(map)); edge.appendChild(labelNode);
if (map.children.length > 1) { return edge;
node.innerText = map.children.length; }
let showSubtree = this.div('showSubtransitions');
showSubtree.addEventListener('click', (e) => this.toggleSubtree(e, node)); addTransitionTo(map) {
node.appendChild(showSubtree); // transition[ transitions[ transition[...], transition[...], ...]];
} else if (map.children.length == 0) {
node.innerHTML = '&#x25CF;' let transition = this.div('transition');
} if (map.isDeprecated()) transition.classList.add('deprecated');
this.currentNode.appendChild(node); if (map.edge) {
return node; transition.appendChild(this.addTransitionEdge(map));
} }
let mapNode = this.addMapNode(map);
addSubtransitions(map) { transition.appendChild(mapNode);
let mapNode = this.addTransitionTo(map);
// Draw outgoing linear transition line. let subtree = this.div('transitions');
let current = map; transition.appendChild(subtree);
while (current.children.length == 1) {
current = current.children[0].to; this.currentNode.appendChild(transition);
this.addTransitionTo(current); this.currentNode = subtree;
}
return mapNode; return mapNode;
} }
addTransitionEdge(map) { toggleSubtree(event, node) {
let classes = ['transitionEdge']; let map = node.map;
let edge = this.div(classes); event.target.classList.toggle('opened');
edge.style.backgroundColor = map.edge.getColor(); let transitionsNode = node.parentElement.querySelector('.transitions');
let labelNode = this.div('transitionLabel'); let subtransitionNodes = transitionsNode.children;
labelNode.innerText = map.edge.toString(); if (subtransitionNodes.length <= 1) {
edge.appendChild(labelNode); // Add subtransitions excepth the one that's already shown.
return edge; let visibleTransitionMap = subtransitionNodes.length == 1 ?
}
addTransitionTo(map) {
// transition[ transitions[ transition[...], transition[...], ...]];
let transition = this.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 = this.div('transitions');
transition.appendChild(subtree);
this.currentNode.appendChild(transition);
this.currentNode = subtree;
return mapNode;
}
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 : transitionsNode.querySelector('.map').map :
void 0; void 0;
map.children.forEach(edge => { map.children.forEach(edge => {
if (edge.to != visibleTransitionMap) { if (edge.to != visibleTransitionMap) {
this.currentNode = transitionsNode; this.currentNode = transitionsNode;
this.addSubtransitions(edge.to); 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]);
} }
});
} else {
// remove all but the first (currently selected) subtransition
for (let i = subtransitionNodes.length - 1; i > 0; i--) {
transitionsNode.removeChild(subtransitionNodes[i]);
} }
} }
}
}); });
...@@ -2,10 +2,12 @@ ...@@ -2,10 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import {defineCustomElement, V8CustomElement, import {
transitionTypeToColor, CSSColor} from '../helper.mjs'; defineCustomElement, V8CustomElement,
import {kChunkWidth, kChunkHeight} from '../map-processor.mjs'; transitionTypeToColor, CSSColor
import {SelectionEvent, SelectEvent} from '../events.mjs'; } from '../helper.mjs';
import { kChunkWidth, kChunkHeight } from '../map-processor.mjs';
import { SelectionEvent, SelectEvent, SelectTimeEvent } from '../events.mjs';
defineCustomElement('./timeline/timeline-track', (templateText) => defineCustomElement('./timeline/timeline-track', (templateText) =>
class TimelineTrack extends V8CustomElement { class TimelineTrack extends V8CustomElement {
...@@ -13,8 +15,14 @@ defineCustomElement('./timeline/timeline-track', (templateText) => ...@@ -13,8 +15,14 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
#nofChunks = 400; #nofChunks = 400;
#chunks; #chunks;
#selectedEntry; #selectedEntry;
#timeToPixel;
#timeSelection = { start: 0, end: Infinity };
constructor() { constructor() {
super(templateText); super(templateText);
this.timeline.addEventListener("mousedown",
e => this.handleTimeRangeSelectionStart(e));
this.timeline.addEventListener("mouseup",
e => this.handleTimeRangeSelectionEnd(e));
this.timeline.addEventListener("scroll", this.timeline.addEventListener("scroll",
e => this.handleTimelineScroll(e)); e => this.handleTimelineScroll(e));
this.backgroundCanvas = document.createElement('canvas'); this.backgroundCanvas = document.createElement('canvas');
...@@ -48,36 +56,36 @@ defineCustomElement('./timeline/timeline-track', (templateText) => ...@@ -48,36 +56,36 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
return this.#timeline; return this.#timeline;
} }
set nofChunks(count){ set nofChunks(count) {
this.#nofChunks = count; this.#nofChunks = count;
this.updateChunks(); this.updateChunks();
this.updateTimeline(); this.updateTimeline();
} }
get nofChunks(){ get nofChunks() {
return this.#nofChunks; return this.#nofChunks;
} }
updateChunks() { updateChunks() {
this.#chunks = this.data.chunks(this.nofChunks); this.#chunks = this.data.chunks(this.nofChunks);
} }
get chunks(){ get chunks() {
return this.#chunks; return this.#chunks;
} }
set selectedEntry(value){ set selectedEntry(value) {
this.#selectedEntry = value; this.#selectedEntry = value;
if(value.edge) this.redraw(); if (value.edge) this.redraw();
} }
get selectedEntry(){ get selectedEntry() {
return this.#selectedEntry; return this.#selectedEntry;
} }
set scrollLeft(offset){ set scrollLeft(offset) {
this.timeline.scrollLeft = offset; this.timeline.scrollLeft = offset;
} }
updateStats(){ updateStats() {
let unique = new Map(); let unique = new Map();
for (const entry of this.data.all) { for (const entry of this.data.all) {
if(!unique.has(entry.type)) { if (!unique.has(entry.type)) {
unique.set(entry.type, [entry]); unique.set(entry.type, [entry]);
} else { } else {
unique.get(entry.type).push(entry); unique.get(entry.type).push(entry);
...@@ -86,7 +94,7 @@ defineCustomElement('./timeline/timeline-track', (templateText) => ...@@ -86,7 +94,7 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
this.renderStatsWindow(unique); this.renderStatsWindow(unique);
} }
renderStatsWindow(unique){ renderStatsWindow(unique) {
let timelineLegendContent = this.timelineLegendContent; let timelineLegendContent = this.timelineLegendContent;
this.removeAllChildren(timelineLegendContent); this.removeAllChildren(timelineLegendContent);
let fragment = document.createDocumentFragment(); let fragment = document.createDocumentFragment();
...@@ -107,7 +115,7 @@ defineCustomElement('./timeline/timeline-track', (templateText) => ...@@ -107,7 +115,7 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
timelineLegendContent.appendChild(fragment); timelineLegendContent.appendChild(fragment);
} }
handleEntryTypeDblClick(e){ handleEntryTypeDblClick(e) {
this.dispatchEvent(new SelectionEvent(e.target.entries)); this.dispatchEvent(new SelectionEvent(e.target.entries));
} }
...@@ -115,11 +123,31 @@ defineCustomElement('./timeline/timeline-track', (templateText) => ...@@ -115,11 +123,31 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
this.timeline.scrollLeft += offset; this.timeline.scrollLeft += offset;
} }
handleTimelineScroll(e){ handleTimeRangeSelectionStart(e) {
this.#timeSelection.start = this.positionToTime(e.clientX);
}
handleTimeRangeSelectionEnd(e) {
this.#timeSelection.end = this.positionToTime(e.clientX);
this.dispatchEvent(new SelectTimeEvent(
Math.min(this.#timeSelection.start, this.#timeSelection.end),
Math.max(this.#timeSelection.start, this.#timeSelection.end)));
}
positionToTime(posX) {
let rect = this.timeline.getBoundingClientRect();
let posClickedX = posX - rect.left + this.timeline.scrollLeft;
let selectedTime = posClickedX / this.#timeToPixel;
return selectedTime;
}
handleTimelineScroll(e) {
let horizontal = e.currentTarget.scrollLeft; let horizontal = e.currentTarget.scrollLeft;
this.dispatchEvent(new CustomEvent( this.dispatchEvent(new CustomEvent(
'scrolltrack', {bubbles: true, composed: true, 'scrolltrack', {
detail: horizontal})); bubbles: true, composed: true,
detail: horizontal
}));
} }
asyncSetTimelineChunkBackground(backgroundTodo) { asyncSetTimelineChunkBackground(backgroundTodo) {
...@@ -176,11 +204,11 @@ defineCustomElement('./timeline/timeline-track', (templateText) => ...@@ -176,11 +204,11 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
let start = this.data.startTime; let start = this.data.startTime;
let end = this.data.endTime; let end = this.data.endTime;
let duration = end - start; let duration = end - start;
const timeToPixel = chunks.length * kChunkWidth / duration; this.#timeToPixel = chunks.length * kChunkWidth / duration;
let addTimestamp = (time, name) => { let addTimestamp = (time, name) => {
let timeNode = this.div('timestamp'); let timeNode = this.div('timestamp');
timeNode.innerText = name; timeNode.innerText = name;
timeNode.style.left = ((time - start) * timeToPixel) + 'px'; timeNode.style.left = ((time - start) * this.#timeToPixel) + 'px';
chunksNode.appendChild(timeNode); chunksNode.appendChild(timeNode);
}; };
let backgroundTodo = []; let backgroundTodo = [];
...@@ -224,7 +252,7 @@ defineCustomElement('./timeline/timeline-track', (templateText) => ...@@ -224,7 +252,7 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
if (!chunk) return; if (!chunk) return;
// topmost map (at chunk.height) == map #0. // topmost map (at chunk.height) == map #0.
let relativeIndex = let relativeIndex =
Math.round(event.layerY / event.target.offsetHeight * chunk.size()); Math.round(event.layerY / event.target.offsetHeight * chunk.size());
let map = chunk.at(relativeIndex); let map = chunk.at(relativeIndex);
this.dispatchEvent(new SelectEvent(map)); this.dispatchEvent(new SelectEvent(map));
} }
...@@ -264,7 +292,7 @@ defineCustomElement('./timeline/timeline-track', (templateText) => ...@@ -264,7 +292,7 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
ctx.fill(); ctx.fill();
let imageData = canvas.toDataURL('image/webp', 0.2); let imageData = canvas.toDataURL('image/webp', 0.2);
this.dispatchEvent(new CustomEvent( this.dispatchEvent(new CustomEvent(
'overviewupdate', {bubbles: true, composed: true, detail: imageData})); 'overviewupdate', { bubbles: true, composed: true, detail: imageData }));
} }
redraw() { redraw() {
...@@ -384,7 +412,7 @@ defineCustomElement('./timeline/timeline-track', (templateText) => ...@@ -384,7 +412,7 @@ defineCustomElement('./timeline/timeline-track', (templateText) =>
ctx.textAlign = 'left'; ctx.textAlign = 'left';
ctx.fillStyle = CSSColor.onBackgroundColor; ctx.fillStyle = CSSColor.onBackgroundColor;
ctx.fillText( ctx.fillText(
edge.toString(), centerX + offsetX + 2, centerY - labelOffset); edge.toString(), centerX + offsetX + 2, centerY - labelOffset);
} }
return [xTo, yTo]; return [xTo, yTo];
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment