1// Copyright (C) 2018 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 {hsluvToHex} from 'hsluv'; 16 17import {Actions} from '../../common/actions'; 18import {cropText, drawIncompleteSlice} from '../../common/canvas_utils'; 19import {hslForSlice} from '../../common/colorizer'; 20import {TRACE_MARGIN_TIME_S} from '../../common/constants'; 21import {TrackState} from '../../common/state'; 22import {checkerboardExcept} from '../../frontend/checkerboard'; 23import {globals} from '../../frontend/globals'; 24import {SliceRect, Track} from '../../frontend/track'; 25import {trackRegistry} from '../../frontend/track_registry'; 26 27import {Config, Data, SLICE_TRACK_KIND} from './common'; 28 29const SLICE_HEIGHT = 18; 30const TRACK_PADDING = 4; 31const CHEVRON_WIDTH_PX = 10; 32const HALF_CHEVRON_WIDTH_PX = CHEVRON_WIDTH_PX / 2; 33const INNER_CHEVRON_OFFSET = -3; 34const INNER_CHEVRON_SCALE = 35 (SLICE_HEIGHT - 2 * INNER_CHEVRON_OFFSET) / SLICE_HEIGHT; 36 37export class ChromeSliceTrack extends Track<Config, Data> { 38 static readonly kind: string = SLICE_TRACK_KIND; 39 static create(trackState: TrackState): Track { 40 return new ChromeSliceTrack(trackState); 41 } 42 43 private hoveredTitleId = -1; 44 45 constructor(trackState: TrackState) { 46 super(trackState); 47 } 48 49 renderCanvas(ctx: CanvasRenderingContext2D): void { 50 // TODO: fonts and colors should come from the CSS and not hardcoded here. 51 52 const {timeScale, visibleWindowTime} = globals.frontendLocalState; 53 const data = this.data(); 54 55 if (data === undefined) return; // Can't possibly draw anything. 56 57 // If the cached trace slices don't fully cover the visible time range, 58 // show a gray rectangle with a "Loading..." label. 59 checkerboardExcept( 60 ctx, 61 this.getHeight(), 62 timeScale.timeToPx(visibleWindowTime.start), 63 timeScale.timeToPx(visibleWindowTime.end), 64 timeScale.timeToPx(data.start), 65 timeScale.timeToPx(data.end), 66 ); 67 68 ctx.font = '12px Roboto Condensed'; 69 ctx.textAlign = 'center'; 70 71 // measuretext is expensive so we only use it once. 72 const charWidth = ctx.measureText('ACBDLqsdfg').width / 10; 73 74 // The draw of the rect on the selected slice must happen after the other 75 // drawings, otherwise it would result under another rect. 76 let drawRectOnSelected = () => {}; 77 78 for (let i = 0; i < data.starts.length; i++) { 79 const tStart = data.starts[i]; 80 let tEnd = data.ends[i]; 81 const depth = data.depths[i]; 82 const titleId = data.titles[i]; 83 const sliceId = data.sliceIds[i]; 84 const isInstant = data.isInstant[i]; 85 const isIncomplete = data.isIncomplete[i]; 86 const title = data.strings[titleId]; 87 const colorOverride = data.colors && data.strings[data.colors[i]]; 88 if (isIncomplete) { // incomplete slice 89 tEnd = visibleWindowTime.end; 90 } 91 92 const rect = this.getSliceRect(tStart, tEnd, depth); 93 if (!rect || !rect.visible) { 94 continue; 95 } 96 97 const currentSelection = globals.state.currentSelection; 98 const isSelected = currentSelection && 99 currentSelection.kind === 'CHROME_SLICE' && 100 currentSelection.id !== undefined && currentSelection.id === sliceId; 101 102 const name = title.replace(/( )?\d+/g, ''); 103 const highlighted = titleId === this.hoveredTitleId || 104 globals.frontendLocalState.highlightedSliceId === sliceId; 105 106 const [hue, saturation, lightness] = 107 hslForSlice(name, highlighted || isSelected); 108 109 let color: string; 110 if (colorOverride === undefined) { 111 color = hsluvToHex([hue, saturation, lightness]); 112 } else { 113 color = colorOverride; 114 } 115 ctx.fillStyle = color; 116 117 // We draw instant events as upward facing chevrons starting at A: 118 // A 119 // ### 120 // ##C## 121 // ## ## 122 // D B 123 // Then B, C, D and back to A: 124 if (isInstant) { 125 if (isSelected) { 126 drawRectOnSelected = () => { 127 ctx.save(); 128 ctx.translate(rect.left, rect.top); 129 130 // Draw outer chevron as dark border 131 ctx.save(); 132 ctx.translate(0, INNER_CHEVRON_OFFSET); 133 ctx.scale(INNER_CHEVRON_SCALE, INNER_CHEVRON_SCALE); 134 ctx.fillStyle = hsluvToHex([hue, 100, 10]); 135 136 this.drawChevron(ctx); 137 ctx.restore(); 138 139 // Draw inner chevron as interior 140 ctx.fillStyle = color; 141 this.drawChevron(ctx); 142 143 ctx.restore(); 144 }; 145 } else { 146 ctx.save(); 147 ctx.translate(rect.left, rect.top); 148 this.drawChevron(ctx); 149 ctx.restore(); 150 } 151 continue; 152 } 153 if (isIncomplete && rect.width > SLICE_HEIGHT / 4) { 154 drawIncompleteSlice( 155 ctx, rect.left, rect.top, rect.width, SLICE_HEIGHT, color); 156 } else { 157 ctx.fillRect(rect.left, rect.top, rect.width, SLICE_HEIGHT); 158 } 159 // Selected case 160 if (isSelected) { 161 drawRectOnSelected = () => { 162 ctx.strokeStyle = hsluvToHex([hue, 100, 10]); 163 ctx.beginPath(); 164 ctx.lineWidth = 3; 165 ctx.strokeRect( 166 rect.left, rect.top - 1.5, rect.width, SLICE_HEIGHT + 3); 167 ctx.closePath(); 168 }; 169 } 170 171 ctx.fillStyle = lightness > 65 ? '#404040' : 'white'; 172 const displayText = cropText(title, charWidth, rect.width); 173 const rectXCenter = rect.left + rect.width / 2; 174 ctx.textBaseline = "middle"; 175 ctx.fillText(displayText, rectXCenter, rect.top + SLICE_HEIGHT / 2); 176 } 177 drawRectOnSelected(); 178 } 179 180 drawChevron(ctx: CanvasRenderingContext2D) { 181 // Draw a chevron at a fixed location and size. Should be used with 182 // ctx.translate and ctx.scale to alter location and size. 183 ctx.beginPath(); 184 ctx.moveTo(0, 0); 185 ctx.lineTo(HALF_CHEVRON_WIDTH_PX, SLICE_HEIGHT); 186 ctx.lineTo(0, SLICE_HEIGHT - HALF_CHEVRON_WIDTH_PX); 187 ctx.lineTo(-HALF_CHEVRON_WIDTH_PX, SLICE_HEIGHT); 188 ctx.lineTo(0, 0); 189 ctx.fill(); 190 } 191 192 getSliceIndex({x, y}: {x: number, y: number}): number|void { 193 const data = this.data(); 194 if (data === undefined) return; 195 const {timeScale} = globals.frontendLocalState; 196 if (y < TRACK_PADDING) return; 197 const instantWidthTime = timeScale.deltaPxToDuration(HALF_CHEVRON_WIDTH_PX); 198 const t = timeScale.pxToTime(x); 199 const depth = Math.floor((y - TRACK_PADDING) / SLICE_HEIGHT); 200 for (let i = 0; i < data.starts.length; i++) { 201 if (depth !== data.depths[i]) { 202 continue; 203 } 204 const tStart = data.starts[i]; 205 if (data.isInstant[i]) { 206 if (Math.abs(tStart - t) < instantWidthTime) { 207 return i; 208 } 209 } else { 210 let tEnd = data.ends[i]; 211 if (data.isIncomplete[i]) { 212 tEnd = globals.frontendLocalState.visibleWindowTime.end; 213 } 214 if (tStart <= t && t <= tEnd) { 215 return i; 216 } 217 } 218 } 219 } 220 221 onMouseMove({x, y}: {x: number, y: number}) { 222 this.hoveredTitleId = -1; 223 globals.frontendLocalState.setHighlightedSliceId(-1); 224 const sliceIndex = this.getSliceIndex({x, y}); 225 if (sliceIndex === undefined) return; 226 const data = this.data(); 227 if (data === undefined) return; 228 this.hoveredTitleId = data.titles[sliceIndex]; 229 globals.frontendLocalState.setHighlightedSliceId(data.sliceIds[sliceIndex]); 230 } 231 232 onMouseOut() { 233 this.hoveredTitleId = -1; 234 globals.frontendLocalState.setHighlightedSliceId(-1); 235 } 236 237 onMouseClick({x, y}: {x: number, y: number}): boolean { 238 const sliceIndex = this.getSliceIndex({x, y}); 239 if (sliceIndex === undefined) return false; 240 const data = this.data(); 241 if (data === undefined) return false; 242 const sliceId = data.sliceIds[sliceIndex]; 243 if (sliceId !== undefined && sliceId !== -1) { 244 globals.makeSelection(Actions.selectChromeSlice({ 245 id: sliceId, 246 trackId: this.trackState.id, 247 table: this.config.namespace 248 })); 249 return true; 250 } 251 return false; 252 } 253 254 getHeight() { 255 return SLICE_HEIGHT * (this.config.maxDepth + 1) + 2 * TRACK_PADDING; 256 } 257 258 getSliceRect(tStart: number, tEnd: number, depth: number): SliceRect 259 |undefined { 260 const {timeScale, visibleWindowTime} = globals.frontendLocalState; 261 const pxEnd = timeScale.timeToPx(visibleWindowTime.end); 262 const left = Math.max(timeScale.timeToPx(tStart), -TRACE_MARGIN_TIME_S); 263 const right = Math.min(timeScale.timeToPx(tEnd), pxEnd); 264 return { 265 left, 266 width: Math.max(right - left, 1), 267 top: TRACK_PADDING + depth * SLICE_HEIGHT, 268 height: SLICE_HEIGHT, 269 visible: 270 !(tEnd <= visibleWindowTime.start || tStart >= visibleWindowTime.end) 271 }; 272 } 273} 274 275trackRegistry.register(ChromeSliceTrack); 276