blob: 8bb2e3b580f680dfed891fa1f1aef45ce9d4f2a2 [file] [log] [blame]
// Copyright (C) 2021 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {searchSegment} from '../../base/binary_search';
import {assertTrue} from '../../base/logging';
import {hueForCpu} from '../../common/colorizer';
import {PluginContext} from '../../common/plugin_api';
import {NUM, NUM_NULL, QueryResult} from '../../common/query_result';
import {fromNs, toNs} from '../../common/time';
import {TrackData} from '../../common/track_data';
import {
TrackController,
} from '../../controller/track_controller';
import {checkerboardExcept} from '../../frontend/checkerboard';
import {globals} from '../../frontend/globals';
import {NewTrackArgs, Track} from '../../frontend/track';
export const CPU_FREQ_TRACK_KIND = 'CpuFreqTrack';
export interface Data extends TrackData {
maximumValue: number;
maxTsEnd: number;
timestamps: Float64Array;
minFreqKHz: Uint32Array;
maxFreqKHz: Uint32Array;
lastFreqKHz: Uint32Array;
lastIdleValues: Int8Array;
}
export interface Config {
cpu: number;
freqTrackId: number;
idleTrackId?: number;
maximumValue?: number;
minimumValue?: number;
}
class CpuFreqTrackController extends TrackController<Config, Data> {
static readonly kind = CPU_FREQ_TRACK_KIND;
private maxDurNs = 0;
private maxTsEndNs = 0;
private maximumValueSeen = 0;
private cachedBucketNs = Number.MAX_SAFE_INTEGER;
async onSetup() {
await this.createFreqIdleViews();
this.maximumValueSeen = await this.queryMaxFrequency();
this.maxDurNs = await this.queryMaxSourceDur();
const iter = (await this.query(`
select max(ts) as maxTs, dur, count(1) as rowCount
from ${this.tableName('freq_idle')}
`)).firstRow({maxTs: NUM_NULL, dur: NUM_NULL, rowCount: NUM});
if (iter.maxTs === null || iter.dur === null) {
// We shoulnd't really hit this because trackDecider shouldn't create
// the track in the first place if there are no entries. But could happen
// if only one cpu has no cpufreq data.
return;
}
this.maxTsEndNs = iter.maxTs + iter.dur;
const rowCount = iter.rowCount;
const bucketNs = this.cachedBucketSizeNs(rowCount);
if (bucketNs === undefined) {
return;
}
await this.query(`
create table ${this.tableName('freq_idle_cached')} as
select
(ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as cachedTsq,
min(freqValue) as minFreq,
max(freqValue) as maxFreq,
value_at_max_ts(ts, freqValue) as lastFreq,
value_at_max_ts(ts, idleValue) as lastIdleValue
from ${this.tableName('freq_idle')}
group by cachedTsq
order by cachedTsq
`);
this.cachedBucketNs = bucketNs;
}
async onBoundsChange(start: number, end: number, resolution: number):
Promise<Data> {
// The resolution should always be a power of two for the logic of this
// function to make sense.
const resolutionNs = toNs(resolution);
assertTrue(Math.log2(resolutionNs) % 1 === 0);
const startNs = toNs(start);
const endNs = toNs(end);
// ns per quantization bucket (i.e. ns per pixel). /2 * 2 is to force it to
// be an even number, so we can snap in the middle.
const bucketNs =
Math.max(Math.round(resolutionNs * this.pxSize() / 2) * 2, 1);
const freqResult = await this.queryData(startNs, endNs, bucketNs);
assertTrue(freqResult.isComplete());
const numRows = freqResult.numRows();
const data: Data = {
start,
end,
resolution,
length: numRows,
maximumValue: this.maximumValue(),
maxTsEnd: this.maxTsEndNs,
timestamps: new Float64Array(numRows),
minFreqKHz: new Uint32Array(numRows),
maxFreqKHz: new Uint32Array(numRows),
lastFreqKHz: new Uint32Array(numRows),
lastIdleValues: new Int8Array(numRows),
};
const it = freqResult.iter({
'tsq': NUM,
'minFreq': NUM,
'maxFreq': NUM,
'lastFreq': NUM,
'lastIdleValue': NUM,
});
for (let i = 0; it.valid(); ++i, it.next()) {
data.timestamps[i] = fromNs(it.tsq);
data.minFreqKHz[i] = it.minFreq;
data.maxFreqKHz[i] = it.maxFreq;
data.lastFreqKHz[i] = it.lastFreq;
data.lastIdleValues[i] = it.lastIdleValue;
}
return data;
}
private async queryData(startNs: number, endNs: number, bucketNs: number):
Promise<QueryResult> {
const isCached = this.cachedBucketNs <= bucketNs;
if (isCached) {
return this.query(`
select
cachedTsq / ${bucketNs} * ${bucketNs} as tsq,
min(minFreq) as minFreq,
max(maxFreq) as maxFreq,
value_at_max_ts(cachedTsq, lastFreq) as lastFreq,
value_at_max_ts(cachedTsq, lastIdleValue) as lastIdleValue
from ${this.tableName('freq_idle_cached')}
where
cachedTsq >= ${startNs - this.maxDurNs} and
cachedTsq <= ${endNs}
group by tsq
order by tsq
`);
}
const minTsFreq = await this.query(`
select ifnull(max(ts), 0) as minTs from ${this.tableName('freq')}
where ts < ${startNs}
`);
let minTs = minTsFreq.iter({minTs: NUM}).minTs;
if (this.config.idleTrackId !== undefined) {
const minTsIdle = await this.query(`
select ifnull(max(ts), 0) as minTs from ${this.tableName('idle')}
where ts < ${startNs}
`);
minTs = Math.min(minTsIdle.iter({minTs: NUM}).minTs, minTs);
}
const geqConstraint = this.config.idleTrackId === undefined ?
`ts >= ${minTs}` :
`source_geq(ts, ${minTs})`;
return this.query(`
select
(ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
min(freqValue) as minFreq,
max(freqValue) as maxFreq,
value_at_max_ts(ts, freqValue) as lastFreq,
value_at_max_ts(ts, idleValue) as lastIdleValue
from ${this.tableName('freq_idle')}
where
${geqConstraint} and
ts <= ${endNs}
group by tsq
order by tsq
`);
}
private async queryMaxFrequency(): Promise<number> {
const result = await this.query(`
select max(freqValue) as maxFreq
from ${this.tableName('freq')}
`);
return result.firstRow({'maxFreq': NUM_NULL}).maxFreq || 0;
}
private async queryMaxSourceDur(): Promise<number> {
const maxDurFreqResult = await this.query(
`select ifnull(max(dur), 0) as maxDur from ${this.tableName('freq')}`);
const maxDurNs = maxDurFreqResult.firstRow({'maxDur': NUM}).maxDur;
if (this.config.idleTrackId === undefined) {
return maxDurNs;
}
const maxDurIdleResult = await this.query(
`select ifnull(max(dur), 0) as maxDur from ${this.tableName('idle')}`);
return Math.max(maxDurNs, maxDurIdleResult.firstRow({maxDur: NUM}).maxDur);
}
private async createFreqIdleViews() {
await this.query(`create view ${this.tableName('freq')} as
select
ts,
dur,
value as freqValue
from experimental_counter_dur c
where track_id = ${this.config.freqTrackId};
`);
if (this.config.idleTrackId === undefined) {
await this.query(`create view ${this.tableName('freq_idle')} as
select
ts,
dur,
-1 as idleValue,
freqValue
from ${this.tableName('freq')};
`);
return;
}
await this.query(`
create view ${this.tableName('idle')} as
select
ts,
dur,
iif(value = 4294967295, -1, cast(value as int)) as idleValue
from experimental_counter_dur c
where track_id = ${this.config.idleTrackId};
`);
await this.query(`
create virtual table ${this.tableName('freq_idle')}
using span_join(${this.tableName('freq')}, ${this.tableName('idle')});
`);
}
private maximumValue() {
return Math.max(this.config.maximumValue || 0, this.maximumValueSeen);
}
}
// 0.5 Makes the horizontal lines sharp.
const MARGIN_TOP = 4.5;
const RECT_HEIGHT = 20;
class CpuFreqTrack extends Track<Config, Data> {
static readonly kind = CPU_FREQ_TRACK_KIND;
static create(args: NewTrackArgs): CpuFreqTrack {
return new CpuFreqTrack(args);
}
private mousePos = {x: 0, y: 0};
private hoveredValue: number|undefined = undefined;
private hoveredTs: number|undefined = undefined;
private hoveredTsEnd: number|undefined = undefined;
private hoveredIdle: number|undefined = undefined;
constructor(args: NewTrackArgs) {
super(args);
}
getHeight() {
return MARGIN_TOP + RECT_HEIGHT;
}
renderCanvas(ctx: CanvasRenderingContext2D): void {
// TODO: fonts and colors should come from the CSS and not hardcoded here.
const {timeScale, visibleWindowTime} = globals.frontendLocalState;
const data = this.data();
if (data === undefined || data.timestamps.length === 0) {
// Can't possibly draw anything.
return;
}
assertTrue(data.timestamps.length === data.lastFreqKHz.length);
assertTrue(data.timestamps.length === data.minFreqKHz.length);
assertTrue(data.timestamps.length === data.maxFreqKHz.length);
assertTrue(data.timestamps.length === data.lastIdleValues.length);
const endPx = timeScale.timeToPx(visibleWindowTime.end);
const zeroY = MARGIN_TOP + RECT_HEIGHT;
// Quantize the Y axis to quarters of powers of tens (7.5K, 10K, 12.5K).
let yMax = data.maximumValue;
const kUnits = ['', 'K', 'M', 'G', 'T', 'E'];
const exp = Math.ceil(Math.log10(Math.max(yMax, 1)));
const pow10 = Math.pow(10, exp);
yMax = Math.ceil(yMax / (pow10 / 4)) * (pow10 / 4);
const unitGroup = Math.floor(exp / 3);
const num = yMax / Math.pow(10, unitGroup * 3);
// The values we have for cpufreq are in kHz so +1 to unitGroup.
const yLabel = `${num} ${kUnits[unitGroup + 1]}Hz`;
// Draw the CPU frequency graph.
const hue = hueForCpu(this.config.cpu);
let saturation = 45;
if (globals.state.hoveredUtid !== -1) {
saturation = 0;
}
ctx.fillStyle = `hsl(${hue}, ${saturation}%, 70%)`;
ctx.strokeStyle = `hsl(${hue}, ${saturation}%, 55%)`;
const calculateX = (timestamp: number) => {
return Math.floor(timeScale.timeToPx(timestamp));
};
const calculateY = (value: number) => {
return zeroY - Math.round((value / yMax) * RECT_HEIGHT);
};
const [rawStartIdx] =
searchSegment(data.timestamps, visibleWindowTime.start);
const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx;
const [, rawEndIdx] = searchSegment(data.timestamps, visibleWindowTime.end);
const endIdx = rawEndIdx === -1 ? data.timestamps.length : rawEndIdx;
ctx.beginPath();
ctx.moveTo(Math.max(calculateX(data.timestamps[startIdx]), 0), zeroY);
let lastDrawnY = zeroY;
for (let i = startIdx; i < endIdx; i++) {
const x = calculateX(data.timestamps[i]);
const minY = calculateY(data.minFreqKHz[i]);
const maxY = calculateY(data.maxFreqKHz[i]);
const lastY = calculateY(data.lastFreqKHz[i]);
ctx.lineTo(x, lastDrawnY);
if (minY === maxY) {
assertTrue(lastY === minY);
ctx.lineTo(x, lastY);
} else {
ctx.lineTo(x, minY);
ctx.lineTo(x, maxY);
ctx.lineTo(x, lastY);
}
lastDrawnY = lastY;
}
// Find the end time for the last frequency event and then draw
// down to zero to show that we do not have data after that point.
const finalX = Math.min(calculateX(data.maxTsEnd), endPx);
ctx.lineTo(finalX, lastDrawnY);
ctx.lineTo(finalX, zeroY);
ctx.lineTo(endPx, zeroY);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Draw CPU idle rectangles that overlay the CPU freq graph.
ctx.fillStyle = `rgba(240, 240, 240, 1)`;
for (let i = 0; i < data.lastIdleValues.length; i++) {
if (data.lastIdleValues[i] < 0) {
continue;
}
// We intentionally don't use the floor function here when computing x
// coordinates. Instead we use floating point which prevents flickering as
// we pan and zoom; this relies on the browser anti-aliasing pixels
// correctly.
const x = timeScale.timeToPx(data.timestamps[i]);
const xEnd = i === data.lastIdleValues.length - 1 ?
finalX :
timeScale.timeToPx(data.timestamps[i + 1]);
const width = xEnd - x;
const height = calculateY(data.lastFreqKHz[i]) - zeroY;
ctx.fillRect(x, zeroY, width, height);
}
ctx.font = '10px Roboto Condensed';
if (this.hoveredValue !== undefined && this.hoveredTs !== undefined) {
let text = `${this.hoveredValue.toLocaleString()}kHz`;
ctx.fillStyle = `hsl(${hue}, 45%, 75%)`;
ctx.strokeStyle = `hsl(${hue}, 45%, 45%)`;
const xStart = Math.floor(timeScale.timeToPx(this.hoveredTs));
const xEnd = this.hoveredTsEnd === undefined ?
endPx :
Math.floor(timeScale.timeToPx(this.hoveredTsEnd));
const y = zeroY - Math.round((this.hoveredValue / yMax) * RECT_HEIGHT);
// Highlight line.
ctx.beginPath();
ctx.moveTo(xStart, y);
ctx.lineTo(xEnd, y);
ctx.lineWidth = 3;
ctx.stroke();
ctx.lineWidth = 1;
// Draw change marker.
ctx.beginPath();
ctx.arc(
xStart, y, 3 /* r*/, 0 /* start angle*/, 2 * Math.PI /* end angle*/);
ctx.fill();
ctx.stroke();
// Display idle value if current hover is idle.
if (this.hoveredIdle !== undefined && this.hoveredIdle !== -1) {
// Display the idle value +1 to be consistent with catapult.
text += ` (Idle: ${(this.hoveredIdle + 1).toLocaleString()})`;
}
// Draw the tooltip.
this.drawTrackHoverTooltip(ctx, this.mousePos, text);
}
// Write the Y scale on the top left corner.
ctx.textBaseline = 'alphabetic';
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
ctx.fillRect(0, 0, 42, 18);
ctx.fillStyle = '#666';
ctx.textAlign = 'left';
ctx.fillText(`${yLabel}`, 4, 14);
// If the cached trace slices don't fully cover the visible time range,
// show a gray rectangle with a "Loading..." label.
checkerboardExcept(
ctx,
this.getHeight(),
timeScale.timeToPx(visibleWindowTime.start),
timeScale.timeToPx(visibleWindowTime.end),
timeScale.timeToPx(data.start),
timeScale.timeToPx(data.end));
}
onMouseMove(pos: {x: number, y: number}) {
const data = this.data();
if (data === undefined) return;
this.mousePos = pos;
const {timeScale} = globals.frontendLocalState;
const time = timeScale.pxToTime(pos.x);
const [left, right] = searchSegment(data.timestamps, time);
this.hoveredTs = left === -1 ? undefined : data.timestamps[left];
this.hoveredTsEnd = right === -1 ? undefined : data.timestamps[right];
this.hoveredValue = left === -1 ? undefined : data.lastFreqKHz[left];
this.hoveredIdle = left === -1 ? undefined : data.lastIdleValues[left];
}
onMouseOut() {
this.hoveredValue = undefined;
this.hoveredTs = undefined;
this.hoveredTsEnd = undefined;
this.hoveredIdle = undefined;
}
}
function activate(ctx: PluginContext) {
ctx.registerTrackController(CpuFreqTrackController);
ctx.registerTrack(CpuFreqTrack);
}
export const plugin = {
pluginId: 'perfetto.CpuFreq',
activate,
};