blob: ef063ca4ce9c56533d8890ce8aef70a582a33945 [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 {
NUM_NULL,
STR_NULL,
LONG_NULL,
NUM,
STR,
} from '../../trace_processor/query_result';
import {Trace} from '../../public/trace';
import {COUNTER_TRACK_KIND} from '../../public/track_kinds';
import {PerfettoPlugin} from '../../public/plugin';
import {getThreadUriPrefix, getTrackName} from '../../public/utils';
import {CounterOptions} from '../../frontend/base_counter_track';
import {TraceProcessorCounterTrack} from './trace_processor_counter_track';
import {exists} from '../../base/utils';
import {TrackNode} from '../../public/workspace';
import {
getOrCreateGroupForProcess,
getOrCreateGroupForThread,
} from '../../public/standard_groups';
import {CounterSelectionAggregator} from './counter_selection_aggregator';
const NETWORK_TRACK_REGEX = new RegExp('^.* (Received|Transmitted)( KB)?$');
const ENTITY_RESIDENCY_REGEX = new RegExp('^Entity residency:');
type Modes = CounterOptions['yMode'];
// Sets the default 'mode' for counter tracks. If the regex matches
// then the paired mode is used. Entries are in priority order so the
// first match wins.
const COUNTER_REGEX: [RegExp, Modes][] = [
// Power counters make more sense in rate mode since you're typically
// interested in the slope of the graph rather than the absolute
// value.
[new RegExp('^power..*$'), 'rate'],
// Same for cumulative PSI stall time counters, e.g., psi.cpu.some.
[new RegExp('^psi..*$'), 'rate'],
// Same for network counters.
[NETWORK_TRACK_REGEX, 'rate'],
// Entity residency
[ENTITY_RESIDENCY_REGEX, 'rate'],
];
function getCounterMode(name: string): Modes | undefined {
for (const [re, mode] of COUNTER_REGEX) {
if (name.match(re)) {
return mode;
}
}
return undefined;
}
function getDefaultCounterOptions(name: string): Partial<CounterOptions> {
const options: Partial<CounterOptions> = {};
options.yMode = getCounterMode(name);
if (name.endsWith('_pct')) {
options.yOverrideMinimum = 0;
options.yOverrideMaximum = 100;
options.unit = '%';
}
if (name.startsWith('power.')) {
options.yRangeSharingKey = 'power';
}
// TODO(stevegolton): We need to rethink how this works for virtual memory.
// The problem is we can easily have > 10GB virtual memory which dwarfs
// physical memory making other memory tracks difficult to read.
// if (name.startsWith('mem.')) {
// options.yRangeSharingKey = 'mem';
// }
// All 'Entity residency: foo bar1234' tracks should share a y-axis
// with 'Entity residency: foo baz5678' etc tracks:
{
const r = new RegExp('Entity residency: ([^ ]+) ');
const m = r.exec(name);
if (m) {
options.yRangeSharingKey = `entity-residency-${m[1]}`;
}
}
{
const r = new RegExp('GPU .* Frequency');
const m = r.exec(name);
if (m) {
options.yRangeSharingKey = 'gpu-frequency';
}
}
return options;
}
export default class implements PerfettoPlugin {
static readonly id = 'dev.perfetto.Counter';
async onTraceLoad(ctx: Trace): Promise<void> {
await this.addCounterTracks(ctx);
await this.addGpuFrequencyTracks(ctx);
await this.addCpuFreqLimitCounterTracks(ctx);
await this.addCpuTimeCounterTracks(ctx);
await this.addCpuPerfCounterTracks(ctx);
await this.addThreadCounterTracks(ctx);
await this.addProcessCounterTracks(ctx);
ctx.selection.registerAreaSelectionAggreagtor(
new CounterSelectionAggregator(),
);
}
private async addCounterTracks(ctx: Trace) {
const result = await ctx.engine.query(`
select name, id, unit
from (
select name, id, unit
from counter_track
join _counter_track_summary using (id)
where type = 'counter_track'
union
select name, id, unit
from gpu_counter_track
join _counter_track_summary using (id)
where name != 'gpufreq'
)
order by name
`);
// Add global or GPU counter tracks that are not bound to any pid/tid.
const it = result.iter({
name: STR,
unit: STR_NULL,
id: NUM,
});
for (; it.valid(); it.next()) {
const trackId = it.id;
const title = it.name;
const unit = it.unit ?? undefined;
const uri = `/counter_${trackId}`;
ctx.tracks.registerTrack({
uri,
title,
tags: {
kind: COUNTER_TRACK_KIND,
trackIds: [trackId],
},
track: new TraceProcessorCounterTrack(
ctx,
uri,
{
...getDefaultCounterOptions(title),
unit,
},
trackId,
title,
),
});
const track = new TrackNode({uri, title});
ctx.workspace.addChildInOrder(track);
}
}
async addCpuFreqLimitCounterTracks(ctx: Trace): Promise<void> {
const cpuFreqLimitCounterTracksSql = `
select name, id
from cpu_counter_track
join _counter_track_summary using (id)
where name glob "Cpu * Freq Limit"
order by name asc
`;
this.addCpuCounterTracks(ctx, cpuFreqLimitCounterTracksSql, 'cpuFreqLimit');
}
async addCpuTimeCounterTracks(ctx: Trace): Promise<void> {
const cpuTimeCounterTracksSql = `
select name, id
from cpu_counter_track
join _counter_track_summary using (id)
where name glob "cpu.times.*"
order by name asc
`;
this.addCpuCounterTracks(ctx, cpuTimeCounterTracksSql, 'cpuTime');
}
async addCpuPerfCounterTracks(ctx: Trace): Promise<void> {
// Perf counter tracks are bound to CPUs, follow the scheduling and
// frequency track naming convention ("Cpu N ...").
// Note: we might not have a track for a given cpu if no data was seen from
// it. This might look surprising in the UI, but placeholder tracks are
// wasteful as there's no way of collapsing global counter tracks at the
// moment.
const addCpuPerfCounterTracksSql = `
select printf("Cpu %u %s", cpu, name) as name, id
from perf_counter_track as pct
join _counter_track_summary using (id)
order by perf_session_id asc, pct.name asc, cpu asc
`;
this.addCpuCounterTracks(ctx, addCpuPerfCounterTracksSql, 'cpuPerf');
}
async addCpuCounterTracks(
ctx: Trace,
sql: string,
scope: string,
): Promise<void> {
const result = await ctx.engine.query(sql);
const it = result.iter({
name: STR,
id: NUM,
});
for (; it.valid(); it.next()) {
const name = it.name;
const trackId = it.id;
const uri = `counter.cpu.${trackId}`;
ctx.tracks.registerTrack({
uri,
title: name,
tags: {
kind: COUNTER_TRACK_KIND,
trackIds: [trackId],
scope,
},
track: new TraceProcessorCounterTrack(
ctx,
uri,
getDefaultCounterOptions(name),
trackId,
name,
),
});
const trackNode = new TrackNode({uri, title: name, sortOrder: -20});
ctx.workspace.addChildInOrder(trackNode);
}
}
async addThreadCounterTracks(ctx: Trace): Promise<void> {
const result = await ctx.engine.query(`
select
thread_counter_track.name as trackName,
utid,
upid,
tid,
thread.name as threadName,
thread_counter_track.id as trackId,
thread.start_ts as startTs,
thread.end_ts as endTs
from thread_counter_track
join _counter_track_summary using (id)
join thread using(utid)
where thread_counter_track.name != 'thread_time'
`);
const it = result.iter({
startTs: LONG_NULL,
trackId: NUM,
endTs: LONG_NULL,
trackName: STR_NULL,
utid: NUM,
upid: NUM_NULL,
tid: NUM_NULL,
threadName: STR_NULL,
});
for (; it.valid(); it.next()) {
const utid = it.utid;
const upid = it.upid;
const tid = it.tid;
const trackId = it.trackId;
const trackName = it.trackName;
const threadName = it.threadName;
const kind = COUNTER_TRACK_KIND;
const name = getTrackName({
name: trackName,
utid,
tid,
kind,
threadName,
threadTrack: true,
});
const uri = `${getThreadUriPrefix(upid, utid)}_counter_${trackId}`;
ctx.tracks.registerTrack({
uri,
title: name,
tags: {
kind,
trackIds: [trackId],
utid,
upid: upid ?? undefined,
scope: 'thread',
},
track: new TraceProcessorCounterTrack(
ctx,
uri,
getDefaultCounterOptions(name),
trackId,
name,
),
});
const group = getOrCreateGroupForThread(ctx.workspace, utid);
const track = new TrackNode({uri, title: name, sortOrder: 30});
group.addChildInOrder(track);
}
}
async addProcessCounterTracks(ctx: Trace): Promise<void> {
const result = await ctx.engine.query(`
select
process_counter_track.id as trackId,
process_counter_track.name as trackName,
upid,
process.pid,
process.name as processName
from process_counter_track
join _counter_track_summary using (id)
join process using(upid);
`);
const it = result.iter({
trackId: NUM,
trackName: STR_NULL,
upid: NUM,
pid: NUM_NULL,
processName: STR_NULL,
});
for (let i = 0; it.valid(); ++i, it.next()) {
const trackId = it.trackId;
const pid = it.pid;
const trackName = it.trackName;
const upid = it.upid;
const processName = it.processName;
const kind = COUNTER_TRACK_KIND;
const name = getTrackName({
name: trackName,
upid,
pid,
kind,
processName,
...(exists(trackName) && {trackName}),
});
const uri = `/process_${upid}/counter_${trackId}`;
ctx.tracks.registerTrack({
uri,
title: name,
tags: {
kind,
trackIds: [trackId],
upid,
scope: 'process',
},
track: new TraceProcessorCounterTrack(
ctx,
uri,
getDefaultCounterOptions(name),
trackId,
name,
),
});
const group = getOrCreateGroupForProcess(ctx.workspace, upid);
const track = new TrackNode({uri, title: name, sortOrder: 20});
group.addChildInOrder(track);
}
}
private async addGpuFrequencyTracks(ctx: Trace) {
const engine = ctx.engine;
const result = await engine.query(`
select id, gpu_id as gpuId
from gpu_counter_track
join _counter_track_summary using (id)
where name = 'gpufreq'
`);
const it = result.iter({id: NUM, gpuId: NUM});
for (; it.valid(); it.next()) {
const uri = `/gpu_frequency_${it.gpuId}`;
const name = `Gpu ${it.gpuId} Frequency`;
ctx.tracks.registerTrack({
uri,
title: name,
tags: {
kind: COUNTER_TRACK_KIND,
trackIds: [it.id],
scope: 'gpuFreq',
},
track: new TraceProcessorCounterTrack(
ctx,
uri,
getDefaultCounterOptions(name),
it.id,
name,
),
});
const track = new TrackNode({uri, title: name, sortOrder: -20});
ctx.workspace.addChildInOrder(track);
}
}
}