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