| // 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 type {Trace} from '../../public/trace'; |
| import type {PerfettoPlugin} from '../../public/plugin'; |
| import {TrackNode} from '../../public/workspace'; |
| import {SliceTrack} from '../../components/tracks/slice_track'; |
| import {CounterTrack} from '../../components/tracks/counter_track'; |
| import {SourceDataset} from '../../trace_processor/dataset'; |
| import type {Engine} from '../../trace_processor/engine'; |
| import {STR, LONG, LONG_NULL} from '../../trace_processor/query_result'; |
| import StandardGroupsPlugin from '../dev.perfetto.StandardGroups'; |
| |
| const VALID_SUBSCRIPTION_IDS = [ |
| -3150162163527618090n, |
| -5078517658656926822n, |
| -897258217068735442n, |
| -4914190112721760517n, |
| ]; |
| |
| export default class WearLongBatteryTracingPlugin implements PerfettoPlugin { |
| static readonly id = 'com.android.WearLongBatteryTracing'; |
| static readonly dependencies = [StandardGroupsPlugin]; |
| |
| private async findAtoms(e: Engine): Promise<Set<string>> { |
| const atoms = new Set<string>(); |
| |
| const result = await e.query(` |
| select distinct s.name as atom |
| from track t join slice s on t.id = s.track_id |
| where t.name = 'Statsd Atoms'`); |
| const it = result.iter({atom: STR}); |
| for (; it.valid(); it.next()) { |
| atoms.add(it.atom); |
| } |
| |
| return atoms; |
| } |
| |
| async addCounterTrack( |
| ctx: Trace, |
| name: string, |
| query: string, |
| group: TrackNode, |
| ) { |
| const uri = `/wear_long_battery_tracing_${name}`; |
| const track = await CounterTrack.createMaterialized({ |
| trace: ctx, |
| uri, |
| sqlSource: query, |
| }); |
| ctx.tracks.registerTrack({ |
| uri, |
| renderer: track, |
| }); |
| const trackNode = new TrackNode({uri, name}); |
| group.addChildInOrder(trackNode); |
| } |
| |
| async addSliceTrack( |
| ctx: Trace, |
| name: string, |
| query: string, |
| group: TrackNode, |
| ) { |
| const uri = `/wear_long_battery_tracing_${name}`; |
| const track = await SliceTrack.createMaterialized({ |
| trace: ctx, |
| uri, |
| dataset: new SourceDataset({ |
| src: query, |
| schema: { |
| ts: LONG, |
| dur: LONG_NULL, |
| name: STR, |
| }, |
| }), |
| }); |
| ctx.tracks.registerTrack({ |
| uri, |
| renderer: track, |
| }); |
| const trackNode = new TrackNode({uri, name}); |
| group.addChildInOrder(trackNode); |
| } |
| |
| async addModes( |
| ctx: Trace, |
| atoms: Set<string>, |
| group: TrackNode, |
| ): Promise<void> { |
| if (!atoms.has('wear_mode_state_changed')) { |
| return; |
| } |
| |
| const modesGroup = new TrackNode({ |
| name: 'Wear Modes', |
| isSummary: true, |
| }); |
| group.addChildInOrder(modesGroup); |
| |
| const e = ctx.engine; |
| await e.query( |
| `INCLUDE PERFETTO MODULE |
| google3.wireless.android.wear.tools.pipelines.perfetto.cujs.modes`, |
| ); |
| const slices = await e.query( |
| `SELECT DISTINCT mode_id AS track_name FROM wear_mode_slices`, |
| ); |
| |
| const slicesIt = slices.iter({track_name: STR}); |
| for (; slicesIt.valid(); slicesIt.next()) { |
| await this.addSliceTrack( |
| ctx, |
| slicesIt.track_name, |
| `SELECT |
| ts, |
| dur, |
| mode_state AS name |
| FROM wear_mode_slices |
| WHERE mode_id = '${slicesIt.track_name}' AND mode_state != 'UNKNOWN' |
| `, |
| modesGroup, |
| ); |
| } |
| } |
| |
| async addRawBatteryGauge(ctx: Trace, atoms: Set<string>): Promise<void> { |
| if (!atoms.has('raw_battery_gauge_stats_reported')) { |
| return; |
| } |
| |
| const e = ctx.engine; |
| await e.query( |
| `INCLUDE PERFETTO MODULE |
| google3.wireless.android.telemetry.trace_extractor.modules.power.battery_gauge_power`, |
| ); |
| |
| const group = new TrackNode({ |
| name: 'Raw Battery Gauge', |
| }); |
| ctx.plugins |
| .getPlugin(StandardGroupsPlugin) |
| .getOrCreateStandardGroup(ctx.defaultWorkspace, 'POWER') |
| .addChildInOrder(group); |
| |
| await this.addCounterTrack( |
| ctx, |
| 'Raw Battery Gauge - mW', |
| `SELECT ts, power_mw AS value FROM _battery_gauge_power_samples`, |
| group, |
| ); |
| } |
| |
| async addCujs(ctx: Trace, atoms: Set<string>): Promise<void> { |
| const group = new TrackNode({ |
| name: 'Wear CUJs', |
| isSummary: true, |
| }); |
| const e = ctx.engine; |
| |
| await e.query(`INCLUDE PERFETTO MODULE |
| google3.wireless.android.wear.tools.pipelines.perfetto.cujs.cuj_tracks`); |
| const result = await e.query( |
| `select track_name, table_name, source_atom FROM wear_cuj_slice_tracks`, |
| ); |
| const it = result.iter({ |
| track_name: STR, |
| table_name: STR, |
| source_atom: STR, |
| }); |
| |
| for (; it.valid(); it.next()) { |
| if (!atoms.has(it.source_atom)) { |
| continue; |
| } |
| await this.addSliceTrack( |
| ctx, |
| it.track_name, |
| `SELECT ts, dur, name FROM ${it.table_name}`, |
| group, |
| ); |
| } |
| |
| await this.addModes(ctx, atoms, group); |
| |
| if (group.children.length > 0) { |
| ctx.defaultWorkspace.addChildInOrder(group); |
| } |
| } |
| |
| /** |
| * This hook is called as the trace is loading. At this point the trace is |
| * loaded into trace processor and it's ready to process queries. This hook |
| * should be used for adding tracks and commands that depend on the trace. |
| * |
| * It should not be used for finding tracks from other plugins as there is no |
| * guarantee those tracks will have been added yet. |
| */ |
| async onTraceLoad(ctx: Trace): Promise<void> { |
| const result = await ctx.engine.query(` |
| SELECT extract_metadata('statsd_triggering_subscription_id') as int_value; |
| `); |
| const row = result.maybeFirstRow({int_value: LONG_NULL}); |
| if (!row || row.int_value === null) { |
| return; |
| } |
| if (!VALID_SUBSCRIPTION_IDS.includes(row.int_value)) { |
| return; |
| } |
| |
| const atoms = await this.findAtoms(ctx.engine); |
| |
| await this.addCujs(ctx, atoms); |
| await this.addRawBatteryGauge(ctx, atoms); |
| } |
| } |