1// Copyright (C) 2023 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 {BigintMath as BIMath} from '../../base/bigint_math'; 16import {searchEq, searchRange} from '../../base/binary_search'; 17import {assertExists, assertTrue} from '../../base/logging'; 18import {duration, time, Time} from '../../base/time'; 19import {Actions} from '../../common/actions'; 20import {drawTrackHoverTooltip} from '../../common/canvas_utils'; 21import {Color} from '../../core/color'; 22import {colorForThread} from '../../core/colorizer'; 23import {TrackData} from '../../common/track_data'; 24import {TimelineFetcher} from '../../common/track_helper'; 25import {checkerboardExcept} from '../../frontend/checkerboard'; 26import {globals} from '../../frontend/globals'; 27import {PanelSize} from '../../frontend/panel'; 28import {Engine, Track} from '../../public'; 29import {LONG, NUM, QueryResult} from '../../trace_processor/query_result'; 30import {uuidv4Sql} from '../../base/uuid'; 31 32export const PROCESS_SCHEDULING_TRACK_KIND = 'ProcessSchedulingTrack'; 33 34const MARGIN_TOP = 5; 35const RECT_HEIGHT = 30; 36const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT; 37 38interface Data extends TrackData { 39 kind: 'slice'; 40 maxCpu: number; 41 42 // Slices are stored in a columnar fashion. All fields have the same length. 43 starts: BigInt64Array; 44 ends: BigInt64Array; 45 utids: Uint32Array; 46 cpus: Uint32Array; 47} 48 49export interface Config { 50 pidForColor: number; 51 upid: number | null; 52 utid: number | null; 53} 54 55export class ProcessSchedulingTrack implements Track { 56 private mousePos?: {x: number; y: number}; 57 private utidHoveredInThisTrack = -1; 58 private fetcher = new TimelineFetcher(this.onBoundsChange.bind(this)); 59 private cpuCount: number; 60 private engine: Engine; 61 private trackUuid = uuidv4Sql(); 62 private config: Config; 63 64 constructor(engine: Engine, config: Config, cpuCount: number) { 65 this.engine = engine; 66 this.config = config; 67 this.cpuCount = cpuCount; 68 } 69 70 async onCreate(): Promise<void> { 71 if (this.config.upid !== null) { 72 await this.engine.query(` 73 create virtual table process_scheduling_${this.trackUuid} 74 using __intrinsic_slice_mipmap(( 75 select 76 id, 77 ts, 78 iif( 79 dur = -1, 80 lead(ts, 1, trace_end()) over (partition by cpu order by ts) - ts, 81 dur 82 ) as dur, 83 cpu as depth 84 from experimental_sched_upid 85 where 86 utid != 0 and 87 upid = ${this.config.upid} 88 )); 89 `); 90 } else { 91 assertExists(this.config.utid); 92 await this.engine.query(` 93 create virtual table process_scheduling_${this.trackUuid} 94 using __intrinsic_slice_mipmap(( 95 select 96 id, 97 ts, 98 iif( 99 dur = -1, 100 lead(ts, 1, trace_end()) over (partition by cpu order by ts) - ts, 101 dur 102 ) as dur, 103 cpu as depth 104 from sched 105 where utid = ${this.config.utid} 106 )); 107 `); 108 } 109 } 110 111 async onUpdate(): Promise<void> { 112 await this.fetcher.requestDataForCurrentTime(); 113 } 114 115 async onDestroy(): Promise<void> { 116 this.fetcher.dispose(); 117 await this.engine.tryQuery(` 118 drop table process_scheduling_${this.trackUuid} 119 `); 120 } 121 122 async onBoundsChange( 123 start: time, 124 end: time, 125 resolution: duration, 126 ): Promise<Data> { 127 // Resolution must always be a power of 2 for this logic to work 128 assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`); 129 130 const queryRes = await this.queryData(start, end, resolution); 131 const numRows = queryRes.numRows(); 132 const slices: Data = { 133 kind: 'slice', 134 start, 135 end, 136 resolution, 137 length: numRows, 138 maxCpu: this.cpuCount, 139 starts: new BigInt64Array(numRows), 140 ends: new BigInt64Array(numRows), 141 cpus: new Uint32Array(numRows), 142 utids: new Uint32Array(numRows), 143 }; 144 145 const it = queryRes.iter({ 146 ts: LONG, 147 dur: LONG, 148 cpu: NUM, 149 utid: NUM, 150 }); 151 152 for (let row = 0; it.valid(); it.next(), row++) { 153 const start = Time.fromRaw(it.ts); 154 const dur = it.dur; 155 const end = Time.add(start, dur); 156 157 slices.starts[row] = start; 158 slices.ends[row] = end; 159 slices.cpus[row] = it.cpu; 160 slices.utids[row] = it.utid; 161 slices.end = Time.max(end, slices.end); 162 } 163 return slices; 164 } 165 166 private async queryData( 167 start: time, 168 end: time, 169 bucketSize: duration, 170 ): Promise<QueryResult> { 171 return this.engine.query(` 172 select 173 (z.ts / ${bucketSize}) * ${bucketSize} as ts, 174 iif(s.dur = -1, s.dur, max(z.dur, ${bucketSize})) as dur, 175 s.id, 176 z.depth as cpu, 177 utid 178 from process_scheduling_${this.trackUuid}( 179 ${start}, ${end}, ${bucketSize} 180 ) z 181 cross join sched s using (id) 182 `); 183 } 184 185 getHeight(): number { 186 return TRACK_HEIGHT; 187 } 188 189 render(ctx: CanvasRenderingContext2D, size: PanelSize): void { 190 // TODO: fonts and colors should come from the CSS and not hardcoded here. 191 const {visibleTimeScale, visibleTimeSpan} = globals.timeline; 192 const data = this.fetcher.data; 193 194 if (data === undefined) return; // Can't possibly draw anything. 195 196 // If the cached trace slices don't fully cover the visible time range, 197 // show a gray rectangle with a "Loading..." label. 198 checkerboardExcept( 199 ctx, 200 this.getHeight(), 201 0, 202 size.width, 203 visibleTimeScale.timeToPx(data.start), 204 visibleTimeScale.timeToPx(data.end), 205 ); 206 207 assertTrue(data.starts.length === data.ends.length); 208 assertTrue(data.starts.length === data.utids.length); 209 210 const cpuTrackHeight = Math.floor(RECT_HEIGHT / data.maxCpu); 211 212 for (let i = 0; i < data.ends.length; i++) { 213 const tStart = Time.fromRaw(data.starts[i]); 214 const tEnd = Time.fromRaw(data.ends[i]); 215 216 // Cull slices that lie completely outside the visible window 217 if (!visibleTimeSpan.intersects(tStart, tEnd)) continue; 218 219 const utid = data.utids[i]; 220 const cpu = data.cpus[i]; 221 222 const rectStart = Math.floor(visibleTimeScale.timeToPx(tStart)); 223 const rectEnd = Math.floor(visibleTimeScale.timeToPx(tEnd)); 224 const rectWidth = Math.max(1, rectEnd - rectStart); 225 226 const threadInfo = globals.threads.get(utid); 227 // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 228 const pid = (threadInfo ? threadInfo.pid : -1) || -1; 229 230 const isHovering = globals.state.hoveredUtid !== -1; 231 const isThreadHovered = globals.state.hoveredUtid === utid; 232 const isProcessHovered = globals.state.hoveredPid === pid; 233 const colorScheme = colorForThread(threadInfo); 234 let color: Color; 235 if (isHovering && !isThreadHovered) { 236 if (!isProcessHovered) { 237 color = colorScheme.disabled; 238 } else { 239 color = colorScheme.variant; 240 } 241 } else { 242 color = colorScheme.base; 243 } 244 ctx.fillStyle = color.cssString; 245 const y = MARGIN_TOP + cpuTrackHeight * cpu + cpu; 246 ctx.fillRect(rectStart, y, rectWidth, cpuTrackHeight); 247 } 248 249 const hoveredThread = globals.threads.get(this.utidHoveredInThisTrack); 250 const height = this.getHeight(); 251 if (hoveredThread !== undefined && this.mousePos !== undefined) { 252 const tidText = `T: ${hoveredThread.threadName} [${hoveredThread.tid}]`; 253 // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 254 if (hoveredThread.pid) { 255 const pidText = `P: ${hoveredThread.procName} [${hoveredThread.pid}]`; 256 drawTrackHoverTooltip(ctx, this.mousePos, height, pidText, tidText); 257 } else { 258 drawTrackHoverTooltip(ctx, this.mousePos, height, tidText); 259 } 260 } 261 } 262 263 onMouseMove(pos: {x: number; y: number}) { 264 const data = this.fetcher.data; 265 this.mousePos = pos; 266 if (data === undefined) return; 267 if (pos.y < MARGIN_TOP || pos.y > MARGIN_TOP + RECT_HEIGHT) { 268 this.utidHoveredInThisTrack = -1; 269 globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1})); 270 return; 271 } 272 273 const cpuTrackHeight = Math.floor(RECT_HEIGHT / data.maxCpu); 274 const cpu = Math.floor((pos.y - MARGIN_TOP) / (cpuTrackHeight + 1)); 275 const {visibleTimeScale} = globals.timeline; 276 const t = visibleTimeScale.pxToHpTime(pos.x).toTime('floor'); 277 278 const [i, j] = searchRange(data.starts, t, searchEq(data.cpus, cpu)); 279 if (i === j || i >= data.starts.length || t > data.ends[i]) { 280 this.utidHoveredInThisTrack = -1; 281 globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1})); 282 return; 283 } 284 285 const utid = data.utids[i]; 286 this.utidHoveredInThisTrack = utid; 287 const threadInfo = globals.threads.get(utid); 288 // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 289 const pid = threadInfo ? (threadInfo.pid ? threadInfo.pid : -1) : -1; 290 globals.dispatch(Actions.setHoveredUtidAndPid({utid, pid})); 291 } 292 293 onMouseOut() { 294 this.utidHoveredInThisTrack = -1; 295 globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1})); 296 this.mousePos = undefined; 297 } 298} 299