blob: 0f4231dd2a3ea750da3d81537472c4e94c0f7938 [file]
// Copyright (C) 2025 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 type {PerfettoPlugin} from '../../public/plugin';
import type {Trace} from '../../public/trace';
import {STR_NULL} from '../../trace_processor/query_result';
import GpuPlugin from '../dev.perfetto.Gpu';
import TraceProcessorTrackPlugin from '../dev.perfetto.TraceProcessorTrack';
import {TraceProcessorCounterTrack} from '../dev.perfetto.TraceProcessorTrack/trace_processor_counter_track';
export default class implements PerfettoPlugin {
static readonly id = 'com.android.AndroidGpu';
static readonly dependencies = [GpuPlugin, TraceProcessorTrackPlugin];
async onTraceLoad(ctx: Trace): Promise<void> {
// Only apply to Android traces.
const meta = await ctx.engine.query(`
select extract_metadata('android_build_fingerprint') as fingerprint
`);
const fingerprint = meta.firstRow({fingerprint: STR_NULL}).fingerprint;
if (fingerprint === null) return;
// Find gpu_counter tracks registered by the GPU plugin and check if
// they need RateDelta treatment using metadata from their tags.
const gpuCounterTracks = ctx.tracks
.getAllTracks()
.filter((t) => t.tags?.type === 'gpu_counter');
console.log(gpuCounterTracks);
for (const track of gpuCounterTracks) {
const trackIds = track.tags?.trackIds;
if (trackIds === undefined || trackIds.length !== 1) continue;
const trackId = trackIds[0];
const name = String(track.tags?.name ?? '');
const unit = String(track.tags?.unit ?? '');
const description = String(track.tags?.description ?? '');
// If it's not a rate delta, just keep the track as-is.
if (!isRateDelta(name, description, unit)) continue;
// Register a new track with cumulative-sum SQL + rate mode.
const newUri = `/android_gpu_counter_${trackId}`;
ctx.tracks.registerTrack({
uri: newUri,
description: description || undefined,
tags: track.tags,
renderer: new CumulativeSumCounterTrack(
ctx,
newUri,
unit,
trackId,
name,
),
});
// Re-point the existing TrackNode to our new track.
const node = ctx.defaultWorkspace.getTrackByUri(track.uri);
if (node !== undefined) {
node.uri = newUri;
}
}
}
}
// A counter track whose SQL source converts already-delta values into a
// running sum. When combined with yMode 'rate', the base counter track
// will compute (cumsum[t+1] - cumsum[t]) / dt which equals delta / dt,
// giving us the per-second rate we want.
class CumulativeSumCounterTrack extends TraceProcessorCounterTrack {
constructor(
trace: Trace,
uri: string,
unit: string,
tid: number,
trackName: string,
) {
super({
trace,
uri,
yMode: 'rate',
unit,
trackId: tid,
trackName,
rootTable: 'counter',
sqlSource: `
select
id,
ts,
sum(value) over (order by ts) - value as value,
arg_set_id
from counter
where track_id = ${tid}
`,
});
}
}
// Extremely hacky function which determines whether a gpu_counter track should
// use RateDelta interpolation. RateDelta means the raw counter values are
// already deltas and we want to display them as per-second rates. Tracks that
// are NOT RateDelta (i.e. they represent instantaneous values) are left
// unchanged.
//
// This exists entirely because:
// 1) When the protos were designed, a field indicating whether or not a
// was delta encoded was *not* added. This meant it was possible for some
// counters to be delta and others to be monotontic without anyone at
// analysis time knowing about it.
// 2) GPU counters were first added to Perfetto in 2019 timeframe. It's now
// far too late for us to do something better in the trace processor level
// about this as it would break backcompat for lots of people who are
// relying on the existing visualization.
// 3) Because of how AGI used to visualize these counters, there's a general
// expectation that this is the "correct" way to do it. For that reason,
// it's also the case that if we inconsistent with AGI, people will think
// we are doing it wrong even if we are more faithful. To reduce the drift
// and keep things consistent, we have to bow to the "correct" way to do it
// even if it is very hacky.
//
// Ideally at some point, we actually add some indication to the proto if this
// is a delta and that way, we can undo the deltafication in trace processor
// and allowing us to slowly get rid of this hack with time.
function isRateDelta(name: string, description: string, unit: string): boolean {
// Percentage and "per" unit counters are instantaneous.
if (unit === '%' || unit.includes('/')) {
return false;
}
// Arm GPU counters with "per" in name are instantaneous.
if (name.includes(' per ')) return false;
// PowerVR-style counters with certain description patterns are instantaneous.
if (
description.includes('Current ') ||
description.includes(' per ') ||
description.includes(' over ') ||
name.includes('Utilization') ||
description.includes('Percentage')
) {
return false;
}
// Qualcomm-style counters are instantaneous.
if (
description.includes('during a given sample period') ||
name.includes('Average')
) {
return false;
}
// Everything else: values are deltas, display as rate.
return true;
}