blob: 711e76b9287873a0b6935d1405d36f08093e26fc [file] [edit]
// 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 m from 'mithril';
import type {Engine} from '../../../trace_processor/engine';
import {
LONG,
NUM_NULL,
STR,
STR_NULL,
} from '../../../trace_processor/query_result';
import {Section} from '../../../widgets/section';
import {Grid, GridCell, GridHeaderCell} from '../../../widgets/grid';
import {Callout} from '../../../widgets/callout';
import {Intent} from '../../../widgets/common';
import {Select} from '../../../widgets/select';
import {FormGrid, FormLabel} from '../../../widgets/form';
import {Time} from '../../../base/time';
import {renderTimecode} from '../../../components/time_utils';
// Row specs
const packageDataSpec = {
packageName: STR,
versionCode: NUM_NULL,
debuggable: NUM_NULL,
profileableFromShell: NUM_NULL,
};
type PackageData = typeof packageDataSpec;
const androidGameInterventionRowSpec = {
package_name: STR,
uid: NUM_NULL,
current_mode: NUM_NULL,
standard_mode_supported: NUM_NULL,
standard_mode_downscale: STR_NULL,
standard_mode_use_angle: STR_NULL,
standard_mode_fps: NUM_NULL,
perf_mode_supported: NUM_NULL,
perf_mode_downscale: STR_NULL,
perf_mode_use_angle: STR_NULL,
perf_mode_fps: NUM_NULL,
battery_mode_supported: NUM_NULL,
battery_mode_downscale: STR_NULL,
battery_mode_use_angle: STR_NULL,
battery_mode_fps: NUM_NULL,
};
type AndroidGameInterventionRow = typeof androidGameInterventionRowSpec;
const aflagRowSpec = {
ts: LONG,
package: STR_NULL,
name: STR_NULL,
container: STR_NULL,
value: STR_NULL,
stagedValue: STR_NULL,
permission: STR_NULL,
valuePickedFrom: STR_NULL,
};
type AflagRow = typeof aflagRowSpec;
export interface AndroidData {
packageList: PackageData[];
gameInterventions: AndroidGameInterventionRow[];
aflags: AflagRow[];
aflagErrors: string[];
}
export async function loadAndroidData(engine: Engine): Promise<AndroidData> {
// Load package list
const packageListResult = await engine.query(`
select
package_name as packageName,
version_code as versionCode,
debuggable,
profileable_from_shell as profileableFromShell
from package_list
`);
const packageList: PackageData[] = [];
for (
const iter = packageListResult.iter(packageDataSpec);
iter.valid();
iter.next()
) {
packageList.push({
packageName: iter.packageName,
versionCode: iter.versionCode,
debuggable: iter.debuggable,
profileableFromShell: iter.profileableFromShell,
});
}
// Load game interventions
const gameInterventionsResult = await engine.query(`
select
package_name,
uid,
current_mode,
standard_mode_supported,
standard_mode_downscale,
standard_mode_use_angle,
standard_mode_fps,
perf_mode_supported,
perf_mode_downscale,
perf_mode_use_angle,
perf_mode_fps,
battery_mode_supported,
battery_mode_downscale,
battery_mode_use_angle,
battery_mode_fps
from android_game_intervention_list
`);
const gameInterventions: AndroidGameInterventionRow[] = [];
for (
const iter = gameInterventionsResult.iter(androidGameInterventionRowSpec);
iter.valid();
iter.next()
) {
gameInterventions.push({
package_name: iter.package_name,
uid: iter.uid,
current_mode: iter.current_mode,
standard_mode_supported: iter.standard_mode_supported,
standard_mode_downscale: iter.standard_mode_downscale,
standard_mode_use_angle: iter.standard_mode_use_angle,
standard_mode_fps: iter.standard_mode_fps,
perf_mode_supported: iter.perf_mode_supported,
perf_mode_downscale: iter.perf_mode_downscale,
perf_mode_use_angle: iter.perf_mode_use_angle,
perf_mode_fps: iter.perf_mode_fps,
battery_mode_supported: iter.battery_mode_supported,
battery_mode_downscale: iter.battery_mode_downscale,
battery_mode_use_angle: iter.battery_mode_use_angle,
battery_mode_fps: iter.battery_mode_fps,
});
}
// Load aflags
const aflagsResult = await engine.query(`
include perfetto module android.aflags;
select
ts,
package,
name,
container,
value,
staged_value as stagedValue,
permission,
value_picked_from as valuePickedFrom
from android_aflags
order by package, name
`);
const aflags: AflagRow[] = [];
for (
const iter = aflagsResult.iter(aflagRowSpec);
iter.valid();
iter.next()
) {
aflags.push({
ts: iter.ts,
package: iter.package,
name: iter.name,
container: iter.container,
value: iter.value,
stagedValue: iter.stagedValue,
permission: iter.permission,
valuePickedFrom: iter.valuePickedFrom,
});
}
// Load aflag errors
const aflagErrorsResult = await engine.query(`
select distinct display_value as error
from _trace_import_logs
join args using (arg_set_id)
where name = 'android_aflags_errors'
`);
const aflagErrors: string[] = [];
for (
const iter = aflagErrorsResult.iter({error: STR});
iter.valid();
iter.next()
) {
aflagErrors.push(iter.error);
}
return {
packageList,
gameInterventions,
aflags,
aflagErrors,
};
}
export function hasAndroidData(data?: AndroidData): boolean {
if (!data) return false;
return (
data.packageList.length > 0 ||
data.gameInterventions.length > 0 ||
data.aflags.length > 0 ||
data.aflagErrors.length > 0
);
}
export interface AndroidTabAttrs {
data: AndroidData;
}
export class AndroidTab implements m.ClassComponent<AndroidTabAttrs> {
view({attrs}: m.CVnode<AndroidTabAttrs>) {
return m(
'.pf-trace-info-page__tab-content',
m(PackageListSection, {packageList: attrs.data.packageList}),
m(AndroidGameInterventionList, {data: attrs.data.gameInterventions}),
m(AndroidAflagsSection, {
aflags: attrs.data.aflags,
aflagErrors: attrs.data.aflagErrors,
}),
);
}
}
// Package List Section
interface PackageListSectionAttrs {
packageList: PackageData[];
}
class PackageListSection implements m.ClassComponent<PackageListSectionAttrs> {
view({attrs}: m.CVnode<PackageListSectionAttrs>) {
const packageList = attrs.packageList;
if (packageList === undefined || packageList.length === 0) {
return undefined;
}
return m(
Section,
{
title: 'Android Packages',
subtitle: 'List of Android packages and their versions in the trace',
},
m(Grid, {
columns: [
{
key: 'packageName',
header: m(GridHeaderCell, 'Package Name'),
},
{
key: 'versionCode',
header: m(GridHeaderCell, 'Version Code'),
},
{
key: 'flags',
header: m(GridHeaderCell, 'Flags'),
},
],
rowData: packageList.map((pkg) => {
const flags = [
pkg.debuggable ?? 0 ? 'debuggable' : '',
pkg.profileableFromShell ?? 0 ? 'profileable' : '',
]
.filter(Boolean)
.join(' ');
return [
m(GridCell, pkg.packageName),
m(GridCell, pkg.versionCode),
m(GridCell, flags),
];
}),
className: 'pf-trace-info-page__logs-grid',
}),
);
}
}
// Android Game Intervention List
interface AndroidGameInterventionListAttrs {
data: AndroidGameInterventionRow[];
}
// Helper to format mode interventions
function formatModeInterventions(
supported: number | null,
angle: string | null,
downscale: string | null,
fps: number | null,
): string {
if (supported === null || supported === 0) {
return 'Not supported';
}
return `angle=${angle},downscale=${downscale},fps=${fps}`;
}
// Helper to format current mode string
function formatCurrentMode(mode: number | null): string {
// Game mode numbers are defined in
// https://cs.android.com/android/platform/superproject/+/main:frameworks/base/core/java/android/app/GameManager.java;l=68
if (mode === 1) return 'Standard';
if (mode === 2) return 'Performance';
if (mode === 3) return 'Battery';
return mode !== null ? String(mode) : 'Unknown';
}
class AndroidGameInterventionList
implements m.ClassComponent<AndroidGameInterventionListAttrs>
{
view({attrs}: m.CVnode<AndroidGameInterventionListAttrs>) {
const data = attrs.data;
if (data === undefined || data.length === 0) {
return undefined;
}
return m(
Section,
{
title: 'Game Interventions',
subtitle: 'Android game intervention data',
},
m(Grid, {
columns: [
{
key: 'packageName',
header: m(GridHeaderCell, 'Name'),
},
{
key: 'currentMode',
header: m(GridHeaderCell, 'Current mode'),
},
{
key: 'standardMode',
header: m(GridHeaderCell, 'Standard mode interventions'),
},
{
key: 'perfMode',
header: m(GridHeaderCell, 'Performance mode interventions'),
},
{
key: 'batteryMode',
header: m(GridHeaderCell, 'Battery mode interventions'),
},
],
rowData: data.map((row) => [
m(GridCell, row.package_name),
m(GridCell, formatCurrentMode(row.current_mode)),
m(
GridCell,
formatModeInterventions(
row.standard_mode_supported,
row.standard_mode_use_angle,
row.standard_mode_downscale,
row.standard_mode_fps,
),
),
m(
GridCell,
formatModeInterventions(
row.perf_mode_supported,
row.perf_mode_use_angle,
row.perf_mode_downscale,
row.perf_mode_fps,
),
),
m(
GridCell,
formatModeInterventions(
row.battery_mode_supported,
row.battery_mode_use_angle,
row.battery_mode_downscale,
row.battery_mode_fps,
),
),
]),
className: 'pf-trace-info-page__logs-grid',
}),
);
}
}
interface AndroidAflagsSectionAttrs {
aflags: AflagRow[];
aflagErrors: string[];
}
class AndroidAflagsSection
implements m.ClassComponent<AndroidAflagsSectionAttrs>
{
private selectedTs?: bigint;
view({attrs}: m.CVnode<AndroidAflagsSectionAttrs>) {
const {aflags, aflagErrors} = attrs;
if (
(aflags === undefined || aflags.length === 0) &&
(aflagErrors === undefined || aflagErrors.length === 0)
) {
return undefined;
}
const timestamps = Array.from(new Set(aflags.map((f) => f.ts))).sort();
if (this.selectedTs === undefined && timestamps.length > 0) {
this.selectedTs = timestamps[0];
}
const filteredFlags = aflags.filter((f) => f.ts === this.selectedTs);
return m(
Section,
{
title: 'Android Aflags',
subtitle: 'List of Android aconfig flags in the trace',
},
aflagErrors.length > 0 &&
m(
Callout,
{
intent: Intent.Danger,
className: 'pf-trace-info-page__banner',
},
'Errors occurred during aflags collection: ',
aflagErrors.map((e) => m('div', e)),
),
timestamps.length > 1 &&
m(
FormGrid,
m(FormLabel, {for: 'snapshot-select'}, 'Select Snapshot Timestamp:'),
m(
Select,
{
id: 'snapshot-select',
value: this.selectedTs?.toString(),
onchange: (e: Event) => {
const target = e.target as HTMLSelectElement;
this.selectedTs = BigInt(target.value);
},
},
timestamps.map((ts) =>
m(
'option',
{value: ts.toString()},
renderTimecode(Time.fromRaw(ts)),
),
),
),
),
aflags.length > 0 &&
m(Grid, {
columns: [
{
key: 'flag',
header: m(GridHeaderCell, 'Flag'),
},
{
key: 'value',
header: m(GridHeaderCell, 'Value'),
},
{
key: 'stagedValue',
header: m(GridHeaderCell, 'Staged Value'),
},
{
key: 'valuePickedFrom',
header: m(GridHeaderCell, 'Value Picked From'),
},
{
key: 'permission',
header: m(GridHeaderCell, 'Permission'),
},
{
key: 'container',
header: m(GridHeaderCell, 'Container'),
},
],
rowData: filteredFlags.map((flag) => [
m(GridCell, `${flag.package ?? ''}.${flag.name ?? ''}`),
m(GridCell, flag.value),
m(GridCell, flag.stagedValue ? `(->${flag.stagedValue})` : '-'),
m(GridCell, flag.valuePickedFrom),
m(GridCell, flag.permission),
m(GridCell, flag.container),
]),
className: 'pf-trace-info-page__logs-grid',
}),
);
}
}