1// Copyright (C) 2021 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 {searchSegment} from '../../base/binary_search'; 17import {assertTrue} from '../../base/logging'; 18import {duration, time, Time} from '../../base/time'; 19import {drawTrackHoverTooltip} from '../../base/canvas_utils'; 20import {colorForCpu} from '../../components/colorizer'; 21import {TrackData} from '../../components/tracks/track_data'; 22import {TimelineFetcher} from '../../components/tracks/track_helper'; 23import {checkerboardExcept} from '../../components/checkerboard'; 24import {TrackRenderer} from '../../public/track'; 25import {LONG, NUM} from '../../trace_processor/query_result'; 26import {uuidv4Sql} from '../../base/uuid'; 27import {TrackMouseEvent, TrackRenderContext} from '../../public/track'; 28import {Point2D} from '../../base/geom'; 29import { 30 createPerfettoTable, 31 createView, 32 createVirtualTable, 33} from '../../trace_processor/sql_utils'; 34import {AsyncDisposableStack} from '../../base/disposable_stack'; 35import {Trace} from '../../public/trace'; 36 37export interface Data extends TrackData { 38 timestamps: BigInt64Array; 39 minFreqKHz: Uint32Array; 40 maxFreqKHz: Uint32Array; 41 lastFreqKHz: Uint32Array; 42 lastIdleValues: Int8Array; 43} 44 45interface Config { 46 cpu: number; 47 freqTrackId: number; 48 idleTrackId?: number; 49 maximumValue: number; 50} 51 52// 0.5 Makes the horizontal lines sharp. 53const MARGIN_TOP = 4.5; 54const RECT_HEIGHT = 20; 55 56export class CpuFreqTrack implements TrackRenderer { 57 private mousePos: Point2D = {x: 0, y: 0}; 58 private hoveredValue: number | undefined = undefined; 59 private hoveredTs: time | undefined = undefined; 60 private hoveredTsEnd: time | undefined = undefined; 61 private hoveredIdle: number | undefined = undefined; 62 private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this)); 63 64 private trackUuid = uuidv4Sql(); 65 66 private trash!: AsyncDisposableStack; 67 68 constructor( 69 private readonly config: Config, 70 private readonly trace: Trace, 71 ) {} 72 73 async onCreate() { 74 this.trash = new AsyncDisposableStack(); 75 await this.trace.engine.query(` 76 INCLUDE PERFETTO MODULE counters.intervals; 77 `); 78 if (this.config.idleTrackId === undefined) { 79 this.trash.use( 80 await createView( 81 this.trace.engine, 82 `raw_freq_idle_${this.trackUuid}`, 83 ` 84 select ts, dur, value as freqValue, -1 as idleValue 85 from counter_leading_intervals!(( 86 select id, ts, track_id, value 87 from counter 88 where track_id = ${this.config.freqTrackId} 89 )) 90 `, 91 ), 92 ); 93 } else { 94 this.trash.use( 95 await createPerfettoTable( 96 this.trace.engine, 97 `raw_freq_${this.trackUuid}`, 98 ` 99 select ts, dur, value as freqValue 100 from counter_leading_intervals!(( 101 select id, ts, track_id, value 102 from counter 103 where track_id = ${this.config.freqTrackId} 104 )) 105 `, 106 ), 107 ); 108 109 this.trash.use( 110 await createPerfettoTable( 111 this.trace.engine, 112 `raw_idle_${this.trackUuid}`, 113 ` 114 select 115 ts, 116 dur, 117 iif(value = 4294967295, -1, cast(value as int)) as idleValue 118 from counter_leading_intervals!(( 119 select id, ts, track_id, value 120 from counter 121 where track_id = ${this.config.idleTrackId} 122 )) 123 `, 124 ), 125 ); 126 127 this.trash.use( 128 await createVirtualTable( 129 this.trace.engine, 130 `raw_freq_idle_${this.trackUuid}`, 131 `span_join(raw_freq_${this.trackUuid}, raw_idle_${this.trackUuid})`, 132 ), 133 ); 134 } 135 136 this.trash.use( 137 await createVirtualTable( 138 this.trace.engine, 139 `cpu_freq_${this.trackUuid}`, 140 ` 141 __intrinsic_counter_mipmap(( 142 select ts, freqValue as value 143 from raw_freq_idle_${this.trackUuid} 144 )) 145 `, 146 ), 147 ); 148 149 this.trash.use( 150 await createVirtualTable( 151 this.trace.engine, 152 `cpu_idle_${this.trackUuid}`, 153 ` 154 __intrinsic_counter_mipmap(( 155 select ts, idleValue as value 156 from raw_freq_idle_${this.trackUuid} 157 )) 158 `, 159 ), 160 ); 161 } 162 163 async onUpdate({ 164 visibleWindow, 165 resolution, 166 }: TrackRenderContext): Promise<void> { 167 await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution); 168 } 169 170 async onDestroy(): Promise<void> { 171 await this.trash.asyncDispose(); 172 } 173 174 async onBoundsChange( 175 start: time, 176 end: time, 177 resolution: duration, 178 ): Promise<Data> { 179 // The resolution should always be a power of two for the logic of this 180 // function to make sense. 181 assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`); 182 183 const freqResult = await this.trace.engine.query(` 184 SELECT 185 min_value as minFreq, 186 max_value as maxFreq, 187 last_ts as ts, 188 last_value as lastFreq 189 FROM cpu_freq_${this.trackUuid}( 190 ${start}, 191 ${end}, 192 ${resolution} 193 ); 194 `); 195 const idleResult = await this.trace.engine.query(` 196 SELECT last_value as lastIdle 197 FROM cpu_idle_${this.trackUuid}( 198 ${start}, 199 ${end}, 200 ${resolution} 201 ); 202 `); 203 204 const freqRows = freqResult.numRows(); 205 const idleRows = idleResult.numRows(); 206 assertTrue(freqRows == idleRows); 207 208 const data: Data = { 209 start, 210 end, 211 resolution, 212 length: freqRows, 213 timestamps: new BigInt64Array(freqRows), 214 minFreqKHz: new Uint32Array(freqRows), 215 maxFreqKHz: new Uint32Array(freqRows), 216 lastFreqKHz: new Uint32Array(freqRows), 217 lastIdleValues: new Int8Array(freqRows), 218 }; 219 220 const freqIt = freqResult.iter({ 221 ts: LONG, 222 minFreq: NUM, 223 maxFreq: NUM, 224 lastFreq: NUM, 225 }); 226 const idleIt = idleResult.iter({ 227 lastIdle: NUM, 228 }); 229 for (let i = 0; freqIt.valid(); ++i, freqIt.next(), idleIt.next()) { 230 data.timestamps[i] = freqIt.ts; 231 data.minFreqKHz[i] = freqIt.minFreq; 232 data.maxFreqKHz[i] = freqIt.maxFreq; 233 data.lastFreqKHz[i] = freqIt.lastFreq; 234 data.lastIdleValues[i] = idleIt.lastIdle; 235 } 236 return data; 237 } 238 239 getHeight() { 240 return MARGIN_TOP + RECT_HEIGHT; 241 } 242 243 render({ctx, size, timescale, visibleWindow}: TrackRenderContext): void { 244 // TODO: fonts and colors should come from the CSS and not hardcoded here. 245 const data = this.fetcher.data; 246 247 if (data === undefined || data.timestamps.length === 0) { 248 // Can't possibly draw anything. 249 return; 250 } 251 252 assertTrue(data.timestamps.length === data.lastFreqKHz.length); 253 assertTrue(data.timestamps.length === data.minFreqKHz.length); 254 assertTrue(data.timestamps.length === data.maxFreqKHz.length); 255 assertTrue(data.timestamps.length === data.lastIdleValues.length); 256 257 const endPx = size.width; 258 const zeroY = MARGIN_TOP + RECT_HEIGHT; 259 260 // Quantize the Y axis to quarters of powers of tens (7.5K, 10K, 12.5K). 261 let yMax = this.config.maximumValue; 262 const kUnits = ['', 'K', 'M', 'G', 'T', 'E']; 263 const exp = Math.ceil(Math.log10(Math.max(yMax, 1))); 264 const pow10 = Math.pow(10, exp); 265 yMax = Math.ceil(yMax / (pow10 / 4)) * (pow10 / 4); 266 const unitGroup = Math.floor(exp / 3); 267 const num = yMax / Math.pow(10, unitGroup * 3); 268 // The values we have for cpufreq are in kHz so +1 to unitGroup. 269 const yLabel = `${num} ${kUnits[unitGroup + 1]}Hz`; 270 271 const color = colorForCpu(this.config.cpu); 272 let saturation = 45; 273 if (this.trace.timeline.hoveredUtid !== undefined) { 274 saturation = 0; 275 } 276 277 ctx.fillStyle = color.setHSL({s: saturation, l: 70}).cssString; 278 ctx.strokeStyle = color.setHSL({s: saturation, l: 55}).cssString; 279 280 const calculateX = (timestamp: time) => { 281 return Math.floor(timescale.timeToPx(timestamp)); 282 }; 283 const calculateY = (value: number) => { 284 return zeroY - Math.round((value / yMax) * RECT_HEIGHT); 285 }; 286 287 const timespan = visibleWindow.toTimeSpan(); 288 const start = timespan.start; 289 const end = timespan.end; 290 291 const [rawStartIdx] = searchSegment(data.timestamps, start); 292 const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx; 293 294 const [, rawEndIdx] = searchSegment(data.timestamps, end); 295 const endIdx = rawEndIdx === -1 ? data.timestamps.length : rawEndIdx; 296 297 // Draw the CPU frequency graph. 298 { 299 ctx.beginPath(); 300 const timestamp = Time.fromRaw(data.timestamps[startIdx]); 301 ctx.moveTo(Math.max(calculateX(timestamp), 0), zeroY); 302 303 let lastDrawnY = zeroY; 304 for (let i = startIdx; i < endIdx; i++) { 305 const timestamp = Time.fromRaw(data.timestamps[i]); 306 const x = Math.max(0, calculateX(timestamp)); 307 const minY = calculateY(data.minFreqKHz[i]); 308 const maxY = calculateY(data.maxFreqKHz[i]); 309 const lastY = calculateY(data.lastFreqKHz[i]); 310 311 ctx.lineTo(x, lastDrawnY); 312 if (minY === maxY) { 313 assertTrue(lastY === minY); 314 ctx.lineTo(x, lastY); 315 } else { 316 ctx.lineTo(x, minY); 317 ctx.lineTo(x, maxY); 318 ctx.lineTo(x, lastY); 319 } 320 lastDrawnY = lastY; 321 } 322 ctx.lineTo(endPx, lastDrawnY); 323 ctx.lineTo(endPx, zeroY); 324 ctx.closePath(); 325 ctx.fill(); 326 ctx.stroke(); 327 } 328 329 // Draw CPU idle rectangles that overlay the CPU freq graph. 330 ctx.fillStyle = `rgba(240, 240, 240, 1)`; 331 { 332 for (let i = startIdx; i < endIdx; i++) { 333 if (data.lastIdleValues[i] < 0) { 334 continue; 335 } 336 337 // We intentionally don't use the floor function here when computing x 338 // coordinates. Instead we use floating point which prevents flickering as 339 // we pan and zoom; this relies on the browser anti-aliasing pixels 340 // correctly. 341 const timestamp = Time.fromRaw(data.timestamps[i]); 342 const x = timescale.timeToPx(timestamp); 343 const xEnd = 344 i === data.lastIdleValues.length - 1 345 ? endPx 346 : timescale.timeToPx(Time.fromRaw(data.timestamps[i + 1])); 347 348 const width = xEnd - x; 349 const height = calculateY(data.lastFreqKHz[i]) - zeroY; 350 351 ctx.fillRect(x, zeroY, width, height); 352 } 353 } 354 355 ctx.font = '10px Roboto Condensed'; 356 357 if (this.hoveredValue !== undefined && this.hoveredTs !== undefined) { 358 let text = `${this.hoveredValue.toLocaleString()}kHz`; 359 360 ctx.fillStyle = color.setHSL({s: 45, l: 75}).cssString; 361 ctx.strokeStyle = color.setHSL({s: 45, l: 45}).cssString; 362 363 const xStart = Math.floor(timescale.timeToPx(this.hoveredTs)); 364 const xEnd = 365 this.hoveredTsEnd === undefined 366 ? endPx 367 : Math.floor(timescale.timeToPx(this.hoveredTsEnd)); 368 const y = zeroY - Math.round((this.hoveredValue / yMax) * RECT_HEIGHT); 369 370 // Highlight line. 371 ctx.beginPath(); 372 ctx.moveTo(xStart, y); 373 ctx.lineTo(xEnd, y); 374 ctx.lineWidth = 3; 375 ctx.stroke(); 376 ctx.lineWidth = 1; 377 378 // Draw change marker. 379 ctx.beginPath(); 380 ctx.arc( 381 xStart, 382 y, 383 3 /* r*/, 384 0 /* start angle*/, 385 2 * Math.PI /* end angle*/, 386 ); 387 ctx.fill(); 388 ctx.stroke(); 389 390 // Display idle value if current hover is idle. 391 if (this.hoveredIdle !== undefined && this.hoveredIdle !== -1) { 392 // Display the idle value +1 to be consistent with catapult. 393 text += ` (Idle: ${(this.hoveredIdle + 1).toLocaleString()})`; 394 } 395 396 // Draw the tooltip. 397 drawTrackHoverTooltip(ctx, this.mousePos, size, text); 398 } 399 400 // Write the Y scale on the top left corner. 401 ctx.textBaseline = 'alphabetic'; 402 ctx.fillStyle = 'rgba(255, 255, 255, 0.6)'; 403 ctx.fillRect(0, 0, 42, 18); 404 ctx.fillStyle = '#666'; 405 ctx.textAlign = 'left'; 406 ctx.fillText(`${yLabel}`, 4, 14); 407 408 // If the cached trace slices don't fully cover the visible time range, 409 // show a gray rectangle with a "Loading..." label. 410 checkerboardExcept( 411 ctx, 412 this.getHeight(), 413 0, 414 size.width, 415 timescale.timeToPx(data.start), 416 timescale.timeToPx(data.end), 417 ); 418 } 419 420 onMouseMove({x, y, timescale}: TrackMouseEvent) { 421 const data = this.fetcher.data; 422 if (data === undefined) return; 423 this.mousePos = {x, y}; 424 const time = timescale.pxToHpTime(x); 425 426 const [left, right] = searchSegment(data.timestamps, time.toTime()); 427 428 this.hoveredTs = 429 left === -1 ? undefined : Time.fromRaw(data.timestamps[left]); 430 this.hoveredTsEnd = 431 right === -1 ? undefined : Time.fromRaw(data.timestamps[right]); 432 this.hoveredValue = left === -1 ? undefined : data.lastFreqKHz[left]; 433 this.hoveredIdle = left === -1 ? undefined : data.lastIdleValues[left]; 434 } 435 436 onMouseOut() { 437 this.hoveredValue = undefined; 438 this.hoveredTs = undefined; 439 this.hoveredTsEnd = undefined; 440 this.hoveredIdle = undefined; 441 } 442} 443