Commit b3b42a30 authored by Zeynep Cankara's avatar Zeynep Cankara Committed by Commit Bot

[tools][system-analyzer] Add Timeline Class

This CL adds a Timeline Class to handle data interaction
between panels. The timeline class enables to filter the
data based on selected time range.

Bug: v8:10644, v8:10735

Change-Id: I7fbbe1741abc69d2889b0547113e5da10b7f5510
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2315983
Commit-Queue: Zeynep Cankara <zcankara@google.com>
Reviewed-by: 's avatarSathya Gunasekaran  <gsathya@chromium.org>
Cr-Commit-Position: refs/heads/master@{#69056}
parent cb5b2eca
...@@ -5,8 +5,11 @@ ...@@ -5,8 +5,11 @@
import {$} from './helper.mjs'; import {$} from './helper.mjs';
class State { class State {
#mapTimeline;
#icTimeline;
#timeline;
#transitions;
constructor(mapPanelId, timelinePanelId) { constructor(mapPanelId, timelinePanelId) {
this._timeline = undefined;
this.mapPanel_ = $(mapPanelId); this.mapPanel_ = $(mapPanelId);
this.timelinePanel_ = $(timelinePanelId); this.timelinePanel_ = $(timelinePanelId);
this._navigation = new Navigation(this); this._navigation = new Navigation(this);
...@@ -25,26 +28,26 @@ class State { ...@@ -25,26 +28,26 @@ class State {
this.map = e.detail; this.map = e.detail;
} }
handleShowMaps(e){ handleShowMaps(e){
this.mapPanel_.mapEntries = e.detail.getUniqueTransitions(); this.mapPanel_.mapEntries = e.detail.filter(event =>
!event.parent() || !this.has(event.parent()));
} }
get icTimeline() {
set filteredEntries(value) { return this.#icTimeline;
this._filteredEntries = value;
if (this._filteredEntries) {
//TODO(zcankara) update timeline view
} }
set icTimeline(value) {
this.#icTimeline = value;
} }
get filteredEntries() { set transitions(value) {
return this._filteredEntries; this.mapPanel_.transitions = value;
} }
get timeline() { get timeline() {
return this._timeline; return this.#timeline;
} }
set timeline(value) { set timeline(value) {
this._timeline = value; this.#timeline = value;
this.timelinePanel.timelineEntries = value; this.timelinePanel.timelineEntries = value;
this.timelinePanel.updateTimeline(this.map); this.timelinePanel.updateTimeline(this.map);
this.mapPanel_.updateStats(this.timeline); this.mapPanel_.timeline = this.timeline;
} }
get chunks() { get chunks() {
return this.timelinePanel.chunks; return this.timelinePanel.chunks;
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// 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 {Timeline} from './timeline.mjs';
/** /**
* Parser for dynamic code optimization state. * Parser for dynamic code optimization state.
*/ */
...@@ -174,9 +176,9 @@ IcProcessor.kProperties = [ ...@@ -174,9 +176,9 @@ IcProcessor.kProperties = [
]; ];
class CustomIcProcessor extends IcProcessor { class CustomIcProcessor extends IcProcessor {
#timeline = new Timeline();
constructor() { constructor() {
super(); super();
this.entries = [];
} }
functionName(pc) { functionName(pc) {
...@@ -188,9 +190,20 @@ class CustomIcProcessor extends IcProcessor { ...@@ -188,9 +190,20 @@ class CustomIcProcessor extends IcProcessor {
type, pc, time, line, column, old_state, new_state, map, key, modifier, type, pc, time, line, column, old_state, new_state, map, key, modifier,
slow_reason) { slow_reason) {
let fnName = this.functionName(pc); let fnName = this.functionName(pc);
this.entries.push(new Entry( let entry = new Entry(
type, fnName, time, line, column, key, old_state, new_state, map, type, fnName, time, line, column, key, old_state, new_state, map,
slow_reason)); slow_reason);
this.#timeline.push(entry);
}
get timeline(){
return this.#timeline;
}
processString(string) {
super.processString(string);
return this.timeline;
} }
}; };
......
...@@ -10,6 +10,7 @@ import './timeline-panel.mjs'; ...@@ -10,6 +10,7 @@ import './timeline-panel.mjs';
import './map-panel.mjs'; import './map-panel.mjs';
import './log-file-reader.mjs'; import './log-file-reader.mjs';
class App { class App {
#timeSelection = {start: 0, end: Infinity};
constructor(fileReaderId, mapPanelId, timelinePanelId, icPanelId) { constructor(fileReaderId, mapPanelId, timelinePanelId, icPanelId) {
this.mapPanelId_ = mapPanelId; this.mapPanelId_ = mapPanelId;
this.timelinePanelId_ = timelinePanelId; this.timelinePanelId_ = timelinePanelId;
...@@ -27,7 +28,6 @@ class App { ...@@ -27,7 +28,6 @@ class App {
'mapclick', e => this.handleMapClick(e)); 'mapclick', e => this.handleMapClick(e));
this.icPanel_.addEventListener( this.icPanel_.addEventListener(
'filepositionclick', e => this.handleFilePositionClick(e)); 'filepositionclick', e => this.handleFilePositionClick(e));
this.entries = undefined;
} }
handleFileUpload(e){ handleFileUpload(e){
...@@ -43,10 +43,11 @@ class App { ...@@ -43,10 +43,11 @@ class App {
} }
handleICTimeFilter(event) { handleICTimeFilter(event) {
let filteredEntries = this.entries.filter( this.#timeSelection.start = event.detail.startTime;
e => e.time >= event.detail.startTime && e.time <= event.detail.endTime); this.#timeSelection.end = event.detail.endTime;
console.log("filtered entries: ", filteredEntries); document.state.icTimeline.selectTimeRange(this.#timeSelection.start,
this.icPanel_.filteredEntries = filteredEntries; this.#timeSelection.end);
this.icPanel_.filteredEntries = document.state.icTimeline.selection;
} }
...@@ -97,11 +98,11 @@ class App { ...@@ -97,11 +98,11 @@ class App {
let reader = new FileReader(); let reader = new FileReader();
reader.onload = (evt) => { reader.onload = (evt) => {
let icProcessor = new CustomIcProcessor(); let icProcessor = new CustomIcProcessor();
icProcessor.processString(fileData.chunk); //TODO(zcankara) Assign timeline directly to the ic panel
let entries = icProcessor.entries; //TODO(zcankara) Exe: this.icPanel_.timeline = document.state.icTimeline
this.entries = entries; document.state.icTimeline = icProcessor.processString(fileData.chunk);
this.icPanel_.filteredEntries = entries; this.icPanel_.filteredEntries = document.state.icTimeline.all;
this.icPanel_.count.innerHTML = entries.length; this.icPanel_.count.innerHTML = document.state.icTimeline.all.length;
} }
reader.readAsText(fileData.file); reader.readAsText(fileData.file);
this.icPanel_.initGroupKeySelect(); this.icPanel_.initGroupKeySelect();
...@@ -115,7 +116,10 @@ class App { ...@@ -115,7 +116,10 @@ class App {
let fileData = e.detail; let fileData = e.detail;
document.state = new State(this.mapPanelId_, this.timelinePanelId_); document.state = new State(this.mapPanelId_, this.timelinePanelId_);
try { try {
document.state.timeline = this.handleLoadTextMapProcessor(fileData.chunk); const timeline = this.handleLoadTextMapProcessor(fileData.chunk);
// Transitions must be set before timeline for stats panel.
document.state.transitions= timeline.transitions;
document.state.timeline = timeline;
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
...@@ -133,8 +137,8 @@ class App { ...@@ -133,8 +137,8 @@ class App {
handleSelectIc(e){ handleSelectIc(e){
if(!e.detail) return; if(!e.detail) return;
// Set selected IC events on the View //TODO(zcankara) Send filtered entries to State
document.state.filteredEntries = e.detail; console.log("filtered IC entried: ", e.detail)
} }
} }
......
...@@ -55,6 +55,12 @@ defineCustomElement('map-panel', (templateText) => ...@@ -55,6 +55,12 @@ defineCustomElement('map-panel', (templateText) =>
this.statsPanel.timeline = value; this.statsPanel.timeline = value;
this.statsPanel.update(); this.statsPanel.update();
} }
get transitions() {
return this.statsPanel.transitions;
}
set transitions(value) {
this.statsPanel.transitions = value;
}
set map(value) { set map(value) {
this.#map = value; this.#map = value;
...@@ -79,10 +85,6 @@ defineCustomElement('map-panel', (templateText) => ...@@ -79,10 +85,6 @@ defineCustomElement('map-panel', (templateText) =>
'click', {bubbles: true, composed: true, detail: dataModel})); 'click', {bubbles: true, composed: true, detail: dataModel}));
} }
updateStats(timeline) {
this.timeline = timeline;
}
set mapEntries(list){ set mapEntries(list){
this.mapTransitionsPanel.mapEntries = list; this.mapTransitionsPanel.mapEntries = list;
} }
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// 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 {Timeline} from './timeline.mjs';
// =========================================================================== // ===========================================================================
const kChunkHeight = 250; const kChunkHeight = 250;
...@@ -113,7 +114,20 @@ class MapProcessor extends LogReader { ...@@ -113,7 +114,20 @@ class MapProcessor extends LogReader {
finalize() { finalize() {
// TODO(cbruni): print stats; // TODO(cbruni): print stats;
this.timeline_.finalize(); this.timeline_.transitions = new Map();
let id = 0;
this.timeline_.forEach(map => {
if (map.isRoot()) id = map.finalizeRootMap(id + 1);
if (map.edge && map.edge.name) {
let edge = map.edge;
let list = this.timeline_.transitions.get(edge.name);
if (list === undefined) {
this.timeline_.transitions.set(edge.name, [edge]);
} else {
list.push(edge);
}
}
});
return this.timeline_; return this.timeline_;
} }
...@@ -502,274 +516,6 @@ class Edge { ...@@ -502,274 +516,6 @@ class Edge {
} }
} }
// ===========================================================================
class Marker {
constructor(time, name) {
this.time = parseInt(time);
this.name = name;
}
}
// ===========================================================================
class Timeline {
constructor() {
this.values = [];
this.transitions = new Map();
this.markers = [];
this.startTime = 0;
this.endTime = 0;
}
push(map) {
let time = map.time;
if (!this.isEmpty() && this.last().time > time) {
// Invalid insertion order, might happen without --single-process,
// finding insertion point.
let insertionPoint = this.find(time);
this.values.splice(insertionPoint, map);
} else {
this.values.push(map);
}
if (time > 0) {
this.endTime = Math.max(this.endTime, time);
if (this.startTime === 0) {
this.startTime = time;
} else {
this.startTime = Math.min(this.startTime, time);
}
}
}
addMarker(time, message) {
this.markers.push(new Marker(time, message));
}
finalize() {
let id = 0;
this.forEach(map => {
if (map.isRoot()) id = map.finalizeRootMap(id + 1);
if (map.edge && map.edge.name) {
let edge = map.edge;
let list = this.transitions.get(edge.name);
if (list === undefined) {
this.transitions.set(edge.name, [edge]);
} else {
list.push(edge);
}
}
});
this.markers.sort((a, b) => b.time - a.time);
}
at(index) {
return this.values[index]
}
isEmpty() {
return this.size() === 0
}
size() {
return this.values.length
}
first() {
return this.values.first()
}
last() {
return this.values.last()
}
duration() {
return this.last().time - this.first().time
}
forEachChunkSize(count, fn) {
const increment = this.duration() / count;
let currentTime = this.first().time + increment;
let index = 0;
for (let i = 0; i < count; i++) {
let nextIndex = this.find(currentTime, index);
let nextTime = currentTime + increment;
fn(index, nextIndex, currentTime, nextTime);
index = nextIndex
currentTime = nextTime;
}
}
chunkSizes(count) {
let chunks = [];
this.forEachChunkSize(count, (start, end) => chunks.push(end - start));
return chunks;
}
chunks(count) {
let chunks = [];
let emptyMarkers = [];
this.forEachChunkSize(count, (start, end, startTime, endTime) => {
let items = this.values.slice(start, end);
let markers = this.markersAt(startTime, endTime);
chunks.push(new Chunk(chunks.length, startTime, endTime, items, markers));
});
return chunks;
}
range(start, end) {
const first = this.find(start);
if (first < 0) return [];
const last = this.find(end, first);
return this.values.slice(first, last);
}
find(time, offset = 0) {
return this.basicFind(this.values, each => each.time - time, offset);
}
markersAt(startTime, endTime) {
let start = this.basicFind(this.markers, each => each.time - startTime);
let end = this.basicFind(this.markers, each => each.time - endTime, start);
return this.markers.slice(start, end);
}
basicFind(array, cmp, offset = 0) {
let min = offset;
let max = array.length;
while (min < max) {
let mid = min + Math.floor((max - min) / 2);
let result = cmp(array[mid]);
if (result > 0) {
max = mid - 1;
} else {
min = mid + 1;
}
}
return min;
}
count(filter) {
return this.values.reduce((sum, each) => {
return sum + (filter(each) === true ? 1 : 0);
}, 0);
}
filter(predicate) {
return this.values.filter(predicate);
}
filterUniqueTransitions(filter) {
// Returns a list of Maps whose parent is not in the list.
return this.values.filter(map => {
if (filter(map) === false) return false;
let parent = map.parent();
if (parent === undefined) return true;
return filter(parent) === false;
});
}
depthHistogram() {
return this.values.histogram(each => each.depth);
}
fanOutHistogram() {
return this.values.histogram(each => each.children.length);
}
forEach(fn) {
return this.values.forEach(fn)
}
}
// ===========================================================================
class Chunk {
constructor(index, start, end, items, markers) {
this.index = index;
this.start = start;
this.end = end;
this.items = items;
this.markers = markers;
this.height = 0;
}
isEmpty() {
return this.items.length === 0;
}
last() {
return this.at(this.size() - 1);
}
first() {
return this.at(0);
}
at(index) {
return this.items[index];
}
size() {
return this.items.length;
}
yOffset(map) {
// items[0] == oldest map, displayed at the top of the chunk
// items[n-1] == youngest map, displayed at the bottom of the chunk
return (1 - (this.indexOf(map) + 0.5) / this.size()) * this.height;
}
indexOf(map) {
return this.items.indexOf(map);
}
has(map) {
if (this.isEmpty()) return false;
return this.first().time <= map.time && map.time <= this.last().time;
}
next(chunks) {
return this.findChunk(chunks, 1);
}
prev(chunks) {
return this.findChunk(chunks, -1);
}
findChunk(chunks, delta) {
let i = this.index + delta;
let chunk = chunks[i];
while (chunk && chunk.size() === 0) {
i += delta;
chunk = chunks[i]
}
return chunk;
}
getTransitionBreakdown() {
return BreakDown(this.items, map => map.getType())
}
getUniqueTransitions() {
// Filter out all the maps that have parents within the same chunk.
return this.items.filter(map => !map.parent() || !this.has(map.parent()));
}
}
// ===========================================================================
function BreakDown(list, map_fn) {
if (map_fn === void 0) {
map_fn = each => each;
}
let breakdown = {__proto__: null};
list.forEach(each => {
let type = map_fn(each);
let v = breakdown[type];
breakdown[type] = (v | 0) + 1
});
return Object.entries(breakdown).sort((a, b) => a[1] - b[1]);
}
// =========================================================================== // ===========================================================================
class ArgumentsProcessor extends BaseArgumentsProcessor { class ArgumentsProcessor extends BaseArgumentsProcessor {
......
...@@ -21,6 +21,24 @@ defineCustomElement('stats-panel', (templateText) => ...@@ -21,6 +21,24 @@ defineCustomElement('stats-panel', (templateText) =>
get timeline(){ get timeline(){
return this.timeline_; return this.timeline_;
} }
//TODO(zcankare) Depreciate timeline
set transitions(value){
this.transitions_ = value;
}
get transitions(){
return this.transitions_;
}
filterUniqueTransitions(filter) {
// Returns a list of Maps whose parent is not in the list.
return this.timeline.filter(map => {
if (filter(map) === false) return false;
let parent = map.parent();
if (parent === undefined) return true;
return filter(parent) === false;
});
}
update() { update() {
this.removeAllChildren(this.stats); this.removeAllChildren(this.stats);
...@@ -54,7 +72,6 @@ defineCustomElement('stats-panel', (templateText) => ...@@ -54,7 +72,6 @@ defineCustomElement('stats-panel', (templateText) =>
tableNode.innerHTML = tableNode.innerHTML =
'<thead><tr><td>Color</td><td>Type</td><td>Count</td><td>Percent</td></tr></thead>'; '<thead><tr><td>Color</td><td>Type</td><td>Count</td><td>Percent</td></tr></thead>';
let name, filter; let name, filter;
//TODO(zc) timeline
let total = this.timeline.size(); let total = this.timeline.size();
pairs.forEach(([name, color, filter]) => { pairs.forEach(([name, color, filter]) => {
let row = this.tr(); let row = this.tr();
...@@ -67,7 +84,7 @@ defineCustomElement('stats-panel', (templateText) => ...@@ -67,7 +84,7 @@ defineCustomElement('stats-panel', (templateText) =>
// lazily compute the stats // lazily compute the stats
let node = e.target.parentNode; let node = e.target.parentNode;
if (node.maps == undefined) { if (node.maps == undefined) {
node.maps = this.timeline.filterUniqueTransitions(filter); node.maps = this.filterUniqueTransitions(filter);
} }
this.dispatchEvent(new CustomEvent( this.dispatchEvent(new CustomEvent(
'change', {bubbles: true, composed: true, detail: node.maps})); 'change', {bubbles: true, composed: true, detail: node.maps}));
...@@ -84,7 +101,7 @@ defineCustomElement('stats-panel', (templateText) => ...@@ -84,7 +101,7 @@ defineCustomElement('stats-panel', (templateText) =>
updateNamedTransitionsStats() { updateNamedTransitionsStats() {
let tableNode = this.table('transitionTable'); let tableNode = this.table('transitionTable');
let nameMapPairs = Array.from(this.timeline.transitions.entries()); let nameMapPairs = Array.from(this.transitions.entries());
tableNode.innerHTML = tableNode.innerHTML =
'<thead><tr><td>Propery Name</td><td>#</td></tr></thead>'; '<thead><tr><td>Propery Name</td><td>#</td></tr></thead>';
nameMapPairs.sort((a, b) => b[1].length - a[1].length).forEach(([ nameMapPairs.sort((a, b) => b[1].length - a[1].length).forEach(([
...@@ -95,7 +112,8 @@ defineCustomElement('stats-panel', (templateText) => ...@@ -95,7 +112,8 @@ defineCustomElement('stats-panel', (templateText) =>
row.addEventListener( row.addEventListener(
'click', 'click',
e => this.dispatchEvent(new CustomEvent( e => this.dispatchEvent(new CustomEvent(
'change', {bubbles: true, composed: true, detail: e.target.parentNode.maps.map(map => map.to)}))); 'change', {bubbles: true, composed: true,
detail: e.target.parentNode.maps.map(map => map.to)})));
row.appendChild(this.td(name)); row.appendChild(this.td(name));
row.appendChild(this.td(maps.length)); row.appendChild(this.td(maps.length));
tableNode.appendChild(row); tableNode.appendChild(row);
......
...@@ -116,7 +116,7 @@ defineCustomElement('timeline-panel', (templateText) => ...@@ -116,7 +116,7 @@ defineCustomElement('timeline-panel', (templateText) =>
let total = chunk.size(); let total = chunk.size();
let type, count; let type, count;
if (true) { if (true) {
chunk.getTransitionBreakdown().forEach(([type, count]) => { chunk.getBreakdown(map => map.getType()).forEach(([type, count]) => {
ctx.fillStyle = this.transitionTypeToColor(type); ctx.fillStyle = this.transitionTypeToColor(type);
let height = count / total * kHeight; let height = count / total * kHeight;
ctx.fillRect(0, y, kWidth, y + height); ctx.fillRect(0, y, kWidth, y + height);
...@@ -191,7 +191,6 @@ defineCustomElement('timeline-panel', (templateText) => ...@@ -191,7 +191,6 @@ defineCustomElement('timeline-panel', (templateText) =>
node.addEventListener('dblclick', e => this.handleChunkDoubleClick(e)); node.addEventListener('dblclick', e => this.handleChunkDoubleClick(e));
backgroundTodo.push([chunk, node]) backgroundTodo.push([chunk, node])
chunksNode.appendChild(node); chunksNode.appendChild(node);
chunk.markers.forEach(marker => addTimestamp(marker.time, marker.name));
} }
this.asyncSetTimelineChunkBackground(backgroundTodo) this.asyncSetTimelineChunkBackground(backgroundTodo)
......
// 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.
class Timeline {
#values;
#selection;
constructor() {
this.#values = [];
this.startTime = 0;
this.endTime = 0;
}
get all(){
return this.#values;
}
get selection(){
return this.#selection;
}
set selection(value){
this.#selection = value;
}
selectTimeRange(start, end){
this.#selection = this.filter(
e => e.time >= start && e.time <= end);
}
getChunks(windowSizeMs){
//TODO(zcankara) Fill this one
return this.chunkSizes(windowSizeMs);
}
get values(){
//TODO(zcankara) Not to break something delete later
return this.#values;
}
count(filter) {
return this.all.reduce((sum, each) => {
return sum + (filter(each) === true ? 1 : 0);
}, 0);
}
filter(predicate) {
return this.all.filter(predicate);
}
push(event) {
let time = event.time;
if (!this.isEmpty() && this.last().time > time) {
// Invalid insertion order, might happen without --single-process,
// finding insertion point.
let insertionPoint = this.find(time);
this.#values.splice(insertionPoint, event);
} else {
this.#values.push(event);
}
if (time > 0) {
this.endTime = Math.max(this.endTime, time);
if (this.startTime === 0) {
this.startTime = time;
} else {
this.startTime = Math.min(this.startTime, time);
}
}
}
at(index) {
return this.#values[index];
}
isEmpty() {
return this.size() === 0;
}
size() {
return this.#values.length;
}
first() {
return this.#values.first();
}
last() {
return this.#values.last();
}
duration() {
return this.last().time - this.first().time;
}
forEachChunkSize(count, fn) {
const increment = this.duration() / count;
let currentTime = this.first().time + increment;
let index = 0;
for (let i = 0; i < count; i++) {
let nextIndex = this.find(currentTime, index);
let nextTime = currentTime + increment;
fn(index, nextIndex, currentTime, nextTime);
index = nextIndex;
currentTime = nextTime;
}
}
chunkSizes(count) {
let chunks = [];
this.forEachChunkSize(count, (start, end) => chunks.push(end - start));
return chunks;
}
chunks(count) {
let chunks = [];
this.forEachChunkSize(count, (start, end, startTime, endTime) => {
let items = this.#values.slice(start, end);
chunks.push(new Chunk(chunks.length, startTime, endTime, items));
});
return chunks;
}
range(start, end) {
const first = this.find(start);
if (first < 0) return [];
const last = this.find(end, first);
return this.#values.slice(first, last);
}
find(time, offset = 0) {
return this.#find(this.#values, each => each.time - time, offset);
}
#find(array, cmp, offset = 0) {
let min = offset;
let max = array.length;
while (min < max) {
let mid = min + Math.floor((max - min) / 2);
let result = cmp(array[mid]);
if (result > 0) {
max = mid - 1;
} else {
min = mid + 1;
}
}
return min;
}
depthHistogram() {
return this.#values.histogram(each => each.depth);
}
fanOutHistogram() {
return this.#values.histogram(each => each.children.length);
}
forEach(fn) {
return this.#values.forEach(fn);
}
}
// ===========================================================================
class Chunk {
constructor(index, start, end, items) {
this.index = index;
this.start = start;
this.end = end;
this.items = items;
this.height = 0;
}
isEmpty() {
return this.items.length === 0;
}
last() {
return this.at(this.size() - 1);
}
first() {
return this.at(0);
}
at(index) {
return this.items[index];
}
size() {
return this.items.length;
}
yOffset(event) {
// items[0] == oldest event, displayed at the top of the chunk
// items[n-1] == youngest event, displayed at the bottom of the chunk
return (1 - (this.indexOf(event) + 0.5) / this.size()) * this.height;
}
indexOf(event) {
return this.items.indexOf(event);
}
has(event) {
if (this.isEmpty()) return false;
return this.first().time <= event.time && event.time <= this.last().time;
}
next(chunks) {
return this.findChunk(chunks, 1);
}
prev(chunks) {
return this.findChunk(chunks, -1);
}
findChunk(chunks, delta) {
let i = this.index + delta;
let chunk = chunks[i];
while (chunk && chunk.size() === 0) {
i += delta;
chunk = chunks[i];
}
return chunk;
}
getBreakdown(event_fn){
if (event_fn === void 0) {
event_fn = 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]);
}
filter(filterFn){
return this.items.filter(filterFn);
}
}
export {Timeline};
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