blob: cf829b03ae8d7e1e373e1dc50118df27a4c2ceaa [file] [edit]
// 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 {
type GridColumn,
GridHeaderCell,
Grid,
GridCell,
} from '../../widgets/grid';
import type {TrackPinningManager} from '../../components/related_events/utils';
import {Icons} from '../../base/semantic_icons';
import type {duration} from '../../base/time';
import {DurationWidget} from '../../components/widgets/duration';
import type {Trace} from '../../public/trace';
import {Anchor} from '../../widgets/anchor';
import {Checkbox} from '../../widgets/checkbox';
import {EmptyState} from '../../widgets/empty_state';
import {DetailsShell} from '../../widgets/details_shell';
import {Spinner} from '../../widgets/spinner';
import type {InputChainRow, NavTarget} from './android_input_event_source';
export interface AndroidInputLifecycleTabAttrs {
trace: Trace;
rows: InputChainRow[];
visibleRowIds: Set<string>;
loading: boolean;
pinningManager: TrackPinningManager;
onToggleVisibility: (rowId: string) => void;
onToggleAllVisibility: () => void;
}
export class AndroidInputLifecycleTab
implements m.ClassComponent<AndroidInputLifecycleTabAttrs>
{
view({attrs}: m.Vnode<AndroidInputLifecycleTabAttrs>): m.Children {
if (attrs.loading) {
return m(
DetailsShell,
{title: 'Android Input Lifecycle'},
m(
'div',
{
style: {
display: 'flex',
justifyContent: 'center',
padding: '20px',
},
},
m(Spinner, {}),
),
);
}
return m(
DetailsShell,
{title: 'Android Input Lifecycle'},
this.renderGrid(attrs),
);
}
private renderGrid(attrs: AndroidInputLifecycleTabAttrs): m.Children {
const {rows, visibleRowIds, trace, pinningManager} = attrs;
const allVisible =
rows.length > 0 && rows.every((r) => visibleRowIds.has(r.uiRowId));
const columns: GridColumn[] = [
{
key: 'show',
widthPx: 40,
header: m(
GridHeaderCell,
{},
m(Checkbox, {
checked: allVisible,
onchange: () => attrs.onToggleAllVisibility(),
}),
),
},
{
key: 'pin',
widthPx: 40,
header: m(GridHeaderCell, {}, 'Pin'),
},
{key: 'chan', header: m(GridHeaderCell, {}, 'Channel')},
{
key: 'total',
minWidthPx: 100,
header: m(GridHeaderCell, {}, 'Total Latency'),
},
{
key: 'read',
minWidthPx: 100,
header: m(GridHeaderCell, {}, 'InputReader'),
},
{
key: 'disp',
minWidthPx: 100,
header: m(GridHeaderCell, {}, 'Dispatcher'),
},
{
key: 'recv',
minWidthPx: 100,
header: m(GridHeaderCell, {}, 'App Receive'),
},
{
key: 'cons',
minWidthPx: 100,
header: m(GridHeaderCell, {}, 'App Consume'),
},
{
key: 'frame',
minWidthPx: 100,
header: m(GridHeaderCell, {}, 'App Frame'),
},
];
return m(Grid, {
columns,
rowData: rows.map((row) => [
m(
GridCell,
{},
m(Checkbox, {
checked: visibleRowIds.has(row.uiRowId),
onchange: () => attrs.onToggleVisibility(row.uiRowId),
}),
),
m(
GridCell,
{},
m(Checkbox, {
checked: isRowPinned(row, pinningManager),
onchange: () => togglePinning(row, pinningManager),
}),
),
m(GridCell, {}, row.channel),
m(
GridCell,
{},
row.totalLatency !== null
? m(DurationWidget, {dur: row.totalLatency, trace})
: '-',
),
renderCell(row.durReader, row.navReader, trace),
renderCell(row.deltaDispatch, row.navDispatch, trace),
renderCell(row.deltaReceive, row.navReceive, trace),
renderCell(row.deltaConsume, row.navConsume, trace),
renderCell(row.deltaFrame, row.navFrame, trace),
]),
emptyState: m(EmptyState, {
title: 'No input event selected',
description: 'Select an input event to see latency breakdown.',
icon: Icons.Android,
}),
});
}
}
function isRowPinned(
row: InputChainRow,
pinningManager: TrackPinningManager,
): boolean {
return (
row.allTrackUris.length > 0 &&
row.allTrackUris.every((uri) => pinningManager.isTrackPinned(uri))
);
}
function togglePinning(
row: InputChainRow,
pinningManager: TrackPinningManager,
) {
if (isRowPinned(row, pinningManager)) {
pinningManager.unpinTracks(row.allTrackUris);
} else {
pinningManager.pinTracks(row.allTrackUris);
}
}
function renderCell(
dur: duration | null,
nav: NavTarget | undefined,
trace: Trace,
) {
return m(
GridCell,
{},
dur !== null ? m(DurationWidget, {dur, trace}) : m('span', '-'),
nav !== undefined &&
m(Anchor, {
icon: Icons.GoTo,
onclick: () => {
trace.selection.selectTrackEvent(nav.trackUri, nav.id, {
scrollToSelection: true,
switchToCurrentSelectionTab: false,
});
},
title: 'Go to event slice',
}),
);
}