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