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