• 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} 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