blob: cb674c27699c6698372c6a36c133801b5149a460 [file] [log] [blame] [edit]
// Copyright (C) 2023 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 {uuidv4} from '../../base/uuid';
import {Trace} from '../../public/trace';
import StandardGroupsPlugin from '../dev.perfetto.StandardGroups';
import {PerfettoPlugin} from '../../public/plugin';
import {createQueryCounterTrack} from '../../components/tracks/query_counter_track';
import {TrackNode} from '../../public/workspace';
import {STR, LONG, LONG_NULL} from '../../trace_processor/query_result';
import {SourceDataset} from '../../trace_processor/dataset';
import {AreaSelection, areaSelectionsEqual} from '../../public/selection';
import {Flamegraph, FLAMEGRAPH_STATE_SCHEMA} from '../../widgets/flamegraph';
import {
metricsFromTableOrSubquery,
QueryFlamegraph,
QueryFlamegraphWithMetrics,
} from '../../components/query_flamegraph';
import SupportPlugin from '../com.android.AndroidLongBatterySupport';
import {Store} from '../../base/store';
import {z} from 'zod';
import {assertExists} from '../../base/logging';
const DAY_EXPLORER_TRACK_KIND = 'day_explorer_counter_track';
const DAY_EXPLORER_PLUGIN_STATE_SCHEMA = z.object({
areaSelectionFlamegraphState: FLAMEGRAPH_STATE_SCHEMA.optional(),
});
type DayExplorerPluginState = z.infer<typeof DAY_EXPLORER_PLUGIN_STATE_SCHEMA>;
export default class DayExplorerPlugin implements PerfettoPlugin {
static readonly id = 'com.android.DayExplorer';
static readonly dependencies = [StandardGroupsPlugin, SupportPlugin];
private store?: Store<DayExplorerPluginState>;
private migrateDayExplorerPluginState(init: unknown): DayExplorerPluginState {
const result = DAY_EXPLORER_PLUGIN_STATE_SCHEMA.safeParse(init);
return result.data ?? {};
}
private support(ctx: Trace) {
return ctx.plugins.getPlugin(SupportPlugin);
}
async addDayExplorerCounters(
ctx: Trace,
support: SupportPlugin,
groupName: string,
limit: number,
): Promise<void> {
await ctx.engine.query(
`INCLUDE PERFETTO MODULE
google3.wireless.android.telemetry.trace_extractor.modules.day_explorer.perfetto_ui_blames`,
);
const group = support.getOrCreateGroup(ctx, groupName);
await this.addDayExplorerRecursive(ctx, group, limit, -1n);
}
private async addDayExplorerRecursive(
ctx: Trace,
parent: TrackNode,
limit: number,
parentId: bigint,
): Promise<void> {
const children = await ctx.engine.query(`
SELECT track_id, display_name, cast(round(total_energy_uws / 3600000) as int) as energy_mwh
FROM day_explorer_ui_hierarchy
WHERE (${parentId} >= 0 AND parent_id = ${parentId})
OR (${parentId} < 0 AND parent_id IS NULL)
ORDER BY energy_mwh DESC
LIMIT ${limit}
`);
const childIter = children.iter({
track_id: LONG,
display_name: STR,
energy_mwh: LONG,
});
for (; childIter.valid(); childIter.next()) {
const query = `
SELECT ts, power_mw AS value
FROM day_explorer_ui_hierarchy_per_ts
WHERE track_id = ${childIter.track_id}
`;
const groupKey = `_day_explorer_ui_hierarchy_under_${parentId}`;
const trackName = `${childIter.display_name} - ${childIter.energy_mwh}mWh`;
const node = await this.createDayExplorerTrack(
ctx,
trackName,
groupKey,
query,
);
parent.addChildInOrder(node);
await this.addDayExplorerRecursive(ctx, node, limit, childIter.track_id);
}
}
private async createDayExplorerTrack(
ctx: Trace,
name: string,
groupKey: string,
query: string,
): Promise<TrackNode> {
const uri = `/day_explorer_${uuidv4()}`;
const renderer = await createQueryCounterTrack({
trace: ctx,
uri,
data: {
sqlSource: query,
},
columns: {
ts: 'ts',
value: 'value',
},
options: {
yRangeSharingKey: groupKey,
},
});
ctx.tracks.registerTrack({
uri,
renderer,
tags: {
kinds: [DAY_EXPLORER_TRACK_KIND],
},
});
return new TrackNode({
name,
uri,
});
}
private createDayExplorerFlameGraphPanel(trace: Trace) {
let previousSelection: AreaSelection | undefined;
let flameagraphWithMetrics: QueryFlamegraphWithMetrics | undefined;
return {
id: 'day_explorer_flamegraph_selection',
name: 'Day Explorer Flamegraph',
render: (selection: AreaSelection) => {
const selectionChanged =
previousSelection === undefined ||
!areaSelectionsEqual(previousSelection, selection);
previousSelection = selection;
if (selectionChanged) {
flameagraphWithMetrics = this.computeDayExplorerFlameGraph(
trace,
selection,
);
}
if (flameagraphWithMetrics === undefined) {
return undefined;
}
const store = assertExists(this.store);
const {flamegraph, metrics} = flameagraphWithMetrics;
return {
isLoading: false,
content: flamegraph.render({
metrics,
state: store.state.areaSelectionFlamegraphState,
onStateChange: (state) => {
store.edit((draft) => {
draft.areaSelectionFlamegraphState = state;
});
},
}),
};
},
};
}
private computeDayExplorerFlameGraph(
trace: Trace,
currentSelection: AreaSelection,
): QueryFlamegraphWithMetrics | undefined {
// The flame graph will be shown when any day explorer track is in the area
// selection. The selection is used to filter by time, but not by track. All
// day explorer tracks are considered for the graph.
let hasDayExplorer = false;
for (const trackInfo of currentSelection.tracks) {
if (trackInfo?.tags?.kinds?.includes(DAY_EXPLORER_TRACK_KIND)) {
hasDayExplorer = true;
break;
}
}
if (!hasDayExplorer) {
return undefined;
}
const metrics = metricsFromTableOrSubquery(
`
(
WITH
total_energy AS (
SELECT track_id, parent_id, display_name, SUM(energy_uws) AS energy_uws
FROM day_explorer_ui_hierarchy_per_ts
WHERE ts >= ${currentSelection.start}
AND ts <= ${currentSelection.end}
GROUP BY 1, 2, 3
),
with_child AS (
SELECT
*,
(
SELECT IFNULL(SUM(energy_uws), 0)
FROM total_energy
WHERE parent_id = P.track_id
) AS child_energy
FROM total_energy AS P
)
SELECT
track_id AS id,
parent_id AS parentId,
display_name AS name,
cast(round((energy_uws - child_energy) / 1000) as int) AS self_count
FROM with_child
)
`,
[
{
name: 'Energy mWs',
unit: '',
columnName: 'self_count',
},
],
);
const store = assertExists(this.store);
store.edit((draft) => {
draft.areaSelectionFlamegraphState = Flamegraph.updateState(
draft.areaSelectionFlamegraphState,
metrics,
);
});
return {flamegraph: new QueryFlamegraph(trace), metrics};
}
async addDayExplorerUsage(
ctx: Trace,
support: SupportPlugin,
groupName: string,
): Promise<void> {
const e = ctx.engine;
await e.query(
`INCLUDE PERFETTO MODULE
google3.wireless.android.telemetry.trace_extractor.modules.day_explorer.perfetto_ui_blames`,
);
await support.addSliceTrack(
ctx,
'Day Explorer Device Usage',
new SourceDataset({
src: `
SELECT
ts,
dur,
usage as name
FROM day_explorer_device_usage
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
},
}),
groupName,
false,
);
}
async addDayExplorerCommand(
ctx: Trace,
support: SupportPlugin,
features: Set<string>,
): Promise<void> {
if (features.has('google3')) {
ctx.commands.registerCommand({
id: 'com.android.DayExplorerBlamesByCategory',
name: 'Add tracks: Day Explorer',
callback: async () => {
const limitStr = await ctx.omnibox.prompt(
'Maximum results per group',
);
const limit = Number(limitStr);
if (!isFinite(limit) || limit <= 0) {
alert('Positive number required');
return;
}
await this.addDayExplorerUsage(ctx, support, 'Day Explorer');
await this.addDayExplorerCounters(
ctx,
support,
'Day Explorer',
limit,
);
},
});
}
}
async onTraceLoad(ctx: Trace): Promise<void> {
this.store = ctx.mountStore(DayExplorerPlugin.id, (init) =>
this.migrateDayExplorerPluginState(init),
);
const support = this.support(ctx);
const features = await support.features(ctx.engine);
ctx.selection.registerAreaSelectionTab(
this.createDayExplorerFlameGraphPanel(ctx),
);
if (features.has('google3')) {
await this.addDayExplorerCommand(ctx, support, features);
}
}
}