xref: /aosp_15_r20/external/perfetto/ui/src/plugins/dev.perfetto.CpuFreq/cpu_freq_track.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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