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 {search, searchEq} from '../../base/binary_search'; 16import {assertTrue} from '../../base/logging'; 17import {Actions} from '../../common/actions'; 18import {cropText, drawDoubleHeadedArrow} from '../../common/canvas_utils'; 19import {TrackState} from '../../common/state'; 20import {timeToString} from '../../common/time'; 21import {checkerboardExcept} from '../../frontend/checkerboard'; 22import {colorForThread, hueForCpu} from '../../frontend/colorizer'; 23import {globals} from '../../frontend/globals'; 24import {Track} from '../../frontend/track'; 25import {trackRegistry} from '../../frontend/track_registry'; 26 27import { 28 Config, 29 CPU_SLICE_TRACK_KIND, 30 Data, 31 SliceData, 32 SummaryData 33} from './common'; 34 35 36const MARGIN_TOP = 5; 37const RECT_HEIGHT = 30; 38 39class CpuSliceTrack extends Track<Config, Data> { 40 static readonly kind = CPU_SLICE_TRACK_KIND; 41 static create(trackState: TrackState): CpuSliceTrack { 42 return new CpuSliceTrack(trackState); 43 } 44 45 private mouseXpos?: number; 46 private hue: number; 47 private utidHoveredInThisTrack = -1; 48 49 constructor(trackState: TrackState) { 50 super(trackState); 51 this.hue = hueForCpu(this.config.cpu); 52 } 53 54 renderCanvas(ctx: CanvasRenderingContext2D): void { 55 // TODO: fonts and colors should come from the CSS and not hardcoded here. 56 const {timeScale, visibleWindowTime} = globals.frontendLocalState; 57 const data = this.data(); 58 59 // If there aren't enough cached slices data in |data| request more to 60 // the controller. 61 const inRange = data !== undefined && 62 (visibleWindowTime.start >= data.start && 63 visibleWindowTime.end <= data.end); 64 if (!inRange || data === undefined || 65 data.resolution !== globals.getCurResolution()) { 66 globals.requestTrackData(this.trackState.id); 67 } 68 if (data === undefined) return; // Can't possibly draw anything. 69 70 // If the cached trace slices don't fully cover the visible time range, 71 // show a gray rectangle with a "Loading..." label. 72 checkerboardExcept( 73 ctx, 74 timeScale.timeToPx(visibleWindowTime.start), 75 timeScale.timeToPx(visibleWindowTime.end), 76 timeScale.timeToPx(data.start), 77 timeScale.timeToPx(data.end)); 78 79 if (data.kind === 'summary') { 80 this.renderSummary(ctx, data); 81 } else if (data.kind === 'slice') { 82 this.renderSlices(ctx, data); 83 } 84 } 85 86 renderSummary(ctx: CanvasRenderingContext2D, data: SummaryData): void { 87 const {timeScale, visibleWindowTime} = globals.frontendLocalState; 88 const startPx = Math.floor(timeScale.timeToPx(visibleWindowTime.start)); 89 const bottomY = MARGIN_TOP + RECT_HEIGHT; 90 91 let lastX = startPx; 92 let lastY = bottomY; 93 94 ctx.fillStyle = `hsl(${this.hue}, 50%, 60%)`; 95 ctx.beginPath(); 96 ctx.moveTo(lastX, lastY); 97 for (let i = 0; i < data.utilizations.length; i++) { 98 const utilization = data.utilizations[i]; 99 const startTime = i * data.bucketSizeSeconds + data.start; 100 101 lastX = Math.floor(timeScale.timeToPx(startTime)); 102 103 ctx.lineTo(lastX, lastY); 104 lastY = MARGIN_TOP + Math.round(RECT_HEIGHT * (1 - utilization)); 105 ctx.lineTo(lastX, lastY); 106 } 107 ctx.lineTo(lastX, bottomY); 108 ctx.closePath(); 109 ctx.fill(); 110 } 111 112 renderSlices(ctx: CanvasRenderingContext2D, data: SliceData): void { 113 const {timeScale, visibleWindowTime} = globals.frontendLocalState; 114 assertTrue(data.starts.length === data.ends.length); 115 assertTrue(data.starts.length === data.utids.length); 116 117 ctx.textAlign = 'center'; 118 ctx.font = '12px Google Sans'; 119 const charWidth = ctx.measureText('dbpqaouk').width / 8; 120 121 for (let i = 0; i < data.starts.length; i++) { 122 const tStart = data.starts[i]; 123 const tEnd = data.ends[i]; 124 const utid = data.utids[i]; 125 if (tEnd <= visibleWindowTime.start || tStart >= visibleWindowTime.end) { 126 continue; 127 } 128 const rectStart = timeScale.timeToPx(tStart); 129 const rectEnd = timeScale.timeToPx(tEnd); 130 const rectWidth = rectEnd - rectStart; 131 if (rectWidth < 0.3) continue; 132 133 const threadInfo = globals.threads.get(utid); 134 135 // TODO: consider de-duplicating this code with the copied one from 136 // chrome_slices/frontend.ts. 137 let title = `[utid:${utid}]`; 138 let subTitle = ''; 139 let pid = -1; 140 if (threadInfo) { 141 if (threadInfo.pid) { 142 pid = threadInfo.pid; 143 const procName = threadInfo.procName || ''; 144 title = `${procName} [${threadInfo.pid}]`; 145 subTitle = `${threadInfo.threadName} [${threadInfo.tid}]`; 146 } else { 147 title = `${threadInfo.threadName} [${threadInfo.tid}]`; 148 } 149 } 150 151 const isHovering = globals.frontendLocalState.hoveredUtid !== -1; 152 const isThreadHovered = globals.frontendLocalState.hoveredUtid === utid; 153 const isProcessHovered = globals.frontendLocalState.hoveredPid === pid; 154 const color = colorForThread(threadInfo); 155 if (isHovering && !isThreadHovered) { 156 if (!isProcessHovered) { 157 color.l = 90; 158 color.s = 0; 159 } else { 160 color.l = Math.min(color.l + 30, 80); 161 color.s -= 20; 162 } 163 } else { 164 color.l = Math.min(color.l + 10, 60); 165 color.s -= 20; 166 } 167 ctx.fillStyle = `hsl(${color.h}, ${color.s}%, ${color.l}%)`; 168 ctx.fillRect(rectStart, MARGIN_TOP, rectEnd - rectStart, RECT_HEIGHT); 169 170 // Don't render text when we have less than 5px to play with. 171 if (rectWidth < 5) continue; 172 173 title = cropText(title, charWidth, rectWidth); 174 subTitle = cropText(subTitle, charWidth, rectWidth); 175 const rectXCenter = rectStart + rectWidth / 2; 176 ctx.fillStyle = '#fff'; 177 ctx.font = '12px Google Sans'; 178 ctx.fillText(title, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 - 3); 179 ctx.fillStyle = 'rgba(255, 255, 255, 0.6)'; 180 ctx.font = '10px Google Sans'; 181 ctx.fillText(subTitle, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 + 11); 182 } 183 184 const selection = globals.state.currentSelection; 185 const details = globals.sliceDetails; 186 if (selection !== null && selection.kind === 'SLICE') { 187 const [startIndex, endIndex] = searchEq(data.ids, selection.id); 188 if (startIndex !== endIndex) { 189 const tStart = data.starts[startIndex]; 190 const tEnd = data.ends[startIndex]; 191 const utid = data.utids[startIndex]; 192 const color = colorForThread(globals.threads.get(utid)); 193 const rectStart = timeScale.timeToPx(tStart); 194 const rectEnd = timeScale.timeToPx(tEnd); 195 // Draw a rectangle around the slice that is currently selected. 196 ctx.strokeStyle = `hsl(${color.h}, ${color.s}%, 30%)`; 197 ctx.beginPath(); 198 ctx.lineWidth = 3; 199 ctx.strokeRect( 200 rectStart, MARGIN_TOP - 1.5, rectEnd - rectStart, RECT_HEIGHT + 3); 201 ctx.closePath(); 202 // Draw arrow from wakeup time of current slice. 203 if (details.wakeupTs) { 204 const wakeupPos = timeScale.timeToPx(details.wakeupTs); 205 const latencyWidth = rectStart - wakeupPos; 206 drawDoubleHeadedArrow( 207 ctx, 208 wakeupPos, 209 MARGIN_TOP + RECT_HEIGHT, 210 latencyWidth, 211 latencyWidth >= 20); 212 // Latency time with a white semi-transparent background. 213 const displayText = timeToString(tStart - details.wakeupTs); 214 const measured = ctx.measureText(displayText); 215 if (latencyWidth >= measured.width + 2) { 216 ctx.fillStyle = 'rgba(255,255,255,0.7)'; 217 ctx.fillRect( 218 wakeupPos + latencyWidth / 2 - measured.width / 2 - 1, 219 MARGIN_TOP + RECT_HEIGHT - 12, 220 measured.width + 2, 221 11); 222 ctx.textBaseline = 'bottom'; 223 ctx.fillStyle = 'black'; 224 ctx.fillText( 225 displayText, 226 wakeupPos + (latencyWidth) / 2, 227 MARGIN_TOP + RECT_HEIGHT - 1); 228 } 229 } 230 } 231 232 // Draw diamond if the track being drawn is the cpu of the waker. 233 if (this.config.cpu === details.wakerCpu && details.wakeupTs) { 234 const wakeupPos = timeScale.timeToPx(details.wakeupTs); 235 ctx.beginPath(); 236 ctx.moveTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 + 8); 237 ctx.fillStyle = 'black'; 238 ctx.lineTo(wakeupPos + 6, MARGIN_TOP + RECT_HEIGHT / 2); 239 ctx.lineTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 - 8); 240 ctx.lineTo(wakeupPos - 6, MARGIN_TOP + RECT_HEIGHT / 2); 241 ctx.fill(); 242 ctx.closePath(); 243 } 244 } 245 246 const hoveredThread = globals.threads.get(this.utidHoveredInThisTrack); 247 if (hoveredThread !== undefined) { 248 let line1 = ''; 249 let line2 = ''; 250 if (hoveredThread.pid) { 251 line1 = `P: ${hoveredThread.procName} [${hoveredThread.pid}]`; 252 line2 = `T: ${hoveredThread.threadName} [${hoveredThread.tid}]`; 253 } else { 254 line1 = `T: ${hoveredThread.threadName} [${hoveredThread.tid}]`; 255 } 256 257 ctx.font = '10px Google Sans'; 258 const line1Width = ctx.measureText(line1).width; 259 const line2Width = ctx.measureText(line2).width; 260 const width = Math.max(line1Width, line2Width); 261 262 ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; 263 ctx.fillRect(this.mouseXpos!, MARGIN_TOP, width + 16, RECT_HEIGHT); 264 ctx.fillStyle = 'hsl(200, 50%, 40%)'; 265 ctx.textAlign = 'left'; 266 ctx.fillText(line1, this.mouseXpos! + 8, 18); 267 ctx.fillText(line2, this.mouseXpos! + 8, 28); 268 } 269 } 270 271 onMouseMove({x, y}: {x: number, y: number}) { 272 const data = this.data(); 273 this.mouseXpos = x; 274 if (data === undefined || data.kind === 'summary') return; 275 const {timeScale} = globals.frontendLocalState; 276 if (y < MARGIN_TOP || y > MARGIN_TOP + RECT_HEIGHT) { 277 this.utidHoveredInThisTrack = -1; 278 globals.frontendLocalState.setHoveredUtidAndPid(-1, -1); 279 return; 280 } 281 const t = timeScale.pxToTime(x); 282 let hoveredUtid = -1; 283 284 for (let i = 0; i < data.starts.length; i++) { 285 const tStart = data.starts[i]; 286 const tEnd = data.ends[i]; 287 const utid = data.utids[i]; 288 if (tStart <= t && t <= tEnd) { 289 hoveredUtid = utid; 290 break; 291 } 292 } 293 this.utidHoveredInThisTrack = hoveredUtid; 294 const threadInfo = globals.threads.get(hoveredUtid); 295 const hoveredPid = threadInfo ? (threadInfo.pid ? threadInfo.pid : -1) : -1; 296 globals.frontendLocalState.setHoveredUtidAndPid(hoveredUtid, hoveredPid); 297 } 298 299 onMouseOut() { 300 this.utidHoveredInThisTrack = -1; 301 globals.frontendLocalState.setHoveredUtidAndPid(-1, -1); 302 this.mouseXpos = 0; 303 } 304 305 onMouseClick({x}: {x: number}) { 306 const data = this.data(); 307 if (data === undefined || data.kind === 'summary') return false; 308 const {timeScale} = globals.frontendLocalState; 309 const time = timeScale.pxToTime(x); 310 const index = search(data.starts, time); 311 const id = index === -1 ? undefined : data.ids[index]; 312 if (id && this.utidHoveredInThisTrack !== -1) { 313 globals.dispatch(Actions.selectSlice( 314 {utid: this.utidHoveredInThisTrack, id})); 315 return true; 316 } 317 return false; 318 } 319} 320 321trackRegistry.register(CpuSliceTrack); 322