• 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 {Actions} from '../common/actions';
17import {cropText, drawIncompleteSlice} from '../common/canvas_utils';
18import {
19  colorCompare,
20  colorToStr,
21  UNEXPECTED_PINK_COLOR,
22} from '../common/colorizer';
23import {NUM} from '../common/query_result';
24import {Selection, SelectionKind} from '../common/state';
25import {
26  fromNs,
27  tpDurationFromNanos,
28  TPTime,
29  tpTimeFromNanos,
30} from '../common/time';
31
32import {checkerboardExcept} from './checkerboard';
33import {globals} from './globals';
34import {Slice} from './slice';
35import {DEFAULT_SLICE_LAYOUT, SliceLayout} from './slice_layout';
36import {NewTrackArgs, SliceRect, Track} from './track';
37import {BUCKETS_PER_PIXEL, CacheKey, TrackCache} from './track_cache';
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 CHEVRON_WIDTH_PX = 10;
48const DEFAULT_SLICE_COLOR = UNEXPECTED_PINK_COLOR;
49
50// Exposed and standalone to allow for testing without making this
51// visible to subclasses.
52function filterVisibleSlices<S extends Slice>(
53    slices: S[], start: TPTime, end: TPTime): S[] {
54  // Here we aim to reduce the number of slices we have to draw
55  // by ignoring those that are not visible. A slice is visible iff:
56  //   slice.start + slice.duration >= start && slice.start <= end
57  // It's allowable to include slices which aren't visible but we
58  // must not exclude visible slices.
59  // We could filter this.slices using this condition but since most
60  // often we should have the case where there are:
61  // - First a bunch of non-visible slices to the left of the viewport
62  // - Then a bunch of visible slices within the viewport
63  // - Finally a second bunch of non-visible slices to the right of the
64  //   viewport.
65  // It seems more sensible to identify the left-most and right-most
66  // visible slices then 'slice' to select these slices and everything
67  // between.
68
69  // We do not need to handle non-ending slices (where dur = -1
70  // but the slice is drawn as 'infinite' length) as this is handled
71  // by a special code path.
72  // TODO(hjd): Implement special code path.
73
74  // While the slices are guaranteed to be ordered by timestamp we must
75  // consider async slices (which are not perfectly nested). This is to
76  // say if we see slice A then B it is guaranteed the A.start <= B.start
77  // but there is no guarantee that (A.end < B.start XOR A.end >= B.end).
78  // Due to this is not possible to use binary search to find the first
79  // visible slice. Consider the following situation:
80  //         start V            V end
81  //     AAA  CCC       DDD   EEEEEEE
82  //      BBBBBBBBBBBB            GGG
83  //                           FFFFFFF
84  // B is visible but A and C are not. In general there could be
85  // arbitrarily many slices between B and D which are not visible.
86
87  // You could binary search to find D (i.e. the first slice which
88  // starts after |start|) then work backwards to find B.
89  // The last visible slice is simpler, since the slices are sorted
90  // by timestamp you can binary search for the last slice such
91  // that slice.start <= end.
92
93  // One specific edge case that will come up often is when:
94  // For all slice in slices: slice.startS > endS (e.g. all slices are to the
95  // right). Since the slices are sorted by startS we can check this easily:
96  const maybeFirstSlice: S|undefined = slices[0];
97  if (maybeFirstSlice && maybeFirstSlice.start > end) {
98    return [];
99  }
100  // It's not possible to easily check the analogous edge case where all slices
101  // are to the left:
102  // For all slice in slices: slice.startS + slice.durationS < startS
103  // as the slices are not ordered by 'endS'.
104
105  // As described above you could do some clever binary search combined with
106  // iteration however that seems quite complicated and error prone so instead
107  // the idea of the code below is that we iterate forward though the
108  // array incrementing startIdx until we find the first visible slice
109  // then backwards through the array decrementing endIdx until we find the
110  // last visible slice. In the worst case we end up doing one full pass on
111  // the array. This code is robust to slices not being sorted.
112  let startIdx = 0;
113  let endIdx = slices.length;
114  for (; startIdx < endIdx; ++startIdx) {
115    const slice = slices[startIdx];
116    const sliceEndS = slice.start + slice.duration;
117    if (sliceEndS >= start && slice.start <= end) {
118      break;
119    }
120  }
121  for (; startIdx < endIdx; --endIdx) {
122    const slice = slices[endIdx - 1];
123    const sliceEndS = slice.start + slice.duration;
124    if (sliceEndS >= start && slice.start <= end) {
125      break;
126    }
127  }
128  return slices.slice(startIdx, endIdx);
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_SLICE_ROW = {
141  id: NUM,      // The slice ID, for selection / lookups.
142  tsq: NUM,     // Quantized |ts|. This class owns the quantization logic.
143  tsqEnd: NUM,  // Quantized |ts+dur|. The end bucket.
144  ts: NUM,      // Start time in nanoseconds.
145  dur: NUM,     // Duration in nanoseconds. -1 = incomplete, 0 = instant.
146  depth: NUM,   // Vertical depth.
147};
148
149export type BaseSliceRow = typeof BASE_SLICE_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: BaseSliceRow;
171  config: {};
172}
173
174export abstract class BaseSliceTrack<T extends BaseSliceTrackTypes =
175                                                   BaseSliceTrackTypes> extends
176    Track<T['config']> {
177  protected sliceLayout: SliceLayout = {...DEFAULT_SLICE_LAYOUT};
178
179  // This is the over-skirted cached bounds:
180  private slicesKey: CacheKey = CacheKey.zero();
181
182  // This is the currently 'cached' slices:
183  private slices = new Array<CastInternal<T['slice']>>();
184
185  // This is the slices cache:
186  private cache: TrackCache<Array<CastInternal<T['slice']>>> =
187      new TrackCache(5);
188
189  protected readonly tableName: string;
190  private maxDurNs = 0;
191  private sqlState: 'UNINITIALIZED'|'INITIALIZING'|'QUERY_PENDING'|
192      'QUERY_DONE' = 'UNINITIALIZED';
193  private extraSqlColumns: string[];
194
195  private charWidth = -1;
196  private hoverPos?: {x: number, y: number};
197  protected hoveredSlice?: T['slice'];
198  private hoverTooltip: string[] = [];
199  private maxDataDepth = 0;
200
201  // Computed layout.
202  private computedTrackHeight = 0;
203  private computedSliceHeight = 0;
204  private computedRowSpacing = 0;
205
206  // True if this track (and any views tables it might have created) has been
207  // destroyed. This is unfortunately error prone (since we must manually check
208  // this between each query).
209  // TODO(hjd): Replace once we have cancellable query sequences.
210  private isDestroyed = false;
211
212  // Extension points.
213  // Each extension point should take a dedicated argument type (e.g.,
214  // OnSliceOverArgs {slice?: T['slice']}) so it makes future extensions
215  // non-API-breaking (e.g. if we want to add the X position).
216  abstract initSqlTable(_tableName: string): Promise<void>;
217  getRowSpec(): T['row'] {
218    return BASE_SLICE_ROW;
219  }
220  onSliceOver(_args: OnSliceOverArgs<T['slice']>): void {}
221  onSliceOut(_args: OnSliceOutArgs<T['slice']>): void {}
222  onSliceClick(_args: OnSliceClickArgs<T['slice']>): void {}
223
224  // The API contract of onUpdatedSlices() is:
225  //  - I am going to draw these slices in the near future.
226  //  - I am not going to draw any slice that I haven't passed here first.
227  //  - This is guaranteed to be called at least once on every global
228  //    state update.
229  //  - This is NOT guaranteed to be called on every frame. For instance you
230  //    cannot use this to do some colour-based animation.
231  onUpdatedSlices(slices: Array<T['slice']>): void {
232    this.highlightHovererdAndSameTitle(slices);
233  }
234
235  // TODO(hjd): Remove.
236  drawSchedLatencyArrow(
237      _: CanvasRenderingContext2D, _selectedSlice?: T['slice']): void {}
238
239  constructor(args: NewTrackArgs) {
240    super(args);
241    this.frontendOnly = true;  // Disable auto checkerboarding.
242    // TODO(hjd): Handle pinned tracks, which current cause a crash
243    // since the tableName we generate is the same for both.
244    this.tableName = `track_${this.trackId}`.replace(/[^a-zA-Z0-9_]+/g, '_');
245
246    // Work out the extra columns.
247    // This is the union of the embedder-defined columns and the base columns
248    // we know about (ts, dur, ...).
249    const allCols = Object.keys(this.getRowSpec());
250    const baseCols = Object.keys(BASE_SLICE_ROW);
251    this.extraSqlColumns = allCols.filter((key) => !baseCols.includes(key));
252  }
253
254  setSliceLayout(sliceLayout: SliceLayout) {
255    if (sliceLayout.minDepth > sliceLayout.maxDepth) {
256      const {maxDepth, minDepth} = sliceLayout;
257      throw new Error(`minDepth ${minDepth} must be <= maxDepth ${maxDepth}`);
258    }
259    this.sliceLayout = sliceLayout;
260  }
261
262  onFullRedraw(): void {
263    // Give a chance to the embedder to change colors and other stuff.
264    this.onUpdatedSlices(this.slices);
265  }
266
267  protected isSelectionHandled(selection: Selection): boolean {
268    // TODO(hjd): Remove when updating selection.
269    // We shouldn't know here about CHROME_SLICE. Maybe should be set by
270    // whatever deals with that. Dunno the namespace of selection is weird. For
271    // most cases in non-ambiguous (because most things are a 'slice'). But some
272    // others (e.g. THREAD_SLICE) have their own ID namespace so we need this.
273    const supportedSelectionKinds: SelectionKind[] = ['SLICE', 'CHROME_SLICE'];
274    return supportedSelectionKinds.includes(selection.kind);
275  }
276
277  renderCanvas(ctx: CanvasRenderingContext2D): void {
278    // TODO(hjd): fonts and colors should come from the CSS and not hardcoded
279    // here.
280    const {
281      visibleTimeScale: timeScale,
282      visibleWindowTime: vizTime,
283    } = globals.frontendLocalState;
284
285    {
286      const windowSizePx = Math.max(1, timeScale.pxSpan.delta);
287      // TODO(stevegolton): Keep these guys as bigints
288      const rawStartNs = vizTime.start.nanos;
289      const rawEndNs = vizTime.end.nanos;
290      const rawSlicesKey = CacheKey.create(rawStartNs, rawEndNs, windowSizePx);
291
292      // If the visible time range is outside the cached area, requests
293      // asynchronously new data from the SQL engine.
294      this.maybeRequestData(rawSlicesKey);
295    }
296
297    // In any case, draw whatever we have (which might be stale/incomplete).
298
299    let charWidth = this.charWidth;
300    if (charWidth < 0) {
301      // TODO(hjd): Centralize font measurement/invalidation.
302      ctx.font = '12px Roboto Condensed';
303      charWidth = this.charWidth = ctx.measureText('dbpqaouk').width / 8;
304    }
305
306    // Filter only the visible slices. |this.slices| will have more slices than
307    // needed because maybeRequestData() over-fetches to handle small pan/zooms.
308    // We don't want to waste time drawing slices that are off screen.
309    const vizSlices = this.getVisibleSlicesInternal(
310        vizTime.start.toTPTime('floor'), vizTime.end.toTPTime('ceil'));
311
312    let selection = globals.state.currentSelection;
313
314    if (!selection || !this.isSelectionHandled(selection)) {
315      selection = null;
316    }
317
318    // Believe it or not, doing 4xO(N) passes is ~2x faster than trying to draw
319    // everything in one go. The key is that state changes operations on the
320    // canvas (e.g., color, fonts) dominate any number crunching we do in JS.
321
322    this.updateSliceAndTrackHeight();
323    const sliceHeight = this.computedSliceHeight;
324    const padding = this.sliceLayout.padding;
325    const rowSpacing = this.computedRowSpacing;
326
327    // First pass: compute geometry of slices.
328    let selSlice: CastInternal<T['slice']>|undefined;
329
330    // pxEnd is the last visible pixel in the visible viewport. Drawing
331    // anything < 0 or > pxEnd doesn't produce any visible effect as it goes
332    // beyond the visible portion of the canvas.
333    const pxEnd = Math.floor(timeScale.hpTimeToPx(vizTime.end));
334
335    for (const slice of vizSlices) {
336      // Compute the basic geometry for any visible slice, even if only
337      // partially visible. This might end up with a negative x if the
338      // slice starts before the visible time or with a width that overflows
339      // pxEnd.
340      slice.x = timeScale.tpTimeToPx(slice.start);
341      slice.w = timeScale.durationToPx(slice.duration);
342      if (slice.flags & SLICE_FLAGS_INSTANT) {
343        // In the case of an instant slice, set the slice geometry on the
344        // bounding box that will contain the chevron.
345        slice.x -= CHEVRON_WIDTH_PX / 2;
346        slice.w = CHEVRON_WIDTH_PX;
347      } else {
348        // If the slice is an actual slice, intersect the slice geometry with
349        // the visible viewport (this affects only the first and last slice).
350        // This is so that text is always centered even if we are zoomed in.
351        // Visually if we have
352        //                   [    visible viewport   ]
353        //  [         slice         ]
354        // The resulting geometry will be:
355        //                   [slice]
356        // So that the slice title stays within the visible region.
357        const sliceVizLimit = Math.min(slice.x + slice.w, pxEnd);
358        slice.x = Math.max(slice.x, 0);
359        slice.w = sliceVizLimit - slice.x;
360      }
361
362      if (selection && (selection as {id: number}).id === slice.id) {
363        selSlice = slice;
364      }
365    }
366
367    // Second pass: fill slices by color.
368    // The .slice() turned out to be an unintended pun.
369    const vizSlicesByColor = vizSlices.slice();
370    vizSlicesByColor.sort((a, b) => colorCompare(a.color, b.color));
371    let lastColor = undefined;
372    for (const slice of vizSlicesByColor) {
373      if (slice.color !== lastColor) {
374        lastColor = slice.color;
375        ctx.fillStyle = colorToStr(slice.color);
376      }
377      const y = padding + slice.depth * (sliceHeight + rowSpacing);
378      if (slice.flags & SLICE_FLAGS_INSTANT) {
379        this.drawChevron(ctx, slice.x, y, sliceHeight);
380      } else if (slice.flags & SLICE_FLAGS_INCOMPLETE) {
381        const w = Math.max(slice.w - 2, 2);
382        drawIncompleteSlice(ctx, slice.x, y, w, sliceHeight);
383      } else {
384        const w = Math.max(slice.w, SLICE_MIN_WIDTH_PX);
385        ctx.fillRect(slice.x, y, w, sliceHeight);
386      }
387    }
388
389    // Third pass, draw the titles (e.g., process name for sched slices).
390    ctx.fillStyle = '#fff';
391    ctx.textAlign = 'center';
392    ctx.font = '12px Roboto Condensed';
393    ctx.textBaseline = 'middle';
394    for (const slice of vizSlices) {
395      if ((slice.flags & SLICE_FLAGS_INSTANT) || !slice.title ||
396          slice.w < SLICE_MIN_WIDTH_FOR_TEXT_PX) {
397        continue;
398      }
399
400      const title = cropText(slice.title, charWidth, slice.w);
401      const rectXCenter = slice.x + slice.w / 2;
402      const y = padding + slice.depth * (sliceHeight + rowSpacing);
403      const yDiv = slice.subTitle ? 3 : 2;
404      const yMidPoint = Math.floor(y + sliceHeight / yDiv) - 0.5;
405      ctx.fillText(title, rectXCenter, yMidPoint);
406    }
407
408    // Fourth pass, draw the subtitles (e.g., thread name for sched slices).
409    ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
410    ctx.font = '10px Roboto Condensed';
411    for (const slice of vizSlices) {
412      if (slice.w < SLICE_MIN_WIDTH_FOR_TEXT_PX || !slice.subTitle ||
413          (slice.flags & SLICE_FLAGS_INSTANT)) {
414        continue;
415      }
416      const rectXCenter = slice.x + slice.w / 2;
417      const subTitle = cropText(slice.subTitle, charWidth, slice.w);
418      const y = padding + slice.depth * (sliceHeight + rowSpacing);
419      const yMidPoint = Math.ceil(y + sliceHeight * 2 / 3) + 1.5;
420      ctx.fillText(subTitle, rectXCenter, yMidPoint);
421    }
422
423    // Draw a thicker border around the selected slice (or chevron).
424    if (selSlice !== undefined) {
425      const color = selSlice.color;
426      const y = padding + selSlice.depth * (sliceHeight + rowSpacing);
427      ctx.strokeStyle = `hsl(${color.h}, ${color.s}%, 30%)`;
428      ctx.beginPath();
429      const THICKNESS = 3;
430      ctx.lineWidth = THICKNESS;
431      ctx.strokeRect(
432          selSlice.x, y - THICKNESS / 2, selSlice.w, sliceHeight + THICKNESS);
433      ctx.closePath();
434    }
435
436    // If the cached trace slices don't fully cover the visible time range,
437    // show a gray rectangle with a "Loading..." label.
438    checkerboardExcept(
439        ctx,
440        this.getHeight(),
441        timeScale.hpTimeToPx(vizTime.start),
442        timeScale.hpTimeToPx(vizTime.end),
443        timeScale.secondsToPx(fromNs(this.slicesKey.startNs)),
444        timeScale.secondsToPx(fromNs(this.slicesKey.endNs)));
445
446    // TODO(hjd): Remove this.
447    // The only thing this does is drawing the sched latency arrow. We should
448    // have some abstraction for that arrow (ideally the same we'd use for
449    // flows).
450    this.drawSchedLatencyArrow(ctx, selSlice);
451
452    // If a slice is hovered, draw the tooltip.
453    const tooltip = this.hoverTooltip;
454    if (this.hoveredSlice !== undefined && tooltip.length > 0 &&
455        this.hoverPos !== undefined) {
456      if (tooltip.length === 1) {
457        this.drawTrackHoverTooltip(ctx, this.hoverPos, tooltip[0]);
458      } else {
459        this.drawTrackHoverTooltip(ctx, this.hoverPos, tooltip[0], tooltip[1]);
460      }
461    }  // if (hoveredSlice)
462  }
463
464  onDestroy() {
465    super.onDestroy();
466    this.isDestroyed = true;
467    this.engine.query(`DROP VIEW IF EXISTS ${this.tableName}`);
468  }
469
470  // This method figures out if the visible window is outside the bounds of
471  // the cached data and if so issues new queries (i.e. sorta subsumes the
472  // onBoundsChange).
473  private async maybeRequestData(rawSlicesKey: CacheKey) {
474    // Important: this method is async and is invoked on every frame. Care
475    // must be taken to avoid piling up queries on every frame, hence the FSM.
476    if (this.sqlState === 'UNINITIALIZED') {
477      this.sqlState = 'INITIALIZING';
478
479      if (this.isDestroyed) {
480        return;
481      }
482      await this.initSqlTable(this.tableName);
483
484      if (this.isDestroyed) {
485        return;
486      }
487      const queryRes = await this.engine.query(`select
488          ifnull(max(dur), 0) as maxDur, count(1) as rowCount
489          from ${this.tableName}`);
490      const row = queryRes.firstRow({maxDur: NUM, rowCount: NUM});
491      this.maxDurNs = row.maxDur;
492      this.sqlState = 'QUERY_DONE';
493    } else if (
494        this.sqlState === 'INITIALIZING' || this.sqlState === 'QUERY_PENDING') {
495      return;
496    }
497
498    if (rawSlicesKey.isCoveredBy(this.slicesKey)) {
499      return;  // We have the data already, no need to re-query
500    }
501
502    // Determine the cache key:
503    const slicesKey = rawSlicesKey.normalize();
504    if (!rawSlicesKey.isCoveredBy(slicesKey)) {
505      throw new Error(`Normalization error ${slicesKey.toString()} ${
506          rawSlicesKey.toString()}`);
507    }
508
509    const maybeCachedSlices = this.cache.lookup(slicesKey);
510    if (maybeCachedSlices) {
511      this.slicesKey = slicesKey;
512      this.onUpdatedSlices(maybeCachedSlices);
513      this.slices = maybeCachedSlices;
514      return;
515    }
516
517    this.sqlState = 'QUERY_PENDING';
518    const bucketNs = slicesKey.bucketNs;
519    let queryTsq;
520    let queryTsqEnd;
521    // When we're zoomed into the level of single ns there is no point
522    // doing quantization (indeed it causes bad artifacts) so instead
523    // we use ts / ts+dur directly.
524    if (bucketNs === 1) {
525      queryTsq = 'ts';
526      queryTsqEnd = 'ts + dur';
527    } else {
528      queryTsq = `(ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs}`;
529      queryTsqEnd = `(ts + dur + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs}`;
530    }
531
532    const extraCols = this.extraSqlColumns.join(',');
533    let depthCol = 'depth';
534    let maybeGroupByDepth = 'depth, ';
535    const layout = this.sliceLayout;
536    const isFlat = (layout.maxDepth - layout.minDepth) <= 1;
537    // maxDepth === minDepth only makes sense if track is empty which on the
538    // one hand isn't very useful (and so maybe should be an error) on the
539    // other hand I can see it happening if someone does:
540    // minDepth = min(slices.depth); maxDepth = max(slices.depth);
541    // and slices is empty, so we treat that as flat.
542    if (isFlat) {
543      depthCol = `${this.sliceLayout.minDepth} as depth`;
544      maybeGroupByDepth = '';
545    }
546
547    // TODO(hjd): Re-reason and improve this query:
548    // - Materialize the unfinished slices one off.
549    // - Avoid the union if we know we don't have any -1 slices.
550    // - Maybe we don't need the union at all and can deal in TS?
551    if (this.isDestroyed) {
552      this.sqlState = 'QUERY_DONE';
553      return;
554    }
555    // TODO(hjd): Count and expose the number of slices summarized in
556    // each bucket?
557    const queryRes = await this.engine.query(`
558    with q1 as (
559      select
560        ${queryTsq} as tsq,
561        ${queryTsqEnd} as tsqEnd,
562        ts,
563        max(dur) as dur,
564        id,
565        ${depthCol}
566        ${extraCols ? ',' + extraCols : ''}
567      from ${this.tableName}
568      where
569        ts >= ${slicesKey.startNs - this.maxDurNs /* - durNs */} and
570        ts <= ${slicesKey.endNs /* + durNs */}
571      group by ${maybeGroupByDepth} tsq
572      order by tsq),
573    q2 as (
574      select
575        ${queryTsq} as tsq,
576        ${queryTsqEnd} as tsqEnd,
577        ts,
578        -1 as dur,
579        id,
580        ${depthCol}
581        ${extraCols ? ',' + extraCols : ''}
582      from ${this.tableName}
583      where dur = -1
584      group by ${maybeGroupByDepth} tsq
585      )
586      select min(dur) as _unused, * from
587      (select * from q1 union all select * from q2)
588      group by ${maybeGroupByDepth} tsq
589      order by tsq
590    `);
591
592    // Here convert each row to a Slice. We do what we can do
593    // generically in the base class, and delegate the rest to the impl
594    // via that rowToSlice() abstract call.
595    const slices = new Array<CastInternal<T['slice']>>(queryRes.numRows());
596    const it = queryRes.iter(this.getRowSpec());
597
598    let maxDataDepth = this.maxDataDepth;
599    this.slicesKey = slicesKey;
600    for (let i = 0; it.valid(); it.next(), ++i) {
601      maxDataDepth = Math.max(maxDataDepth, it.depth);
602      // Construct the base slice. The Impl will construct and return
603      // the full derived T["slice"] (e.g. CpuSlice) in the
604      // rowToSlice() method.
605      slices[i] = this.rowToSliceInternal(it);
606    }
607    this.maxDataDepth = maxDataDepth;
608    this.onUpdatedSlices(slices);
609    this.cache.insert(slicesKey, slices);
610    this.slices = slices;
611
612    this.sqlState = 'QUERY_DONE';
613    globals.rafScheduler.scheduleRedraw();
614  }
615
616  private rowToSliceInternal(row: T['row']): CastInternal<T['slice']> {
617    const slice = this.rowToSlice(row) as CastInternal<T['slice']>;
618    slice.x = -1;
619    slice.w = -1;
620    return slice;
621  }
622
623  rowToSlice(row: T['row']): T['slice'] {
624    const startNsQ = row.tsq;
625    const endNsQ = row.tsqEnd;
626    let flags = 0;
627    if (row.dur === -1) {
628      flags |= SLICE_FLAGS_INCOMPLETE;
629    } else if (row.dur === 0) {
630      flags |= SLICE_FLAGS_INSTANT;
631    }
632
633    return {
634      id: row.id,
635      start: tpTimeFromNanos(startNsQ),
636      duration: tpDurationFromNanos(endNsQ - startNsQ),
637      flags,
638      depth: row.depth,
639      title: '',
640      subTitle: '',
641
642      // The derived class doesn't need to initialize these. They are
643      // rewritten on every renderCanvas() call. We just need to initialize
644      // them to something.
645      baseColor: DEFAULT_SLICE_COLOR,
646      color: DEFAULT_SLICE_COLOR,
647    };
648  }
649
650  private findSlice({x, y}: {x: number, y: number}): undefined|Slice {
651    const trackHeight = this.computedTrackHeight;
652    const sliceHeight = this.computedSliceHeight;
653    const padding = this.sliceLayout.padding;
654    const rowSpacing = this.computedRowSpacing;
655
656    // Need at least a draw pass to resolve the slice layout.
657    if (sliceHeight === 0) {
658      return undefined;
659    }
660
661    if (y >= padding && y <= trackHeight - padding) {
662      const depth = Math.floor((y - padding) / (sliceHeight + rowSpacing));
663      for (const slice of this.slices) {
664        if (slice.depth === depth && slice.x <= x && x <= slice.x + slice.w) {
665          return slice;
666        }
667      }
668    }
669
670    return undefined;
671  }
672
673  onMouseMove(position: {x: number, y: number}): void {
674    this.hoverPos = position;
675    this.updateHoveredSlice(this.findSlice(position));
676  }
677
678  onMouseOut(): void {
679    this.updateHoveredSlice(undefined);
680  }
681
682  private updateHoveredSlice(slice?: T['slice']): void {
683    const lastHoveredSlice = this.hoveredSlice;
684    this.hoveredSlice = slice;
685
686    // Only notify the Impl if the hovered slice changes:
687    if (slice === lastHoveredSlice) return;
688
689    if (this.hoveredSlice === undefined) {
690      globals.dispatch(Actions.setHighlightedSliceId({sliceId: -1}));
691      this.onSliceOut({slice: assertExists(lastHoveredSlice)});
692      this.hoverTooltip = [];
693      this.hoverPos = undefined;
694    } else {
695      const args: OnSliceOverArgs<T['slice']> = {slice: this.hoveredSlice};
696      globals.dispatch(
697          Actions.setHighlightedSliceId({sliceId: this.hoveredSlice.id}));
698      this.onSliceOver(args);
699      this.hoverTooltip = args.tooltip || [];
700    }
701  }
702
703  onMouseClick(position: {x: number, y: number}): boolean {
704    const slice = this.findSlice(position);
705    if (slice === undefined) {
706      return false;
707    }
708    const args: OnSliceClickArgs<T['slice']> = {slice};
709    this.onSliceClick(args);
710    return true;
711  }
712
713  private getVisibleSlicesInternal(start: TPTime, end: TPTime):
714      Array<CastInternal<T['slice']>> {
715    return filterVisibleSlices<CastInternal<T['slice']>>(
716        this.slices, start, end);
717  }
718
719  private updateSliceAndTrackHeight() {
720    const lay = this.sliceLayout;
721
722    const rows =
723        Math.min(Math.max(this.maxDataDepth + 1, lay.minDepth), lay.maxDepth);
724
725    // Compute the track height.
726    let trackHeight;
727    if (lay.heightMode === 'FIXED') {
728      trackHeight = lay.fixedHeight;
729    } else {
730      trackHeight = 2 * lay.padding + rows * (lay.sliceHeight + lay.rowSpacing);
731    }
732
733    // Compute the slice height.
734    let sliceHeight: number;
735    let rowSpacing: number = lay.rowSpacing;
736    if (lay.heightMode === 'FIXED') {
737      const rowHeight = (trackHeight - 2 * lay.padding) / rows;
738      sliceHeight = Math.floor(Math.max(rowHeight - lay.rowSpacing, 0.5));
739      rowSpacing = Math.max(lay.rowSpacing, rowHeight - sliceHeight);
740      rowSpacing = Math.floor(rowSpacing * 2) / 2;
741    } else {
742      sliceHeight = lay.sliceHeight;
743    }
744    this.computedSliceHeight = sliceHeight;
745    this.computedTrackHeight = trackHeight;
746    this.computedRowSpacing = rowSpacing;
747  }
748
749  private drawChevron(
750      ctx: CanvasRenderingContext2D, x: number, y: number, h: number) {
751    // Draw an upward facing chevrons, in order: A, B, C, D, and back to A.
752    // . (x, y)
753    //      A
754    //     ###
755    //    ##C##
756    //   ##   ##
757    //  D       B
758    //            . (x + CHEVRON_WIDTH_PX, y + h)
759    const HALF_CHEVRON_WIDTH_PX = CHEVRON_WIDTH_PX / 2;
760    const midX = x + HALF_CHEVRON_WIDTH_PX;
761    ctx.beginPath();
762    ctx.moveTo(midX, y);                              // A.
763    ctx.lineTo(x + CHEVRON_WIDTH_PX, y + h);          // B.
764    ctx.lineTo(midX, y + h - HALF_CHEVRON_WIDTH_PX);  // C.
765    ctx.lineTo(x, y + h);                             // D.
766    ctx.lineTo(midX, y);                              // Back to A.
767    ctx.closePath();
768    ctx.fill();
769  }
770
771  // This is a good default implementation for highlighting slices. By default
772  // onUpdatedSlices() calls this. However, if the XxxSliceTrack impl overrides
773  // onUpdatedSlices() this gives them a chance to call the highlighting without
774  // having to reimplement it.
775  protected highlightHovererdAndSameTitle(slices: Slice[]) {
776    for (const slice of slices) {
777      const isHovering = globals.state.highlightedSliceId === slice.id ||
778          (this.hoveredSlice && this.hoveredSlice.title === slice.title);
779      if (isHovering) {
780        slice.color = {
781          c: slice.baseColor.c,
782          h: slice.baseColor.h,
783          s: slice.baseColor.s,
784          l: 30,
785        };
786      } else {
787        slice.color = slice.baseColor;
788      }
789    }
790  }
791
792  getHeight(): number {
793    this.updateSliceAndTrackHeight();
794    return this.computedTrackHeight;
795  }
796
797  getSliceRect(_tStart: number, _tEnd: number, _depth: number): SliceRect
798      |undefined {
799    // TODO(hjd): Implement this as part of updating flow events.
800    return undefined;
801  }
802}
803
804// This is the argument passed to onSliceOver(args).
805// This is really a workaround for the fact that TypeScript doesn't allow
806// inner types within a class (whether the class is templated or not).
807export interface OnSliceOverArgs<S extends Slice> {
808  // Input args (BaseSliceTrack -> Impl):
809  slice: S;  // The slice being hovered.
810
811  // Output args (Impl -> BaseSliceTrack):
812  tooltip?: string[];  // One entry per row, up to a max of 2.
813}
814
815export interface OnSliceOutArgs<S extends Slice> {
816  // Input args (BaseSliceTrack -> Impl):
817  slice: S;  // The slice which is not hovered anymore.
818}
819
820export interface OnSliceClickArgs<S extends Slice> {
821  // Input args (BaseSliceTrack -> Impl):
822  slice: S;  // The slice which is clicked.
823}
824