blob: 9c2f41a52db1f6381b5043461c1617b1a0a8ade5 [file]
// Copyright (C) 2026 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} from '../../../trace_processor/query_result';
import type {Trace} from '../../../public/trace';
import {
expandProcessName,
type MetricHandler,
type ProcessMetricData,
} from './metricUtils';
export class SimpleProcessMetricHandler implements MetricHandler {
/**
* Base class for simple logic track pinning
* Use when you have a Regexp which can extract a process name from an url
* And pin tracks in the found process
*
* @param {RegExp[]} matchers List of matchers for metric keys
* @param {string[]} trackPrefixMatchers Matches track in the process based on prefix
* @param {RegExp[]} trackRegexpMatchers Matches track in the process based on RegExp
*/
constructor(
private readonly matchers: RegExp[],
private readonly trackPrefixMatchers: string[],
private readonly trackRegexpMatchers: RegExp[] = [],
) {}
/**
* Matches metric key & return parsed data if successful.
*
* @param {string} metricKey The metric key to match.
* @returns {ProcessMetricData | undefined} Parsed data or undefined if no match.
*/
public match(metricKey: string): ProcessMetricData | undefined {
for (const matcher of this.matchers) {
const match = matcher.exec(metricKey);
if (match?.groups?.processName) {
return {
process: expandProcessName(match.groups.processName),
};
}
}
return undefined;
}
/**
* Pins matching tracks for the specified process.
*
* @param {ProcessMetricData} metricData Parsed metric data.
* @param {Trace} ctx Trace context.
*/
public async addMetricTrack(metricData: ProcessMetricData, ctx: Trace) {
const processName = metricData.process;
const upid = await getUpidForProcess(ctx, processName);
if (upid === undefined) {
return;
}
// Filter tracks for this process first
const processTracks = ctx.currentWorkspace.flatTracks.filter((track) => {
if (!track.uri) {
return false;
}
const descriptor = ctx.tracks.getTrack(track.uri);
return descriptor?.tags?.upid === upid;
});
const pinnedUris = new Set<string>();
// Pin tracks matching prefix matchers in order.
for (const prefixMatcher of this.trackPrefixMatchers) {
const tracksToPin = processTracks.filter((track) => {
if (pinnedUris.has(track.uri!)) {
return false;
}
return track.name.startsWith(prefixMatcher);
});
tracksToPin.forEach((track) => {
track.pin();
pinnedUris.add(track.uri!);
});
}
// Pin tracks matching regex matchers in order.
for (const regexMatcher of this.trackRegexpMatchers) {
const tracksToPin = processTracks.filter((track) => {
if (pinnedUris.has(track.uri!)) {
return false;
}
return regexMatcher.test(track.name);
});
tracksToPin.forEach((track) => {
track.pin();
pinnedUris.add(track.uri!);
});
}
}
}
async function getUpidForProcess(
ctx: Trace,
processName: string,
): Promise<number | undefined> {
const query = `
INCLUDE PERFETTO MODULE android.process_metadata;
select
_process_available_info_summary.upid
from _process_available_info_summary
join process using(upid)
where process.name = '${processName}'
limit 1;
`;
const res = await ctx.engine.query(query);
if (res.numRows() === 0) {
return undefined;
}
return res.firstRow({upid: NUM}).upid;
}