• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2021 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 {assertExists} from '../../base/logging';
16import {clamp, floatEqual} from '../../base/math_utils';
17import {Time, time} from '../../base/time';
18import {exists} from '../../base/utils';
19import {
20  drawIncompleteSlice,
21  drawTrackHoverTooltip,
22} from '../../base/canvas_utils';
23import {cropText} from '../../base/string_utils';
24import {colorCompare} from '../../base/color';
25import {UNEXPECTED_PINK} from '../colorizer';
26import {featureFlags} from '../../core/feature_flags';
27import {raf} from '../../core/raf_scheduler';
28import {TrackRenderer} from '../../public/track';
29import {Slice} from '../../public/track';
30import {LONG, NUM} from '../../trace_processor/query_result';
31import {checkerboardExcept} from '../checkerboard';
32import {BUCKETS_PER_PIXEL, CacheKey} from './timeline_cache';
33import {uuidv4Sql} from '../../base/uuid';
34import {AsyncDisposableStack} from '../../base/disposable_stack';
35import {TrackMouseEvent, TrackRenderContext} from '../../public/track';
36import {Point2D, VerticalBounds} from '../../base/geom';
37import {Trace} from '../../public/trace';
38
39// The common class that underpins all tracks drawing slices.
40
41export const SLICE_FLAGS_INCOMPLETE = 1;
42export const SLICE_FLAGS_INSTANT = 2;
43
44// Slices smaller than this don't get any text:
45const SLICE_MIN_WIDTH_FOR_TEXT_PX = 5;
46const SLICE_MIN_WIDTH_PX = 1 / BUCKETS_PER_PIXEL;
47const SLICE_MIN_WIDTH_FADED_PX = 0.1;
48
49const CHEVRON_WIDTH_PX = 10;
50const DEFAULT_SLICE_COLOR = UNEXPECTED_PINK;
51const INCOMPLETE_SLICE_WIDTH_PX = 20;
52
53export const CROP_INCOMPLETE_SLICE_FLAG = featureFlags.register({
54  id: 'cropIncompleteSlice',
55  name: 'Crop incomplete slices',
56  description: 'Display incomplete slices in short form',
57  defaultValue: false,
58});
59
60export const FADE_THIN_SLICES_FLAG = featureFlags.register({
61  id: 'fadeThinSlices',
62  name: 'Fade thin slices',
63  description: 'Display sub-pixel slices in a faded way',
64  defaultValue: false,
65});
66
67// Exposed and standalone to allow for testing without making this
68// visible to subclasses.
69function filterVisibleSlices<S extends Slice>(
70  slices: S[],
71  start: time,
72  end: time,
73): S[] {
74  // Here we aim to reduce the number of slices we have to draw
75  // by ignoring those that are not visible. A slice is visible iff:
76  //   slice.endNsQ >= start && slice.startNsQ <= end
77  // It's allowable to include slices which aren't visible but we
78  // must not exclude visible slices.
79  // We could filter this.slices using this condition but since most
80  // often we should have the case where there are:
81  // - First a bunch of non-visible slices to the left of the viewport
82  // - Then a bunch of visible slices within the viewport
83  // - Finally a second bunch of non-visible slices to the right of the
84  //   viewport.
85  // It seems more sensible to identify the left-most and right-most
86  // visible slices then 'slice' to select these slices and everything
87  // between.
88
89  // We do not need to handle non-ending slices (where dur = -1
90  // but the slice is drawn as 'infinite' length) as this is handled
91  // by a special code path. See 'incomplete' in maybeRequestData.
92
93  // While the slices are guaranteed to be ordered by timestamp we must
94  // consider async slices (which are not perfectly nested). This is to
95  // say if we see slice A then B it is guaranteed the A.start <= B.start
96  // but there is no guarantee that (A.end < B.start XOR A.end >= B.end).
97  // Due to this is not possible to use binary search to find the first
98  // visible slice. Consider the following situation:
99  //         start V            V end
100  //     AAA  CCC       DDD   EEEEEEE
101  //      BBBBBBBBBBBB            GGG
102  //                           FFFFFFF
103  // B is visible but A and C are not. In general there could be
104  // arbitrarily many slices between B and D which are not visible.
105
106  // You could binary search to find D (i.e. the first slice which
107  // starts after |start|) then work backwards to find B.
108  // The last visible slice is simpler, since the slices are sorted
109  // by timestamp you can binary search for the last slice such
110  // that slice.start <= end.
111
112  // One specific edge case that will come up often is when:
113  // For all slice in slices: slice.startNsQ > end (e.g. all slices are
114  // to the right).
115  // Since the slices are sorted by startS we can check this easily:
116  const maybeFirstSlice: S | undefined = slices[0];
117  if (exists(maybeFirstSlice) && maybeFirstSlice.startNs > end) {
118    return [];
119  }
120
121  return slices.filter((slice) => slice.startNs <= end && slice.endNs >= start);
122}
123
124export const filterVisibleSlicesForTesting = filterVisibleSlices;
125
126// The minimal set of columns that any table/view must expose to render tracks.
127// Note: this class assumes that, at the SQL level, slices are:
128// - Not temporally overlapping (unless they are nested at inner depth).
129// - Strictly stacked (i.e. a slice at depth N+1 cannot be larger than any
130//   slices at depth 0..N.
131// If you need temporally overlapping slices, look at AsyncSliceTrack, which
132// merges several tracks into one visual track.
133export const BASE_ROW = {
134  id: NUM, // The slice ID, for selection / lookups.
135  ts: LONG, // True ts in nanoseconds.
136  dur: LONG, // True duration in nanoseconds. -1 = incomplete, 0 = instant.
137  tsQ: LONG, // Quantized start time in nanoseconds.
138  durQ: LONG, // Quantized duration in nanoseconds.
139  depth: NUM, // Vertical depth.
140};
141
142export type BaseRow = typeof BASE_ROW;
143
144// These properties change @ 60FPS and shouldn't be touched by the subclass.
145// since the Impl doesn't see every frame attempting to reason on them in a
146// subclass will run in to issues.
147interface SliceInternal {
148  x: number;
149  w: number;
150}
151
152// We use this to avoid exposing subclasses to the properties that live on
153// SliceInternal. Within BaseSliceTrack the underlying storage and private
154// methods use CastInternal<S> (i.e. whatever the subclass requests
155// plus our implementation fields) but when we call 'virtual' methods that
156// the subclass should implement we use just S hiding x & w.
157type CastInternal<S extends Slice> = S & SliceInternal;
158
159export interface SliceLayout {
160  // Vertical spacing between slices and track.
161  readonly padding: number;
162
163  // Spacing between rows.
164  readonly rowGap: number;
165
166  // Height of each slice (i.e. height of each row).
167  readonly sliceHeight: number;
168
169  // Title font size.
170  readonly titleSizePx: number;
171
172  // Subtitle font size.
173  readonly subtitleSizePx: number;
174}
175
176export abstract class BaseSliceTrack<
177  SliceT extends Slice = Slice,
178  RowT extends BaseRow = BaseRow,
179> implements TrackRenderer
180{
181  protected readonly sliceLayout: SliceLayout;
182  protected trackUuid = uuidv4Sql();
183
184  // This is the over-skirted cached bounds:
185  private slicesKey: CacheKey = CacheKey.zero();
186
187  // This is the currently 'cached' slices:
188  private slices = new Array<CastInternal<SliceT>>();
189
190  // Incomplete slices (dur = -1). Rather than adding a lot of logic to
191  // the SQL queries to handle this case we materialise them one off
192  // then unconditionally render them. This should be efficient since
193  // there are at most |depth| slices.
194  private incomplete = new Array<CastInternal<SliceT>>();
195
196  // The currently selected slice.
197  // TODO(hjd): We should fetch this from the underlying data rather
198  // than just remembering it when we see it.
199  private selectedSlice?: CastInternal<SliceT>;
200
201  private extraSqlColumns: string[];
202
203  private charWidth = -1;
204  private hoverPos?: Point2D;
205  protected hoveredSlice?: SliceT;
206  private hoverTooltip: string[] = [];
207  private maxDataDepth = 0;
208
209  // Computed layout.
210  private computedTrackHeight = 0;
211
212  private readonly trash: AsyncDisposableStack;
213
214  // Extension points.
215  // Each extension point should take a dedicated argument type (e.g.,
216  // OnSliceOverArgs {slice?: S}) so it makes future extensions
217  // non-API-breaking (e.g. if we want to add the X position).
218
219  // onInit hook lets you do asynchronous set up e.g. creating a table
220  // etc. We guarantee that this will be resolved before doing any
221  // queries using the result of getSqlSource(). All persistent
222  // state in trace_processor should be cleaned up when dispose is
223  // called on the returned hook. In the common case of where
224  // the data for this track is a SQL fragment this does nothing.
225  async onInit(): Promise<AsyncDisposable | void> {}
226
227  // This should be an SQL expression returning all the columns listed
228  // mentioned by getRowSpec() excluding tsq and tsqEnd.
229  // For example you might return an SQL expression of the form:
230  // `select id, ts, dur, 0 as depth from foo where bar = 'baz'`
231  abstract getSqlSource(): string;
232
233  // This should return a fast sql select statement or table name with slightly
234  // relaxed constraints compared to getSqlSource(). It's the query used to join
235  // the results of the mipmap table in order to fetch the real `ts` and `dur`
236  // from the quantized slices.
237  //
238  // This query only needs to provide `ts`, `dur` and `id` columns (no depth
239  // required), and it doesn't even need to be filtered (because it's just used
240  // in the join), it just needs to be fast! This means that most of the time
241  // this can just be the root table of wherever this track comes from (e.g. the
242  // slice table).
243  //
244  // If in doubt, this can just return the same query as getSqlSource(), which
245  // is perfectly valid, however you may be leaving some performance on the
246  // table.
247  //
248  // TODO(stevegolton): If we merge BST with DST, this abstraction can be
249  // avoided.
250  abstract getJoinSqlSource(): string;
251
252  onSliceOver(_args: OnSliceOverArgs<SliceT>): void {}
253  onSliceOut(_args: OnSliceOutArgs<SliceT>): void {}
254
255  // By default, clicked slices create track selections
256  onSliceClick({slice}: OnSliceClickArgs<Slice>): void {
257    this.trace.selection.selectTrackEvent(this.uri, slice.id);
258  }
259
260  // The API contract of onUpdatedSlices() is:
261  //  - I am going to draw these slices in the near future.
262  //  - I am not going to draw any slice that I haven't passed here first.
263  //  - This is guaranteed to be called at least once on every global
264  //    state update.
265  //  - This is NOT guaranteed to be called on every frame. For instance you
266  //    cannot use this to do some colour-based animation.
267  onUpdatedSlices(slices: Array<SliceT>): void {
268    this.highlightHoveredAndSameTitle(slices);
269  }
270
271  // TODO(hjd): Remove.
272  drawSchedLatencyArrow(
273    _: CanvasRenderingContext2D,
274    _selectedSlice?: SliceT,
275  ): void {}
276
277  constructor(
278    protected readonly trace: Trace,
279    protected readonly uri: string,
280    protected readonly rowSpec: RowT,
281    sliceLayout: Partial<SliceLayout> = {},
282    protected readonly depthGuess: number = 0,
283    protected readonly instantWidthPx: number = CHEVRON_WIDTH_PX,
284  ) {
285    // Work out the extra columns.
286    // This is the union of the embedder-defined columns and the base columns
287    // we know about (ts, dur, ...).
288    const allCols = Object.keys(rowSpec);
289    const baseCols = Object.keys(BASE_ROW);
290    this.extraSqlColumns = allCols.filter((key) => !baseCols.includes(key));
291
292    this.trash = new AsyncDisposableStack();
293
294    this.sliceLayout = {
295      padding: sliceLayout.padding ?? 3,
296      rowGap: sliceLayout.rowGap ?? 0,
297      sliceHeight: sliceLayout.sliceHeight ?? 18,
298      titleSizePx: sliceLayout.titleSizePx ?? 12,
299      subtitleSizePx: sliceLayout.subtitleSizePx ?? 8,
300    };
301  }
302
303  onFullRedraw(): void {
304    // Give a chance to the embedder to change colors and other stuff.
305    this.onUpdatedSlices(this.slices);
306    this.onUpdatedSlices(this.incomplete);
307    if (this.selectedSlice !== undefined) {
308      this.onUpdatedSlices([this.selectedSlice]);
309    }
310  }
311
312  private getTitleFont(): string {
313    const size = this.sliceLayout.titleSizePx;
314    return `${size}px Roboto Condensed`;
315  }
316
317  private getSubtitleFont(): string {
318    const size = this.sliceLayout.subtitleSizePx;
319    return `${size}px Roboto Condensed`;
320  }
321
322  private getTableName(): string {
323    return `slice_${this.trackUuid}`;
324  }
325
326  async onCreate(): Promise<void> {
327    const result = await this.onInit();
328    result && this.trash.use(result);
329
330    // TODO(hjd): Consider case below:
331    // raw:
332    // 0123456789
333    //   [A     did not end)
334    //     [B ]
335    //
336    //
337    // quantised:
338    // 0123456789
339    //   [A     did not end)
340    // [     B  ]
341    // Does it lead to odd results?
342    const extraCols = this.extraSqlColumns.join(',');
343    let queryRes;
344    if (CROP_INCOMPLETE_SLICE_FLAG.get()) {
345      queryRes = await this.engine.query(`
346          select
347            depth,
348            ts as tsQ,
349            ts,
350            -1 as durQ,
351            -1 as dur,
352            id
353            ${extraCols ? ',' + extraCols : ''}
354          from (${this.getSqlSource()})
355          where dur = -1;
356        `);
357    } else {
358      queryRes = await this.engine.query(`
359        select
360          depth,
361          max(ts) as tsQ,
362          ts,
363          -1 as durQ,
364          -1 as dur,
365          id
366          ${extraCols ? ',' + extraCols : ''}
367        from (${this.getSqlSource()})
368        group by 1
369        having dur = -1
370      `);
371    }
372    const incomplete = new Array<CastInternal<SliceT>>(queryRes.numRows());
373    const it = queryRes.iter(this.rowSpec);
374    for (let i = 0; it.valid(); it.next(), ++i) {
375      incomplete[i] = this.rowToSliceInternal(it);
376    }
377    this.onUpdatedSlices(incomplete);
378    this.incomplete = incomplete;
379
380    await this.engine.query(`
381      create virtual table ${this.getTableName()}
382      using __intrinsic_slice_mipmap((
383        select id, ts, dur, depth
384        from (${this.getSqlSource()})
385        where dur != -1
386      ));
387    `);
388
389    this.trash.defer(async () => {
390      await this.engine.tryQuery(`drop table ${this.getTableName()}`);
391    });
392  }
393
394  async onUpdate({visibleWindow, size}: TrackRenderContext): Promise<void> {
395    const windowSizePx = Math.max(1, size.width);
396    const timespan = visibleWindow.toTimeSpan();
397    const rawSlicesKey = CacheKey.create(
398      timespan.start,
399      timespan.end,
400      windowSizePx,
401    );
402
403    // If the visible time range is outside the cached area, requests
404    // asynchronously new data from the SQL engine.
405    await this.maybeRequestData(rawSlicesKey);
406  }
407
408  render({ctx, size, visibleWindow, timescale}: TrackRenderContext): void {
409    // TODO(hjd): fonts and colors should come from the CSS and not hardcoded
410    // here.
411
412    // In any case, draw whatever we have (which might be stale/incomplete).
413    let charWidth = this.charWidth;
414    if (charWidth < 0) {
415      // TODO(hjd): Centralize font measurement/invalidation.
416      ctx.font = this.getTitleFont();
417      charWidth = this.charWidth = ctx.measureText('dbpqaouk').width / 8;
418    }
419
420    // Filter only the visible slices. |this.slices| will have more slices than
421    // needed because maybeRequestData() over-fetches to handle small pan/zooms.
422    // We don't want to waste time drawing slices that are off screen.
423    const vizSlices = this.getVisibleSlicesInternal(
424      visibleWindow.start.toTime('floor'),
425      visibleWindow.end.toTime('ceil'),
426    );
427
428    const selection = this.trace.selection.selection;
429    const selectedId =
430      selection.kind === 'track_event' && selection.trackUri === this.uri
431        ? selection.eventId
432        : undefined;
433
434    if (selectedId === undefined) {
435      this.selectedSlice = undefined;
436    }
437    let discoveredSelection: CastInternal<SliceT> | undefined;
438
439    // Believe it or not, doing 4xO(N) passes is ~2x faster than trying to draw
440    // everything in one go. The key is that state changes operations on the
441    // canvas (e.g., color, fonts) dominate any number crunching we do in JS.
442
443    const sliceHeight = this.sliceLayout.sliceHeight;
444    const padding = this.sliceLayout.padding;
445    const rowSpacing = this.sliceLayout.rowGap;
446
447    // First pass: compute geometry of slices.
448
449    // pxEnd is the last visible pixel in the visible viewport. Drawing
450    // anything < 0 or > pxEnd doesn't produce any visible effect as it goes
451    // beyond the visible portion of the canvas.
452    const pxEnd = size.width;
453
454    for (const slice of vizSlices) {
455      // Compute the basic geometry for any visible slice, even if only
456      // partially visible. This might end up with a negative x if the
457      // slice starts before the visible time or with a width that overflows
458      // pxEnd.
459      slice.x = timescale.timeToPx(slice.startNs);
460      slice.w = timescale.durationToPx(slice.durNs);
461
462      if (slice.flags & SLICE_FLAGS_INSTANT) {
463        // In the case of an instant slice, set the slice geometry on the
464        // bounding box that will contain the chevron.
465        slice.x -= this.instantWidthPx / 2;
466        slice.w = this.instantWidthPx;
467      } else if (slice.flags & SLICE_FLAGS_INCOMPLETE) {
468        let widthPx;
469        if (CROP_INCOMPLETE_SLICE_FLAG.get()) {
470          widthPx =
471            slice.x > 0
472              ? Math.min(pxEnd, INCOMPLETE_SLICE_WIDTH_PX)
473              : Math.max(0, INCOMPLETE_SLICE_WIDTH_PX + slice.x);
474          slice.x = Math.max(slice.x, 0);
475        } else {
476          slice.x = Math.max(slice.x, 0);
477          widthPx = pxEnd - slice.x;
478        }
479        slice.w = widthPx;
480      } else {
481        // If the slice is an actual slice, intersect the slice geometry with
482        // the visible viewport (this affects only the first and last slice).
483        // This is so that text is always centered even if we are zoomed in.
484        // Visually if we have
485        //                   [    visible viewport   ]
486        //  [         slice         ]
487        // The resulting geometry will be:
488        //                   [slice]
489        // So that the slice title stays within the visible region.
490        const sliceVizLimit = Math.min(slice.x + slice.w, pxEnd);
491        slice.x = Math.max(slice.x, 0);
492        slice.w = sliceVizLimit - slice.x;
493      }
494
495      if (selectedId === slice.id) {
496        discoveredSelection = slice;
497      }
498    }
499
500    // Second pass: fill slices by color.
501    const vizSlicesByColor = vizSlices.slice();
502    vizSlicesByColor.sort((a, b) =>
503      colorCompare(a.colorScheme.base, b.colorScheme.base),
504    );
505    let lastColor = undefined;
506    for (const slice of vizSlicesByColor) {
507      const color = slice.isHighlighted
508        ? slice.colorScheme.variant.cssString
509        : slice.colorScheme.base.cssString;
510      if (color !== lastColor) {
511        lastColor = color;
512        ctx.fillStyle = color;
513      }
514      const y = padding + slice.depth * (sliceHeight + rowSpacing);
515      if (slice.flags & SLICE_FLAGS_INSTANT) {
516        this.drawChevron(ctx, slice.x, y, sliceHeight);
517      } else if (slice.flags & SLICE_FLAGS_INCOMPLETE) {
518        const w = CROP_INCOMPLETE_SLICE_FLAG.get()
519          ? slice.w
520          : Math.max(slice.w - 2, 2);
521        drawIncompleteSlice(
522          ctx,
523          slice.x,
524          y,
525          w,
526          sliceHeight,
527          !CROP_INCOMPLETE_SLICE_FLAG.get(),
528        );
529      } else {
530        const w = Math.max(
531          slice.w,
532          FADE_THIN_SLICES_FLAG.get()
533            ? SLICE_MIN_WIDTH_FADED_PX
534            : SLICE_MIN_WIDTH_PX,
535        );
536        ctx.fillRect(slice.x, y, w, sliceHeight);
537      }
538    }
539
540    // Pass 2.5: Draw fillRatio light section.
541    ctx.fillStyle = `#FFFFFF50`;
542    for (const slice of vizSlicesByColor) {
543      // Can't draw fill ratio on incomplete or instant slices.
544      if (slice.flags & (SLICE_FLAGS_INCOMPLETE | SLICE_FLAGS_INSTANT)) {
545        continue;
546      }
547
548      // Clamp fillRatio between 0.0 -> 1.0
549      const fillRatio = clamp(slice.fillRatio, 0, 1);
550
551      // Don't draw anything if the fill ratio is 1.0ish
552      if (floatEqual(fillRatio, 1)) {
553        continue;
554      }
555
556      // Work out the width of the light section
557      const sliceDrawWidth = Math.max(slice.w, SLICE_MIN_WIDTH_PX);
558      const lightSectionDrawWidth = sliceDrawWidth * (1 - fillRatio);
559
560      // Don't draw anything if the light section is smaller than 1 px
561      if (lightSectionDrawWidth < 1) {
562        continue;
563      }
564
565      const y = padding + slice.depth * (sliceHeight + rowSpacing);
566      const x = slice.x + (sliceDrawWidth - lightSectionDrawWidth);
567      ctx.fillRect(x, y, lightSectionDrawWidth, sliceHeight);
568    }
569
570    // Third pass, draw the titles (e.g., process name for sched slices).
571    ctx.textAlign = 'center';
572    ctx.font = this.getTitleFont();
573    ctx.textBaseline = 'middle';
574    for (const slice of vizSlices) {
575      if (
576        slice.flags & SLICE_FLAGS_INSTANT ||
577        !slice.title ||
578        slice.w < SLICE_MIN_WIDTH_FOR_TEXT_PX
579      ) {
580        continue;
581      }
582
583      // Change the title color dynamically depending on contrast.
584      const textColor = slice.isHighlighted
585        ? slice.colorScheme.textVariant
586        : slice.colorScheme.textBase;
587      ctx.fillStyle = textColor.cssString;
588      const title = cropText(slice.title, charWidth, slice.w);
589      const rectXCenter = slice.x + slice.w / 2;
590      const y = padding + slice.depth * (sliceHeight + rowSpacing);
591      const yDiv = slice.subTitle ? 3 : 2;
592      const yMidPoint = Math.floor(y + sliceHeight / yDiv) + 0.5;
593      ctx.fillText(title, rectXCenter, yMidPoint);
594    }
595
596    // Fourth pass, draw the subtitles (e.g., thread name for sched slices).
597    ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
598    ctx.font = this.getSubtitleFont();
599    for (const slice of vizSlices) {
600      if (
601        slice.w < SLICE_MIN_WIDTH_FOR_TEXT_PX ||
602        !slice.subTitle ||
603        slice.flags & SLICE_FLAGS_INSTANT
604      ) {
605        continue;
606      }
607      const rectXCenter = slice.x + slice.w / 2;
608      const subTitle = cropText(slice.subTitle, charWidth, slice.w);
609      const y = padding + slice.depth * (sliceHeight + rowSpacing);
610      const yMidPoint = Math.ceil(y + (sliceHeight * 2) / 3) + 1.5;
611      ctx.fillText(subTitle, rectXCenter, yMidPoint);
612    }
613
614    // Here we need to ensure we never draw a slice that hasn't been
615    // updated via the math above so we don't use this.selectedSlice
616    // directly.
617    if (discoveredSelection !== undefined) {
618      this.selectedSlice = discoveredSelection;
619
620      // Draw a thicker border around the selected slice (or chevron).
621      const slice = discoveredSelection;
622      const color = slice.colorScheme;
623      const y = padding + slice.depth * (sliceHeight + rowSpacing);
624      ctx.strokeStyle = color.base.setHSL({s: 100, l: 10}).cssString;
625      ctx.beginPath();
626      const THICKNESS = 3;
627      ctx.lineWidth = THICKNESS;
628      ctx.strokeRect(
629        slice.x,
630        y - THICKNESS / 2,
631        slice.w,
632        sliceHeight + THICKNESS,
633      );
634      ctx.closePath();
635    }
636
637    // If the cached trace slices don't fully cover the visible time range,
638    // show a gray rectangle with a "Loading..." label.
639    checkerboardExcept(
640      ctx,
641      this.getHeight(),
642      0,
643      size.width,
644      timescale.timeToPx(this.slicesKey.start),
645      timescale.timeToPx(this.slicesKey.end),
646    );
647
648    // TODO(hjd): Remove this.
649    // The only thing this does is drawing the sched latency arrow. We should
650    // have some abstraction for that arrow (ideally the same we'd use for
651    // flows).
652    this.drawSchedLatencyArrow(ctx, this.selectedSlice);
653
654    // If a slice is hovered, draw the tooltip.
655    const tooltip = this.hoverTooltip;
656    if (
657      this.hoveredSlice !== undefined &&
658      tooltip.length > 0 &&
659      this.hoverPos !== undefined
660    ) {
661      if (tooltip.length === 1) {
662        drawTrackHoverTooltip(ctx, this.hoverPos, size, tooltip[0]);
663      } else {
664        drawTrackHoverTooltip(ctx, this.hoverPos, size, tooltip[0], tooltip[1]);
665      }
666    } // if (hoveredSlice)
667  }
668
669  async onDestroy(): Promise<void> {
670    await this.trash.asyncDispose();
671  }
672
673  // This method figures out if the visible window is outside the bounds of
674  // the cached data and if so issues new queries (i.e. sorta subsumes the
675  // onBoundsChange).
676  private async maybeRequestData(rawSlicesKey: CacheKey) {
677    if (rawSlicesKey.isCoveredBy(this.slicesKey)) {
678      return; // We have the data already, no need to re-query
679    }
680
681    // Determine the cache key:
682    const slicesKey = rawSlicesKey.normalize();
683    if (!rawSlicesKey.isCoveredBy(slicesKey)) {
684      throw new Error(
685        `Normalization error ${slicesKey.toString()} ${rawSlicesKey.toString()}`,
686      );
687    }
688
689    // Here convert each row to a Slice. We do what we can do
690    // generically in the base class, and delegate the rest to the impl
691    // via that rowToSlice() abstract call.
692    const slices = new Array<CastInternal<SliceT>>();
693
694    // The mipmap virtual table will error out when passed a 0 length time span.
695    const resolution = slicesKey.bucketSize;
696    const extraCols = this.extraSqlColumns.join(',');
697    const queryRes = await this.engine.query(`
698      SELECT
699        (z.ts / ${resolution}) * ${resolution} as tsQ,
700        ((z.dur + ${resolution - 1n}) / ${resolution}) * ${resolution} as durQ,
701        s.ts as ts,
702        s.dur as dur,
703        s.id,
704        z.depth
705        ${extraCols ? ',' + extraCols : ''}
706      FROM ${this.getTableName()}(
707        ${slicesKey.start},
708        ${slicesKey.end},
709        ${resolution}
710      ) z
711      CROSS JOIN (${this.getJoinSqlSource()}) s using (id)
712    `);
713
714    const it = queryRes.iter(this.rowSpec);
715
716    let maxDataDepth = this.maxDataDepth;
717    for (let i = 0; it.valid(); it.next(), ++i) {
718      if (it.dur === -1n) {
719        continue;
720      }
721
722      maxDataDepth = Math.max(maxDataDepth, it.depth);
723      // Construct the base slice. The Impl will construct and return
724      // the full derived T["slice"] (e.g. CpuSlice) in the
725      // rowToSlice() method.
726      slices.push(this.rowToSliceInternal(it));
727    }
728    for (const incomplete of this.incomplete) {
729      maxDataDepth = Math.max(maxDataDepth, incomplete.depth);
730    }
731    this.maxDataDepth = maxDataDepth;
732
733    this.slicesKey = slicesKey;
734    this.onUpdatedSlices(slices);
735    this.slices = slices;
736
737    raf.scheduleCanvasRedraw();
738  }
739
740  private rowToSliceInternal(row: RowT): CastInternal<SliceT> {
741    const slice = this.rowToSlice(row);
742
743    // If this is a more updated version of the selected slice throw
744    // away the old one.
745    if (this.selectedSlice?.id === slice.id) {
746      this.selectedSlice = undefined;
747    }
748
749    return {
750      ...slice,
751      x: -1,
752      w: -1,
753    };
754  }
755
756  protected abstract rowToSlice(row: RowT): SliceT;
757
758  protected rowToSliceBase(row: RowT): Slice {
759    let flags = 0;
760    if (row.dur === -1n) {
761      flags |= SLICE_FLAGS_INCOMPLETE;
762    } else if (row.dur === 0n) {
763      flags |= SLICE_FLAGS_INSTANT;
764    }
765
766    return {
767      id: row.id,
768      startNs: Time.fromRaw(row.tsQ),
769      endNs: Time.fromRaw(row.tsQ + row.durQ),
770      durNs: row.durQ,
771      ts: Time.fromRaw(row.ts),
772      dur: row.dur,
773      flags,
774      depth: row.depth,
775      title: '',
776      subTitle: '',
777      fillRatio: 1,
778
779      // The derived class doesn't need to initialize these. They are
780      // rewritten on every renderCanvas() call. We just need to initialize
781      // them to something.
782      colorScheme: DEFAULT_SLICE_COLOR,
783      isHighlighted: false,
784    };
785  }
786
787  private findSlice({x, y, timescale}: TrackMouseEvent): undefined | SliceT {
788    const trackHeight = this.computedTrackHeight;
789    const sliceHeight = this.sliceLayout.sliceHeight;
790    const padding = this.sliceLayout.padding;
791    const rowGap = this.sliceLayout.rowGap;
792
793    // Need at least a draw pass to resolve the slice layout.
794    if (sliceHeight === 0) {
795      return undefined;
796    }
797
798    const depth = Math.floor((y - padding) / (sliceHeight + rowGap));
799
800    if (y >= padding && y <= trackHeight - padding) {
801      for (const slice of this.slices) {
802        if (slice.depth === depth && slice.x <= x && x <= slice.x + slice.w) {
803          return slice;
804        }
805      }
806    }
807
808    for (const slice of this.incomplete) {
809      const startPx = CROP_INCOMPLETE_SLICE_FLAG.get()
810        ? timescale.timeToPx(slice.startNs)
811        : slice.x;
812      const cropUnfinishedSlicesCondition = CROP_INCOMPLETE_SLICE_FLAG.get()
813        ? startPx + INCOMPLETE_SLICE_WIDTH_PX >= x
814        : true;
815
816      if (
817        slice.depth === depth &&
818        startPx <= x &&
819        cropUnfinishedSlicesCondition
820      ) {
821        return slice;
822      }
823    }
824
825    return undefined;
826  }
827
828  onMouseMove(event: TrackMouseEvent): void {
829    const {x, y} = event;
830    this.hoverPos = {x, y};
831    this.updateHoveredSlice(this.findSlice(event));
832
833    // We need to do a full redraw in order to update the hovered slice properly
834    // due to the way this system is plumbed together right now, despite the
835    // fact that changing the hovered slice SHOULD only require a canvas redraw.
836    //
837    // TODO(stevegolton): Fix this.
838    this.trace.raf.scheduleFullRedraw();
839  }
840
841  onMouseOut(): void {
842    this.updateHoveredSlice(undefined);
843  }
844
845  private updateHoveredSlice(slice?: SliceT): void {
846    const lastHoveredSlice = this.hoveredSlice;
847    this.hoveredSlice = slice;
848
849    // Only notify the Impl if the hovered slice changes:
850    if (slice === lastHoveredSlice) return;
851
852    if (this.hoveredSlice === undefined) {
853      this.trace.timeline.highlightedSliceId = undefined;
854      this.onSliceOut({slice: assertExists(lastHoveredSlice)});
855      this.hoverTooltip = [];
856      this.hoverPos = undefined;
857    } else {
858      const args: OnSliceOverArgs<SliceT> = {slice: this.hoveredSlice};
859      this.trace.timeline.highlightedSliceId = this.hoveredSlice.id;
860      this.onSliceOver(args);
861      this.hoverTooltip = args.tooltip || [];
862    }
863  }
864
865  onMouseClick(event: TrackMouseEvent): boolean {
866    const slice = this.findSlice(event);
867    if (slice === undefined) {
868      return false;
869    }
870    const args: OnSliceClickArgs<SliceT> = {slice};
871    this.onSliceClick(args);
872    return true;
873  }
874
875  private getVisibleSlicesInternal(
876    start: time,
877    end: time,
878  ): Array<CastInternal<SliceT>> {
879    // Slice visibility is computed using tsq / endTsq. The means an
880    // event at ts=100n can end up with tsq=90n depending on the bucket
881    // calculation. start and end here are the direct unquantised
882    // boundaries so when start=100n we should see the event at tsq=90n
883    // Ideally we would quantize start and end via the same calculation
884    // we used for slices but since that calculation happens in SQL
885    // this is hard. Instead we increase the range by +1 bucket in each
886    // direction. It's fine to overestimate since false positives
887    // (incorrectly marking a slice as visible) are not a problem it's
888    // only false negatives we have to avoid.
889    start = Time.sub(start, this.slicesKey.bucketSize);
890    end = Time.add(end, this.slicesKey.bucketSize);
891
892    let slices = filterVisibleSlices<CastInternal<SliceT>>(
893      this.slices,
894      start,
895      end,
896    );
897    slices = slices.concat(this.incomplete);
898    // The selected slice is always visible:
899    if (this.selectedSlice && !this.slices.includes(this.selectedSlice)) {
900      slices.push(this.selectedSlice);
901    }
902    return slices;
903  }
904
905  private updateSliceAndTrackHeight() {
906    const rows = Math.max(this.maxDataDepth, this.depthGuess) + 1;
907    const {padding = 2, sliceHeight = 12, rowGap = 0} = this.sliceLayout;
908
909    // Compute the track height.
910    const trackHeight = 2 * padding + rows * (sliceHeight + rowGap);
911
912    // Compute the slice height.
913    this.computedTrackHeight = trackHeight;
914  }
915
916  protected drawChevron(
917    ctx: CanvasRenderingContext2D,
918    x: number,
919    y: number,
920    h: number,
921  ) {
922    // Draw an upward facing chevrons, in order: A, B, C, D, and back to A.
923    // . (x, y)
924    //      A
925    //     ###
926    //    ##C##
927    //   ##   ##
928    //  D       B
929    //            . (x + CHEVRON_WIDTH_PX, y + h)
930    const HALF_CHEVRON_WIDTH_PX = CHEVRON_WIDTH_PX / 2;
931    const midX = x + HALF_CHEVRON_WIDTH_PX;
932    ctx.beginPath();
933    ctx.moveTo(midX, y); // A.
934    ctx.lineTo(x + CHEVRON_WIDTH_PX, y + h); // B.
935    ctx.lineTo(midX, y + h - HALF_CHEVRON_WIDTH_PX); // C.
936    ctx.lineTo(x, y + h); // D.
937    ctx.lineTo(midX, y); // Back to A.
938    ctx.closePath();
939    ctx.fill();
940  }
941
942  // This is a good default implementation for highlighting slices. By default
943  // onUpdatedSlices() calls this. However, if the XxxSliceTrack impl overrides
944  // onUpdatedSlices() this gives them a chance to call the highlighting without
945  // having to reimplement it.
946  protected highlightHoveredAndSameTitle(slices: Slice[]) {
947    for (const slice of slices) {
948      const isHovering =
949        this.trace.timeline.highlightedSliceId === slice.id ||
950        (this.hoveredSlice && this.hoveredSlice.title === slice.title);
951      slice.isHighlighted = !!isHovering;
952    }
953  }
954
955  getHeight(): number {
956    this.updateSliceAndTrackHeight();
957    return this.computedTrackHeight;
958  }
959
960  getSliceVerticalBounds(depth: number): VerticalBounds | undefined {
961    this.updateSliceAndTrackHeight();
962
963    const totalSliceHeight =
964      this.sliceLayout.rowGap + this.sliceLayout.sliceHeight;
965    const top = this.sliceLayout.padding + depth * totalSliceHeight;
966
967    return {
968      top,
969      bottom: top + this.sliceLayout.sliceHeight,
970    };
971  }
972
973  protected get engine() {
974    return this.trace.engine;
975  }
976}
977
978// This is the argument passed to onSliceOver(args).
979// This is really a workaround for the fact that TypeScript doesn't allow
980// inner types within a class (whether the class is templated or not).
981export interface OnSliceOverArgs<S extends Slice> {
982  // Input args (BaseSliceTrack -> Impl):
983  slice: S; // The slice being hovered.
984
985  // Output args (Impl -> BaseSliceTrack):
986  tooltip?: string[]; // One entry per row, up to a max of 2.
987}
988
989export interface OnSliceOutArgs<S extends Slice> {
990  // Input args (BaseSliceTrack -> Impl):
991  slice: S; // The slice which is not hovered anymore.
992}
993
994export interface OnSliceClickArgs<S extends Slice> {
995  // Input args (BaseSliceTrack -> Impl):
996  slice: S; // The slice which is clicked.
997}
998