xref: /aosp_15_r20/external/perfetto/ui/src/frontend/overview_timeline_panel.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2018 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import m from 'mithril';
16import {Duration, Time, TimeSpan, duration, time} from '../base/time';
17import {colorForCpu} from '../components/colorizer';
18import {timestampFormat} from '../core/timestamp_format';
19import {
20  OVERVIEW_TIMELINE_NON_VISIBLE_COLOR,
21  TRACK_SHELL_WIDTH,
22} from './css_constants';
23import {BorderDragStrategy} from './drag/border_drag_strategy';
24import {DragStrategy} from './drag/drag_strategy';
25import {InnerDragStrategy} from './drag/inner_drag_strategy';
26import {OuterDragStrategy} from './drag/outer_drag_strategy';
27import {DragGestureHandler} from '../base/drag_gesture_handler';
28import {
29  getMaxMajorTicks,
30  MIN_PX_PER_STEP,
31  generateTicks,
32  TickType,
33} from './gridline_helper';
34import {Size2D} from '../base/geom';
35import {Panel} from './panel_container';
36import {TimeScale} from '../base/time_scale';
37import {HighPrecisionTimeSpan} from '../base/high_precision_time_span';
38import {TraceImpl} from '../core/trace_impl';
39import {LONG, NUM} from '../trace_processor/query_result';
40import {raf} from '../core/raf_scheduler';
41import {getOrCreate} from '../base/utils';
42import {assertUnreachable} from '../base/logging';
43import {TimestampFormat} from '../public/timeline';
44
45const tracesData = new WeakMap<TraceImpl, OverviewDataLoader>();
46
47export class OverviewTimelinePanel implements Panel {
48  private static HANDLE_SIZE_PX = 5;
49  readonly kind = 'panel';
50  readonly selectable = false;
51  private width = 0;
52  private gesture?: DragGestureHandler;
53  private timeScale?: TimeScale;
54  private dragStrategy?: DragStrategy;
55  private readonly boundOnMouseMove = this.onMouseMove.bind(this);
56  private readonly overviewData: OverviewDataLoader;
57
58  constructor(private trace: TraceImpl) {
59    this.overviewData = getOrCreate(
60      tracesData,
61      trace,
62      () => new OverviewDataLoader(trace),
63    );
64  }
65
66  // Must explicitly type now; arguments types are no longer auto-inferred.
67  // https://github.com/Microsoft/TypeScript/issues/1373
68  onupdate({dom}: m.CVnodeDOM) {
69    this.width = dom.getBoundingClientRect().width;
70    const traceTime = this.trace.traceInfo;
71    if (this.width > TRACK_SHELL_WIDTH) {
72      const pxBounds = {left: TRACK_SHELL_WIDTH, right: this.width};
73      const hpTraceTime = HighPrecisionTimeSpan.fromTime(
74        traceTime.start,
75        traceTime.end,
76      );
77      this.timeScale = new TimeScale(hpTraceTime, pxBounds);
78      if (this.gesture === undefined) {
79        this.gesture = new DragGestureHandler(
80          dom as HTMLElement,
81          this.onDrag.bind(this),
82          this.onDragStart.bind(this),
83          this.onDragEnd.bind(this),
84        );
85      }
86    } else {
87      this.timeScale = undefined;
88    }
89  }
90
91  oncreate(vnode: m.CVnodeDOM) {
92    this.onupdate(vnode);
93    (vnode.dom as HTMLElement).addEventListener(
94      'mousemove',
95      this.boundOnMouseMove,
96    );
97  }
98
99  onremove({dom}: m.CVnodeDOM) {
100    if (this.gesture) {
101      this.gesture[Symbol.dispose]();
102      this.gesture = undefined;
103    }
104    (dom as HTMLElement).removeEventListener(
105      'mousemove',
106      this.boundOnMouseMove,
107    );
108  }
109
110  render(): m.Children {
111    return m('.overview-timeline', {
112      oncreate: (vnode) => this.oncreate(vnode),
113      onupdate: (vnode) => this.onupdate(vnode),
114      onremove: (vnode) => this.onremove(vnode),
115    });
116  }
117
118  renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) {
119    if (this.width === undefined) return;
120    if (this.timeScale === undefined) return;
121
122    const headerHeight = 20;
123    const tracksHeight = size.height - headerHeight;
124    const traceContext = new TimeSpan(
125      this.trace.traceInfo.start,
126      this.trace.traceInfo.end,
127    );
128
129    if (size.width > TRACK_SHELL_WIDTH && traceContext.duration > 0n) {
130      const maxMajorTicks = getMaxMajorTicks(this.width - TRACK_SHELL_WIDTH);
131      const offset = this.trace.timeline.timestampOffset();
132      const tickGen = generateTicks(traceContext, maxMajorTicks, offset);
133
134      // Draw time labels
135      ctx.font = '10px Roboto Condensed';
136      ctx.fillStyle = '#999';
137      for (const {type, time} of tickGen) {
138        const xPos = Math.floor(this.timeScale.timeToPx(time));
139        if (xPos <= 0) continue;
140        if (xPos > this.width) break;
141        if (type === TickType.MAJOR) {
142          ctx.fillRect(xPos - 1, 0, 1, headerHeight - 5);
143          const domainTime = this.trace.timeline.toDomainTime(time);
144          renderTimestamp(ctx, domainTime, xPos + 5, 18, MIN_PX_PER_STEP);
145        } else if (type == TickType.MEDIUM) {
146          ctx.fillRect(xPos - 1, 0, 1, 8);
147        } else if (type == TickType.MINOR) {
148          ctx.fillRect(xPos - 1, 0, 1, 5);
149        }
150      }
151    }
152
153    // Draw mini-tracks with quanitzed density for each process.
154    const overviewData = this.overviewData.overviewData;
155    if (overviewData.size > 0) {
156      const numTracks = overviewData.size;
157      let y = 0;
158      const trackHeight = (tracksHeight - 1) / numTracks;
159      for (const key of overviewData.keys()) {
160        const loads = overviewData.get(key)!;
161        for (let i = 0; i < loads.length; i++) {
162          const xStart = Math.floor(this.timeScale.timeToPx(loads[i].start));
163          const xEnd = Math.ceil(this.timeScale.timeToPx(loads[i].end));
164          const yOff = Math.floor(headerHeight + y * trackHeight);
165          const lightness = Math.ceil((1 - loads[i].load * 0.7) * 100);
166          const color = colorForCpu(y).setHSL({s: 50, l: lightness});
167          ctx.fillStyle = color.cssString;
168          ctx.fillRect(xStart, yOff, xEnd - xStart, Math.ceil(trackHeight));
169        }
170        y++;
171      }
172    }
173
174    // Draw bottom border.
175    ctx.fillStyle = '#dadada';
176    ctx.fillRect(0, size.height - 1, this.width, 1);
177
178    // Draw semi-opaque rects that occlude the non-visible time range.
179    const [vizStartPx, vizEndPx] = this.extractBounds(this.timeScale);
180
181    ctx.fillStyle = OVERVIEW_TIMELINE_NON_VISIBLE_COLOR;
182    ctx.fillRect(
183      TRACK_SHELL_WIDTH - 1,
184      headerHeight,
185      vizStartPx - TRACK_SHELL_WIDTH,
186      tracksHeight,
187    );
188    ctx.fillRect(vizEndPx, headerHeight, this.width - vizEndPx, tracksHeight);
189
190    // Draw brushes.
191    ctx.fillStyle = '#999';
192    ctx.fillRect(vizStartPx - 1, headerHeight, 1, tracksHeight);
193    ctx.fillRect(vizEndPx, headerHeight, 1, tracksHeight);
194
195    const hbarWidth = OverviewTimelinePanel.HANDLE_SIZE_PX;
196    const hbarHeight = tracksHeight * 0.4;
197    // Draw handlebar
198    ctx.fillRect(
199      vizStartPx - Math.floor(hbarWidth / 2) - 1,
200      headerHeight,
201      hbarWidth,
202      hbarHeight,
203    );
204    ctx.fillRect(
205      vizEndPx - Math.floor(hbarWidth / 2),
206      headerHeight,
207      hbarWidth,
208      hbarHeight,
209    );
210  }
211
212  private onMouseMove(e: MouseEvent) {
213    if (this.gesture === undefined || this.gesture.isDragging) {
214      return;
215    }
216    (e.target as HTMLElement).style.cursor = this.chooseCursor(e.offsetX);
217  }
218
219  private chooseCursor(x: number) {
220    if (this.timeScale === undefined) return 'default';
221    const [startBound, endBound] = this.extractBounds(this.timeScale);
222    if (
223      OverviewTimelinePanel.inBorderRange(x, startBound) ||
224      OverviewTimelinePanel.inBorderRange(x, endBound)
225    ) {
226      return 'ew-resize';
227    } else if (x < TRACK_SHELL_WIDTH) {
228      return 'default';
229    } else if (x < startBound || endBound < x) {
230      return 'crosshair';
231    } else {
232      return 'all-scroll';
233    }
234  }
235
236  onDrag(x: number) {
237    if (this.dragStrategy === undefined) return;
238    this.dragStrategy.onDrag(x);
239  }
240
241  onDragStart(x: number) {
242    if (this.timeScale === undefined) return;
243
244    const cb = (vizTime: HighPrecisionTimeSpan) => {
245      this.trace.timeline.updateVisibleTimeHP(vizTime);
246      raf.scheduleCanvasRedraw();
247    };
248    const pixelBounds = this.extractBounds(this.timeScale);
249    const timeScale = this.timeScale;
250    if (
251      OverviewTimelinePanel.inBorderRange(x, pixelBounds[0]) ||
252      OverviewTimelinePanel.inBorderRange(x, pixelBounds[1])
253    ) {
254      this.dragStrategy = new BorderDragStrategy(timeScale, pixelBounds, cb);
255    } else if (x < pixelBounds[0] || pixelBounds[1] < x) {
256      this.dragStrategy = new OuterDragStrategy(timeScale, cb);
257    } else {
258      this.dragStrategy = new InnerDragStrategy(timeScale, pixelBounds, cb);
259    }
260    this.dragStrategy.onDragStart(x);
261  }
262
263  onDragEnd() {
264    this.dragStrategy = undefined;
265  }
266
267  private extractBounds(timeScale: TimeScale): [number, number] {
268    const vizTime = this.trace.timeline.visibleWindow;
269    return [
270      Math.floor(timeScale.hpTimeToPx(vizTime.start)),
271      Math.ceil(timeScale.hpTimeToPx(vizTime.end)),
272    ];
273  }
274
275  private static inBorderRange(a: number, b: number): boolean {
276    return Math.abs(a - b) < this.HANDLE_SIZE_PX / 2;
277  }
278}
279
280// Print a timestamp in the configured time format
281function renderTimestamp(
282  ctx: CanvasRenderingContext2D,
283  time: time,
284  x: number,
285  y: number,
286  minWidth: number,
287): void {
288  const fmt = timestampFormat();
289  switch (fmt) {
290    case TimestampFormat.UTC:
291    case TimestampFormat.TraceTz:
292    case TimestampFormat.Timecode:
293      renderTimecode(ctx, time, x, y, minWidth);
294      break;
295    case TimestampFormat.TraceNs:
296      ctx.fillText(time.toString(), x, y, minWidth);
297      break;
298    case TimestampFormat.TraceNsLocale:
299      ctx.fillText(time.toLocaleString(), x, y, minWidth);
300      break;
301    case TimestampFormat.Seconds:
302      ctx.fillText(Time.formatSeconds(time), x, y, minWidth);
303      break;
304    case TimestampFormat.Milliseconds:
305      ctx.fillText(Time.formatMilliseconds(time), x, y, minWidth);
306      break;
307    case TimestampFormat.Microseconds:
308      ctx.fillText(Time.formatMicroseconds(time), x, y, minWidth);
309      break;
310    default:
311      assertUnreachable(fmt);
312  }
313}
314
315// Print a timecode over 2 lines with this formatting:
316// DdHH:MM:SS
317// mmm uuu nnn
318function renderTimecode(
319  ctx: CanvasRenderingContext2D,
320  time: time,
321  x: number,
322  y: number,
323  minWidth: number,
324): void {
325  const timecode = Time.toTimecode(time);
326  const {dhhmmss} = timecode;
327  ctx.fillText(dhhmmss, x, y, minWidth);
328}
329
330interface QuantizedLoad {
331  start: time;
332  end: time;
333  load: number;
334}
335
336// Kicks of a sequence of promises that load the overiew data in steps.
337// Each step schedules an animation frame.
338class OverviewDataLoader {
339  overviewData = new Map<string, QuantizedLoad[]>();
340
341  constructor(private trace: TraceImpl) {
342    this.beginLoad();
343  }
344
345  async beginLoad() {
346    const traceSpan = new TimeSpan(
347      this.trace.traceInfo.start,
348      this.trace.traceInfo.end,
349    );
350    const engine = this.trace.engine;
351    const stepSize = Duration.max(1n, traceSpan.duration / 100n);
352    const hasSchedSql = 'select ts from sched limit 1';
353    const hasSchedOverview = (await engine.query(hasSchedSql)).numRows() > 0;
354    if (hasSchedOverview) {
355      await this.loadSchedOverview(traceSpan, stepSize);
356    } else {
357      await this.loadSliceOverview(traceSpan, stepSize);
358    }
359  }
360
361  async loadSchedOverview(traceSpan: TimeSpan, stepSize: duration) {
362    const stepPromises = [];
363    for (
364      let start = traceSpan.start;
365      start < traceSpan.end;
366      start = Time.add(start, stepSize)
367    ) {
368      const progress = start - traceSpan.start;
369      const ratio = Number(progress) / Number(traceSpan.duration);
370      this.trace.omnibox.showStatusMessage(
371        'Loading overview ' + `${Math.round(ratio * 100)}%`,
372      );
373      const end = Time.add(start, stepSize);
374      // The (async() => {})() queues all the 100 async promises in one batch.
375      // Without that, we would wait for each step to be rendered before
376      // kicking off the next one. That would interleave an animation frame
377      // between each step, slowing down significantly the overall process.
378      stepPromises.push(
379        (async () => {
380          const schedResult = await this.trace.engine.query(
381            `select cast(sum(dur) as float)/${stepSize} as load, cpu from sched ` +
382              `where ts >= ${start} and ts < ${end} and utid != 0 ` +
383              'group by cpu order by cpu',
384          );
385          const schedData: {[key: string]: QuantizedLoad} = {};
386          const it = schedResult.iter({load: NUM, cpu: NUM});
387          for (; it.valid(); it.next()) {
388            const load = it.load;
389            const cpu = it.cpu;
390            schedData[cpu] = {start, end, load};
391          }
392          this.appendData(schedData);
393        })(),
394      );
395    } // for(start = ...)
396    await Promise.all(stepPromises);
397  }
398
399  async loadSliceOverview(traceSpan: TimeSpan, stepSize: duration) {
400    // Slices overview.
401    const sliceResult = await this.trace.engine.query(`select
402            bucket,
403            upid,
404            ifnull(sum(utid_sum) / cast(${stepSize} as float), 0) as load
405          from thread
406          inner join (
407            select
408              ifnull(cast((ts - ${traceSpan.start})/${stepSize} as int), 0) as bucket,
409              sum(dur) as utid_sum,
410              utid
411            from slice
412            inner join thread_track on slice.track_id = thread_track.id
413            group by bucket, utid
414          ) using(utid)
415          where upid is not null
416          group by bucket, upid`);
417
418    const slicesData: {[key: string]: QuantizedLoad[]} = {};
419    const it = sliceResult.iter({bucket: LONG, upid: NUM, load: NUM});
420    for (; it.valid(); it.next()) {
421      const bucket = it.bucket;
422      const upid = it.upid;
423      const load = it.load;
424
425      const start = Time.add(traceSpan.start, stepSize * bucket);
426      const end = Time.add(start, stepSize);
427
428      const upidStr = upid.toString();
429      let loadArray = slicesData[upidStr];
430      if (loadArray === undefined) {
431        loadArray = slicesData[upidStr] = [];
432      }
433      loadArray.push({start, end, load});
434    }
435    this.appendData(slicesData);
436  }
437
438  appendData(data: {[key: string]: QuantizedLoad | QuantizedLoad[]}) {
439    for (const [key, value] of Object.entries(data)) {
440      if (!this.overviewData.has(key)) {
441        this.overviewData.set(key, []);
442      }
443      if (value instanceof Array) {
444        this.overviewData.get(key)!.push(...value);
445      } else {
446        this.overviewData.get(key)!.push(value);
447      }
448    }
449    raf.scheduleCanvasRedraw();
450  }
451}
452