blob: 0eb0108d8f484cd9b925cf6efbac9d0978a1c43c [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 {Trace} from '../../public/trace';
import StandardGroupsPlugin from '../dev.perfetto.StandardGroups';
import {PerfettoPlugin} from '../../public/plugin';
import {
STR,
LONG,
UNKNOWN,
SqlValue,
LONG_NULL,
} from '../../trace_processor/query_result';
import {SourceDataset} from '../../trace_processor/dataset';
import SupportPlugin from '../com.android.AndroidLongBatterySupport';
const PACKAGE_LOOKUP = `
create or replace perfetto table package_name_lookup as
with installed as (
select uid, string_agg(package_name, ',') as name
from package_list
where uid >= 10000
group by 1
),
system(uid, name) as (
values
(0, 'AID_ROOT'),
(1000, 'AID_SYSTEM_USER'),
(1001, 'AID_RADIO'),
(1082, 'AID_ARTD')
)
select uid, name from installed
union all
select uid, name from system
order by uid;
-- Adds a "package_name" column by joining on "uid" from the source table.
create or replace perfetto macro add_package_name(src TableOrSubquery) returns TableOrSubquery as (
select A.*, ifnull(B.name, "uid=" || A.uid) as package_name
from $src as A
left join package_name_lookup as B
on (B.uid = (A.uid % 100000))
);
`;
const DEFAULT_NETWORK_DATASET = new SourceDataset({
src: `
with base as (
select
ts,
substr(s.name, 6) as conn
from track t join slice s on t.id = s.track_id
where t.name = 'battery_stats.conn'
),
diff as (
select
ts,
conn,
conn != lag(conn) over (order by ts) as keep
from base
)
select
ts,
ifnull(lead(ts) over (order by ts), (select end_ts from trace_bounds)) - ts as dur,
case
when conn like '-1:%' then 'Disconnected'
when conn like '0:%' then 'Modem'
when conn like '1:%' then 'WiFi'
when conn like '4:%' then 'VPN'
else conn
end as name
from diff where keep is null or keep
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
},
});
const RADIO_TRANSPORT_TYPE = `
create or replace perfetto view radio_transport_data_conn as
select ts, safe_dur AS dur, value_name as data_conn, value AS data_conn_val
from android_battery_stats_state
where track_name = "battery_stats.data_conn";
create or replace perfetto view radio_transport_nr_state as
select ts, safe_dur AS dur, value AS nr_state_val
from android_battery_stats_state
where track_name = "battery_stats.nr_state";
drop table if exists radio_transport_join;
create virtual table radio_transport_join
using span_left_join(radio_transport_data_conn, radio_transport_nr_state);
create or replace perfetto view radio_transport as
select
ts, dur,
case data_conn_val
-- On LTE with NR connected is 5G NSA.
when 13 then iif(nr_state_val = 3, '5G (NSA)', data_conn)
-- On NR with NR state present, is 5G SA.
when 20 then iif(nr_state_val is null, '5G (SA or NSA)', '5G (SA)')
else data_conn
end as name
from radio_transport_join;`;
const TETHERING_DATASET = new SourceDataset({
src: `
with base as (
select
ts as ts_end,
EXTRACT_ARG(arg_set_id, 'network_tethering_reported.duration_millis') * 1000000 as dur
from track t join slice s on t.id = s.track_id
where t.name = 'Statsd Atoms'
and s.name = 'network_tethering_reported'
)
select ts_end - dur as ts, dur, 'Tethering' as name from base
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
},
});
const NETWORK_SUMMARY = `
create or replace perfetto table network_summary as
with base as (
select
cast(ts / 5000000000 as int64) * 5000000000 AS ts,
case
when iface glob '*wlan*' then 'wifi'
when iface glob '*rmnet*' then 'modem'
else 'unknown'
end as dev_type,
package_name as pkg,
sum(packet_length) AS value
from android_network_packets
where (iface glob '*wlan*' or iface glob '*rmnet*')
group by 1,2,3
),
zeroes as (
select
ts,
dev_type,
pkg,
value
from base
union all
select
ts + 5000000000 as ts,
dev_type,
pkg,
0 as value
from base
),
final as (
select
ts,
dev_type,
pkg,
sum(value) as value
from zeroes
group by 1, 2, 3
)
select * from final where ts is not null`;
const SUSPEND_RESUME_DATASET = new SourceDataset({
src: `
SELECT
ts,
dur,
'Suspended' AS name
FROM android_suspend_state
WHERE power_state = 'suspended'
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
},
});
const THERMAL_THROTTLING_DATASET = new SourceDataset({
src: `
with step1 as (
select
ts,
EXTRACT_ARG(arg_set_id, 'thermal_throttling_severity_state_changed.sensor_type') as sensor_type,
EXTRACT_ARG(arg_set_id, 'thermal_throttling_severity_state_changed.sensor_name') as sensor_name,
EXTRACT_ARG(arg_set_id, 'thermal_throttling_severity_state_changed.temperature_deci_celsius') / 10.0 as temperature_celcius,
EXTRACT_ARG(arg_set_id, 'thermal_throttling_severity_state_changed.severity') as severity
from track t join slice s on t.id = s.track_id
where t.name = 'Statsd Atoms'
and s.name = 'thermal_throttling_severity_state_changed'
),
step2 as (
select
ts,
lead(ts) over (partition by sensor_type, sensor_name order by ts) - ts as dur,
sensor_type,
sensor_name,
temperature_celcius,
severity
from step1
where sensor_type not like 'TEMPERATURE_TYPE_BCL_%'
)
select
ts,
dur,
case sensor_name
when 'VIRTUAL-SKIN' then ''
else sensor_name || ' is '
end || severity || ' (' || temperature_celcius || 'C)' as name
from step2
where severity != 'NONE'
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
},
});
const KERNEL_WAKELOCKS_STATSD = `
create or replace perfetto table kernel_wakelocks_statsd as
with kernel_wakelock_args as (
select
arg_set_id,
min(iif(key = 'kernel_wakelock.name', string_value, null)) as wakelock_name,
min(iif(key = 'kernel_wakelock.count', int_value, null)) as count,
min(iif(key = 'kernel_wakelock.time_micros', int_value, null)) as time_micros
from args
where key in (
'kernel_wakelock.name',
'kernel_wakelock.count',
'kernel_wakelock.time_micros'
)
group by 1
),
interesting as (
select wakelock_name
from (
select wakelock_name, max(time_micros)-min(time_micros) as delta_us
from kernel_wakelock_args
group by 1
)
-- Only consider wakelocks with over 1 second of time during the whole trace
where delta_us > 1e6
),
step1 as (
select ts, wakelock_name, count, time_micros
from kernel_wakelock_args
join interesting using (wakelock_name)
join slice using (arg_set_id)
),
step2 as (
select
ts,
wakelock_name,
lead(ts) over (partition by wakelock_name order by ts) as ts_end,
lead(count) over (partition by wakelock_name order by ts) - count as count,
(lead(time_micros) over (partition by wakelock_name order by ts) - time_micros) * 1000 as wakelock_dur
from step1
),
step3 as (
select
ts,
ts_end,
ifnull((select sum(dur) from android_suspend_state s
where power_state = 'suspended'
and s.ts > step2.ts
and s.ts < step2.ts_end), 0) as suspended_dur,
wakelock_name,
count,
wakelock_dur
from step2
where wakelock_dur is not null
and wakelock_dur >= 0
)
select
ts,
ts_end - ts as dur,
wakelock_name,
min(100.0 * wakelock_dur / (ts_end - ts - suspended_dur), 100) as value
from step3`;
const KERNEL_WAKELOCKS_STATSD_SUMMARY = `
select wakelock_name, max(value) as max_value
from kernel_wakelocks_statsd
where wakelock_name not in ('PowerManager.SuspendLockout', 'PowerManagerService.Display')
group by 1
having max_value > 1
order by 1;`;
const HIGH_CPU = `
create or replace perfetto table high_cpu as
with cpu_cycles_args AS (
select
arg_set_id,
min(iif(key = 'cpu_cycles_per_uid_cluster.uid', int_value, null)) as uid,
min(iif(key = 'cpu_cycles_per_uid_cluster.cluster', int_value, null)) as cluster,
min(iif(key = 'cpu_cycles_per_uid_cluster.time_millis', int_value, null)) as time_millis
from args
where key in (
'cpu_cycles_per_uid_cluster.uid',
'cpu_cycles_per_uid_cluster.cluster',
'cpu_cycles_per_uid_cluster.time_millis'
)
group by 1
),
interesting AS (
select uid, cluster
from (
select uid, cluster, max(time_millis)-min(time_millis) as delta_ms
from cpu_cycles_args
group by 1, 2
)
-- Only consider tracks with over 1 second of cpu during the whole trace
where delta_ms > 1e3
),
base as (
select ts, uid, cluster, sum(time_millis) as time_millis
from cpu_cycles_args
join interesting using (uid, cluster)
join slice using (arg_set_id)
group by 1, 2, 3
),
with_windows as (
select
ts,
uid,
cluster,
lead(ts) over (partition by uid, cluster order by ts) - ts as dur,
(lead(time_millis) over (partition by uid, cluster order by ts) - time_millis) * 1000000.0 as cpu_dur
from base
),
with_ratio as (
select
ts,
iif(dur is null, 0, max(0, 100.0 * cpu_dur / dur)) as value,
case cluster when 0 then 'little' when 1 then 'mid' when 2 then 'big' else 'cl-' || cluster end as cluster,
package_name as pkg
from add_package_name!(with_windows)
)
select ts, sum(value) as value, cluster, pkg
from with_ratio
group by 1, 3, 4`;
export default class implements PerfettoPlugin {
static readonly id = 'com.android.AndroidLongBatteryTracing';
static readonly dependencies = [StandardGroupsPlugin, SupportPlugin];
private support(ctx: Trace) {
return ctx.plugins.getPlugin(SupportPlugin);
}
async addBatteryStatsState(
ctx: Trace,
support: SupportPlugin,
name: string,
track: string,
groupName: string,
features: Set<string>,
) {
if (!features.has(`track.${track}`)) {
return;
}
await support.addSliceTrack(
ctx,
name,
new SourceDataset({
src: `
SELECT
ts,
safe_dur AS dur,
value_name AS name
FROM android_battery_stats_state
WHERE track_name = "${track}"
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
},
}),
groupName,
);
}
async addBatteryStatsEvent(
ctx: Trace,
support: SupportPlugin,
name: string,
track: string,
groupName: string,
features: Set<string>,
) {
if (!features.has(`track.${track}`)) {
return;
}
await support.addSliceTrack(
ctx,
name,
new SourceDataset({
src: `
SELECT
ts,
safe_dur AS dur,
str_value AS name
FROM android_battery_stats_event_slices
WHERE track_name = "${track}"
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
},
}),
groupName,
);
}
async addDeviceState(
ctx: Trace,
support: SupportPlugin,
features: Set<string>,
): Promise<void> {
if (!features.has('track.battery_stats.*')) {
return;
}
const groupName = 'Device State';
const deviceStateGroup = ctx.plugins
.getPlugin(StandardGroupsPlugin)
.getOrCreateStandardGroup(ctx.defaultWorkspace, 'DEVICE_STATE');
support.groups.set(groupName, deviceStateGroup);
const query = (name: string, track: string) =>
this.addBatteryStatsEvent(ctx, support, name, track, groupName, features);
const e = ctx.engine;
await e.query(`INCLUDE PERFETTO MODULE android.battery_stats;`);
await e.query(`INCLUDE PERFETTO MODULE android.suspend;`);
await e.query(`INCLUDE PERFETTO MODULE android.battery.charging_states;`);
await e.query(`INCLUDE PERFETTO MODULE android.battery.doze;`);
await e.query(`INCLUDE PERFETTO MODULE android.screen_state;`);
await support.addSliceTrack(
ctx,
'Screen state',
new SourceDataset({
src: `
SELECT
ts,
dur,
screen_state AS name
FROM android_screen_state
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
},
}),
groupName,
);
await support.addSliceTrack(
ctx,
'Charging',
new SourceDataset({
src: `
SELECT
ts,
dur,
charging_state AS name
FROM android_charging_states
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
},
}),
groupName,
);
await support.addSliceTrack(
ctx,
'Suspend / resume',
SUSPEND_RESUME_DATASET,
groupName,
);
await support.addSliceTrack(
ctx,
'Doze light state',
new SourceDataset({
src: `
SELECT
ts,
dur,
light_idle_state AS name
FROM android_light_idle_state
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
},
}),
groupName,
);
await support.addSliceTrack(
ctx,
'Doze deep state',
new SourceDataset({
src: `
SELECT
ts,
dur,
deep_idle_state AS name
FROM android_deep_idle_state
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
},
}),
groupName,
);
query('Top app', 'battery_stats.top');
await support.addSliceTrack(
ctx,
'Long wakelocks',
new SourceDataset({
src: `
SELECT
-- Clamp start time to > 0 to avoid negative timestamps.
MAX(0, ts - 60000000000) AS ts,
-- The end time is (ts + safe_dur), so the duration is the original
-- end time minus the clamped start time.
(ts + safe_dur) - MAX(0, ts - 60000000000) AS dur,
str_value AS name,
package_name AS package
FROM add_package_name!((
SELECT
*,
int_value AS uid
FROM android_battery_stats_event_slices
WHERE track_name = "battery_stats.longwake"
))
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
package: STR,
},
}),
groupName,
);
query('Foreground apps', 'battery_stats.fg');
if (
features.has('atom.scheduled_job_state_changed') &&
features.has('google3')
) {
await e.query(`INCLUDE PERFETTO MODULE
google3.wireless.android.telemetry.trace_extractor.modules.power.jobs;`);
await support.addSliceTrack(
ctx,
'Jobs',
new SourceDataset({
src: `
SELECT
ts,
dur,
tag AS name,
uid
FROM jobs
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
uid: UNKNOWN,
},
}),
groupName,
);
} else {
query('Jobs', 'battery_stats.job');
}
if (features.has('atom.thermal_throttling_severity_state_changed')) {
await support.addSliceTrack(
ctx,
'Thermal throttling',
THERMAL_THROTTLING_DATASET,
groupName,
);
}
}
async addAtomCounters(ctx: Trace, support: SupportPlugin): Promise<void> {
const e = ctx.engine;
await e.query(
`INCLUDE PERFETTO MODULE
google3.wireless.android.telemetry.trace_extractor.modules.atom_counters_slices`,
);
const counters = await e.query(
`select distinct ui_group, ui_name, ui_unit, counter_name
from atom_counters
where ui_name is not null`,
);
const countersIt = counters.iter({
ui_group: STR,
ui_name: STR,
ui_unit: STR,
counter_name: STR,
});
for (; countersIt.valid(); countersIt.next()) {
const unit = countersIt.ui_unit;
const opts =
unit === '%'
? {yOverrideMaximum: 100, unit: '%'}
: unit !== undefined
? {unit}
: undefined;
await support.addCounterTrack(
ctx,
countersIt.ui_name,
`select ts, ${unit === '%' ? 100.0 : 1.0} * counter_value as value
from atom_counters
where counter_name = '${countersIt.counter_name}'`,
countersIt.ui_group,
opts,
);
}
}
async addAtomSlices(ctx: Trace, support: SupportPlugin): Promise<void> {
const e = ctx.engine;
await e.query(
`INCLUDE PERFETTO MODULE
google3.wireless.android.telemetry.trace_extractor.modules.atom_counters_slices`,
);
const sliceTracks = await e.query(
`select distinct ui_group, ui_name, atom, field
from atom_slices
where ui_name is not null
order by 1, 2, 3, 4`,
);
const slicesIt = sliceTracks.iter({
atom: STR,
ui_group: STR,
ui_name: STR,
field: STR,
});
const tracks = new Map<
string,
{
ui_group: string;
ui_name: string;
}
>();
const fields = new Map<string, string[]>();
for (; slicesIt.valid(); slicesIt.next()) {
const atom = slicesIt.atom;
let args = fields.get(atom);
if (args === undefined) {
args = [];
fields.set(atom, args);
}
args.push(slicesIt.field);
tracks.set(atom, {
ui_group: slicesIt.ui_group,
ui_name: slicesIt.ui_name,
});
}
for (const [atom, args] of fields) {
function safeArg(arg: string) {
return arg.replaceAll(/[[\]]/g, '').replaceAll(/\./g, '_');
}
// We need to make arg names compatible with SQL here because they pass through several
// layers of SQL without being quoted in "".
function argSql(arg: string) {
return `max(case when field = '${arg}' then ifnull(string_value, int_value) end)
as ${safeArg(arg)}`;
}
// Add schema entries for dynamic columns
const argsSchema: Record<string, SqlValue> = {};
for (const arg of args) {
argsSchema[safeArg(arg)] = UNKNOWN;
}
await support.addSliceTrack(
ctx,
tracks.get(atom)!.ui_name,
new SourceDataset({
src: `
SELECT
ts,
dur,
slice_name as name,
${args.map((a) => argSql(a)).join(',')}
FROM atom_slices
WHERE atom = '${atom}'
GROUP BY ts, dur, name
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
...argsSchema,
},
}),
tracks.get(atom)!.ui_group,
);
}
}
async addNetworkSummary(
ctx: Trace,
support: SupportPlugin,
features: Set<string>,
): Promise<void> {
if (!features.has('net.modem') && !features.has('net.wifi')) {
return;
}
const groupName = 'Network Summary';
const e = ctx.engine;
await e.query(`INCLUDE PERFETTO MODULE android.battery_stats;`);
await e.query(`INCLUDE PERFETTO MODULE android.network_packets;`);
await e.query(NETWORK_SUMMARY);
await e.query(RADIO_TRANSPORT_TYPE);
await support.addSliceTrack(
ctx,
'Default network',
DEFAULT_NETWORK_DATASET,
groupName,
);
if (features.has('atom.network_tethering_reported')) {
await support.addSliceTrack(
ctx,
'Tethering',
TETHERING_DATASET,
groupName,
);
}
if (features.has('net.wifi')) {
await support.addCounterTrack(
ctx,
'Wifi total bytes',
`select ts, sum(value) as value from network_summary where dev_type = 'wifi' group by 1`,
groupName,
{yDisplay: 'log', yRangeSharingKey: 'net_bytes', unit: 'byte'},
);
const result = await e.query(
`select pkg, sum(value) from network_summary where dev_type='wifi' group by 1 order by 2 desc limit 10`,
);
const it = result.iter({pkg: STR});
for (; it.valid(); it.next()) {
await support.addCounterTrack(
ctx,
`Top wifi: ${it.pkg}`,
`select ts, value from network_summary where dev_type = 'wifi' and pkg = '${it.pkg}'`,
groupName,
{yDisplay: 'log', yRangeSharingKey: 'net_bytes', unit: 'byte'},
);
}
}
this.addBatteryStatsState(
ctx,
support,
'Wifi interface',
'battery_stats.wifi_radio',
groupName,
features,
);
this.addBatteryStatsState(
ctx,
support,
'Wifi supplicant state',
'battery_stats.wifi_suppl',
groupName,
features,
);
this.addBatteryStatsState(
ctx,
support,
'Wifi strength',
'battery_stats.wifi_signal_strength',
groupName,
features,
);
if (features.has('net.modem')) {
await support.addCounterTrack(
ctx,
'Modem total bytes',
`select ts, sum(value) as value from network_summary where dev_type = 'modem' group by 1`,
groupName,
{yDisplay: 'log', yRangeSharingKey: 'net_bytes', unit: 'byte'},
);
const result = await e.query(
`select pkg, sum(value) from network_summary where dev_type='modem' group by 1 order by 2 desc limit 10`,
);
const it = result.iter({pkg: STR});
for (; it.valid(); it.next()) {
await support.addCounterTrack(
ctx,
`Top modem: ${it.pkg}`,
`select ts, value from network_summary where dev_type = 'modem' and pkg = '${it.pkg}'`,
groupName,
{yDisplay: 'log', yRangeSharingKey: 'net_bytes', unit: 'byte'},
);
}
}
this.addBatteryStatsState(
ctx,
support,
'Cellular interface',
'battery_stats.mobile_radio',
groupName,
features,
);
await support.addSliceTrack(
ctx,
'Cellular connection',
new SourceDataset({
src: `
SELECT
ts,
dur,
name
FROM radio_transport
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
},
}),
groupName,
);
this.addBatteryStatsState(
ctx,
support,
'Cellular strength',
'battery_stats.phone_signal_strength',
groupName,
features,
);
}
async addModemMintData(ctx: Trace, support: SupportPlugin): Promise<void> {
const e = ctx.engine;
const groupName = 'Modem Detail';
await e.query(
`INCLUDE PERFETTO MODULE
google3.wireless.android.telemetry.trace_extractor.modules.modem_mint_metrics`,
);
const counters = await e.query(
`select distinct name from pixel_modem_counters`,
);
const countersIt = counters.iter({name: STR});
for (; countersIt.valid(); countersIt.next()) {
await support.addCounterTrack(
ctx,
countersIt.name,
`select ts, value from pixel_modem_counters where name = '${countersIt.name}'`,
groupName,
);
}
const slices = await e.query(
`select distinct track_name from pixel_modem_slices`,
);
const slicesIt = slices.iter({track_name: STR});
for (; slicesIt.valid(); slicesIt.next()) {
await support.addSliceTrack(
ctx,
slicesIt.track_name,
new SourceDataset({
src: `
SELECT
ts,
dur,
slice_name as name
FROM pixel_modem_slices
WHERE track_name = '${slicesIt.track_name}'
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
},
}),
groupName,
);
}
}
async addKernelWakelocks(ctx: Trace, support: SupportPlugin): Promise<void> {
const e = ctx.engine;
await e.query(`INCLUDE PERFETTO MODULE android.kernel_wakelocks;`);
const result = await e.query(
`SELECT DISTINCT name, type FROM android_kernel_wakelocks`,
);
const it = result.iter({name: STR, type: STR});
for (; it.valid(); it.next()) {
await support.addCounterTrack(
ctx,
it.name,
`SELECT ts, dur, held_ratio * 100 AS value
FROM android_kernel_wakelocks
WHERE name = "${it.name}"`,
'Kernel Wakelock Summary',
{yRangeSharingKey: 'kernel_wakelock', unit: '%'},
);
}
}
async addKernelWakelocksStatsd(
ctx: Trace,
support: SupportPlugin,
features: Set<string>,
): Promise<void> {
if (!features.has('atom.kernel_wakelock')) {
return;
}
const groupName = 'Kernel Wakelock Summary (statsd)';
const e = ctx.engine;
await e.query(`INCLUDE PERFETTO MODULE android.suspend;`);
await e.query(KERNEL_WAKELOCKS_STATSD);
const result = await e.query(KERNEL_WAKELOCKS_STATSD_SUMMARY);
const it = result.iter({wakelock_name: STR});
for (; it.valid(); it.next()) {
await support.addCounterTrack(
ctx,
`${it.wakelock_name} (statsd)`,
`select ts, dur, value
from kernel_wakelocks_statsd
where wakelock_name = "${it.wakelock_name}"`,
groupName,
{yRangeSharingKey: 'kernel_wakelock_statsd', unit: '%'},
);
}
}
async addWakeups(
ctx: Trace,
support: SupportPlugin,
features: Set<string>,
): Promise<void> {
if (!features.has('track.suspend_backoff')) {
return;
}
const e = ctx.engine;
const groupName = 'Wakeups';
await e.query(`INCLUDE PERFETTO MODULE android.wakeups;`);
const result = await e.query(`select
item,
sum(dur) as sum_dur
from android_wakeups
group by 1
having sum_dur > 600e9`);
const it = result.iter({item: STR});
const sqlPrefix = `select
ts,
dur,
item || case backoff_reason
when 'short' then ' (Short suspend backoff)'
when 'failed' then ' (Failed suspend backoff)'
else ''
end as name,
item,
type,
raw_wakeup,
on_device_attribution,
suspend_quality,
backoff_state,
backoff_reason,
backoff_count,
backoff_millis
from android_wakeups`;
const items = [];
let labelOther = false;
for (; it.valid(); it.next()) {
labelOther = true;
await support.addSliceTrack(
ctx,
`Wakeup ${it.item}`,
new SourceDataset({
src: `${sqlPrefix} WHERE item="${it.item}"`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
item: UNKNOWN,
type: UNKNOWN,
raw_wakeup: UNKNOWN,
on_device_attribution: UNKNOWN,
suspend_quality: UNKNOWN,
backoff_state: UNKNOWN,
backoff_reason: UNKNOWN,
backoff_count: UNKNOWN,
backoff_millis: UNKNOWN,
},
}),
groupName,
);
items.push(it.item);
}
await support.addSliceTrack(
ctx,
labelOther ? 'Other wakeups' : 'Wakeups',
new SourceDataset({
src: `${sqlPrefix} WHERE item NOT IN ('${items.join("','")}')`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
item: UNKNOWN,
type: UNKNOWN,
raw_wakeup: UNKNOWN,
on_device_attribution: UNKNOWN,
suspend_quality: UNKNOWN,
backoff_state: UNKNOWN,
backoff_reason: UNKNOWN,
backoff_count: UNKNOWN,
backoff_millis: UNKNOWN,
},
}),
groupName,
);
}
async addHighCpu(
ctx: Trace,
support: SupportPlugin,
features: Set<string>,
): Promise<void> {
if (!features.has('atom.cpu_cycles_per_uid_cluster')) {
return;
}
const groupName = 'CPU per UID (major users, from statsd)';
const e = ctx.engine;
await e.query(HIGH_CPU);
const result = await e.query(
`select distinct pkg, cluster from high_cpu where value > 10 order by 1, 2`,
);
const it = result.iter({pkg: STR, cluster: STR});
for (; it.valid(); it.next()) {
await support.addCounterTrack(
ctx,
`${it.pkg} (${it.cluster})`,
`select ts, value from high_cpu where pkg = "${it.pkg}" and cluster="${it.cluster}"`,
groupName,
{yOverrideMaximum: 100, unit: '%'},
);
}
}
async onTraceLoad(ctx: Trace): Promise<void> {
const support = this.support(ctx);
const features = await support.features(ctx.engine);
await ctx.engine.query(PACKAGE_LOOKUP);
await this.addNetworkSummary(ctx, support, features);
await this.addKernelWakelocks(ctx, support);
await this.addKernelWakelocksStatsd(ctx, support, features);
await this.addWakeups(ctx, support, features);
await this.addDeviceState(ctx, support, features);
await this.addHighCpu(ctx, support, features);
if (features.has('google3')) {
await this.addAtomCounters(ctx, support);
await this.addAtomSlices(ctx, support);
await this.addModemMintData(ctx, support);
}
}
}