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