2020-07-24 13:51:36 +00:00
|
|
|
// 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.
|
|
|
|
|
2020-12-07 08:44:54 +00:00
|
|
|
import {groupBy} from './helper.mjs'
|
|
|
|
|
2020-07-24 13:51:36 +00:00
|
|
|
class Timeline {
|
2020-10-19 10:13:33 +00:00
|
|
|
// Class:
|
2020-10-19 10:45:42 +00:00
|
|
|
_model;
|
2020-10-19 10:13:33 +00:00
|
|
|
// Array of #model instances:
|
2020-10-19 10:45:42 +00:00
|
|
|
_values;
|
2020-10-19 10:13:33 +00:00
|
|
|
// Current selection, subset of #values:
|
2020-10-19 10:45:42 +00:00
|
|
|
_selection;
|
2020-11-30 15:09:54 +00:00
|
|
|
_breakdown;
|
2020-10-19 10:13:33 +00:00
|
|
|
|
2021-06-17 15:09:24 +00:00
|
|
|
constructor(model, values = [], startTime = null, endTime = null) {
|
2020-10-19 10:45:42 +00:00
|
|
|
this._model = model;
|
2020-11-30 15:09:54 +00:00
|
|
|
this._values = values;
|
|
|
|
this.startTime = startTime;
|
|
|
|
this.endTime = endTime;
|
2021-06-08 08:46:47 +00:00
|
|
|
if (values.length > 0) {
|
2021-06-17 15:09:24 +00:00
|
|
|
if (startTime === null) this.startTime = values[0].time;
|
|
|
|
if (endTime === null) this.endTime = values[values.length - 1].time;
|
|
|
|
} else {
|
|
|
|
if (startTime === null) this.startTime = 0;
|
|
|
|
if (endTime === null) this.endTime = 0;
|
2021-06-08 08:46:47 +00:00
|
|
|
}
|
2020-07-24 13:51:36 +00:00
|
|
|
}
|
2020-10-19 10:13:33 +00:00
|
|
|
|
|
|
|
get model() {
|
2020-10-19 10:45:42 +00:00
|
|
|
return this._model;
|
2020-10-19 10:13:33 +00:00
|
|
|
}
|
|
|
|
|
2020-08-25 06:31:43 +00:00
|
|
|
get all() {
|
2020-10-19 10:45:42 +00:00
|
|
|
return this._values;
|
2020-07-24 13:51:36 +00:00
|
|
|
}
|
2020-10-19 10:13:33 +00:00
|
|
|
|
2020-08-25 06:31:43 +00:00
|
|
|
get selection() {
|
2020-10-19 10:45:42 +00:00
|
|
|
return this._selection;
|
2020-07-24 13:51:36 +00:00
|
|
|
}
|
2020-10-19 10:13:33 +00:00
|
|
|
|
2020-12-07 08:44:54 +00:00
|
|
|
get selectionOrSelf() {
|
|
|
|
return this._selection ?? this;
|
|
|
|
}
|
|
|
|
|
2020-08-25 06:31:43 +00:00
|
|
|
set selection(value) {
|
2020-10-19 10:45:42 +00:00
|
|
|
this._selection = value;
|
2020-07-24 13:51:36 +00:00
|
|
|
}
|
2020-10-19 10:13:33 +00:00
|
|
|
|
2020-11-27 18:43:26 +00:00
|
|
|
selectTimeRange(startTime, endTime) {
|
2020-11-30 15:09:54 +00:00
|
|
|
const items = this.range(startTime, endTime);
|
|
|
|
this._selection = new Timeline(this._model, items, startTime, endTime);
|
|
|
|
}
|
|
|
|
|
|
|
|
clearSelection() {
|
|
|
|
this._selection = undefined;
|
2020-07-24 13:51:36 +00:00
|
|
|
}
|
2020-10-19 10:13:33 +00:00
|
|
|
|
2020-08-25 06:31:43 +00:00
|
|
|
getChunks(windowSizeMs) {
|
2020-07-24 13:51:36 +00:00
|
|
|
return this.chunkSizes(windowSizeMs);
|
|
|
|
}
|
2020-10-19 10:13:33 +00:00
|
|
|
|
2020-08-25 06:31:43 +00:00
|
|
|
get values() {
|
2020-10-19 10:45:42 +00:00
|
|
|
return this._values;
|
2020-07-24 13:51:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
2020-10-19 10:45:42 +00:00
|
|
|
this._values.splice(insertionPoint, event);
|
2020-07-24 13:51:36 +00:00
|
|
|
} else {
|
2020-10-19 10:45:42 +00:00
|
|
|
this._values.push(event);
|
2020-07-24 13:51:36 +00:00
|
|
|
}
|
|
|
|
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) {
|
2020-10-19 10:45:42 +00:00
|
|
|
return this._values[index];
|
2020-07-24 13:51:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
isEmpty() {
|
|
|
|
return this.size() === 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
size() {
|
2020-10-19 10:45:42 +00:00
|
|
|
return this._values.length;
|
2020-07-24 13:51:36 +00:00
|
|
|
}
|
|
|
|
|
2020-10-27 14:31:24 +00:00
|
|
|
get length() {
|
|
|
|
return this._values.length;
|
|
|
|
}
|
|
|
|
|
2020-11-30 15:09:54 +00:00
|
|
|
slice(startIndex, endIndex) {
|
|
|
|
return this._values.slice(startIndex, endIndex);
|
|
|
|
}
|
|
|
|
|
2020-07-24 13:51:36 +00:00
|
|
|
first() {
|
2020-10-19 10:45:42 +00:00
|
|
|
return this._values[0];
|
2020-07-24 13:51:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
last() {
|
2020-10-19 10:45:42 +00:00
|
|
|
return this._values[this._values.length - 1];
|
2020-07-24 13:51:36 +00:00
|
|
|
}
|
|
|
|
|
2020-11-30 15:09:54 +00:00
|
|
|
* [Symbol.iterator]() {
|
|
|
|
yield* this._values;
|
|
|
|
}
|
|
|
|
|
2020-07-24 13:51:36 +00:00
|
|
|
duration() {
|
2021-06-07 14:57:44 +00:00
|
|
|
return this.endTime - this.startTime;
|
2020-07-24 13:51:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
forEachChunkSize(count, fn) {
|
2020-11-30 15:09:54 +00:00
|
|
|
if (this.isEmpty()) return;
|
2020-07-24 13:51:36 +00:00
|
|
|
const increment = this.duration() / count;
|
2021-06-17 15:09:24 +00:00
|
|
|
let currentTime = this.startTime;
|
2020-07-24 13:51:36 +00:00
|
|
|
let index = 0;
|
2021-06-17 15:09:24 +00:00
|
|
|
for (let i = 0; i < count - 1; i++) {
|
2020-12-02 08:34:43 +00:00
|
|
|
const nextTime = currentTime + increment;
|
2021-06-17 15:09:24 +00:00
|
|
|
const nextIndex = this.findLast(nextTime, index);
|
2020-07-24 13:51:36 +00:00
|
|
|
fn(index, nextIndex, currentTime, nextTime);
|
2021-06-17 15:09:24 +00:00
|
|
|
index = nextIndex + 1;
|
2020-07-24 13:51:36 +00:00
|
|
|
currentTime = nextTime;
|
|
|
|
}
|
2021-06-17 15:09:24 +00:00
|
|
|
fn(index, this._values.length - 1, currentTime, this.endTime);
|
2020-07-24 13:51:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
chunkSizes(count) {
|
2020-12-02 08:34:43 +00:00
|
|
|
const chunks = [];
|
2020-07-24 13:51:36 +00:00
|
|
|
this.forEachChunkSize(count, (start, end) => chunks.push(end - start));
|
|
|
|
return chunks;
|
|
|
|
}
|
|
|
|
|
2020-12-02 08:34:43 +00:00
|
|
|
chunks(count, predicate = undefined) {
|
|
|
|
const chunks = [];
|
2020-07-24 13:51:36 +00:00
|
|
|
this.forEachChunkSize(count, (start, end, startTime, endTime) => {
|
2021-06-17 15:09:24 +00:00
|
|
|
let items = this._values.slice(start, end + 1);
|
2020-12-02 08:34:43 +00:00
|
|
|
if (predicate !== undefined) items = items.filter(predicate);
|
2020-07-24 13:51:36 +00:00
|
|
|
chunks.push(new Chunk(chunks.length, startTime, endTime, items));
|
|
|
|
});
|
|
|
|
return chunks;
|
|
|
|
}
|
|
|
|
|
2020-11-27 18:43:26 +00:00
|
|
|
// Return all entries in ({startTime}, {endTime}]
|
|
|
|
range(startTime, endTime) {
|
|
|
|
const firstIndex = this.find(startTime);
|
|
|
|
if (firstIndex < 0) return [];
|
|
|
|
const lastIndex = this.find(endTime, firstIndex + 1);
|
|
|
|
return this._values.slice(firstIndex, lastIndex);
|
2020-07-24 13:51:36 +00:00
|
|
|
}
|
|
|
|
|
2021-06-17 15:09:24 +00:00
|
|
|
// Return the first index with element.time >= time.
|
2020-07-24 13:51:36 +00:00
|
|
|
find(time, offset = 0) {
|
2021-06-17 15:09:24 +00:00
|
|
|
return this.findFirst(time, offset);
|
|
|
|
}
|
|
|
|
|
|
|
|
findFirst(time, offset = 0) {
|
2020-10-19 10:45:42 +00:00
|
|
|
return this._find(this._values, each => each.time - time, offset);
|
2020-07-24 13:51:36 +00:00
|
|
|
}
|
|
|
|
|
2021-06-17 15:09:24 +00:00
|
|
|
// Return the last index with element.time <= time.
|
|
|
|
findLast(time, offset = 0) {
|
|
|
|
const nextTime = time + 1;
|
|
|
|
let index = (this.last().time <= nextTime) ?
|
|
|
|
this.length :
|
|
|
|
this.findFirst(nextTime + 1, offset);
|
|
|
|
// Typically we just have to look at the previous element.
|
|
|
|
while (index > 0) {
|
|
|
|
index--;
|
|
|
|
if (this._values[index].time <= time) return index;
|
|
|
|
}
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2020-11-27 18:43:26 +00:00
|
|
|
// Return the first index for which compareFn(item) is >= 0;
|
|
|
|
_find(array, compareFn, offset = 0) {
|
|
|
|
let minIndex = offset;
|
|
|
|
let maxIndex = array.length - 1;
|
|
|
|
while (minIndex < maxIndex) {
|
|
|
|
const midIndex = minIndex + (((maxIndex - minIndex) / 2) | 0);
|
|
|
|
if (compareFn(array[midIndex]) < 0) {
|
|
|
|
minIndex = midIndex + 1;
|
2020-07-24 13:51:36 +00:00
|
|
|
} else {
|
2020-11-27 18:43:26 +00:00
|
|
|
maxIndex = midIndex;
|
2020-07-24 13:51:36 +00:00
|
|
|
}
|
|
|
|
}
|
2020-11-27 18:43:26 +00:00
|
|
|
return minIndex;
|
2020-07-24 13:51:36 +00:00
|
|
|
}
|
|
|
|
|
2021-07-05 08:01:09 +00:00
|
|
|
getBreakdown(keyFunction, collect = false) {
|
|
|
|
if (keyFunction || collect) {
|
|
|
|
if (!keyFunction) {
|
|
|
|
keyFunction = each => each.type;
|
|
|
|
}
|
|
|
|
return groupBy(this._values, keyFunction, collect);
|
|
|
|
}
|
2020-11-30 15:09:54 +00:00
|
|
|
if (this._breakdown === undefined) {
|
2020-12-07 08:44:54 +00:00
|
|
|
this._breakdown = groupBy(this._values, each => each.type);
|
2020-10-19 10:13:33 +00:00
|
|
|
}
|
2020-11-30 15:09:54 +00:00
|
|
|
return this._breakdown;
|
2020-10-19 10:13:33 +00:00
|
|
|
}
|
|
|
|
|
2020-07-24 13:51:36 +00:00
|
|
|
depthHistogram() {
|
2020-10-19 10:45:42 +00:00
|
|
|
return this._values.histogram(each => each.depth);
|
2020-07-24 13:51:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fanOutHistogram() {
|
2020-10-19 10:45:42 +00:00
|
|
|
return this._values.histogram(each => each.children.length);
|
2020-07-24 13:51:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
forEach(fn) {
|
2020-10-19 10:45:42 +00:00
|
|
|
return this._values.forEach(fn);
|
2020-07-24 13:51:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ===========================================================================
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2021-01-08 09:37:22 +00:00
|
|
|
get length() {
|
|
|
|
return this.items.length;
|
|
|
|
}
|
|
|
|
|
2020-07-24 13:51:36 +00:00
|
|
|
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
|
2021-06-07 14:57:44 +00:00
|
|
|
return ((this.indexOf(event) + 0.5) / this.size()) * this.height;
|
2020-07-24 13:51:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-11-16 13:19:25 +00:00
|
|
|
getBreakdown(keyFunction) {
|
2020-12-07 08:44:54 +00:00
|
|
|
return groupBy(this.items, keyFunction);
|
2020-07-24 13:51:36 +00:00
|
|
|
}
|
|
|
|
|
2020-08-25 06:31:43 +00:00
|
|
|
filter() {
|
2021-06-15 11:19:00 +00:00
|
|
|
return this.items.filter(map => !map.parent || !this.has(map.parent));
|
2020-07-24 13:51:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-03 08:01:33 +00:00
|
|
|
export {Timeline, Chunk};
|