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