• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2020 the V8 project authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import {groupBy} from './helper.mjs'
6
7class Timeline {
8  // Class:
9  _model;
10  // Array of #model instances:
11  _values;
12  // Current selection, subset of #values:
13  _selection;
14  _breakdown;
15
16  constructor(model, values = [], startTime = null, endTime = null) {
17    this._model = model;
18    this._values = values;
19    this.startTime = startTime;
20    this.endTime = endTime;
21    if (values.length > 0) {
22      if (startTime === null) this.startTime = values[0].time;
23      if (endTime === null) this.endTime = values[values.length - 1].time;
24    } else {
25      if (startTime === null) this.startTime = 0;
26      if (endTime === null) this.endTime = 0;
27    }
28  }
29
30  get model() {
31    return this._model;
32  }
33
34  get all() {
35    return this._values;
36  }
37
38  get selection() {
39    return this._selection;
40  }
41
42  get selectionOrSelf() {
43    return this._selection ?? this;
44  }
45
46  set selection(value) {
47    this._selection = value;
48  }
49
50  selectTimeRange(startTime, endTime) {
51    const items = this.range(startTime, endTime);
52    this._selection = new Timeline(this._model, items, startTime, endTime);
53  }
54
55  clearSelection() {
56    this._selection = undefined;
57  }
58
59  getChunks(windowSizeMs) {
60    return this.chunkSizes(windowSizeMs);
61  }
62
63  get values() {
64    return this._values;
65  }
66
67  count(filter) {
68    return this.all.reduce((sum, each) => {
69      return sum + (filter(each) === true ? 1 : 0);
70    }, 0);
71  }
72
73  filter(predicate) {
74    return this.all.filter(predicate);
75  }
76
77  push(event) {
78    let time = event.time;
79    if (!this.isEmpty() && this.last().time > time) {
80      // Invalid insertion order, might happen without --single-process,
81      // finding insertion point.
82      let insertionPoint = this.find(time);
83      this._values.splice(insertionPoint, event);
84    } else {
85      this._values.push(event);
86    }
87    if (time > 0) {
88      this.endTime = Math.max(this.endTime, time);
89      if (this.startTime === 0) {
90        this.startTime = time;
91      } else {
92        this.startTime = Math.min(this.startTime, time);
93      }
94    }
95  }
96
97  at(index) {
98    return this._values[index];
99  }
100
101  isEmpty() {
102    return this.size() === 0;
103  }
104
105  size() {
106    return this._values.length;
107  }
108
109  get length() {
110    return this._values.length;
111  }
112
113  slice(startIndex, endIndex) {
114    return this._values.slice(startIndex, endIndex);
115  }
116
117  first() {
118    return this._values[0];
119  }
120
121  last() {
122    return this._values[this._values.length - 1];
123  }
124
125  * [Symbol.iterator]() {
126    yield* this._values;
127  }
128
129  duration() {
130    return this.endTime - this.startTime;
131  }
132
133  forEachChunkSize(count, fn) {
134    if (this.isEmpty()) return;
135    const increment = this.duration() / count;
136    let currentTime = this.startTime;
137    let index = 0;
138    for (let i = 0; i < count - 1; i++) {
139      const nextTime = currentTime + increment;
140      const nextIndex = this.findLast(nextTime, index);
141      fn(index, nextIndex, currentTime, nextTime);
142      index = nextIndex + 1;
143      currentTime = nextTime;
144    }
145    fn(index, this._values.length - 1, currentTime, this.endTime);
146  }
147
148  chunkSizes(count) {
149    const chunks = [];
150    this.forEachChunkSize(count, (start, end) => chunks.push(end - start));
151    return chunks;
152  }
153
154  chunks(count, predicate = undefined) {
155    const chunks = [];
156    this.forEachChunkSize(count, (start, end, startTime, endTime) => {
157      let items = this._values.slice(start, end + 1);
158      if (predicate !== undefined) items = items.filter(predicate);
159      chunks.push(new Chunk(chunks.length, startTime, endTime, items));
160    });
161    return chunks;
162  }
163
164  // Return all entries in ({startTime}, {endTime}]
165  range(startTime, endTime) {
166    const firstIndex = this.find(startTime);
167    if (firstIndex < 0) return [];
168    const lastIndex = this.find(endTime, firstIndex + 1);
169    return this._values.slice(firstIndex, lastIndex);
170  }
171
172  // Return the first index with element.time >= time.
173  find(time, offset = 0) {
174    return this.findFirst(time, offset);
175  }
176
177  findFirst(time, offset = 0) {
178    return this._find(this._values, each => each.time - time, offset);
179  }
180
181  // Return the last index with element.time <= time.
182  findLast(time, offset = 0) {
183    const nextTime = time + 1;
184    let index = (this.last().time <= nextTime) ?
185        this.length :
186        this.findFirst(nextTime + 1, offset);
187    // Typically we just have to look at the previous element.
188    while (index > 0) {
189      index--;
190      if (this._values[index].time <= time) return index;
191    }
192    return -1;
193  }
194
195  // Return the first index for which compareFn(item) is >= 0;
196  _find(array, compareFn, offset = 0) {
197    let minIndex = offset;
198    let maxIndex = array.length - 1;
199    while (minIndex < maxIndex) {
200      const midIndex = minIndex + (((maxIndex - minIndex) / 2) | 0);
201      if (compareFn(array[midIndex]) < 0) {
202        minIndex = midIndex + 1;
203      } else {
204        maxIndex = midIndex;
205      }
206    }
207    return minIndex;
208  }
209
210  getBreakdown(keyFunction, collect = false) {
211    if (keyFunction || collect) {
212      if (!keyFunction) {
213        keyFunction = each => each.type;
214      }
215      return groupBy(this._values, keyFunction, collect);
216    }
217    if (this._breakdown === undefined) {
218      this._breakdown = groupBy(this._values, each => each.type);
219    }
220    return this._breakdown;
221  }
222
223  depthHistogram() {
224    return this._values.histogram(each => each.depth);
225  }
226
227  fanOutHistogram() {
228    return this._values.histogram(each => each.children.length);
229  }
230
231  forEach(fn) {
232    return this._values.forEach(fn);
233  }
234}
235
236// ===========================================================================
237class Chunk {
238  constructor(index, start, end, items) {
239    this.index = index;
240    this.start = start;
241    this.end = end;
242    this.items = items;
243    this.height = 0;
244  }
245
246  isEmpty() {
247    return this.items.length === 0;
248  }
249
250  last() {
251    return this.at(this.size() - 1);
252  }
253
254  first() {
255    return this.at(0);
256  }
257
258  at(index) {
259    return this.items[index];
260  }
261
262  size() {
263    return this.items.length;
264  }
265
266  get length() {
267    return this.items.length;
268  }
269
270  yOffset(event) {
271    // items[0]   == oldest event, displayed at the top of the chunk
272    // items[n-1] == youngest event, displayed at the bottom of the chunk
273    return ((this.indexOf(event) + 0.5) / this.size()) * this.height;
274  }
275
276  indexOf(event) {
277    return this.items.indexOf(event);
278  }
279
280  has(event) {
281    if (this.isEmpty()) return false;
282    return this.first().time <= event.time && event.time <= this.last().time;
283  }
284
285  next(chunks) {
286    return this.findChunk(chunks, 1);
287  }
288
289  prev(chunks) {
290    return this.findChunk(chunks, -1);
291  }
292
293  findChunk(chunks, delta) {
294    let i = this.index + delta;
295    let chunk = chunks[i];
296    while (chunk && chunk.size() === 0) {
297      i += delta;
298      chunk = chunks[i];
299    }
300    return chunk;
301  }
302
303  getBreakdown(keyFunction) {
304    return groupBy(this.items, keyFunction);
305  }
306
307  filter() {
308    return this.items.filter(map => !map.parent || !this.has(map.parent));
309  }
310}
311
312export {Timeline, Chunk};
313