| // Copyright (C) 2024 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 './styles.scss'; |
| import {addWattsonThreadTrack} from './wattson_thread_utils'; |
| import type {App} from '../../public/app'; |
| import {createAggregationTab} from '../../components/aggregation_adapter'; |
| import {CounterTrack} from '../../components/tracks/counter_track'; |
| import {SliceTrack} from '../../components/tracks/slice_track'; |
| import type {PerfettoPlugin} from '../../public/plugin'; |
| import type {Trace} from '../../public/trace'; |
| import {SLICE_TRACK_KIND} from '../../public/track_kinds'; |
| import {TrackNode} from '../../public/workspace'; |
| import type {Engine} from '../../trace_processor/engine'; |
| import {SourceDataset} from '../../trace_processor/dataset'; |
| import {LONG, LONG_NULL, NUM, STR} from '../../trace_processor/query_result'; |
| import type {RouteArgs} from '../../public/route_schema'; |
| import {WattsonEstimateSelectionAggregator} from './estimate_aggregator'; |
| import { |
| WattsonCpuPackageSelectionAggregator, |
| WattsonGpuPackageSelectionAggregator, |
| } from './package_aggregator'; |
| import {WattsonProcessSelectionAggregator} from './process_aggregator'; |
| import {WattsonThreadSelectionAggregator} from './thread_aggregator'; |
| import { |
| CPUSS_ESTIMATE_TRACK_KIND, |
| GPUSS_ESTIMATE_TRACK_KIND, |
| TPUSS_ESTIMATE_TRACK_KIND, |
| } from './track_kinds'; |
| import {createCpuWarnings, missingWattsonCpuConfigs} from './warning'; |
| |
| const WINDOW_MAP: Record<string, string> = { |
| perfetto_wattson_markers: 'markers', |
| perfetto_wattson_trace: 'trace', |
| perfetto_wattson_apps: 'atrace_apps', |
| perfetto_wattson_app_startup: 'app_startup', |
| }; |
| |
| export default class Wattson implements PerfettoPlugin { |
| static readonly id = `org.kernel.Wattson`; |
| static readonly dependencies = []; |
| public static windowsOfInterest = new Set<string>(); |
| |
| static onActivate(_app: App, args: RouteArgs): void { |
| const metrics: string[] = []; |
| if (typeof args.metrics === 'string') { |
| metrics.push(...args.metrics.split('--')); |
| } |
| Wattson.updateWindowsOfInterest(metrics); |
| } |
| |
| async onTraceLoad(ctx: Trace): Promise<void> { |
| const [ |
| markersSupported, |
| cpuSupported, |
| gpuSupported, |
| tpuSupported, |
| realCpuIdleCounters, |
| ] = await Promise.all([ |
| hasWattsonMarkersSupport(ctx.engine), |
| hasWattsonCpuSupport(ctx.engine), |
| hasWattsonGpuSupport(ctx.engine), |
| hasWattsonTpuSupport(ctx.engine), |
| hasCpuIdleCounters(ctx.engine), |
| ]); |
| |
| const missingEvents = markersSupported |
| ? await missingWattsonCpuConfigs(ctx.engine) |
| : []; |
| |
| // Short circuit if Wattson is not supported for this Perfetto trace |
| if (!(markersSupported || cpuSupported || gpuSupported)) return; |
| |
| const group = new TrackNode({name: 'Wattson', isSummary: true}); |
| ctx.defaultWorkspace.addChildInOrder(group); |
| |
| if (markersSupported) { |
| await addWattsonMarkersElements(ctx, group); |
| } |
| if (cpuSupported || markersSupported) { |
| await addWattsonCpuElements( |
| ctx, |
| group, |
| missingEvents, |
| realCpuIdleCounters, |
| ); |
| } |
| if (gpuSupported) { |
| await addWattsonGpuElements(ctx, group); |
| } |
| if (tpuSupported) { |
| await addWattsonTpuElements(ctx, group); |
| } |
| |
| if (Wattson.windowsOfInterest.size > 0) { |
| await this.pinThreadPowerTracks(ctx); |
| Wattson.windowsOfInterest.clear(); |
| } |
| } |
| |
| async pinThreadPowerTracks(ctx: Trace) { |
| if (Wattson.windowsOfInterest.size === 0) return; |
| |
| // Gather all utids from all windows |
| const windowQueries = Array.from(Wattson.windowsOfInterest) |
| .map((window) => { |
| const suffix = WINDOW_MAP[window]; |
| if (suffix === undefined) return undefined; |
| return ` |
| INCLUDE PERFETTO MODULE wattson.aggregation; |
| INCLUDE PERFETTO MODULE wattson.windows; |
| |
| SELECT utid |
| FROM wattson_threads_aggregation!(wattson_window_${suffix}) |
| GROUP BY utid |
| ORDER BY SUM(total_mws) DESC |
| LIMIT 3 |
| `; |
| }) |
| .filter((q): q is string => q !== undefined); |
| |
| const queryResults = await Promise.all( |
| windowQueries.map((q) => ctx.engine.query(q)), |
| ); |
| |
| const utidsToPin = new Set<number>(); |
| for (const result of queryResults) { |
| const it = result.iter({utid: NUM}); |
| for (; it.valid(); it.next()) { |
| utidsToPin.add(it.utid); |
| } |
| } |
| |
| // Only add tracks of unique utids |
| for (const utid of utidsToPin) { |
| await addWattsonThreadTrack(ctx, utid, {pin: true, scrollTo: false}); |
| } |
| } |
| |
| public static updateWindowsOfInterest(metrics: string[]) { |
| for (const metric of metrics) { |
| for (const key of Object.keys(WINDOW_MAP)) { |
| if (metric.includes(key)) { |
| Wattson.windowsOfInterest.add(key); |
| } |
| } |
| } |
| } |
| } |
| |
| function makeWattsonEstimateTrack( |
| trace: Trace, |
| uri: string, |
| queryKey: string, |
| yRangeKey: string, |
| ): CounterTrack { |
| return CounterTrack.create({ |
| trace, |
| uri, |
| sqlSource: `SELECT ts, ${queryKey} AS value FROM _system_state_${queryKey}`, |
| unit: 'mW', |
| yRangeSharingKey: yRangeKey, |
| onInit: async () => { |
| await trace.engine.query( |
| `INCLUDE PERFETTO MODULE wattson.ui.continuous_estimates;`, |
| ); |
| }, |
| }); |
| } |
| |
| async function hasCpuIdleCounters(engine: Engine): Promise<boolean> { |
| const result = await engine.query(` |
| SELECT EXISTS ( |
| SELECT 1 |
| FROM cpu_counter_track |
| WHERE type = 'cpu_idle' |
| ) AS supported |
| `); |
| return !!result.firstRow({supported: NUM}).supported; |
| } |
| |
| async function hasWattsonMarkersSupport(engine: Engine): Promise<boolean> { |
| const result = await engine.query(` |
| INCLUDE PERFETTO MODULE wattson.windows; |
| SELECT EXISTS ( |
| SELECT 1 FROM wattson_window_markers |
| ) AS supported |
| `); |
| return !!result.firstRow({supported: NUM}).supported; |
| } |
| |
| async function hasWattsonCpuSupport(engine: Engine): Promise<boolean> { |
| const result = await engine.query(` |
| INCLUDE PERFETTO MODULE wattson.device_infos; |
| SELECT |
| EXISTS (SELECT 1 FROM _wattson_device) as device, |
| EXISTS (SELECT 1 FROM cpu_counter_track WHERE type = 'cpu_frequency') as freq, |
| EXISTS (SELECT 1 FROM cpu_counter_track WHERE type = 'cpu_idle') as idle |
| `); |
| const row = result.firstRow({device: NUM, freq: NUM, idle: NUM}); |
| return !!row.device && !!row.freq && !!row.idle; |
| } |
| |
| async function hasWattsonGpuSupport(engine: Engine): Promise<boolean> { |
| const result = await engine.query(` |
| INCLUDE PERFETTO MODULE android.gpu.frequency; |
| INCLUDE PERFETTO MODULE wattson.gpu.freq_idle; |
| INCLUDE PERFETTO MODULE wattson.curves.utils; |
| SELECT |
| EXISTS (SELECT 1 FROM android_gpu_frequency) as freq, |
| EXISTS (SELECT 1 FROM _gpu_power_state) as idle, |
| EXISTS (SELECT 1 FROM _gpu_filtered_curves) as has_curves |
| `); |
| const row = result.firstRow({freq: NUM, idle: NUM, has_curves: NUM}); |
| return !!row.freq && !!row.idle && !!row.has_curves; |
| } |
| |
| async function hasWattsonTpuSupport(engine: Engine): Promise<boolean> { |
| const result = await engine.query(` |
| INCLUDE PERFETTO MODULE wattson.tpu.freq_idle; |
| SELECT EXISTS ( |
| SELECT 1 FROM _tpu_freq WHERE dur IS NOT NULL |
| ) AS supported |
| `); |
| return !!result.firstRow({supported: NUM}).supported; |
| } |
| |
| async function addWattsonMarkersElements(ctx: Trace, group: TrackNode) { |
| const uri = `/wattson/markers_window`; |
| const track = await SliceTrack.createMaterialized({ |
| trace: ctx, |
| uri, |
| dataset: new SourceDataset({ |
| schema: { |
| ts: LONG, |
| dur: LONG_NULL, |
| name: STR, |
| }, |
| src: '_wattson_markers_window', |
| }), |
| // Use default details panel |
| }); |
| ctx.tracks.registerTrack({ |
| uri, |
| tags: { |
| kinds: [SLICE_TRACK_KIND], |
| }, |
| renderer: track, |
| }); |
| group.addChildInOrder(new TrackNode({uri, name: 'Wattson markers window'})); |
| } |
| |
| async function addWattsonCpuElements( |
| ctx: Trace, |
| group: TrackNode, |
| missingEvents: string[], |
| hasCpuIdleCounters: boolean, |
| ) { |
| const warningDesc = createCpuWarnings(missingEvents, hasCpuIdleCounters); |
| |
| // CPUs estimate as part of CPU subsystem |
| const estimateSuffix = `${hasCpuIdleCounters ? '' : ' crude'} estimate`; |
| const cpuResult = await ctx.engine.query( |
| `SELECT cpu FROM cpu WHERE machine_id = 0`, |
| ); |
| const it = cpuResult.iter({cpu: NUM}); |
| for (; it.valid(); it.next()) { |
| const queryKey = `cpu${it.cpu}_mw`; |
| const uri = `/wattson/cpu_subsystem_estimate_cpu${it.cpu}`; |
| ctx.tracks.registerTrack({ |
| uri, |
| description: () => warningDesc, |
| renderer: makeWattsonEstimateTrack(ctx, uri, queryKey, `CpuSubsystem`), |
| tags: { |
| kinds: [CPUSS_ESTIMATE_TRACK_KIND], |
| wattson: `CPU${it.cpu}`, |
| }, |
| }); |
| group.addChildInOrder( |
| new TrackNode({ |
| uri, |
| name: `Cpu${it.cpu}${estimateSuffix}`, |
| }), |
| ); |
| } |
| |
| const uri = `/wattson/cpu_subsystem_estimate_dsu_scu`; |
| ctx.tracks.registerTrack({ |
| uri, |
| renderer: makeWattsonEstimateTrack(ctx, uri, `dsu_scu_mw`, `CpuSubsystem`), |
| tags: { |
| kinds: [CPUSS_ESTIMATE_TRACK_KIND], |
| wattson: 'Dsu_Scu', |
| }, |
| }); |
| group.addChildInOrder(new TrackNode({uri, name: `DSU/SCU${estimateSuffix}`})); |
| |
| // Register selection aggregators. |
| // NOTE: the registration order matters because the laste two aggregators |
| // depend on views created by the first two. |
| ctx.selection.registerAreaSelectionTab( |
| createAggregationTab(ctx, new WattsonEstimateSelectionAggregator()), |
| ); |
| ctx.selection.registerAreaSelectionTab( |
| createAggregationTab(ctx, new WattsonThreadSelectionAggregator(ctx)), |
| ); |
| ctx.selection.registerAreaSelectionTab( |
| createAggregationTab(ctx, new WattsonProcessSelectionAggregator()), |
| ); |
| |
| if (await isProcessMetadataPresent(ctx.engine)) { |
| ctx.selection.registerAreaSelectionTab( |
| createAggregationTab(ctx, new WattsonCpuPackageSelectionAggregator()), |
| ); |
| } |
| } |
| |
| async function isProcessMetadataPresent(engine: Engine) { |
| const packageInfo = await engine.query(` |
| INCLUDE PERFETTO MODULE android.process_metadata; |
| SELECT COUNT(*) as count FROM android_process_metadata |
| WHERE package_name IS NOT NULL |
| `); |
| return packageInfo.firstRow({count: NUM}).count > 0; |
| } |
| |
| async function addWattsonGpuElements(ctx: Trace, group: TrackNode) { |
| const id = `/wattson/gpu_subsystem_estimate`; |
| ctx.tracks.registerTrack({ |
| uri: id, |
| renderer: makeWattsonEstimateTrack(ctx, id, `gpu_mw`, `GpuSubsystem`), |
| tags: { |
| kinds: [GPUSS_ESTIMATE_TRACK_KIND], |
| wattson: 'Gpu', |
| }, |
| }); |
| group.addChildInOrder(new TrackNode({uri: id, name: `GPU Estimate`})); |
| |
| ctx.selection.registerAreaSelectionTab( |
| createAggregationTab(ctx, new WattsonGpuPackageSelectionAggregator()), |
| ); |
| } |
| |
| async function addWattsonTpuElements(ctx: Trace, group: TrackNode) { |
| const id = `/wattson/tpu_subsystem_estimate`; |
| ctx.tracks.registerTrack({ |
| uri: id, |
| renderer: makeWattsonEstimateTrack(ctx, id, `tpu_mw`, `TpuSubsystem`), |
| tags: { |
| kinds: [TPUSS_ESTIMATE_TRACK_KIND], |
| wattson: 'Tpu', |
| }, |
| }); |
| group.addChildInOrder(new TrackNode({uri: id, name: `TPU Estimate`})); |
| } |