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} from '../../base/bigint_math'; 16import {searchSegment} from '../../base/binary_search'; 17import {assertTrue} from '../../base/logging'; 18import {hueForCpu} from '../../common/colorizer'; 19import {PluginContext} from '../../common/plugin_api'; 20import {NUM, NUM_NULL, QueryResult} from '../../common/query_result'; 21import {fromNs, TPDuration, TPTime, tpTimeToNanos} from '../../common/time'; 22import {TrackData} from '../../common/track_data'; 23import { 24 TrackController, 25} from '../../controller/track_controller'; 26import {checkerboardExcept} from '../../frontend/checkerboard'; 27import {globals} from '../../frontend/globals'; 28import {NewTrackArgs, Track} from '../../frontend/track'; 29 30 31export const CPU_FREQ_TRACK_KIND = 'CpuFreqTrack'; 32 33export interface Data extends TrackData { 34 maximumValue: number; 35 maxTsEnd: number; 36 37 timestamps: Float64Array; 38 minFreqKHz: Uint32Array; 39 maxFreqKHz: Uint32Array; 40 lastFreqKHz: Uint32Array; 41 lastIdleValues: Int8Array; 42} 43 44export interface Config { 45 cpu: number; 46 freqTrackId: number; 47 idleTrackId?: number; 48 maximumValue?: number; 49 minimumValue?: number; 50} 51 52class CpuFreqTrackController extends TrackController<Config, Data> { 53 static readonly kind = CPU_FREQ_TRACK_KIND; 54 55 private maxDurNs = 0; 56 private maxTsEndNs = 0; 57 private maximumValueSeen = 0; 58 private cachedBucketNs = Number.MAX_SAFE_INTEGER; 59 60 async onSetup() { 61 await this.createFreqIdleViews(); 62 63 this.maximumValueSeen = await this.queryMaxFrequency(); 64 this.maxDurNs = await this.queryMaxSourceDur(); 65 66 const iter = (await this.query(` 67 select max(ts) as maxTs, dur, count(1) as rowCount 68 from ${this.tableName('freq_idle')} 69 `)).firstRow({maxTs: NUM_NULL, dur: NUM_NULL, rowCount: NUM}); 70 if (iter.maxTs === null || iter.dur === null) { 71 // We shoulnd't really hit this because trackDecider shouldn't create 72 // the track in the first place if there are no entries. But could happen 73 // if only one cpu has no cpufreq data. 74 return; 75 } 76 this.maxTsEndNs = iter.maxTs + iter.dur; 77 78 const rowCount = iter.rowCount; 79 const bucketNs = this.cachedBucketSizeNs(rowCount); 80 if (bucketNs === undefined) { 81 return; 82 } 83 84 await this.query(` 85 create table ${this.tableName('freq_idle_cached')} as 86 select 87 (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as cachedTsq, 88 min(freqValue) as minFreq, 89 max(freqValue) as maxFreq, 90 value_at_max_ts(ts, freqValue) as lastFreq, 91 value_at_max_ts(ts, idleValue) as lastIdleValue 92 from ${this.tableName('freq_idle')} 93 group by cachedTsq 94 order by cachedTsq 95 `); 96 97 this.cachedBucketNs = bucketNs; 98 } 99 100 async onBoundsChange(start: TPTime, end: TPTime, resolution: TPDuration): 101 Promise<Data> { 102 // The resolution should always be a power of two for the logic of this 103 // function to make sense. 104 assertTrue( 105 BigintMath.popcount(resolution) === 1, 106 `${resolution} is not a power of 2`); 107 const resolutionNs = Number(resolution); 108 109 const startNs = tpTimeToNanos(start); 110 const endNs = tpTimeToNanos(end); 111 112 // ns per quantization bucket (i.e. ns per pixel). /2 * 2 is to force it to 113 // be an even number, so we can snap in the middle. 114 const bucketNs = 115 Math.max(Math.round(resolutionNs * this.pxSize() / 2) * 2, 1); 116 const freqResult = await this.queryData(startNs, endNs, bucketNs); 117 assertTrue(freqResult.isComplete()); 118 119 const numRows = freqResult.numRows(); 120 const data: Data = { 121 start, 122 end, 123 resolution, 124 length: numRows, 125 maximumValue: this.maximumValue(), 126 maxTsEnd: this.maxTsEndNs, 127 timestamps: new Float64Array(numRows), 128 minFreqKHz: new Uint32Array(numRows), 129 maxFreqKHz: new Uint32Array(numRows), 130 lastFreqKHz: new Uint32Array(numRows), 131 lastIdleValues: new Int8Array(numRows), 132 }; 133 134 const it = freqResult.iter({ 135 'tsq': NUM, 136 'minFreq': NUM, 137 'maxFreq': NUM, 138 'lastFreq': NUM, 139 'lastIdleValue': NUM, 140 }); 141 for (let i = 0; it.valid(); ++i, it.next()) { 142 data.timestamps[i] = fromNs(it.tsq); 143 data.minFreqKHz[i] = it.minFreq; 144 data.maxFreqKHz[i] = it.maxFreq; 145 data.lastFreqKHz[i] = it.lastFreq; 146 data.lastIdleValues[i] = it.lastIdleValue; 147 } 148 149 return data; 150 } 151 152 private async queryData(startNs: number, endNs: number, bucketNs: number): 153 Promise<QueryResult> { 154 const isCached = this.cachedBucketNs <= bucketNs; 155 156 if (isCached) { 157 return this.query(` 158 select 159 cachedTsq / ${bucketNs} * ${bucketNs} as tsq, 160 min(minFreq) as minFreq, 161 max(maxFreq) as maxFreq, 162 value_at_max_ts(cachedTsq, lastFreq) as lastFreq, 163 value_at_max_ts(cachedTsq, lastIdleValue) as lastIdleValue 164 from ${this.tableName('freq_idle_cached')} 165 where 166 cachedTsq >= ${startNs - this.maxDurNs} and 167 cachedTsq <= ${endNs} 168 group by tsq 169 order by tsq 170 `); 171 } 172 const minTsFreq = await this.query(` 173 select ifnull(max(ts), 0) as minTs from ${this.tableName('freq')} 174 where ts < ${startNs} 175 `); 176 177 let minTs = minTsFreq.iter({minTs: NUM}).minTs; 178 if (this.config.idleTrackId !== undefined) { 179 const minTsIdle = await this.query(` 180 select ifnull(max(ts), 0) as minTs from ${this.tableName('idle')} 181 where ts < ${startNs} 182 `); 183 minTs = Math.min(minTsIdle.iter({minTs: NUM}).minTs, minTs); 184 } 185 186 const geqConstraint = this.config.idleTrackId === undefined ? 187 `ts >= ${minTs}` : 188 `source_geq(ts, ${minTs})`; 189 return this.query(` 190 select 191 (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq, 192 min(freqValue) as minFreq, 193 max(freqValue) as maxFreq, 194 value_at_max_ts(ts, freqValue) as lastFreq, 195 value_at_max_ts(ts, idleValue) as lastIdleValue 196 from ${this.tableName('freq_idle')} 197 where 198 ${geqConstraint} and 199 ts <= ${endNs} 200 group by tsq 201 order by tsq 202 `); 203 } 204 205 private async queryMaxFrequency(): Promise<number> { 206 const result = await this.query(` 207 select max(freqValue) as maxFreq 208 from ${this.tableName('freq')} 209 `); 210 return result.firstRow({'maxFreq': NUM_NULL}).maxFreq || 0; 211 } 212 213 private async queryMaxSourceDur(): Promise<number> { 214 const maxDurFreqResult = await this.query( 215 `select ifnull(max(dur), 0) as maxDur from ${this.tableName('freq')}`); 216 const maxDurNs = maxDurFreqResult.firstRow({'maxDur': NUM}).maxDur; 217 if (this.config.idleTrackId === undefined) { 218 return maxDurNs; 219 } 220 221 const maxDurIdleResult = await this.query( 222 `select ifnull(max(dur), 0) as maxDur from ${this.tableName('idle')}`); 223 return Math.max(maxDurNs, maxDurIdleResult.firstRow({maxDur: NUM}).maxDur); 224 } 225 226 private async createFreqIdleViews() { 227 await this.query(`create view ${this.tableName('freq')} as 228 select 229 ts, 230 dur, 231 value as freqValue 232 from experimental_counter_dur c 233 where track_id = ${this.config.freqTrackId}; 234 `); 235 236 if (this.config.idleTrackId === undefined) { 237 await this.query(`create view ${this.tableName('freq_idle')} as 238 select 239 ts, 240 dur, 241 -1 as idleValue, 242 freqValue 243 from ${this.tableName('freq')}; 244 `); 245 return; 246 } 247 248 await this.query(` 249 create view ${this.tableName('idle')} as 250 select 251 ts, 252 dur, 253 iif(value = 4294967295, -1, cast(value as int)) as idleValue 254 from experimental_counter_dur c 255 where track_id = ${this.config.idleTrackId}; 256 `); 257 258 await this.query(` 259 create virtual table ${this.tableName('freq_idle')} 260 using span_join(${this.tableName('freq')}, ${this.tableName('idle')}); 261 `); 262 } 263 264 private maximumValue() { 265 return Math.max(this.config.maximumValue || 0, this.maximumValueSeen); 266 } 267} 268 269// 0.5 Makes the horizontal lines sharp. 270const MARGIN_TOP = 4.5; 271const RECT_HEIGHT = 20; 272 273class CpuFreqTrack extends Track<Config, Data> { 274 static readonly kind = CPU_FREQ_TRACK_KIND; 275 static create(args: NewTrackArgs): CpuFreqTrack { 276 return new CpuFreqTrack(args); 277 } 278 279 private mousePos = {x: 0, y: 0}; 280 private hoveredValue: number|undefined = undefined; 281 private hoveredTs: number|undefined = undefined; 282 private hoveredTsEnd: number|undefined = undefined; 283 private hoveredIdle: number|undefined = undefined; 284 285 constructor(args: NewTrackArgs) { 286 super(args); 287 } 288 289 getHeight() { 290 return MARGIN_TOP + RECT_HEIGHT; 291 } 292 293 renderCanvas(ctx: CanvasRenderingContext2D): void { 294 // TODO: fonts and colors should come from the CSS and not hardcoded here. 295 const { 296 visibleTimeScale, 297 visibleWindowTime, 298 windowSpan, 299 } = globals.frontendLocalState; 300 const data = this.data(); 301 302 if (data === undefined || data.timestamps.length === 0) { 303 // Can't possibly draw anything. 304 return; 305 } 306 307 assertTrue(data.timestamps.length === data.lastFreqKHz.length); 308 assertTrue(data.timestamps.length === data.minFreqKHz.length); 309 assertTrue(data.timestamps.length === data.maxFreqKHz.length); 310 assertTrue(data.timestamps.length === data.lastIdleValues.length); 311 312 const endPx = windowSpan.end; 313 const zeroY = MARGIN_TOP + RECT_HEIGHT; 314 315 // Quantize the Y axis to quarters of powers of tens (7.5K, 10K, 12.5K). 316 let yMax = data.maximumValue; 317 const kUnits = ['', 'K', 'M', 'G', 'T', 'E']; 318 const exp = Math.ceil(Math.log10(Math.max(yMax, 1))); 319 const pow10 = Math.pow(10, exp); 320 yMax = Math.ceil(yMax / (pow10 / 4)) * (pow10 / 4); 321 const unitGroup = Math.floor(exp / 3); 322 const num = yMax / Math.pow(10, unitGroup * 3); 323 // The values we have for cpufreq are in kHz so +1 to unitGroup. 324 const yLabel = `${num} ${kUnits[unitGroup + 1]}Hz`; 325 326 // Draw the CPU frequency graph. 327 const hue = hueForCpu(this.config.cpu); 328 let saturation = 45; 329 if (globals.state.hoveredUtid !== -1) { 330 saturation = 0; 331 } 332 ctx.fillStyle = `hsl(${hue}, ${saturation}%, 70%)`; 333 ctx.strokeStyle = `hsl(${hue}, ${saturation}%, 55%)`; 334 335 const calculateX = (timestamp: number) => { 336 return Math.floor(visibleTimeScale.secondsToPx(timestamp)); 337 }; 338 const calculateY = (value: number) => { 339 return zeroY - Math.round((value / yMax) * RECT_HEIGHT); 340 }; 341 342 const startSec = visibleWindowTime.start.seconds; 343 const endSec = visibleWindowTime.end.seconds; 344 const [rawStartIdx] = searchSegment(data.timestamps, startSec); 345 const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx; 346 347 const [, rawEndIdx] = searchSegment(data.timestamps, endSec); 348 const endIdx = rawEndIdx === -1 ? data.timestamps.length : rawEndIdx; 349 350 ctx.beginPath(); 351 ctx.moveTo(Math.max(calculateX(data.timestamps[startIdx]), 0), zeroY); 352 353 let lastDrawnY = zeroY; 354 for (let i = startIdx; i < endIdx; i++) { 355 const x = calculateX(data.timestamps[i]); 356 357 const minY = calculateY(data.minFreqKHz[i]); 358 const maxY = calculateY(data.maxFreqKHz[i]); 359 const lastY = calculateY(data.lastFreqKHz[i]); 360 361 ctx.lineTo(x, lastDrawnY); 362 if (minY === maxY) { 363 assertTrue(lastY === minY); 364 ctx.lineTo(x, lastY); 365 } else { 366 ctx.lineTo(x, minY); 367 ctx.lineTo(x, maxY); 368 ctx.lineTo(x, lastY); 369 } 370 lastDrawnY = lastY; 371 } 372 // Find the end time for the last frequency event and then draw 373 // down to zero to show that we do not have data after that point. 374 const finalX = Math.min(calculateX(data.maxTsEnd), endPx); 375 ctx.lineTo(finalX, lastDrawnY); 376 ctx.lineTo(finalX, zeroY); 377 ctx.lineTo(endPx, zeroY); 378 ctx.closePath(); 379 ctx.fill(); 380 ctx.stroke(); 381 382 // Draw CPU idle rectangles that overlay the CPU freq graph. 383 ctx.fillStyle = `rgba(240, 240, 240, 1)`; 384 385 for (let i = 0; i < data.lastIdleValues.length; i++) { 386 if (data.lastIdleValues[i] < 0) { 387 continue; 388 } 389 390 // We intentionally don't use the floor function here when computing x 391 // coordinates. Instead we use floating point which prevents flickering as 392 // we pan and zoom; this relies on the browser anti-aliasing pixels 393 // correctly. 394 const x = visibleTimeScale.secondsToPx(data.timestamps[i]); 395 const xEnd = i === data.lastIdleValues.length - 1 ? 396 finalX : 397 visibleTimeScale.secondsToPx(data.timestamps[i + 1]); 398 399 const width = xEnd - x; 400 const height = calculateY(data.lastFreqKHz[i]) - zeroY; 401 402 ctx.fillRect(x, zeroY, width, height); 403 } 404 405 ctx.font = '10px Roboto Condensed'; 406 407 if (this.hoveredValue !== undefined && this.hoveredTs !== undefined) { 408 let text = `${this.hoveredValue.toLocaleString()}kHz`; 409 410 ctx.fillStyle = `hsl(${hue}, 45%, 75%)`; 411 ctx.strokeStyle = `hsl(${hue}, 45%, 45%)`; 412 413 const xStart = Math.floor(visibleTimeScale.secondsToPx(this.hoveredTs)); 414 const xEnd = this.hoveredTsEnd === undefined ? 415 endPx : 416 Math.floor(visibleTimeScale.secondsToPx(this.hoveredTsEnd)); 417 const y = zeroY - Math.round((this.hoveredValue / yMax) * RECT_HEIGHT); 418 419 // Highlight line. 420 ctx.beginPath(); 421 ctx.moveTo(xStart, y); 422 ctx.lineTo(xEnd, y); 423 ctx.lineWidth = 3; 424 ctx.stroke(); 425 ctx.lineWidth = 1; 426 427 // Draw change marker. 428 ctx.beginPath(); 429 ctx.arc( 430 xStart, y, 3 /* r*/, 0 /* start angle*/, 2 * Math.PI /* end angle*/); 431 ctx.fill(); 432 ctx.stroke(); 433 434 // Display idle value if current hover is idle. 435 if (this.hoveredIdle !== undefined && this.hoveredIdle !== -1) { 436 // Display the idle value +1 to be consistent with catapult. 437 text += ` (Idle: ${(this.hoveredIdle + 1).toLocaleString()})`; 438 } 439 440 // Draw the tooltip. 441 this.drawTrackHoverTooltip(ctx, this.mousePos, text); 442 } 443 444 // Write the Y scale on the top left corner. 445 ctx.textBaseline = 'alphabetic'; 446 ctx.fillStyle = 'rgba(255, 255, 255, 0.6)'; 447 ctx.fillRect(0, 0, 42, 18); 448 ctx.fillStyle = '#666'; 449 ctx.textAlign = 'left'; 450 ctx.fillText(`${yLabel}`, 4, 14); 451 452 // If the cached trace slices don't fully cover the visible time range, 453 // show a gray rectangle with a "Loading..." label. 454 checkerboardExcept( 455 ctx, 456 this.getHeight(), 457 windowSpan.start, 458 windowSpan.end, 459 visibleTimeScale.tpTimeToPx(data.start), 460 visibleTimeScale.tpTimeToPx(data.end)); 461 } 462 463 onMouseMove(pos: {x: number, y: number}) { 464 const data = this.data(); 465 if (data === undefined) return; 466 this.mousePos = pos; 467 const {visibleTimeScale} = globals.frontendLocalState; 468 const time = visibleTimeScale.pxToHpTime(pos.x).seconds; 469 470 const [left, right] = searchSegment(data.timestamps, time); 471 this.hoveredTs = left === -1 ? undefined : data.timestamps[left]; 472 this.hoveredTsEnd = right === -1 ? undefined : data.timestamps[right]; 473 this.hoveredValue = left === -1 ? undefined : data.lastFreqKHz[left]; 474 this.hoveredIdle = left === -1 ? undefined : data.lastIdleValues[left]; 475 } 476 477 onMouseOut() { 478 this.hoveredValue = undefined; 479 this.hoveredTs = undefined; 480 this.hoveredTsEnd = undefined; 481 this.hoveredIdle = undefined; 482 } 483} 484 485function activate(ctx: PluginContext) { 486 ctx.registerTrackController(CpuFreqTrackController); 487 ctx.registerTrack(CpuFreqTrack); 488} 489 490export const plugin = { 491 pluginId: 'perfetto.CpuFreq', 492 activate, 493}; 494