blob: bebbb3fd78771b1184b0693d25a079e751f59981 [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 type {Trace} from '../../public/trace';
import {
LONG_NULL,
NUM_NULL,
STR_NULL,
type Row,
type SqlValue,
} from '../../trace_processor/query_result';
import {type duration, Time, Duration} from '../../base/time';
import {
getTrackUriForTrackId,
enrichDepths,
} from '../../components/related_events/utils';
import {QuerySlot, type QueryResult} from '../../base/query_slot';
import type {
InputLifecycleExtension,
CellData,
NavTarget,
StageDefinition,
} from './extensions/interface';
export interface InputChainRow {
uiRowId: string;
inputEventId: string | null;
channel: string;
totalLatency: duration | null;
stagesData: Map<string, CellData>;
allTrackUris: string[];
}
interface InputLifecycleSpec extends Row {
input_id: string | null;
channel: string | null;
total_latency: bigint | null;
[key: string]: SqlValue;
}
export class AndroidInputEventSource {
private readonly dataSlot = new QuerySlot<InputChainRow[]>();
constructor(
private readonly trace: Trace,
private readonly activeExtensions: ReadonlyArray<InputLifecycleExtension>,
) {}
use(sliceId: number): QueryResult<InputChainRow[]> {
return this.dataSlot.use({
key: {sliceId},
queryFn: async () => {
const resolvedSliceId = await this.resolveSliceId(sliceId);
const rows = await this.fetchRows(resolvedSliceId);
await this.enrichAllDepths(rows);
return rows;
},
});
}
/**
* Resolves an extension-specific slice ID back to a core slice ID.
*
* If the clicked slice belongs to an extension, we resolve it to the framework
* input event ID, and then look up the corresponding 'InputReader' slice ID.
* This core slice ID is used to "root" the core lifecycle query.
*/
private async resolveSliceId(sliceId: number): Promise<number> {
for (const ext of this.activeExtensions) {
if (!ext.resolveInputId) continue;
const inputId = await ext.resolveInputId(this.trace, sliceId);
if (!inputId) continue;
// Map the framework input ID to the core InputReader notifyMotion slice ID.
const coreSliceResult = await this.trace.engine.query(`
SELECT s.id
FROM android_input_events e
JOIN slice s ON s.ts = e.read_time AND s.track_id != 0
WHERE e.input_event_id = '${inputId}'
LIMIT 1
`);
const it = coreSliceResult.iter({id: NUM_NULL});
if (it.valid() && it.id !== null) {
return it.id;
}
}
return sliceId;
}
/**
* Compiles the full sequence of lifecycle stage specifications by combining
* the core framework stages with those injected by active extensions, and
* sorting them chronologically based on their sequence numbers.
*/
static getStageSpecs(
activeExtensions: ReadonlyArray<InputLifecycleExtension>,
): StageDefinition[] {
const specs = [...CORE_STAGES];
for (const ext of activeExtensions) {
for (const stage of ext.getStages()) {
specs.push({
...stage,
key: `${ext.id}-${stage.key}`,
});
}
}
return specs.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
}
getStageSpecs(): StageDefinition[] {
return AndroidInputEventSource.getStageSpecs(this.activeExtensions);
}
private async fetchRows(sliceId: number): Promise<InputChainRow[]> {
const baseQuery = `SELECT * FROM _android_input_lifecycle_by_slice_id(${sliceId})`;
let selectCols = 'core.*, a_evt.event_time';
let joinSql = '';
for (const ext of this.activeExtensions) {
const spec = ext.getSqlJoinSpec();
const alias = spec.tableAlias ?? spec.tableName;
selectCols += `, ${alias}.*`;
joinSql += ` LEFT JOIN ${spec.tableName}${spec.tableAlias ? ` AS ${spec.tableAlias}` : ''} ON ${spec.joinOn}`;
}
const sql = `
SELECT ${selectCols}
FROM (${baseQuery}) core
LEFT JOIN android_input_events a_evt ON
core.input_id = a_evt.input_event_id AND
core.channel = a_evt.event_channel
${joinSql}
`;
const result = await this.trace.engine.query(sql);
const rows: InputChainRow[] = [];
let index = 0;
const stages = this.getStageSpecs();
const spec: InputLifecycleSpec = {
input_id: STR_NULL,
channel: STR_NULL,
total_latency: LONG_NULL,
};
for (const stage of stages) {
spec[stage.idField] = NUM_NULL;
spec[stage.trackField] = NUM_NULL;
spec[stage.tsField] = LONG_NULL;
spec[stage.durField] = LONG_NULL;
}
const it = result.iter(spec);
while (it.valid()) {
const stagesData = new Map<string, CellData>();
const allTrackUris: string[] = [];
for (const stage of stages) {
const id = it.get(stage.idField) as number | null;
const trackId = it.get(stage.trackField) as number | null;
const ts = it.get(stage.tsField) as bigint | null;
const dur = (it.get(stage.durField) as bigint | null) ?? 0n;
const cellDur =
it.get(stage.durField) !== null ? Duration.fromRaw(dur) : null;
let nav: NavTarget | undefined = undefined;
if (id !== null && trackId !== null && ts !== null) {
nav = {
id,
trackUri: getTrackUriForTrackId(this.trace, trackId),
ts: Time.fromRaw(ts),
dur: Duration.fromRaw(dur),
depth: 0,
};
allTrackUris.push(nav.trackUri);
}
stagesData.set(stage.key, {dur: cellDur, nav});
}
// TODO(ivankc) Consider how to properly handle this in the context of extensions.
const totalLatency =
it.total_latency !== null ? Duration.fromRaw(it.total_latency) : null;
rows.push({
uiRowId: `row-${index++}`,
inputEventId: it.input_id,
channel: it.channel ?? '',
totalLatency,
stagesData,
allTrackUris,
});
it.next();
}
return rows;
}
private async enrichAllDepths(rows: InputChainRow[]) {
const targets: NavTarget[] = [];
for (const row of rows) {
for (const stageData of row.stagesData.values()) {
if (stageData.nav) {
targets.push(stageData.nav);
}
}
}
if (targets.length === 0) return;
await enrichDepths(this.trace, targets);
}
}
const CORE_STAGES: StageDefinition[] = [
{
key: 'read',
headerName: 'InputReader',
sequenceNumber: 1000,
idField: 'id_reader',
trackField: 'track_reader',
tsField: 'ts_reader',
durField: 'dur_reader',
},
{
key: 'disp',
headerName: 'Dispatcher',
sequenceNumber: 2000,
idField: 'id_dispatch',
trackField: 'track_dispatch',
tsField: 'ts_dispatch',
durField: 'dur_dispatch',
},
{
key: 'recv',
headerName: 'App Receive',
sequenceNumber: 3000,
idField: 'id_receive',
trackField: 'track_receive',
tsField: 'ts_receive',
durField: 'dur_receive',
},
{
key: 'cons',
headerName: 'App Consume',
sequenceNumber: 4000,
idField: 'id_consume',
trackField: 'track_consume',
tsField: 'ts_consume',
durField: 'dur_consume',
},
{
key: 'frame',
headerName: 'App Frame',
sequenceNumber: 5000,
idField: 'id_frame',
trackField: 'track_frame',
tsField: 'ts_frame',
durField: 'dur_frame',
},
];