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, searchSegment} from '../../base/binary_search'; 16import {assertTrue} from '../../base/logging'; 17import {Actions} from '../../common/actions'; 18import {cropText, drawDoubleHeadedArrow} from '../../common/canvas_utils'; 19import {colorForThread} from '../../common/colorizer'; 20import {TrackState} from '../../common/state'; 21import {timeToString} from '../../common/time'; 22import {checkerboardExcept} from '../../frontend/checkerboard'; 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} from './common'; 32 33const MARGIN_TOP = 3; 34const RECT_HEIGHT = 24; 35const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT; 36 37class CpuSliceTrack extends Track<Config, Data> { 38 static readonly kind = CPU_SLICE_TRACK_KIND; 39 static create(trackState: TrackState): CpuSliceTrack { 40 return new CpuSliceTrack(trackState); 41 } 42 43 private mouseXpos?: number; 44 private utidHoveredInThisTrack = -1; 45 46 constructor(trackState: TrackState) { 47 super(trackState); 48 } 49 50 getHeight(): number { 51 return TRACK_HEIGHT; 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 (data === undefined) return; // Can't possibly draw anything. 60 61 // If the cached trace slices don't fully cover the visible time range, 62 // show a gray rectangle with a "Loading..." label. 63 checkerboardExcept( 64 ctx, 65 this.getHeight(), 66 timeScale.timeToPx(visibleWindowTime.start), 67 timeScale.timeToPx(visibleWindowTime.end), 68 timeScale.timeToPx(data.start), 69 timeScale.timeToPx(data.end)); 70 71 this.renderSlices(ctx, data); 72 } 73 74 renderSlices(ctx: CanvasRenderingContext2D, data: Data): void { 75 const {timeScale, visibleWindowTime} = globals.frontendLocalState; 76 assertTrue(data.starts.length === data.ends.length); 77 assertTrue(data.starts.length === data.utids.length); 78 79 ctx.textAlign = 'center'; 80 ctx.font = '12px Roboto Condensed'; 81 const charWidth = ctx.measureText('dbpqaouk').width / 8; 82 83 const rawStartIdx = 84 data.ends.findIndex(end => end >= visibleWindowTime.start); 85 const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx; 86 87 const [, rawEndIdx] = searchSegment(data.starts, visibleWindowTime.end); 88 const endIdx = rawEndIdx === -1 ? data.starts.length : rawEndIdx; 89 90 for (let i = startIdx; i < endIdx; i++) { 91 const tStart = data.starts[i]; 92 const tEnd = data.ends[i]; 93 const utid = data.utids[i]; 94 95 const rectStart = timeScale.timeToPx(tStart); 96 const rectEnd = timeScale.timeToPx(tEnd); 97 const rectWidth = Math.max(1, rectEnd - rectStart); 98 99 const threadInfo = globals.threads.get(utid); 100 const pid = threadInfo && threadInfo.pid ? threadInfo.pid : -1; 101 102 const isHovering = globals.frontendLocalState.hoveredUtid !== -1; 103 const isThreadHovered = globals.frontendLocalState.hoveredUtid === utid; 104 const isProcessHovered = globals.frontendLocalState.hoveredPid === pid; 105 const color = colorForThread(threadInfo); 106 if (isHovering && !isThreadHovered) { 107 if (!isProcessHovered) { 108 color.l = 90; 109 color.s = 0; 110 } else { 111 color.l = Math.min(color.l + 30, 80); 112 color.s -= 20; 113 } 114 } else { 115 color.l = Math.min(color.l + 10, 60); 116 color.s -= 20; 117 } 118 ctx.fillStyle = `hsl(${color.h}, ${color.s}%, ${color.l}%)`; 119 ctx.fillRect(rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT); 120 121 // Don't render text when we have less than 5px to play with. 122 if (rectWidth < 5) continue; 123 124 // TODO: consider de-duplicating this code with the copied one from 125 // chrome_slices/frontend.ts. 126 let title = `[utid:${utid}]`; 127 let subTitle = ''; 128 if (threadInfo) { 129 if (threadInfo.pid) { 130 let procName = threadInfo.procName || ''; 131 if (procName.startsWith('/')) { // Remove folder paths from name 132 procName = procName.substring(procName.lastIndexOf('/') + 1); 133 } 134 title = `${procName} [${threadInfo.pid}]`; 135 subTitle = `${threadInfo.threadName} [${threadInfo.tid}]`; 136 } else { 137 title = `${threadInfo.threadName} [${threadInfo.tid}]`; 138 } 139 } 140 title = cropText(title, charWidth, rectWidth); 141 subTitle = cropText(subTitle, charWidth, rectWidth); 142 const rectXCenter = rectStart + rectWidth / 2; 143 ctx.fillStyle = '#fff'; 144 ctx.font = '12px Roboto Condensed'; 145 ctx.fillText(title, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 - 1); 146 ctx.fillStyle = 'rgba(255, 255, 255, 0.6)'; 147 ctx.font = '10px Roboto Condensed'; 148 ctx.fillText(subTitle, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 + 9); 149 } 150 151 const selection = globals.state.currentSelection; 152 const details = globals.sliceDetails; 153 if (selection !== null && selection.kind === 'SLICE') { 154 const [startIndex, endIndex] = searchEq(data.ids, selection.id); 155 if (startIndex !== endIndex) { 156 const tStart = data.starts[startIndex]; 157 const tEnd = data.ends[startIndex]; 158 const utid = data.utids[startIndex]; 159 const color = colorForThread(globals.threads.get(utid)); 160 const rectStart = timeScale.timeToPx(tStart); 161 const rectEnd = timeScale.timeToPx(tEnd); 162 const rectWidth = Math.max(1, rectEnd - rectStart); 163 164 // Draw a rectangle around the slice that is currently selected. 165 ctx.strokeStyle = `hsl(${color.h}, ${color.s}%, 30%)`; 166 ctx.beginPath(); 167 ctx.lineWidth = 3; 168 ctx.strokeRect(rectStart, MARGIN_TOP - 1.5, rectWidth, RECT_HEIGHT + 3); 169 ctx.closePath(); 170 // Draw arrow from wakeup time of current slice. 171 if (details.wakeupTs) { 172 const wakeupPos = timeScale.timeToPx(details.wakeupTs); 173 const latencyWidth = rectStart - wakeupPos; 174 drawDoubleHeadedArrow( 175 ctx, 176 wakeupPos, 177 MARGIN_TOP + RECT_HEIGHT, 178 latencyWidth, 179 latencyWidth >= 20); 180 // Latency time with a white semi-transparent background. 181 const displayText = timeToString(tStart - details.wakeupTs); 182 const measured = ctx.measureText(displayText); 183 if (latencyWidth >= measured.width + 2) { 184 ctx.fillStyle = 'rgba(255,255,255,0.7)'; 185 ctx.fillRect( 186 wakeupPos + latencyWidth / 2 - measured.width / 2 - 1, 187 MARGIN_TOP + RECT_HEIGHT - 12, 188 measured.width + 2, 189 11); 190 ctx.textBaseline = 'bottom'; 191 ctx.fillStyle = 'black'; 192 ctx.fillText( 193 displayText, 194 wakeupPos + (latencyWidth) / 2, 195 MARGIN_TOP + RECT_HEIGHT - 1); 196 } 197 } 198 } 199 200 // Draw diamond if the track being drawn is the cpu of the waker. 201 if (this.config.cpu === details.wakerCpu && details.wakeupTs) { 202 const wakeupPos = Math.floor(timeScale.timeToPx(details.wakeupTs)); 203 ctx.beginPath(); 204 ctx.moveTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 + 8); 205 ctx.fillStyle = 'black'; 206 ctx.lineTo(wakeupPos + 6, MARGIN_TOP + RECT_HEIGHT / 2); 207 ctx.lineTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 - 8); 208 ctx.lineTo(wakeupPos - 6, MARGIN_TOP + RECT_HEIGHT / 2); 209 ctx.fill(); 210 ctx.closePath(); 211 } 212 } 213 214 const hoveredThread = globals.threads.get(this.utidHoveredInThisTrack); 215 if (hoveredThread !== undefined && this.mouseXpos !== undefined) { 216 const tidText = `T: ${hoveredThread.threadName} [${hoveredThread.tid}]`; 217 if (hoveredThread.pid) { 218 const pidText = `P: ${hoveredThread.procName} [${hoveredThread.pid}]`; 219 this.drawTrackHoverTooltip(ctx, this.mouseXpos, pidText, tidText); 220 } else { 221 this.drawTrackHoverTooltip(ctx, this.mouseXpos, tidText); 222 } 223 } 224 } 225 226 onMouseMove({x, y}: {x: number, y: number}) { 227 const data = this.data(); 228 this.mouseXpos = x; 229 if (data === undefined) return; 230 const {timeScale} = globals.frontendLocalState; 231 if (y < MARGIN_TOP || y > MARGIN_TOP + RECT_HEIGHT) { 232 this.utidHoveredInThisTrack = -1; 233 globals.frontendLocalState.setHoveredUtidAndPid(-1, -1); 234 return; 235 } 236 const t = timeScale.pxToTime(x); 237 let hoveredUtid = -1; 238 239 for (let i = 0; i < data.starts.length; i++) { 240 const tStart = data.starts[i]; 241 const tEnd = data.ends[i]; 242 const utid = data.utids[i]; 243 if (tStart <= t && t <= tEnd) { 244 hoveredUtid = utid; 245 break; 246 } 247 } 248 this.utidHoveredInThisTrack = hoveredUtid; 249 const threadInfo = globals.threads.get(hoveredUtid); 250 const hoveredPid = threadInfo ? (threadInfo.pid ? threadInfo.pid : -1) : -1; 251 globals.frontendLocalState.setHoveredUtidAndPid(hoveredUtid, hoveredPid); 252 } 253 254 onMouseOut() { 255 this.utidHoveredInThisTrack = -1; 256 globals.frontendLocalState.setHoveredUtidAndPid(-1, -1); 257 this.mouseXpos = 0; 258 } 259 260 onMouseClick({x}: {x: number}) { 261 const data = this.data(); 262 if (data === undefined) return false; 263 const {timeScale} = globals.frontendLocalState; 264 const time = timeScale.pxToTime(x); 265 const index = search(data.starts, time); 266 const id = index === -1 ? undefined : data.ids[index]; 267 if (!id || this.utidHoveredInThisTrack === -1) return false; 268 globals.makeSelection( 269 Actions.selectSlice({id, trackId: this.trackState.id})); 270 return true; 271 } 272} 273 274trackRegistry.register(CpuSliceTrack); 275