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