blob: b4b5ca09c4f723619064f43313f9c402a426996c [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 m from 'mithril';
import {Icons} from '../../base/semantic_icons';
import type {
AggregatePivotModel,
Aggregation,
Aggregator,
} from '../../components/aggregation_adapter';
import type {AreaSelection} from '../../public/selection';
import type {Trace} from '../../public/trace';
import {ANDROID_LOGS_TRACK_KIND} from '../../public/track_kinds';
import type {Engine} from '../../trace_processor/engine';
import {Anchor} from '../../widgets/anchor';
export class AndroidLogSelectionAggregator implements Aggregator {
readonly id = 'android_log_aggregation';
constructor(private readonly trace: Trace) {}
probe(area: AreaSelection): Aggregation | undefined {
const logTracks = area.tracks.filter((t) =>
t.tags?.kinds?.includes(ANDROID_LOGS_TRACK_KIND),
);
if (logTracks.length === 0) return undefined;
const utids = logTracks
.map((t) => t.tags?.utid as number | undefined)
.filter((u): u is number => u !== undefined);
return {
prepareData: async (engine: Engine) => {
let whereClause = `al.ts >= ${area.start} AND al.ts <= ${area.end}`;
if (utids.length > 0) {
whereClause += ` AND al.utid IN (${utids.join(', ')})`;
}
await engine.query(`
CREATE OR REPLACE PERFETTO TABLE ${this.id} AS
SELECT
al.id,
al.ts,
al.prio,
al.tag,
al.msg,
t.tid,
t.name AS thread_name,
p.pid,
p.name AS process_name
FROM android_logs al
LEFT JOIN thread t ON al.utid = t.utid
LEFT JOIN process p ON t.upid = p.upid
WHERE ${whereClause}
`);
return {tableName: this.id};
},
};
}
getTabName() {
return 'Android Logs';
}
getColumnDefinitions(): AggregatePivotModel {
return {
groupBy: [
{id: 'tag', field: 'tag'},
{id: 'prio', field: 'prio'},
],
aggregates: [{id: 'count', function: 'COUNT'}],
columns: [
{
title: 'ID',
columnId: 'id',
formatHint: 'NUMERIC',
cellRenderer: (value) => {
const id = typeof value === 'bigint' ? Number(value) : value;
if (typeof id !== 'number') return String(value);
return m(
Anchor,
{
icon: Icons.UpdateSelection,
onclick: () => {
this.trace.selection.selectSqlEvent('android_logs', id, {
scrollToSelection: true,
});
},
},
String(id),
);
},
},
{title: 'Timestamp', columnId: 'ts', formatHint: 'TIMESTAMP_NS'},
{title: 'Priority', columnId: 'prio', formatHint: 'NUMERIC'},
{title: 'Tag', columnId: 'tag', formatHint: 'STRING'},
{title: 'Message', columnId: 'msg', formatHint: 'STRING'},
{title: 'TID', columnId: 'tid', formatHint: 'NUMERIC'},
{title: 'Thread', columnId: 'thread_name', formatHint: 'STRING'},
{title: 'PID', columnId: 'pid', formatHint: 'NUMERIC'},
{title: 'Process', columnId: 'process_name', formatHint: 'STRING'},
],
};
}
}