• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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