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