| // 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', |
| }), |
| ); |
| } |
| } |