| // Copyright (C) 2023 The Android Open Source Project | 
 | // | 
 | // Licensed under the Apache License, Version 2.0 (the "License"); | 
 | // you may not use this file except in compliance with the License. | 
 | // You may obtain a copy of the License at | 
 | // | 
 | //      http://www.apache.org/licenses/LICENSE-2.0 | 
 | // | 
 | // Unless required by applicable law or agreed to in writing, software | 
 | // distributed under the License is distributed on an "AS IS" BASIS, | 
 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
 | // See the License for the specific language governing permissions and | 
 | // limitations under the License. | 
 |  | 
 | import {duration, Span, Time, time} from '../base/time'; | 
 | import {Actions} from '../common/actions'; | 
 | import {cropText, drawIncompleteSlice} from '../common/canvas_utils'; | 
 | import {getColorForSlice} from '../common/colorizer'; | 
 | import {HighPrecisionTime} from '../common/high_precision_time'; | 
 | import {TrackData} from '../common/track_data'; | 
 | import {TrackHelperLEGACY} from '../common/track_helper'; | 
 | import {SliceRect} from '../public'; | 
 |  | 
 | import {CROP_INCOMPLETE_SLICE_FLAG} from './base_slice_track'; | 
 | import {checkerboardExcept} from './checkerboard'; | 
 | import {globals} from './globals'; | 
 | import {PxSpan, TimeScale} from './time_scale'; | 
 |  | 
 | export const SLICE_TRACK_KIND = 'ChromeSliceTrack'; | 
 | const SLICE_HEIGHT = 18; | 
 | const TRACK_PADDING = 2; | 
 | const CHEVRON_WIDTH_PX = 10; | 
 | const HALF_CHEVRON_WIDTH_PX = CHEVRON_WIDTH_PX / 2; | 
 | const INCOMPLETE_SLICE_WIDTH_PX = 20; | 
 |  | 
 | export interface SliceData extends TrackData { | 
 |   // Slices are stored in a columnar fashion. | 
 |   strings: string[]; | 
 |   sliceIds: Float64Array; | 
 |   starts: BigInt64Array; | 
 |   ends: BigInt64Array; | 
 |   depths: Uint16Array; | 
 |   titles: Uint16Array;   // Index into strings. | 
 |   colors?: Uint16Array;  // Index into strings. | 
 |   isInstant: Uint16Array; | 
 |   isIncomplete: Uint16Array; | 
 |   cpuTimeRatio?: Float64Array; | 
 | } | 
 |  | 
 | // Track base class which handles rendering slices in a generic way. | 
 | // This is the old way of rendering slices - i.e. "track v1" format  - and | 
 | // exists as a patch to allow old tracks to be converted to controller-less | 
 | // tracks before they are ported to v2. | 
 | // Slice tracks should extend this class and implement the abstract methods, | 
 | // notably onBoundsChange(). | 
 | // Note: This class is deprecated and should not be used for new tracks. Use | 
 | // |BaseSliceTrack| instead. | 
 | export abstract class SliceTrackLEGACY extends TrackHelperLEGACY<SliceData> { | 
 |   constructor( | 
 |       private maxDepth: number, protected trackKey: string, | 
 |       private tableName: string, private namespace?: string) { | 
 |     super(); | 
 |   } | 
 |  | 
 |   protected namespaceTable(tableName: string = this.tableName): string { | 
 |     if (this.namespace) { | 
 |       return this.namespace + '_' + tableName; | 
 |     } else { | 
 |       return tableName; | 
 |     } | 
 |   } | 
 |  | 
 |   private hoveredTitleId = -1; | 
 |  | 
 |   // Font used to render the slice name on the current track. | 
 |   protected getFont() { | 
 |     return '12px Roboto Condensed'; | 
 |   } | 
 |  | 
 |   renderCanvas(ctx: CanvasRenderingContext2D): void { | 
 |     // TODO: fonts and colors should come from the CSS and not hardcoded here. | 
 |     const data = this.data; | 
 |     if (data === undefined) return;  // Can't possibly draw anything. | 
 |  | 
 |     const {visibleTimeSpan, visibleWindowTime, visibleTimeScale, windowSpan} = | 
 |         globals.frontendLocalState; | 
 |  | 
 |     // If the cached trace slices don't fully cover the visible time range, | 
 |     // show a gray rectangle with a "Loading..." label. | 
 |     checkerboardExcept( | 
 |         ctx, | 
 |         this.getHeight(), | 
 |         visibleTimeScale.hpTimeToPx(visibleWindowTime.start), | 
 |         visibleTimeScale.hpTimeToPx(visibleWindowTime.end), | 
 |         visibleTimeScale.timeToPx(data.start), | 
 |         visibleTimeScale.timeToPx(data.end), | 
 |     ); | 
 |  | 
 |     ctx.textAlign = 'center'; | 
 |  | 
 |     // measuretext is expensive so we only use it once. | 
 |     const charWidth = ctx.measureText('ACBDLqsdfg').width / 10; | 
 |  | 
 |     // The draw of the rect on the selected slice must happen after the other | 
 |     // drawings, otherwise it would result under another rect. | 
 |     let drawRectOnSelected = () => {}; | 
 |  | 
 |  | 
 |     for (let i = 0; i < data.starts.length; i++) { | 
 |       const tStart = Time.fromRaw(data.starts[i]); | 
 |       let tEnd = Time.fromRaw(data.ends[i]); | 
 |       const depth = data.depths[i]; | 
 |       const titleId = data.titles[i]; | 
 |       const sliceId = data.sliceIds[i]; | 
 |       const isInstant = data.isInstant[i]; | 
 |       const isIncomplete = data.isIncomplete[i]; | 
 |       const title = data.strings[titleId]; | 
 |       const colorOverride = data.colors && data.strings[data.colors[i]]; | 
 |       if (isIncomplete) {  // incomplete slice | 
 |         // TODO(stevegolton): This isn't exactly equivalent, ideally we should | 
 |         // choose tEnd once we've converted to screen space coords. | 
 |         tEnd = this.getEndTimeIfInComplete(tStart); | 
 |       } | 
 |  | 
 |       if (!visibleTimeSpan.intersects(tStart, tEnd)) { | 
 |         continue; | 
 |       } | 
 |  | 
 |       const rect = this.getSliceRect( | 
 |           visibleTimeScale, visibleTimeSpan, windowSpan, tStart, tEnd, depth); | 
 |       if (!rect || !rect.visible) { | 
 |         continue; | 
 |       } | 
 |  | 
 |       const currentSelection = globals.state.currentSelection; | 
 |       const isSelected = currentSelection && | 
 |           currentSelection.kind === 'CHROME_SLICE' && | 
 |           currentSelection.id !== undefined && currentSelection.id === sliceId; | 
 |  | 
 |       const highlighted = titleId === this.hoveredTitleId || | 
 |           globals.state.highlightedSliceId === sliceId; | 
 |  | 
 |       const hasFocus = highlighted || isSelected; | 
 |       const colorScheme = getColorForSlice(title); | 
 |       const colorObj = hasFocus ? colorScheme.variant : colorScheme.base; | 
 |       const textColor = | 
 |           hasFocus ? colorScheme.textVariant : colorScheme.textBase; | 
 |  | 
 |       let color: string; | 
 |       if (colorOverride === undefined) { | 
 |         color = colorObj.cssString; | 
 |       } else { | 
 |         color = colorOverride; | 
 |       } | 
 |       ctx.fillStyle = color; | 
 |  | 
 |       // We draw instant events as upward facing chevrons starting at A: | 
 |       //     A | 
 |       //    ### | 
 |       //   ##C## | 
 |       //  ##   ## | 
 |       // D       B | 
 |       // Then B, C, D and back to A: | 
 |       if (isInstant) { | 
 |         if (isSelected) { | 
 |           drawRectOnSelected = () => { | 
 |             ctx.save(); | 
 |             ctx.translate(rect.left, rect.top); | 
 |  | 
 |             // Draw a rectangle around the selected slice | 
 |             ctx.strokeStyle = colorObj.setHSL({s: 100, l: 10}).cssString; | 
 |             ctx.beginPath(); | 
 |             ctx.lineWidth = 3; | 
 |             ctx.strokeRect( | 
 |                 -HALF_CHEVRON_WIDTH_PX, 0, CHEVRON_WIDTH_PX, SLICE_HEIGHT); | 
 |             ctx.closePath(); | 
 |  | 
 |             // Draw inner chevron as interior | 
 |             ctx.fillStyle = color; | 
 |             this.drawChevron(ctx); | 
 |  | 
 |             ctx.restore(); | 
 |           }; | 
 |         } else { | 
 |           ctx.save(); | 
 |           ctx.translate(rect.left, rect.top); | 
 |           this.drawChevron(ctx); | 
 |           ctx.restore(); | 
 |         } | 
 |         continue; | 
 |       } | 
 |  | 
 |       if (isIncomplete && rect.width > SLICE_HEIGHT / 4) { | 
 |         drawIncompleteSlice( | 
 |             ctx, | 
 |             rect.left, | 
 |             rect.top, | 
 |             rect.width, | 
 |             SLICE_HEIGHT, | 
 |             !CROP_INCOMPLETE_SLICE_FLAG.get()); | 
 |       } else if ( | 
 |           data.cpuTimeRatio !== undefined && data.cpuTimeRatio[i] < 1 - 1e-9) { | 
 |         // We draw two rectangles, representing the ratio between wall time and | 
 |         // time spent on cpu. | 
 |         const cpuTimeRatio = data.cpuTimeRatio![i]; | 
 |         const firstPartWidth = rect.width * cpuTimeRatio; | 
 |         const secondPartWidth = rect.width * (1 - cpuTimeRatio); | 
 |         ctx.fillRect(rect.left, rect.top, rect.width, SLICE_HEIGHT); | 
 |         ctx.fillStyle = '#FFFFFF50'; | 
 |         ctx.fillRect( | 
 |             rect.left + firstPartWidth, | 
 |             rect.top, | 
 |             secondPartWidth, | 
 |             SLICE_HEIGHT); | 
 |       } else { | 
 |         ctx.fillRect(rect.left, rect.top, rect.width, SLICE_HEIGHT); | 
 |       } | 
 |  | 
 |       // Selected case | 
 |       if (isSelected) { | 
 |         drawRectOnSelected = () => { | 
 |           ctx.strokeStyle = colorObj.setHSL({s: 100, l: 10}).cssString; | 
 |           ctx.beginPath(); | 
 |           ctx.lineWidth = 3; | 
 |           ctx.strokeRect( | 
 |               rect.left, rect.top - 1.5, rect.width, SLICE_HEIGHT + 3); | 
 |           ctx.closePath(); | 
 |         }; | 
 |       } | 
 |  | 
 |       // Don't render text when we have less than 5px to play with. | 
 |       if (rect.width >= 5) { | 
 |         ctx.fillStyle = textColor.cssString; | 
 |         const displayText = cropText(title, charWidth, rect.width); | 
 |         const rectXCenter = rect.left + rect.width / 2; | 
 |         ctx.textBaseline = 'middle'; | 
 |         ctx.font = this.getFont(); | 
 |         ctx.fillText(displayText, rectXCenter, rect.top + SLICE_HEIGHT / 2); | 
 |       } | 
 |     } | 
 |     drawRectOnSelected(); | 
 |   } | 
 |  | 
 |   drawChevron(ctx: CanvasRenderingContext2D) { | 
 |     // Draw a chevron at a fixed location and size. Should be used with | 
 |     // ctx.translate and ctx.scale to alter location and size. | 
 |     ctx.beginPath(); | 
 |     ctx.moveTo(0, 0); | 
 |     ctx.lineTo(HALF_CHEVRON_WIDTH_PX, SLICE_HEIGHT); | 
 |     ctx.lineTo(0, SLICE_HEIGHT - HALF_CHEVRON_WIDTH_PX); | 
 |     ctx.lineTo(-HALF_CHEVRON_WIDTH_PX, SLICE_HEIGHT); | 
 |     ctx.lineTo(0, 0); | 
 |     ctx.fill(); | 
 |   } | 
 |  | 
 |   getSliceIndex({x, y}: {x: number, y: number}): number|void { | 
 |     const data = this.data; | 
 |     if (data === undefined) return; | 
 |     const { | 
 |       visibleTimeScale: timeScale, | 
 |     } = globals.frontendLocalState; | 
 |     if (y < TRACK_PADDING) return; | 
 |     const instantWidthTime = timeScale.pxDeltaToDuration(HALF_CHEVRON_WIDTH_PX); | 
 |     const t = timeScale.pxToHpTime(x); | 
 |     const depth = Math.floor((y - TRACK_PADDING) / SLICE_HEIGHT); | 
 |  | 
 |     for (let i = 0; i < data.starts.length; i++) { | 
 |       if (depth !== data.depths[i]) { | 
 |         continue; | 
 |       } | 
 |       const start = Time.fromRaw(data.starts[i]); | 
 |       const tStart = HighPrecisionTime.fromTime(start); | 
 |       if (data.isInstant[i]) { | 
 |         if (tStart.sub(t).abs().lt(instantWidthTime)) { | 
 |           return i; | 
 |         } | 
 |       } else { | 
 |         const end = Time.fromRaw(data.ends[i]); | 
 |         let tEnd = HighPrecisionTime.fromTime(end); | 
 |         if (data.isIncomplete[i]) { | 
 |           const endTime = this.getEndTimeIfInComplete(start); | 
 |           tEnd = HighPrecisionTime.fromTime(endTime); | 
 |         } | 
 |         if (tStart.lte(t) && t.lte(tEnd)) { | 
 |           return i; | 
 |         } | 
 |       } | 
 |     } | 
 |   } | 
 |  | 
 |   getEndTimeIfInComplete(start: time): time { | 
 |     const {visibleTimeScale, visibleWindowTime} = globals.frontendLocalState; | 
 |  | 
 |     let end = visibleWindowTime.end.toTime('ceil'); | 
 |     if (CROP_INCOMPLETE_SLICE_FLAG.get()) { | 
 |       const widthTime = | 
 |           visibleTimeScale.pxDeltaToDuration(INCOMPLETE_SLICE_WIDTH_PX) | 
 |               .toTime(); | 
 |       end = Time.add(start, widthTime); | 
 |     } | 
 |  | 
 |     return end; | 
 |   } | 
 |  | 
 |   onMouseMove({x, y}: {x: number, y: number}) { | 
 |     this.hoveredTitleId = -1; | 
 |     globals.dispatch(Actions.setHighlightedSliceId({sliceId: -1})); | 
 |     const sliceIndex = this.getSliceIndex({x, y}); | 
 |     if (sliceIndex === undefined) return; | 
 |     const data = this.data; | 
 |     if (data === undefined) return; | 
 |     this.hoveredTitleId = data.titles[sliceIndex]; | 
 |     const sliceId = data.sliceIds[sliceIndex]; | 
 |     globals.dispatch(Actions.setHighlightedSliceId({sliceId})); | 
 |   } | 
 |  | 
 |   onMouseOut() { | 
 |     this.hoveredTitleId = -1; | 
 |     globals.dispatch(Actions.setHighlightedSliceId({sliceId: -1})); | 
 |   } | 
 |  | 
 |   onMouseClick({x, y}: {x: number, y: number}): boolean { | 
 |     const sliceIndex = this.getSliceIndex({x, y}); | 
 |     if (sliceIndex === undefined) return false; | 
 |     const data = this.data; | 
 |     if (data === undefined) return false; | 
 |     const sliceId = data.sliceIds[sliceIndex]; | 
 |     if (sliceId !== undefined && sliceId !== -1) { | 
 |       globals.makeSelection(Actions.selectChromeSlice({ | 
 |         id: sliceId, | 
 |         trackKey: this.trackKey, | 
 |         table: this.namespace, | 
 |       })); | 
 |       return true; | 
 |     } | 
 |     return false; | 
 |   } | 
 |  | 
 |   getHeight() { | 
 |     return SLICE_HEIGHT * (this.maxDepth + 1) + 2 * TRACK_PADDING; | 
 |   } | 
 |  | 
 |   getSliceRect( | 
 |       visibleTimeScale: TimeScale, visibleWindow: Span<time, duration>, | 
 |       windowSpan: PxSpan, tStart: time, tEnd: time, depth: number): SliceRect | 
 |       |undefined { | 
 |     const pxEnd = windowSpan.end; | 
 |     const left = Math.max(visibleTimeScale.timeToPx(tStart), 0); | 
 |     const right = Math.min(visibleTimeScale.timeToPx(tEnd), pxEnd); | 
 |  | 
 |     const visible = visibleWindow.intersects(tStart, tEnd); | 
 |  | 
 |     return { | 
 |       left, | 
 |       width: Math.max(right - left, 1), | 
 |       top: TRACK_PADDING + depth * SLICE_HEIGHT, | 
 |       height: SLICE_HEIGHT, | 
 |       visible, | 
 |     }; | 
 |   } | 
 | } |