1// Copyright (C) 2019 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 {searchEq, searchRange} from '../../base/binary_search'; 16import {assertTrue} from '../../base/logging'; 17import {TrackState} from '../../common/state'; 18import {checkerboardExcept} from '../../frontend/checkerboard'; 19import {colorForThread, colorForTid} from '../../frontend/colorizer'; 20import {globals} from '../../frontend/globals'; 21import {Track} from '../../frontend/track'; 22import {trackRegistry} from '../../frontend/track_registry'; 23 24import { 25 Config, 26 Data, 27 PROCESS_SCHEDULING_TRACK_KIND, 28 SliceData, 29 SummaryData 30} from './common'; 31 32const MARGIN_TOP = 5; 33const RECT_HEIGHT = 30; 34 35class ProcessSchedulingTrack extends Track<Config, Data> { 36 static readonly kind = PROCESS_SCHEDULING_TRACK_KIND; 37 static create(trackState: TrackState): ProcessSchedulingTrack { 38 return new ProcessSchedulingTrack(trackState); 39 } 40 41 private mouseXpos?: number; 42 private utidHoveredInThisTrack = -1; 43 44 constructor(trackState: TrackState) { 45 super(trackState); 46 } 47 48 renderCanvas(ctx: CanvasRenderingContext2D): void { 49 // TODO: fonts and colors should come from the CSS and not hardcoded here. 50 const {timeScale, visibleWindowTime} = globals.frontendLocalState; 51 const data = this.data(); 52 53 // If there aren't enough cached slices data in |data| request more to 54 // the controller. 55 const inRange = data !== undefined && 56 (visibleWindowTime.start >= data.start && 57 visibleWindowTime.end <= data.end); 58 if (!inRange || data === undefined || 59 data.resolution !== globals.getCurResolution()) { 60 globals.requestTrackData(this.trackState.id); 61 } 62 if (data === undefined) return; // Can't possibly draw anything. 63 64 // If the cached trace slices don't fully cover the visible time range, 65 // show a gray rectangle with a "Loading..." label. 66 checkerboardExcept( 67 ctx, 68 timeScale.timeToPx(visibleWindowTime.start), 69 timeScale.timeToPx(visibleWindowTime.end), 70 timeScale.timeToPx(data.start), 71 timeScale.timeToPx(data.end)); 72 73 if (data.kind === 'summary') { 74 this.renderSummary(ctx, data); 75 } else if (data.kind === 'slice') { 76 this.renderSlices(ctx, data); 77 } 78 } 79 80 renderSummary(ctx: CanvasRenderingContext2D, data: SummaryData): void { 81 const {timeScale, visibleWindowTime} = globals.frontendLocalState; 82 const startPx = Math.floor(timeScale.timeToPx(visibleWindowTime.start)); 83 const bottomY = MARGIN_TOP + RECT_HEIGHT; 84 85 let lastX = startPx; 86 let lastY = bottomY; 87 88 const color = colorForTid(this.config.pidForColor); 89 ctx.fillStyle = `hsl(${color.h}, ${color.s}%, ${color.l}%)`; 90 ctx.beginPath(); 91 ctx.moveTo(lastX, lastY); 92 for (let i = 0; i < data.utilizations.length; i++) { 93 const utilization = data.utilizations[i]; 94 const startTime = i * data.bucketSizeSeconds + data.start; 95 96 lastX = Math.floor(timeScale.timeToPx(startTime)); 97 98 ctx.lineTo(lastX, lastY); 99 lastY = MARGIN_TOP + Math.round(RECT_HEIGHT * (1 - utilization)); 100 ctx.lineTo(lastX, lastY); 101 } 102 ctx.lineTo(lastX, bottomY); 103 ctx.closePath(); 104 ctx.fill(); 105 } 106 107 renderSlices(ctx: CanvasRenderingContext2D, data: SliceData): void { 108 const {timeScale, visibleWindowTime} = globals.frontendLocalState; 109 assertTrue(data.starts.length === data.ends.length); 110 assertTrue(data.starts.length === data.utids.length); 111 112 const cpuTrackHeight = Math.floor(RECT_HEIGHT / data.numCpus); 113 114 for (let i = 0; i < data.starts.length; i++) { 115 const tStart = data.starts[i]; 116 const tEnd = data.ends[i]; 117 const utid = data.utids[i]; 118 const cpu = data.cpus[i]; 119 if (tEnd <= visibleWindowTime.start || tStart >= visibleWindowTime.end) { 120 continue; 121 } 122 const rectStart = timeScale.timeToPx(tStart); 123 const rectEnd = timeScale.timeToPx(tEnd); 124 const rectWidth = rectEnd - rectStart; 125 if (rectWidth < 0.3) continue; 126 127 const threadInfo = globals.threads.get(utid); 128 const pid = (threadInfo ? threadInfo.pid : -1) || -1; 129 130 const isHovering = globals.frontendLocalState.hoveredUtid !== -1; 131 const isThreadHovered = globals.frontendLocalState.hoveredUtid === utid; 132 const isProcessHovered = globals.frontendLocalState.hoveredPid === pid; 133 const color = colorForThread(threadInfo); 134 if (isHovering && !isThreadHovered) { 135 if (!isProcessHovered) { 136 color.l = 90; 137 color.s = 0; 138 } else { 139 color.l = Math.min(color.l + 30, 80); 140 color.s -= 20; 141 } 142 } else { 143 color.l = Math.min(color.l + 10, 60); 144 color.s -= 20; 145 } 146 ctx.fillStyle = `hsl(${color.h}, ${color.s}%, ${color.l}%)`; 147 const y = MARGIN_TOP + cpuTrackHeight * cpu + cpu; 148 ctx.fillRect(rectStart, y, rectEnd - rectStart, cpuTrackHeight); 149 } 150 151 const hoveredThread = globals.threads.get(this.utidHoveredInThisTrack); 152 if (hoveredThread !== undefined) { 153 let line1 = ''; 154 let line2 = ''; 155 if (hoveredThread.pid) { 156 line1 = `P: ${hoveredThread.procName} [${hoveredThread.pid}]`; 157 line2 = `T: ${hoveredThread.threadName} [${hoveredThread.tid}]`; 158 } else { 159 line1 = `T: ${hoveredThread.threadName} [${hoveredThread.tid}]`; 160 } 161 162 ctx.font = '10px Google Sans'; 163 const line1Width = ctx.measureText(line1).width; 164 const line2Width = ctx.measureText(line2).width; 165 const width = Math.max(line1Width, line2Width); 166 167 ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; 168 ctx.fillRect(this.mouseXpos!, MARGIN_TOP, width + 16, RECT_HEIGHT); 169 ctx.fillStyle = 'hsl(200, 50%, 40%)'; 170 ctx.textAlign = 'left'; 171 ctx.fillText(line1, this.mouseXpos! + 8, 18); 172 ctx.fillText(line2, this.mouseXpos! + 8, 28); 173 } 174 } 175 176 onMouseMove({x, y}: {x: number, y: number}) { 177 const data = this.data(); 178 this.mouseXpos = x; 179 if (data === undefined || data.kind === 'summary') return; 180 if (y < MARGIN_TOP || y > MARGIN_TOP + RECT_HEIGHT) { 181 this.utidHoveredInThisTrack = -1; 182 globals.frontendLocalState.setHoveredUtidAndPid(-1, -1); 183 return; 184 } 185 186 const cpuTrackHeight = Math.floor(RECT_HEIGHT / data.numCpus); 187 const cpu = Math.floor((y - MARGIN_TOP) / (cpuTrackHeight + 1)); 188 const {timeScale} = globals.frontendLocalState; 189 const t = timeScale.pxToTime(x); 190 191 const [i, j] = searchRange(data.starts, t, searchEq(data.cpus, cpu)); 192 if (i === j || i >= data.starts.length || t > data.ends[i]) { 193 this.utidHoveredInThisTrack = -1; 194 globals.frontendLocalState.setHoveredUtidAndPid(-1, -1); 195 return; 196 } 197 198 const utid = data.utids[i]; 199 this.utidHoveredInThisTrack = utid; 200 const threadInfo = globals.threads.get(utid); 201 const pid = threadInfo ? (threadInfo.pid ? threadInfo.pid : -1) : -1; 202 globals.frontendLocalState.setHoveredUtidAndPid(utid, pid); 203 } 204 205 onMouseOut() { 206 this.utidHoveredInThisTrack = -1; 207 globals.frontendLocalState.setHoveredUtidAndPid(-1, -1); 208 this.mouseXpos = 0; 209 } 210} 211 212trackRegistry.register(ProcessSchedulingTrack); 213