• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2023 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import m from 'mithril';
16import {ColorScheme} from '../../base/color_scheme';
17import {assertTrue} from '../../base/logging';
18import {Time} from '../../base/time';
19import {TrackEventDetailsPanel} from '../../public/details_panel';
20import {TrackEventDetails, TrackEventSelection} from '../../public/selection';
21import {Trace} from '../../public/trace';
22import {Slice} from '../../public/track';
23import {DatasetSchema, SourceDataset} from '../../trace_processor/dataset';
24import {ColumnType, LONG, NUM} from '../../trace_processor/query_result';
25import {getColorForSlice} from '../colorizer';
26import {generateSqlWithInternalLayout} from '../sql_utils/layout';
27import {formatDuration} from '../time_utils';
28import {
29  BASE_ROW,
30  BaseRow,
31  BaseSliceTrack,
32  OnSliceOverArgs,
33  SLICE_FLAGS_INCOMPLETE,
34  SLICE_FLAGS_INSTANT,
35  SliceLayout,
36} from './base_slice_track';
37import {Point2D, Size2D} from '../../base/geom';
38
39export interface InstantStyle {
40  /**
41   * Defines the width of an instant event. This, combined with the row height,
42   * defines the event's hitbox. This width is forwarded to the render function.
43   */
44  readonly width: number;
45
46  /**
47   * Customize how instant events are rendered.
48   *
49   * @param ctx - CanvasRenderingContext to draw to.
50   * @param rect - Position of the TL corner & size of the instant event's
51   * bounding box.
52   */
53  render(ctx: CanvasRenderingContext2D, rect: Size2D & Point2D): void;
54}
55
56export interface DatasetSliceTrackAttrs<T extends DatasetSchema> {
57  /**
58   * The trace object used by the track for accessing the query engine and other
59   * trace-related resources.
60   */
61  readonly trace: Trace;
62
63  /**
64   * The URI of this track, which must match the URI specified in the track
65   * descriptor.
66   *
67   * TODO(stevegolton): Sort out `Track` and `TrackRenderer` to avoid
68   * duplication.
69   */
70  readonly uri: string;
71
72  /**
73   * The source dataset defining the content of this track.
74   *
75   * A source dataset consists of a SQL select statement or table name with a
76   * column schema and optional filtering information. It represents a set of
77   * instructions to extract slice-like rows from trace processor that
78   * represents the content of this track, which avoids the need to materialize
79   * all slices into JavaScript beforehand. This approach minimizes memory usage
80   * and improves performance by only materializing the necessary rows on
81   * demand.
82   *
83   * Required columns:
84   * - `id` (NUM): Unique identifier for slices in the track.
85   * - `ts` (LONG): Timestamp of each event (in nanoseconds). Serves as the
86   *   start time for slices with a `dur` column or the instant time otherwise.
87   *
88   * Optional columns:
89   * - `dur` (LONG): Duration of each event (in nanoseconds). Without this
90   *   column, all slices are treated as instant events and rendered as
91   *   chevrons. With this column, each slice is rendered as a box where the
92   *   width corresponds to the duration of the slice.
93   * - `depth` (NUM): Depth of each event, used for vertical arrangement. Higher
94   *   depth values are rendered lower down on the track.
95   */
96  readonly dataset: SourceDataset<T>;
97
98  /**
99   * An optional initial estimate for the maximum depth value. Helps minimize
100   * flickering while scrolling by stabilizing the track height before all
101   * slices are loaded. Even without this value, the height of the track still
102   * adjusts dynamically as slices are loaded to accommodate the highest depth
103   * value.
104   */
105  readonly initialMaxDepth?: number;
106
107  /**
108   * An optional root table name for the track's data source.
109   *
110   * This typically represents a well-known table name and serves as the root
111   * `id` namespace for the track. It is primarily used for resolving events
112   * with a combination of table name and `id`.
113   *
114   * TODO(stevegolton): Consider moving this to dataset.
115   */
116  readonly rootTableName?: string;
117
118  /**
119   * Override the default geometry and layout of the slices rendered on the
120   * track.
121   */
122  readonly sliceLayout?: Partial<SliceLayout>;
123
124  /**
125   * Override the appearance of instant events.
126   */
127  readonly instantStyle?: InstantStyle;
128
129  /**
130   * This function can optionally be used to override the query that is
131   * generated for querying the slices rendered on the track. This is typically
132   * used to provide a non-standard depth value, but can be used as an escape
133   * hatch to completely override the query if required.
134   *
135   * The returned query must be in the form of a select statement or table name
136   * with the following columns:
137   * - id: NUM
138   * - ts: LONG
139   * - dur: LONG
140   * - depth: NUM
141   */
142  queryGenerator?(dataset: SourceDataset): string;
143
144  /**
145   * An optional function to override the color scheme for each event.
146   * If omitted, the default slice color scheme is used.
147   */
148  colorizer?(row: T): ColorScheme;
149
150  /**
151   * An optional function to override the text displayed on each event. If
152   * omitted, the value in the `name` column from the dataset is used, otherwise
153   * the slice is left blank.
154   */
155  sliceName?(row: T): string;
156
157  /**
158   * An optional function to override the tooltip content for each event. If
159   * omitted, the title & slice duration will be used.
160   */
161  tooltip?(row: T): string[];
162
163  /**
164   * An optional callback to customize the details panel for events on this
165   * track. Called whenever an event is selected.
166   */
167  detailsPanel?(row: T): TrackEventDetailsPanel;
168
169  /**
170   * An optional callback to define the fill ratio for slices. The fill ratio is
171   * an extra bit of information that can be rendered on each slice, where the
172   * slice essentially contains a single horizontal bar chart. The value
173   * returned can be a figure between 0.0 and 1.0 where 0 is empty and 1 is
174   * full. If omitted, all slices will be rendered with their fill ratios set to
175   * 'full'.
176   */
177  fillRatio?(row: T): number;
178
179  /**
180   * An optional function to define buttons which are displayed on the track
181   * shell. This function is called every Mithril render cycle.
182   */
183  shellButtons?(): m.Children;
184}
185
186const rowSchema = {
187  id: NUM,
188  ts: LONG,
189};
190
191export type ROW_SCHEMA = typeof rowSchema;
192
193// We attach a copy of our rows to each slice, so that the tooltip can be
194// resolved properly.
195type SliceWithRow<T> = Slice & {row: T};
196
197export class DatasetSliceTrack<T extends ROW_SCHEMA> extends BaseSliceTrack<
198  SliceWithRow<T>,
199  BaseRow & T
200> {
201  protected readonly sqlSource: string;
202  readonly rootTableName?: string;
203
204  constructor(private readonly attrs: DatasetSliceTrackAttrs<T>) {
205    super(
206      attrs.trace,
207      attrs.uri,
208      {...BASE_ROW, ...attrs.dataset.schema},
209      attrs.sliceLayout,
210      attrs.initialMaxDepth,
211      attrs.instantStyle?.width,
212    );
213    const {dataset, queryGenerator} = attrs;
214
215    // This is the minimum viable implementation that the source dataset must
216    // implement for the track to work properly. Typescript should enforce this
217    // now, but typescript can be worked around, and checking it is cheap.
218    // Better to error out early.
219    assertTrue(this.attrs.dataset.implements(rowSchema));
220
221    this.sqlSource =
222      queryGenerator?.(dataset) ?? this.generateRenderQuery(dataset);
223    this.rootTableName = attrs.rootTableName;
224  }
225
226  rowToSlice(row: BaseRow & T): SliceWithRow<T> {
227    const slice = this.rowToSliceBase(row);
228    const title = this.getTitle(row);
229    const color = this.getColor(row, title);
230
231    // Take a copy of the row, only copying the keys listed in the schema.
232    const cols = Object.keys(this.attrs.dataset.schema);
233    const clonedRow = Object.fromEntries(
234      Object.entries(row).filter(([key]) => cols.includes(key)),
235    ) as T;
236
237    return {
238      ...slice,
239      title,
240      colorScheme: color,
241      fillRatio: this.attrs.fillRatio?.(row) ?? slice.fillRatio,
242      row: clonedRow,
243    };
244  }
245
246  // Generate a query to use for generating slices to be rendered
247  private generateRenderQuery(dataset: SourceDataset<T>) {
248    if (dataset.implements({dur: LONG, depth: NUM})) {
249      // Both depth and dur provided, we can use the dataset as-is.
250      return dataset.query();
251    } else if (dataset.implements({depth: NUM})) {
252      // Depth provided but no dur, assume each event is an instant event by
253      // hard coding dur to 0.
254      return `select 0 as dur, * from (${dataset.query()})`;
255    } else if (dataset.implements({dur: LONG})) {
256      // Dur provided but no depth, automatically calculate the depth using
257      // internal_layout().
258      return generateSqlWithInternalLayout({
259        columns: ['*'],
260        source: dataset.query(),
261        ts: 'ts',
262        dur: 'dur',
263        orderByClause: 'ts',
264      });
265    } else {
266      // No depth nor dur provided, use 0 for both.
267      return `select 0 as dur, 0 as depth, * from (${dataset.query()})`;
268    }
269  }
270
271  private getTitle(row: T) {
272    if (this.attrs.sliceName) return this.attrs.sliceName(row);
273    if ('name' in row && typeof row.name === 'string') return row.name;
274    return undefined;
275  }
276
277  private getColor(row: T, title: string | undefined) {
278    if (this.attrs.colorizer) return this.attrs.colorizer(row);
279    if (title) return getColorForSlice(title);
280    return getColorForSlice(`${row.id}`);
281  }
282
283  override getSqlSource(): string {
284    return this.sqlSource;
285  }
286
287  override getJoinSqlSource(): string {
288    // This is a little performance optimization. Internally BST joins the
289    // results of the mipmap table query with the sqlSource in order to get the
290    // original ts, dur and id. However this sqlSource can sometimes be a
291    // contrived, slow query, usually to calculate the depth (e.g. something
292    // based on experimental_slice_layout).
293    //
294    // We don't actually need a depth value at this point, so calculating it is
295    // worthless. We only need ts, id, and dur. We don't even need this query to
296    // be correctly filtered, as we are merely joining on this table. We do
297    // however need it to be fast.
298    //
299    // In conclusion, if the dataset source has a dur column present (ts, and id
300    // are mandatory), then we can take a shortcut and just use this much
301    // simpler query to join on.
302    if (this.attrs.dataset.implements({dur: LONG})) {
303      return this.attrs.dataset.src;
304    } else {
305      return this.sqlSource;
306    }
307  }
308
309  getDataset() {
310    return this.attrs.dataset;
311  }
312
313  detailsPanel(sel: TrackEventSelection): TrackEventDetailsPanel | undefined {
314    if (this.attrs.detailsPanel) {
315      // This type assertion is required as a temporary patch while the
316      // specifics of selection details are being worked out. Eventually we will
317      // change the selection details to be purely based on dataset, but there
318      // are currently some use cases preventing us from doing so. For now, this
319      // type assertion is safe as we know we just returned the entire row from
320      // from getSelectionDetails() so we know it must at least implement the
321      // row's type `T`.
322      return this.attrs.detailsPanel(sel as unknown as T);
323    } else {
324      return undefined;
325    }
326  }
327
328  async getSelectionDetails(
329    id: number,
330  ): Promise<TrackEventDetails | undefined> {
331    const {trace, dataset} = this.attrs;
332    const result = await trace.engine.query(`
333      SELECT *
334      FROM (${dataset.query()})
335      WHERE id = ${id}
336    `);
337
338    const row = result.iter(dataset.schema);
339    if (!row.valid()) return undefined;
340
341    // Pull the fields out from the results
342    const data: {[key: string]: ColumnType} = {};
343    for (const col of result.columns()) {
344      data[col] = row.get(col);
345    }
346
347    return {
348      ...data,
349      ts: Time.fromRaw(row.ts),
350    };
351  }
352
353  override onUpdatedSlices(slices: Slice[]) {
354    for (const slice of slices) {
355      slice.isHighlighted = slice === this.hoveredSlice;
356    }
357  }
358
359  getTrackShellButtons() {
360    return this.attrs.shellButtons?.();
361  }
362
363  onSliceOver(args: OnSliceOverArgs<SliceWithRow<T>>) {
364    const {title, dur, flags} = args.slice;
365    let duration;
366    if (flags & SLICE_FLAGS_INCOMPLETE) {
367      duration = 'Incomplete';
368    } else if (flags & SLICE_FLAGS_INSTANT) {
369      duration = 'Instant';
370    } else {
371      duration = formatDuration(this.trace, dur);
372    }
373    if (title) {
374      args.tooltip = [`${title} - [${duration}]`];
375    } else {
376      args.tooltip = [`[${duration}]`];
377    }
378
379    args.tooltip = this.attrs.tooltip?.(args.slice.row) ?? args.tooltip;
380  }
381
382  // Override the drawChevron function.
383  protected override drawChevron(
384    ctx: CanvasRenderingContext2D,
385    x: number,
386    y: number,
387    h: number,
388  ) {
389    if (this.attrs.instantStyle?.render) {
390      this.attrs.instantStyle.render(ctx, {
391        x,
392        y,
393        height: h,
394        width: this.attrs.instantStyle.width,
395      });
396    } else {
397      super.drawChevron(ctx, x, y, h);
398    }
399  }
400}
401