1// Copyright 2021 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 {TickLogEntry} from '../../log/tick.mjs'; 6import {Timeline} from '../../timeline.mjs'; 7import {delay, DOM, SVG} from '../helper.mjs'; 8 9import {TimelineTrackStackedBase} from './timeline-track-stacked-base.mjs' 10 11class Flame { 12 constructor(time, logEntry, depth) { 13 this._time = time; 14 this._logEntry = logEntry; 15 this.depth = depth; 16 this._duration = -1; 17 this.parent = undefined; 18 this.children = []; 19 } 20 21 static add(time, logEntry, stack, flames) { 22 const depth = stack.length; 23 const newFlame = new Flame(time, logEntry, depth) 24 if (depth > 0) { 25 const parent = stack[depth - 1]; 26 newFlame.parent = parent; 27 parent.children.push(newFlame); 28 } 29 flames.push(newFlame); 30 stack.push(newFlame); 31 } 32 33 stop(time) { 34 if (this._duration !== -1) throw new Error('Already stopped'); 35 this._duration = time - this._time 36 } 37 38 get time() { 39 return this._time; 40 } 41 42 get logEntry() { 43 return this._logEntry; 44 } 45 46 get startTime() { 47 return this._time; 48 } 49 50 get endTime() { 51 return this._time + this._duration; 52 } 53 54 get duration() { 55 return this._duration; 56 } 57 58 get type() { 59 return TickLogEntry.extractCodeEntryType(this._logEntry?.entry); 60 } 61} 62 63DOM.defineCustomElement( 64 'view/timeline/timeline-track', 'timeline-track-tick', 65 (templateText) => class TimelineTrackTick extends TimelineTrackStackedBase { 66 constructor() { 67 super(templateText); 68 this._annotations = new Annotations(this); 69 } 70 71 _prepareDrawableItems() { 72 const tmpFlames = []; 73 // flameStack = [bottom, ..., top]; 74 const flameStack = []; 75 const ticks = this._timeline.values; 76 let maxDepth = 0; 77 for (let tickIndex = 0; tickIndex < ticks.length; tickIndex++) { 78 const tick = ticks[tickIndex]; 79 const tickStack = tick.stack; 80 maxDepth = Math.max(maxDepth, tickStack.length); 81 // tick.stack = [top, .... , bottom]; 82 for (let stackIndex = tickStack.length - 1; stackIndex >= 0; 83 stackIndex--) { 84 const codeEntry = tickStack[stackIndex]; 85 // codeEntry is either a CodeEntry or a raw pc. 86 const logEntry = codeEntry?.logEntry; 87 const flameStackIndex = tickStack.length - stackIndex - 1; 88 if (flameStackIndex < flameStack.length) { 89 if (flameStack[flameStackIndex].logEntry === logEntry) continue; 90 for (let k = flameStackIndex; k < flameStack.length; k++) { 91 flameStack[k].stop(tick.time); 92 } 93 flameStack.length = flameStackIndex; 94 } 95 Flame.add(tick.time, logEntry, flameStack, tmpFlames); 96 } 97 if (tickStack.length < flameStack.length) { 98 for (let k = tickStack.length; k < flameStack.length; k++) { 99 flameStack[k].stop(tick.time); 100 } 101 flameStack.length = tickStack.length; 102 } 103 } 104 const lastTime = ticks[ticks.length - 1].time; 105 for (let k = 0; k < flameStack.length; k++) { 106 flameStack[k].stop(lastTime); 107 } 108 this._drawableItems = new Timeline(Flame, tmpFlames); 109 this._annotations.flames = this._drawableItems; 110 this._adjustStackDepth(maxDepth); 111 } 112 113 _drawAnnotations(logEntry, time) { 114 if (time === undefined) { 115 time = this.relativePositionToTime(this._timelineScrollLeft); 116 } 117 this._annotations.update(logEntry, time); 118 } 119 120 _drawableItemToLogEntry(flame) { 121 const logEntry = flame?.logEntry; 122 if (logEntry === undefined || typeof logEntry == 'number') 123 return undefined; 124 return logEntry; 125 } 126 }) 127 128class Annotations { 129 _flames; 130 _logEntry; 131 _buffer; 132 133 constructor(track) { 134 this._track = track; 135 } 136 137 set flames(flames) { 138 this._flames = flames; 139 } 140 141 get _node() { 142 return this._track.timelineAnnotationsNode; 143 } 144 145 async update(logEntry, time) { 146 if (this._logEntry == logEntry) return; 147 this._logEntry = logEntry; 148 this._node.innerHTML = ''; 149 if (logEntry === undefined) return; 150 this._buffer = ''; 151 const start = this._flames.find(time); 152 let offset = 0; 153 // Draw annotations gradually outwards starting form the given time. 154 let deadline = performance.now() + 100; 155 for (let range = 0; range < this._flames.length; range += 10000) { 156 this._markFlames(start - range, start - offset); 157 this._markFlames(start + offset, start + range); 158 offset = range; 159 if ((navigator?.scheduling?.isInputPending({includeContinuous: true}) ?? 160 false) || 161 performance.now() >= deadline) { 162 // Yield if we have to handle an input event, or we're out of time. 163 await delay(50); 164 // Abort if we started another update asynchronously. 165 if (this._logEntry != logEntry) return; 166 167 deadline = performance.now() + 100; 168 } 169 this._drawBuffer(); 170 } 171 this._drawBuffer(); 172 } 173 174 _markFlames(start, end) { 175 const rawFlames = this._flames.values; 176 if (start < 0) start = 0; 177 if (end > rawFlames.length) end = rawFlames.length; 178 const logEntry = this._logEntry; 179 // Also compare against the function, if any. 180 const func = logEntry.entry?.func ?? -1; 181 for (let i = start; i < end; i++) { 182 const flame = rawFlames[i]; 183 const flameLogEntry = flame.logEntry; 184 if (!flameLogEntry) continue; 185 if (flameLogEntry !== logEntry) { 186 if (flameLogEntry.entry?.func !== func) continue; 187 } 188 this._buffer += this._track._drawItem(flame, i, true); 189 } 190 } 191 192 _drawBuffer() { 193 if (this._buffer.length == 0) return; 194 const svg = SVG.svg(); 195 svg.innerHTML = this._buffer; 196 this._node.appendChild(svg); 197 this._buffer = ''; 198 } 199} 200