blob: 69ac136f4f5c9ba40354d873746425a649cd5562 [file] [log] [blame] [edit]
// 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 {Trace} from '../../public/trace';
import StandardGroupsPlugin from '../dev.perfetto.StandardGroups';
import {PerfettoPlugin} from '../../public/plugin';
import {
STR,
LONG,
UNKNOWN,
LONG_NULL,
} from '../../trace_processor/query_result';
import {SourceDataset} from '../../trace_processor/dataset';
import SupportPlugin from '../com.android.AndroidLongBatterySupport';
function bleScanDataset(condition: string) {
return new SourceDataset({
src: `
with step1 as (
select
ts,
extract_arg(arg_set_id, 'ble_scan_state_changed.attribution_node[0].tag') as name,
extract_arg(arg_set_id, 'ble_scan_state_changed.is_opportunistic') as opportunistic,
extract_arg(arg_set_id, 'ble_scan_state_changed.is_filtered') as filtered,
extract_arg(arg_set_id, 'ble_scan_state_changed.state') as state
from track t join slice s on t.id = s.track_id
where t.name = 'Statsd Atoms'
and s.name = 'ble_scan_state_changed'
),
step2 as (
select
ts,
name,
state,
opportunistic,
filtered,
lead(ts) over (partition by name order by ts) - ts as dur
from step1
)
select ts, dur, name from step2 where state = 'ON' and ${condition} and dur is not null
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
},
});
}
const BLE_RESULTS_DATASET = new SourceDataset({
src: `
with step1 as (
select
ts,
extract_arg(arg_set_id, 'ble_scan_result_received.attribution_node[0].tag') as name,
extract_arg(arg_set_id, 'ble_scan_result_received.num_results') as num_results
from track t join slice s on t.id = s.track_id
where t.name = 'Statsd Atoms'
and s.name = 'ble_scan_result_received'
)
select
ts,
0 as dur,
name || ' (' || num_results || ' results)' as name
from step1
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
},
});
const BT_A2DP_AUDIO_DATASET = new SourceDataset({
src: `
with step1 as (
select
ts,
EXTRACT_ARG(arg_set_id, 'bluetooth_a2dp_playback_state_changed.playback_state') as playback_state,
EXTRACT_ARG(arg_set_id, 'bluetooth_a2dp_playback_state_changed.audio_coding_mode') as audio_coding_mode,
EXTRACT_ARG(arg_set_id, 'bluetooth_a2dp_playback_state_changed.metric_id') as metric_id
from track t join slice s on t.id = s.track_id
where t.name = 'Statsd Atoms'
and s.name = 'bluetooth_a2dp_playback_state_changed'
),
step2 as (
select
ts,
lead(ts) over (partition by metric_id order by ts) - ts as dur,
playback_state,
audio_coding_mode,
metric_id
from step1
)
select
ts,
dur,
audio_coding_mode as name
from step2
where playback_state = 'PLAYBACK_STATE_PLAYING'
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
},
});
const BT_CONNS_ACL_DATASET = new SourceDataset({
src: `
with acl1 as (
select
ts,
EXTRACT_ARG(arg_set_id, 'bluetooth_acl_connection_state_changed.state') as state,
EXTRACT_ARG(arg_set_id, 'bluetooth_acl_connection_state_changed.transport') as transport,
EXTRACT_ARG(arg_set_id, 'bluetooth_acl_connection_state_changed.metric_id') as metric_id
from track t join slice s on t.id = s.track_id
where t.name = 'Statsd Atoms'
and s.name = 'bluetooth_acl_connection_state_changed'
),
acl2 as (
select
ts,
lead(ts) over (partition by metric_id, transport order by ts) - ts as dur,
state,
transport,
metric_id
from acl1
)
select
ts,
dur,
'Device ' || metric_id ||
' (' || case transport when 'TRANSPORT_TYPE_BREDR' then 'Classic' when 'TRANSPORT_TYPE_LE' then 'BLE' end || ')' as name
from acl2
where state != 'CONNECTION_STATE_DISCONNECTED' and dur is not null
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
},
});
const BT_CONNS_SCO_DATASET = new SourceDataset({
src: `
with sco1 as (
select
ts,
EXTRACT_ARG(arg_set_id, 'bluetooth_sco_connection_state_changed.state') as state,
EXTRACT_ARG(arg_set_id, 'bluetooth_sco_connection_state_changed.codec') as codec,
EXTRACT_ARG(arg_set_id, 'bluetooth_sco_connection_state_changed.metric_id') as metric_id
from track t join slice s on t.id = s.track_id
where t.name = 'Statsd Atoms'
and s.name = 'bluetooth_sco_connection_state_changed'
),
sco2 as (
select
ts,
lead(ts) over (partition by metric_id, codec order by ts) - ts as dur,
state,
codec,
metric_id
from sco1
)
select
ts,
dur,
case state when 'CONNECTION_STATE_CONNECTED' then '' when 'CONNECTION_STATE_CONNECTING' then 'Connecting ' when 'CONNECTION_STATE_DISCONNECTING' then 'Disconnecting ' else 'unknown ' end ||
'Device ' || metric_id || ' (' ||
case codec when 'SCO_CODEC_CVSD' then 'CVSD' when 'SCO_CODEC_MSBC' then 'MSBC' end || ')' as name
from sco2
where state != 'CONNECTION_STATE_DISCONNECTED' and dur is not null
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
},
});
const BT_LINK_LEVEL_EVENTS_DATASET = new SourceDataset({
src: `
with base as (
select
ts,
EXTRACT_ARG(arg_set_id, 'bluetooth_link_layer_connection_event.direction') as direction,
EXTRACT_ARG(arg_set_id, 'bluetooth_link_layer_connection_event.type') as type,
EXTRACT_ARG(arg_set_id, 'bluetooth_link_layer_connection_event.hci_cmd') as hci_cmd,
EXTRACT_ARG(arg_set_id, 'bluetooth_link_layer_connection_event.hci_event') as hci_event,
EXTRACT_ARG(arg_set_id, 'bluetooth_link_layer_connection_event.hci_ble_event') as hci_ble_event,
EXTRACT_ARG(arg_set_id, 'bluetooth_link_layer_connection_event.cmd_status') as cmd_status,
EXTRACT_ARG(arg_set_id, 'bluetooth_link_layer_connection_event.reason_code') as reason_code,
EXTRACT_ARG(arg_set_id, 'bluetooth_link_layer_connection_event.metric_id') as metric_id
from track t join slice s on t.id = s.track_id
where t.name = 'Statsd Atoms'
and s.name = 'bluetooth_link_layer_connection_event'
)
select
*,
0 as dur,
'Device '|| metric_id as name
from base
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
direction: UNKNOWN,
type: UNKNOWN,
hci_cmd: UNKNOWN,
hci_event: UNKNOWN,
hci_ble_event: UNKNOWN,
cmd_status: UNKNOWN,
reason_code: UNKNOWN,
metric_id: UNKNOWN,
},
});
const BT_QUALITY_REPORTS_DATASET = new SourceDataset({
src: `
with base as (
select
ts,
EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.quality_report_id') as quality_report_id,
EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.packet_types') as packet_types,
EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.connection_handle') as connection_handle,
EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.connection_role') as connection_role,
EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.tx_power_level') as tx_power_level,
EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.rssi') as rssi,
EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.snr') as snr,
EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.unused_afh_channel_count') as unused_afh_channel_count,
EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.afh_select_unideal_channel_count') as afh_select_unideal_channel_count,
EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.lsto') as lsto,
EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.connection_piconet_clock') as connection_piconet_clock,
EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.retransmission_count') as retransmission_count,
EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.no_rx_count') as no_rx_count,
EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.nak_count') as nak_count,
EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.flow_off_count') as flow_off_count,
EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.buffer_overflow_bytes') as buffer_overflow_bytes,
EXTRACT_ARG(arg_set_id, 'bluetooth_quality_report_reported.buffer_underflow_bytes') as buffer_underflow_bytes
from track t join slice s on t.id = s.track_id
where t.name = 'Statsd Atoms'
and s.name = 'bluetooth_quality_report_reported'
)
select
*,
0 as dur,
'Connection '|| connection_handle as name
from base
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
quality_report_id: UNKNOWN,
packet_types: UNKNOWN,
connection_handle: UNKNOWN,
connection_role: UNKNOWN,
tx_power_level: UNKNOWN,
rssi: UNKNOWN,
snr: UNKNOWN,
unused_afh_channel_count: UNKNOWN,
afh_select_unideal_channel_count: UNKNOWN,
lsto: UNKNOWN,
connection_piconet_clock: UNKNOWN,
retransmission_count: UNKNOWN,
no_rx_count: UNKNOWN,
nak_count: UNKNOWN,
flow_off_count: UNKNOWN,
buffer_overflow_bytes: UNKNOWN,
buffer_underflow_bytes: UNKNOWN,
},
});
const BT_RSSI_REPORTS_DATASET = new SourceDataset({
src: `
with base as (
select
ts,
EXTRACT_ARG(arg_set_id, 'bluetooth_device_rssi_reported.connection_handle') as connection_handle,
EXTRACT_ARG(arg_set_id, 'bluetooth_device_rssi_reported.hci_status') as hci_status,
EXTRACT_ARG(arg_set_id, 'bluetooth_device_rssi_reported.rssi') as rssi,
EXTRACT_ARG(arg_set_id, 'bluetooth_device_rssi_reported.metric_id') as metric_id
from track t join slice s on t.id = s.track_id
where t.name = 'Statsd Atoms'
and s.name = 'bluetooth_device_rssi_reported'
)
select
*,
0 as dur,
'Connection '|| connection_handle as name
from base
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
connection_handle: UNKNOWN,
hci_status: UNKNOWN,
rssi: UNKNOWN,
metric_id: UNKNOWN,
},
});
const BT_CODE_PATH_COUNTER_DATASET = new SourceDataset({
src: `
with base as (
select
ts,
EXTRACT_ARG(arg_set_id, 'bluetooth_code_path_counter.key') as key,
EXTRACT_ARG(arg_set_id, 'bluetooth_code_path_counter.number') as number
from track t join slice s on t.id = s.track_id
where t.name = 'Statsd Atoms'
and s.name = 'bluetooth_code_path_counter'
)
select
*,
0 as dur,
key as name
from base
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
key: UNKNOWN,
number: UNKNOWN,
},
});
const BT_HAL_CRASHES_DATASET = new SourceDataset({
src: `
with base as (
select
ts,
EXTRACT_ARG(arg_set_id, 'bluetooth_hal_crash_reason_reported.metric_id') as metric_id,
EXTRACT_ARG(arg_set_id, 'bluetooth_hal_crash_reason_reported.error_code') as error_code,
EXTRACT_ARG(arg_set_id, 'bluetooth_hal_crash_reason_reported.vendor_error_code') as vendor_error_code
from track t join slice s on t.id = s.track_id
where t.name = 'Statsd Atoms'
and s.name = 'bluetooth_hal_crash_reason_reported'
)
select
*,
0 as dur,
'Device ' || metric_id as name
from base
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
metric_id: UNKNOWN,
error_code: UNKNOWN,
vendor_error_code: UNKNOWN,
},
});
const BT_BYTES_DATASET = new SourceDataset({
src: `
with step1 as (
select
ts,
EXTRACT_ARG(arg_set_id, 'bluetooth_bytes_transfer.uid') as uid,
EXTRACT_ARG(arg_set_id, 'bluetooth_bytes_transfer.tx_bytes') as tx_bytes,
EXTRACT_ARG(arg_set_id, 'bluetooth_bytes_transfer.rx_bytes') as rx_bytes
from track t join slice s on t.id = s.track_id
where t.name = 'Statsd Atoms'
and s.name = 'bluetooth_bytes_transfer'
),
step2 as (
select
ts,
lead(ts) over (partition by uid order by ts) - ts as dur,
uid,
lead(tx_bytes) over (partition by uid order by ts) - tx_bytes as tx_bytes,
lead(rx_bytes) over (partition by uid order by ts) - rx_bytes as rx_bytes
from step1
),
step3 as (
select
ts,
dur,
uid % 100000 as uid,
sum(tx_bytes) as tx_bytes,
sum(rx_bytes) as rx_bytes
from step2
where tx_bytes >=0 and rx_bytes >=0
group by 1,2,3
having tx_bytes > 0 or rx_bytes > 0
)
select
ts,
dur,
format("%s: TX %d bytes / RX %d bytes", package_name, tx_bytes, rx_bytes) as name
from add_package_name!(step3)
`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
},
});
// See go/bt_system_context_report for reference on the bit-twiddling.
const BT_ACTIVITY = `
create perfetto table bt_activity as
with step1 as (
select
EXTRACT_ARG(arg_set_id, 'bluetooth_activity_info.timestamp_millis') * 1000000 as ts,
EXTRACT_ARG(arg_set_id, 'bluetooth_activity_info.bluetooth_stack_state') as bluetooth_stack_state,
EXTRACT_ARG(arg_set_id, 'bluetooth_activity_info.controller_idle_time_millis') * 1000000 as controller_idle_dur,
EXTRACT_ARG(arg_set_id, 'bluetooth_activity_info.controller_tx_time_millis') * 1000000 as controller_tx_dur,
EXTRACT_ARG(arg_set_id, 'bluetooth_activity_info.controller_rx_time_millis') * 1000000 as controller_rx_dur
from track t join slice s on t.id = s.track_id
where t.name = 'Statsd Atoms'
and s.name = 'bluetooth_activity_info'
),
step2 as (
select
ts,
lead(ts) over (order by ts) - ts as dur,
bluetooth_stack_state,
lead(controller_idle_dur) over (order by ts) - controller_idle_dur as controller_idle_dur,
lead(controller_tx_dur) over (order by ts) - controller_tx_dur as controller_tx_dur,
lead(controller_rx_dur) over (order by ts) - controller_rx_dur as controller_rx_dur
from step1
)
select
ts,
dur,
bluetooth_stack_state & 0x0000000F as acl_active_count,
bluetooth_stack_state & 0x000000F0 >> 4 as acl_sniff_count,
bluetooth_stack_state & 0x00000F00 >> 8 as acl_ble_count,
bluetooth_stack_state & 0x0000F000 >> 12 as advertising_count,
case bluetooth_stack_state & 0x000F0000 >> 16
when 0 then 0
when 1 then 5
when 2 then 10
when 3 then 25
when 4 then 100
else -1
end as le_scan_duty_cycle,
bluetooth_stack_state & 0x00100000 >> 20 as inquiry_active,
bluetooth_stack_state & 0x00200000 >> 21 as sco_active,
bluetooth_stack_state & 0x00400000 >> 22 as a2dp_active,
bluetooth_stack_state & 0x00800000 >> 23 as le_audio_active,
max(0, 100.0 * controller_idle_dur / dur) as controller_idle_pct,
max(0, 100.0 * controller_tx_dur / dur) as controller_tx_pct,
max(0, 100.0 * controller_rx_dur / dur) as controller_rx_pct
from step2
`;
export default class implements PerfettoPlugin {
static readonly id = 'com.android.Bluetooth';
static readonly dependencies = [StandardGroupsPlugin, SupportPlugin];
private support(ctx: Trace) {
return ctx.plugins.getPlugin(SupportPlugin);
}
async onTraceLoad(ctx: Trace): Promise<void> {
const support = this.support(ctx);
const features = await support.features(ctx.engine);
if (
!Array.from(features.values()).some(
(f) => f.startsWith('atom.bluetooth_') || f.startsWith('atom.ble_'),
)
) {
return;
}
const groupName = 'Bluetooth';
await support.addSliceTrack(
ctx,
'BLE Scans (opportunistic)',
bleScanDataset('opportunistic'),
groupName,
);
await support.addSliceTrack(
ctx,
'BLE Scans (filtered)',
bleScanDataset('filtered'),
groupName,
);
await support.addSliceTrack(
ctx,
'BLE Scans (unfiltered)',
bleScanDataset('not filtered'),
groupName,
);
await support.addSliceTrack(
ctx,
'BLE Scan Results',
BLE_RESULTS_DATASET,
groupName,
);
await support.addSliceTrack(
ctx,
'Connections (ACL)',
BT_CONNS_ACL_DATASET,
groupName,
);
await support.addSliceTrack(
ctx,
'Connections (SCO)',
BT_CONNS_SCO_DATASET,
groupName,
);
await support.addSliceTrack(
ctx,
'Link-level Events',
BT_LINK_LEVEL_EVENTS_DATASET,
groupName,
);
await support.addSliceTrack(
ctx,
'A2DP Audio',
BT_A2DP_AUDIO_DATASET,
groupName,
);
await support.addSliceTrack(
ctx,
'Bytes Transferred (L2CAP/RFCOMM)',
BT_BYTES_DATASET,
groupName,
);
await ctx.engine.query(BT_ACTIVITY);
await support.addCounterTrack(
ctx,
'ACL Classic Active Count',
'select ts, dur, acl_active_count as value from bt_activity',
groupName,
);
await support.addCounterTrack(
ctx,
'ACL Classic Sniff Count',
'select ts, dur, acl_sniff_count as value from bt_activity',
groupName,
);
await support.addCounterTrack(
ctx,
'ACL BLE Count',
'select ts, dur, acl_ble_count as value from bt_activity',
groupName,
);
await support.addCounterTrack(
ctx,
'Advertising Instance Count',
'select ts, dur, advertising_count as value from bt_activity',
groupName,
);
await support.addCounterTrack(
ctx,
'LE Scan Duty Cycle Maximum',
'select ts, dur, le_scan_duty_cycle as value from bt_activity',
groupName,
{unit: '%'},
);
await support.addSliceTrack(
ctx,
'Inquiry Active',
new SourceDataset({
src: `SELECT
ts,
dur,
'Active' as name
FROM bt_activity
WHERE inquiry_active`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
},
}),
groupName,
);
await support.addSliceTrack(
ctx,
'SCO Active',
new SourceDataset({
src: `SELECT
ts,
dur,
'Active' as name
FROM bt_activity
WHERE sco_active`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
},
}),
groupName,
);
await support.addSliceTrack(
ctx,
'A2DP Active',
new SourceDataset({
src: `SELECT
ts,
dur,
'Active' as name
FROM bt_activity
WHERE a2dp_active`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
},
}),
groupName,
);
await support.addSliceTrack(
ctx,
'LE Audio Active',
new SourceDataset({
src: `SELECT
ts,
dur,
'Active' as name
FROM bt_activity
WHERE le_audio_active`,
schema: {
ts: LONG,
dur: LONG_NULL,
name: STR,
},
}),
groupName,
);
await support.addCounterTrack(
ctx,
'Controller Idle Time',
'select ts, dur, controller_idle_pct as value from bt_activity',
groupName,
{yRangeSharingKey: 'bt_controller_time', unit: '%'},
);
await support.addCounterTrack(
ctx,
'Controller TX Time',
'select ts, dur, controller_tx_pct as value from bt_activity',
groupName,
{yRangeSharingKey: 'bt_controller_time', unit: '%'},
);
await support.addCounterTrack(
ctx,
'Controller RX Time',
'select ts, dur, controller_rx_pct as value from bt_activity',
groupName,
{yRangeSharingKey: 'bt_controller_time', unit: '%'},
);
await support.addSliceTrack(
ctx,
'Quality reports',
BT_QUALITY_REPORTS_DATASET,
groupName,
);
await support.addSliceTrack(
ctx,
'RSSI Reports',
BT_RSSI_REPORTS_DATASET,
groupName,
);
await support.addSliceTrack(
ctx,
'HAL Crashes',
BT_HAL_CRASHES_DATASET,
groupName,
);
await support.addSliceTrack(
ctx,
'Code Path Counter',
BT_CODE_PATH_COUNTER_DATASET,
groupName,
);
}
}