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