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