blob: 21733ad2ed7990509f74fb3c41d7d93603276d8b [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
mod convert;
pub mod experiment;
mod inspect_time_series;
mod windowed_stats;
use {
crate::{
client,
telemetry::{inspect_time_series::TimeSeriesStats, windowed_stats::WindowedStats},
util::historical_list::{HistoricalList, Timestamped},
},
anyhow::{format_err, Context, Error},
cobalt_client::traits::AsEventCode,
fidl_fuchsia_metrics::{MetricEvent, MetricEventPayload},
fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211, fidl_fuchsia_wlan_internal as fidl_internal,
fidl_fuchsia_wlan_sme as fidl_sme,
fuchsia_async::{self as fasync, TimeoutExt},
fuchsia_inspect::{
ArrayProperty, InspectType, Inspector, LazyNode, Node as InspectNode, NumericProperty,
UintProperty,
},
fuchsia_inspect_contrib::{
auto_persist::{self, AutoPersist},
inspect_insert, inspect_log,
inspectable::{InspectableBool, InspectableU64},
log::{InspectBytes, InspectList},
make_inspect_loggable,
nodes::BoundedListNode,
},
fuchsia_sync::Mutex,
fuchsia_zircon::{self as zx, DurationNum},
futures::{
channel::{mpsc, oneshot},
future::BoxFuture,
select, Future, FutureExt, StreamExt,
},
ieee80211::OuiFmt,
num_traits::SaturatingAdd,
static_assertions::const_assert_eq,
std::{
cmp::{max, min, Reverse},
collections::{HashMap, HashSet},
ops::Add,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
},
tracing::{error, info, warn},
wlan_metrics_registry as metrics,
};
// Include a timeout on stats calls so that if the driver deadlocks, telemtry doesn't get stuck.
const GET_IFACE_STATS_TIMEOUT: zx::Duration = zx::Duration::from_seconds(5);
// If there are commands to turn off then turn on client connections within this amount of time
// through the policy API, it is likely that a user intended to restart WLAN connections.
const USER_RESTART_TIME_THRESHOLD: zx::Duration = zx::Duration::from_seconds(5);
// Short duration connection for metrics purposes.
pub const METRICS_SHORT_CONNECT_DURATION: zx::Duration = zx::Duration::from_seconds(90);
// Minimum connection duration for logging average connection score deltas.
pub const AVERAGE_SCORE_DELTA_MINIMUM_DURATION: zx::Duration = zx::Duration::from_seconds(30);
// Maximum value of reason code accepted by cobalt metrics (set by max_event_code)
pub const COBALT_REASON_CODE_MAX: u16 = 1000;
// Time between cobalt error reports to prevent cluttering up the syslog.
pub const MINUTES_BETWEEN_COBALT_SYSLOG_WARNINGS: i64 = 60;
#[derive(Clone, Debug, PartialEq)]
// Connection score and the time at which it was calculated.
pub struct TimestampedConnectionScore {
pub score: u8,
pub time: fasync::Time,
}
impl TimestampedConnectionScore {
pub fn new(score: u8, time: fasync::Time) -> Self {
Self { score, time }
}
}
impl Timestamped for TimestampedConnectionScore {
fn time(&self) -> fasync::Time {
self.time
}
}
#[derive(Clone)]
#[cfg_attr(test, derive(Debug))]
pub struct TelemetrySender {
sender: Arc<Mutex<mpsc::Sender<TelemetryEvent>>>,
sender_is_blocked: Arc<AtomicBool>,
}
impl TelemetrySender {
pub fn new(sender: mpsc::Sender<TelemetryEvent>) -> Self {
Self {
sender: Arc::new(Mutex::new(sender)),
sender_is_blocked: Arc::new(AtomicBool::new(false)),
}
}
// Send telemetry event. Log an error if it fails
pub fn send(&self, event: TelemetryEvent) {
match self.sender.lock().try_send(event) {
Ok(_) => {
// If sender has been blocked before, set bool to false and log message
if self
.sender_is_blocked
.compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
info!("TelemetrySender recovered and resumed sending");
}
}
Err(_) => {
// If sender has not been blocked before, set bool to true and log error message
if self
.sender_is_blocked
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
warn!("TelemetrySender dropped a msg: either buffer is full or no receiver is waiting");
}
}
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct DisconnectInfo {
pub connected_duration: zx::Duration,
pub is_sme_reconnecting: bool,
pub disconnect_source: fidl_sme::DisconnectSource,
pub previous_connect_reason: client::types::ConnectReason,
pub ap_state: client::types::ApState,
pub connection_scores: HistoricalList<TimestampedConnectionScore>,
}
pub trait DisconnectSourceExt {
fn inspect_string(&self) -> String;
fn flattened_reason_code(&self) -> u32;
fn cobalt_reason_code(&self) -> u16;
fn locally_initiated(&self) -> bool;
}
impl DisconnectSourceExt for fidl_sme::DisconnectSource {
fn inspect_string(&self) -> String {
match self {
fidl_sme::DisconnectSource::User(reason) => {
format!("source: user, reason: {:?}", reason)
}
fidl_sme::DisconnectSource::Ap(cause) => format!(
"source: ap, reason: {:?}, mlme_event_name: {:?}",
cause.reason_code, cause.mlme_event_name
),
fidl_sme::DisconnectSource::Mlme(cause) => format!(
"source: mlme, reason: {:?}, mlme_event_name: {:?}",
cause.reason_code, cause.mlme_event_name
),
}
}
/// If disconnect comes from AP, then get the 802.11 reason code.
/// If disconnect comes from MLME, return (1u32 << 17) + reason code.
/// If disconnect comes from user, return (1u32 << 16) + user disconnect reason.
/// This is mainly used for metric.
fn flattened_reason_code(&self) -> u32 {
match self {
fidl_sme::DisconnectSource::Ap(cause) => cause.reason_code.into_primitive() as u32,
fidl_sme::DisconnectSource::User(reason) => (1u32 << 16) + *reason as u32,
fidl_sme::DisconnectSource::Mlme(cause) => {
(1u32 << 17) + (cause.reason_code.into_primitive() as u32)
}
}
}
fn cobalt_reason_code(&self) -> u16 {
match self {
// Cobalt metrics expects reason_code value to be less than COBALT_REASON_CODE_MAX.
fidl_sme::DisconnectSource::Ap(cause) => {
std::cmp::min(cause.reason_code.into_primitive(), COBALT_REASON_CODE_MAX)
}
fidl_sme::DisconnectSource::User(reason) => {
std::cmp::min(*reason as u16, COBALT_REASON_CODE_MAX)
}
fidl_sme::DisconnectSource::Mlme(cause) => {
std::cmp::min(cause.reason_code.into_primitive(), COBALT_REASON_CODE_MAX)
}
}
}
fn locally_initiated(&self) -> bool {
match self {
fidl_sme::DisconnectSource::Ap(..) => false,
fidl_sme::DisconnectSource::Mlme(..) | fidl_sme::DisconnectSource::User(..) => true,
}
}
}
#[derive(Debug, PartialEq)]
pub struct ScanEventInspectData {
pub unknown_protection_ies: Vec<String>,
}
impl ScanEventInspectData {
pub fn new() -> Self {
Self { unknown_protection_ies: vec![] }
}
}
#[cfg_attr(test, derive(Debug))]
pub enum TelemetryEvent {
/// Request telemetry for the latest status
QueryStatus {
sender: oneshot::Sender<QueryStatusResult>,
},
/// Notify the telemetry event loop that the process of establishing connection is started
StartEstablishConnection {
/// If set to true, use the current time as the start time of the establish connection
/// process. If set to false, then use the start time initialized from the previous
/// StartEstablishConnection event, or use the current time if there isn't an existing
/// start time.
reset_start_time: bool,
},
/// Clear any existing start time of establish connection process tracked by telemetry.
ClearEstablishConnectionStartTime,
/// Notify the telemetry event loop of an active scan being requested.
ActiveScanRequested {
num_ssids_requested: usize,
},
/// Notify the telemetry event loop of an active scan being requested via Policy API.
ActiveScanRequestedViaApi {
num_ssids_requested: usize,
},
/// Notify the telemetry event loop that network selection is complete.
NetworkSelectionDecision {
/// Type of network selection. If it's undirected and no candidate network is found,
/// telemetry will toggle the "no saved neighbor" flag.
network_selection_type: NetworkSelectionType,
/// When there's a scan error, `num_candidates` should be Err.
/// When `num_candidates` is `Ok(0)` for an undirected network selection, telemetry
/// will toggle the "no saved neighbor" flag. If the event loop is tracking downtime,
/// the subsequent downtime period will also be used to increment the,
/// `downtime_no_saved_neighbor_duration` counter. This counter is used to
/// adjust the raw downtime.
num_candidates: Result<usize, ()>,
/// Count of number of networks selected. This will be 0 if there are no candidates selected
/// including if num_candidates is Ok(0) or Err. However, this will only be logged to
/// Cobalt is num_candidates is not Err and is greater than 0.
selected_count: usize,
},
/// Notify the telemetry event loop of connection result.
/// If connection result is successful, telemetry will move its internal state to
/// connected. Subsequently, the telemetry event loop will increment the `connected_duration`
/// counter periodically.
ConnectResult {
iface_id: u16,
policy_connect_reason: Option<client::types::ConnectReason>,
result: fidl_sme::ConnectResult,
multiple_bss_candidates: bool,
ap_state: client::types::ApState,
network_is_likely_hidden: bool,
},
/// Notify the telemetry event loop that the client has disconnected.
/// Subsequently, the telemetry event loop will increment the downtime counters periodically
/// if TelemetrySender has requested downtime to be tracked via `track_subsequent_downtime`
/// flag.
Disconnected {
/// Indicates whether subsequent period should be used to increment the downtime counters.
track_subsequent_downtime: bool,
info: DisconnectInfo,
},
OnSignalReport {
ind: fidl_internal::SignalReportIndication,
rssi_velocity: f64,
},
OnChannelSwitched {
info: fidl_internal::ChannelSwitchInfo,
},
/// Notify telemetry that there was a decision to look for networks to roam to after evaluating
/// the existing connection.
RoamingScan,
/// Proactive roams do not happen yet, but we want to analyze metrics for when they would
/// happen. Roams are set up to log metrics when disconnects happen to roam, so this event
/// covers when roams would happen but no actual disconnect happens.
WouldRoamConnect,
/// Counts of saved networks and count of configurations for each of those networks, to be
/// recorded periodically.
SavedNetworkCount {
saved_network_count: usize,
config_count_per_saved_network: Vec<usize>,
},
/// Record the time since the last network selection scan
NetworkSelectionScanInterval {
time_since_last_scan: zx::Duration,
},
/// Statistics about networks observed in scan results for Connection Selection
ConnectionSelectionScanResults {
saved_network_count: usize,
bss_count_per_saved_network: Vec<usize>,
saved_network_count_found_by_active_scan: usize,
},
/// Notify telemetry event loop that connection duration has reached threshold to log
/// post-connect score deltas.
PostConnectionScores {
connect_time: fasync::Time,
score_at_connect: u8,
scores: HistoricalList<TimestampedConnectionScore>,
},
/// Notify telemetry of an API request to start client connections.
StartClientConnectionsRequest,
/// Notify telemetry of an API request to stop client connections.
StopClientConnectionsRequest,
/// Notify telemetry of when AP is stopped, and how long it was started.
StopAp {
enabled_duration: zx::Duration,
},
/// Notify telemetry that its experiment group has changed and that a new metrics logger must
/// be created.
UpdateExperiment {
experiment: experiment::ExperimentUpdate,
},
/// Notify telemetry of the result of a create iface request.
IfaceCreationResult(Result<(), ()>),
/// Notify telemetry of the result of destroying an interface.
IfaceDestructionResult(Result<(), ()>),
/// Notify telemetry of the result of a StartAp request.
StartApResult(Result<(), ()>),
/// Record scan fulfillment time
ScanRequestFulfillmentTime {
duration: zx::Duration,
reason: client::scan::ScanReason,
},
/// Record scan queue length upon scan completion
ScanQueueStatistics {
fulfilled_requests: usize,
remaining_requests: usize,
},
/// Record the results of a completed BSS selection
BssSelectionResult {
reason: client::types::ConnectReason,
scored_candidates: Vec<(client::types::ScannedCandidate, i16)>,
selected_candidate: Option<(client::types::ScannedCandidate, i16)>,
},
ScanEvent {
inspect_data: ScanEventInspectData,
scan_defects: Vec<ScanIssue>,
},
LongDurationConnectionScoreAverage {
scores: Vec<TimestampedConnectionScore>,
},
/// Record recovery events and store recovery-related metadata so that the
/// efficacy of the recovery mechanism can be evaluated later.
RecoveryEvent {
reason: RecoveryReason,
},
/// Get the TimeSeries held by telemetry loop. Intended for test only.
GetTimeSeries {
sender: oneshot::Sender<Arc<Mutex<TimeSeriesStats>>>,
},
}
#[derive(Clone, Debug)]
pub struct QueryStatusResult {
connection_state: ConnectionStateInfo,
}
// TODO(https://fxbug.dev/324167674): fix.
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug)]
pub enum ConnectionStateInfo {
Idle,
Disconnected,
Connected {
iface_id: u16,
ap_state: client::types::ApState,
telemetry_proxy: Option<fidl_fuchsia_wlan_sme::TelemetryProxy>,
},
}
#[derive(Clone, Debug, PartialEq)]
pub enum NetworkSelectionType {
/// Looking for the best BSS from any saved networks
Undirected,
/// Looking for the best BSS for a particular network
Directed,
}
#[derive(Debug, PartialEq)]
pub enum ScanIssue {
ScanFailure,
AbortedScan,
EmptyScanResults,
}
impl ScanIssue {
fn as_metric_id(&self) -> u32 {
match self {
ScanIssue::ScanFailure => metrics::CLIENT_SCAN_FAILURE_METRIC_ID,
ScanIssue::AbortedScan => metrics::ABORTED_SCAN_METRIC_ID,
ScanIssue::EmptyScanResults => metrics::EMPTY_SCAN_RESULTS_METRIC_ID,
}
}
}
pub type ClientRecoveryMechanism = metrics::ConnectivityWlanMetricDimensionClientRecoveryMechanism;
pub type ApRecoveryMechanism = metrics::ConnectivityWlanMetricDimensionApRecoveryMechanism;
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum PhyRecoveryMechanism {
PhyReset = 0,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum RecoveryReason {
CreateIfaceFailure(PhyRecoveryMechanism),
DestroyIfaceFailure(PhyRecoveryMechanism),
ConnectFailure(ClientRecoveryMechanism),
StartApFailure(ApRecoveryMechanism),
ScanFailure(ClientRecoveryMechanism),
ScanCancellation(ClientRecoveryMechanism),
ScanResultsEmpty(ClientRecoveryMechanism),
}
struct RecoveryRecord {
scan_failure: Option<RecoveryReason>,
scan_cancellation: Option<RecoveryReason>,
scan_results_empty: Option<RecoveryReason>,
connect_failure: Option<RecoveryReason>,
start_ap_failure: Option<RecoveryReason>,
create_iface_failure: Option<RecoveryReason>,
destroy_iface_failure: Option<RecoveryReason>,
}
impl RecoveryRecord {
fn new() -> Self {
RecoveryRecord {
scan_failure: None,
scan_cancellation: None,
scan_results_empty: None,
connect_failure: None,
start_ap_failure: None,
create_iface_failure: None,
destroy_iface_failure: None,
}
}
fn record_recovery_attempt(&mut self, reason: RecoveryReason) {
match reason {
RecoveryReason::ScanFailure(_) => self.scan_failure = Some(reason),
RecoveryReason::ScanCancellation(_) => self.scan_cancellation = Some(reason),
RecoveryReason::ScanResultsEmpty(_) => self.scan_results_empty = Some(reason),
RecoveryReason::ConnectFailure(_) => self.connect_failure = Some(reason),
RecoveryReason::StartApFailure(_) => self.start_ap_failure = Some(reason),
RecoveryReason::CreateIfaceFailure(_) => self.create_iface_failure = Some(reason),
RecoveryReason::DestroyIfaceFailure(_) => self.destroy_iface_failure = Some(reason),
}
}
}
pub type RecoveryOutcome = metrics::ConnectivityWlanMetricDimensionResult;
pub type CreateMetricsLoggerFn = Box<
dyn Fn(
Vec<u32>,
) -> BoxFuture<
'static,
Result<fidl_fuchsia_metrics::MetricEventLoggerProxy, anyhow::Error>,
>,
>;
/// Capacity of "first come, first serve" slots available to clients of
/// the mpsc::Sender<TelemetryEvent>.
const TELEMETRY_EVENT_BUFFER_SIZE: usize = 100;
/// How often to request RSSI stats and dispatcher packet counts from MLME.
const TELEMETRY_QUERY_INTERVAL: zx::Duration = zx::Duration::from_seconds(15);
/// Create a struct for sending TelemetryEvent, and a future representing the telemetry loop.
///
/// Every 15 seconds, the telemetry loop will query for MLME/PHY stats and update various
/// time-interval stats. The telemetry loop also handles incoming TelemetryEvent to update
/// the appropriate stats.
pub fn serve_telemetry(
monitor_svc_proxy: fidl_fuchsia_wlan_device_service::DeviceMonitorProxy,
cobalt_1dot1_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
new_cobalt_1dot1_proxy: CreateMetricsLoggerFn,
inspect_node: InspectNode,
external_inspect_node: InspectNode,
persistence_req_sender: auto_persist::PersistenceReqSender,
) -> (TelemetrySender, impl Future<Output = ()>) {
let (sender, mut receiver) = mpsc::channel::<TelemetryEvent>(TELEMETRY_EVENT_BUFFER_SIZE);
let sender = TelemetrySender::new(sender);
let cloned_sender = sender.clone();
let fut = async move {
let mut report_interval_stream = fasync::Interval::new(TELEMETRY_QUERY_INTERVAL);
const ONE_MINUTE: zx::Duration = zx::Duration::from_minutes(1);
const_assert_eq!(ONE_MINUTE.into_nanos() % TELEMETRY_QUERY_INTERVAL.into_nanos(), 0);
const INTERVAL_TICKS_PER_MINUTE: u64 =
(ONE_MINUTE.into_nanos() / TELEMETRY_QUERY_INTERVAL.into_nanos()) as u64;
const INTERVAL_TICKS_PER_HR: u64 = INTERVAL_TICKS_PER_MINUTE * 60;
const INTERVAL_TICKS_PER_DAY: u64 = INTERVAL_TICKS_PER_HR * 24;
let mut interval_tick = 0u64;
let mut telemetry = Telemetry::new(
cloned_sender,
monitor_svc_proxy,
cobalt_1dot1_proxy,
new_cobalt_1dot1_proxy,
inspect_node,
external_inspect_node,
persistence_req_sender,
);
loop {
select! {
event = receiver.next() => {
if let Some(event) = event {
telemetry.handle_telemetry_event(event).await;
}
}
_ = report_interval_stream.next() => {
telemetry.handle_periodic_telemetry().await;
interval_tick += 1;
if interval_tick % INTERVAL_TICKS_PER_DAY == 0 {
telemetry.log_daily_cobalt_metrics().await;
}
if interval_tick % (5 * INTERVAL_TICKS_PER_MINUTE) == 0 {
telemetry.persist_client_stats_counters().await;
}
// This ensures that `signal_hr_passed` is always called after
// `handle_periodic_telemetry` at the hour mark. This helps with
// ease of testing. Additionally, logging to Cobalt before sliding
// the window ensures that Cobalt uses the last 24 hours of data
// rather than 23 hours.
if interval_tick % INTERVAL_TICKS_PER_HR == 0 {
telemetry.signal_hr_passed().await;
}
}
}
}
};
(sender, fut)
}
// TODO(https://fxbug.dev/324167674): fix.
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
enum ConnectionState {
// Like disconnected, but no downtime is tracked.
Idle(IdleState),
Connected(ConnectedState),
Disconnected(DisconnectedState),
}
#[derive(Debug)]
struct IdleState {
connect_start_time: Option<fasync::Time>,
}
#[derive(Debug)]
struct ConnectedState {
iface_id: u16,
/// Time when the user manually initiates connecting to another network via the
/// Policy ClientController::Connect FIDL call.
new_connect_start_time: Option<fasync::Time>,
prev_counters: Option<fidl_fuchsia_wlan_stats::IfaceCounterStats>,
multiple_bss_candidates: bool,
ap_state: client::types::ApState,
network_is_likely_hidden: bool,
last_signal_report: fasync::Time,
num_consecutive_get_counter_stats_failures: InspectableU64,
is_driver_unresponsive: InspectableBool,
telemetry_proxy: Option<fidl_fuchsia_wlan_sme::TelemetryProxy>,
}
#[derive(Debug)]
pub struct DisconnectedState {
disconnected_since: fasync::Time,
disconnect_info: DisconnectInfo,
connect_start_time: Option<fasync::Time>,
/// The latest time when the device's no saved neighbor duration was accounted.
/// If this has a value, then conceptually we say that "no saved neighbor" flag
/// is set.
latest_no_saved_neighbor_time: Option<fasync::Time>,
accounted_no_saved_neighbor_duration: zx::Duration,
}
fn inspect_create_counters(
inspect_node: &InspectNode,
child_name: &str,
counters: Arc<Mutex<WindowedStats<StatCounters>>>,
) -> LazyNode {
inspect_node.create_lazy_child(child_name, move || {
let counters = Arc::clone(&counters);
async move {
let inspector = Inspector::default();
{
let counters_mutex_guard = counters.lock();
let counters = counters_mutex_guard.windowed_stat(None);
inspect_insert!(inspector.root(), {
total_duration: counters.total_duration.into_nanos(),
connected_duration: counters.connected_duration.into_nanos(),
downtime_duration: counters.downtime_duration.into_nanos(),
downtime_no_saved_neighbor_duration: counters.downtime_no_saved_neighbor_duration.into_nanos(),
connect_attempts_count: counters.connect_attempts_count,
connect_successful_count: counters.connect_successful_count,
disconnect_count: counters.disconnect_count,
roaming_disconnect_count: counters.roaming_disconnect_count,
non_roaming_non_user_disconnect_count: counters.non_roaming_disconnect_count,
tx_high_packet_drop_duration: counters.tx_high_packet_drop_duration.into_nanos(),
rx_high_packet_drop_duration: counters.rx_high_packet_drop_duration.into_nanos(),
tx_very_high_packet_drop_duration: counters.tx_very_high_packet_drop_duration.into_nanos(),
rx_very_high_packet_drop_duration: counters.rx_very_high_packet_drop_duration.into_nanos(),
no_rx_duration: counters.no_rx_duration.into_nanos(),
});
}
Ok(inspector)
}
.boxed()
})
}
fn inspect_record_connection_status(inspect_node: &InspectNode, telemetry_sender: TelemetrySender) {
inspect_node.record_lazy_child("connection_status", move|| {
let telemetry_sender = telemetry_sender.clone();
async move {
let inspector = Inspector::default();
let (sender, receiver) = oneshot::channel();
telemetry_sender.send(TelemetryEvent::QueryStatus { sender });
let info = match receiver.await {
Ok(result) => result.connection_state,
Err(e) => {
warn!("Unable to query data for Inspect connection status node: {}", e);
return Ok(inspector)
}
};
inspector.root().record_string("status_string", match &info {
ConnectionStateInfo::Idle => "idle".to_string(),
ConnectionStateInfo::Disconnected => "disconnected".to_string(),
ConnectionStateInfo::Connected { .. } => "connected".to_string(),
});
if let ConnectionStateInfo::Connected { ap_state, .. } = info {
inspect_insert!(inspector.root(), connected_network: {
rssi_dbm: ap_state.tracked.signal.rssi_dbm,
snr_db: ap_state.tracked.signal.snr_db,
bssid: ap_state.original().bssid.to_string(),
ssid: ap_state.original().ssid.to_string(),
protection: format!("{:?}", ap_state.original().protection()),
channel: format!("{}", ap_state.original().channel),
ht_cap?: ap_state.original().raw_ht_cap().map(|cap| InspectBytes(cap.bytes)),
vht_cap?: ap_state.original().raw_vht_cap().map(|cap| InspectBytes(cap.bytes)),
wsc?: ap_state.original().probe_resp_wsc().as_ref().map(|wsc| make_inspect_loggable!(
device_name: String::from_utf8_lossy(&wsc.device_name[..]).to_string(),
manufacturer: String::from_utf8_lossy(&wsc.manufacturer[..]).to_string(),
model_name: String::from_utf8_lossy(&wsc.model_name[..]).to_string(),
model_number: String::from_utf8_lossy(&wsc.model_number[..]).to_string(),
)),
is_wmm_assoc: ap_state.original().find_wmm_param().is_some(),
wmm_param?: ap_state.original().find_wmm_param().map(|bytes| InspectBytes(bytes)),
});
}
Ok(inspector)
}
.boxed()
});
}
fn inspect_record_external_data(
external_inspect_node: &ExternalInspectNode,
telemetry_sender: TelemetrySender,
) {
external_inspect_node.node.record_lazy_child("connection_status", move || {
let telemetry_sender = telemetry_sender.clone();
async move {
let inspector = Inspector::default();
let (sender, receiver) = oneshot::channel();
telemetry_sender.send(TelemetryEvent::QueryStatus { sender });
let info = match receiver.await {
Ok(result) => result.connection_state,
Err(e) => {
warn!("Unable to query data for Inspect external node: {}", e);
return Ok(inspector);
}
};
if let ConnectionStateInfo::Connected { ap_state, telemetry_proxy, .. } = info {
inspect_insert!(inspector.root(), connected_network: {
rssi_dbm: ap_state.tracked.signal.rssi_dbm,
snr_db: ap_state.tracked.signal.snr_db,
wsc?: ap_state.original().probe_resp_wsc().as_ref().map(|wsc| make_inspect_loggable!(
device_name: String::from_utf8_lossy(&wsc.device_name[..]).to_string(),
manufacturer: String::from_utf8_lossy(&wsc.manufacturer[..]).to_string(),
model_name: String::from_utf8_lossy(&wsc.model_name[..]).to_string(),
model_number: String::from_utf8_lossy(&wsc.model_number[..]).to_string(),
)),
});
if let Some(proxy) = telemetry_proxy {
match proxy.get_histogram_stats()
.on_timeout(GET_IFACE_STATS_TIMEOUT, || {
warn!("Timed out waiting for histogram stats");
Ok(Err(zx::Status::TIMED_OUT.into_raw()))
})
.await {
Ok(Ok(stats)) => {
let mut histograms = HistogramsNode::new(
inspector.root().create_child("histograms"),
);
histograms
.log_per_antenna_snr_histograms(&stats.snr_histograms[..]);
histograms.log_per_antenna_rx_rate_histograms(
&stats.rx_rate_index_histograms[..],
);
histograms.log_per_antenna_noise_floor_histograms(
&stats.noise_floor_histograms[..],
);
histograms.log_per_antenna_rssi_histograms(
&stats.rssi_histograms[..],
);
inspector.root().record(histograms);
}
error => {
info!("Error reading histogram stats: {:?}", error);
},
}
}
}
Ok(inspector)
}
.boxed()
});
}
#[derive(Debug)]
struct HistogramsNode {
node: InspectNode,
antenna_nodes: HashMap<fidl_fuchsia_wlan_stats::AntennaId, InspectNode>,
}
impl InspectType for HistogramsNode {}
macro_rules! fn_log_per_antenna_histograms {
($name:ident, $field:ident, $histogram_ty:ty, $sample:ident => $sample_index_expr:expr) => {
paste::paste! {
pub fn [<log_per_antenna_ $name _histograms>](
&mut self,
histograms: &[$histogram_ty],
) {
for histogram in histograms {
// Only antenna histograms are logged (STATION scope histograms are discarded)
let antenna_id = match &histogram.antenna_id {
Some(id) => **id,
None => continue,
};
let antenna_node = self.create_or_get_antenna_node(antenna_id);
let samples = &histogram.$field;
let histogram_prop_name = concat!(stringify!($name), "_histogram");
let histogram_prop =
antenna_node.create_int_array(histogram_prop_name, samples.len() * 2);
for (i, sample) in samples.iter().enumerate() {
let $sample = sample;
histogram_prop.set(i * 2, $sample_index_expr);
histogram_prop.set(i * 2 + 1, $sample.num_samples as i64);
}
let invalid_samples_name = concat!(stringify!($name), "_invalid_samples");
let invalid_samples =
antenna_node.create_uint(invalid_samples_name, histogram.invalid_samples);
antenna_node.record(histogram_prop);
antenna_node.record(invalid_samples);
}
}
}
};
}
impl HistogramsNode {
pub fn new(node: InspectNode) -> Self {
Self { node, antenna_nodes: HashMap::new() }
}
// fn log_per_antenna_snr_histograms
fn_log_per_antenna_histograms!(snr, snr_samples, fidl_fuchsia_wlan_stats::SnrHistogram,
sample => sample.bucket_index as i64);
// fn log_per_antenna_rx_rate_histograms
fn_log_per_antenna_histograms!(rx_rate, rx_rate_index_samples,
fidl_fuchsia_wlan_stats::RxRateIndexHistogram,
sample => sample.bucket_index as i64);
// fn log_per_antenna_noise_floor_histograms
fn_log_per_antenna_histograms!(noise_floor, noise_floor_samples,
fidl_fuchsia_wlan_stats::NoiseFloorHistogram,
sample => sample.bucket_index as i64 - 255);
// fn log_per_antenna_rssi_histograms
fn_log_per_antenna_histograms!(rssi, rssi_samples, fidl_fuchsia_wlan_stats::RssiHistogram,
sample => sample.bucket_index as i64 - 255);
fn create_or_get_antenna_node(
&mut self,
antenna_id: fidl_fuchsia_wlan_stats::AntennaId,
) -> &mut InspectNode {
let histograms_node = &self.node;
self.antenna_nodes.entry(antenna_id).or_insert_with(|| {
let freq = match antenna_id.freq {
fidl_fuchsia_wlan_stats::AntennaFreq::Antenna2G => "2Ghz",
fidl_fuchsia_wlan_stats::AntennaFreq::Antenna5G => "5Ghz",
};
let node =
histograms_node.create_child(format!("antenna{}_{}", antenna_id.index, freq));
node.record_uint("antenna_index", antenna_id.index as u64);
node.record_string("antenna_freq", freq);
node
})
}
}
// Macro wrapper for logging simple events (occurrence, integer, histogram, string)
// and log a warning when the status is not Ok
macro_rules! log_cobalt_1dot1 {
($cobalt_proxy:expr, $method_name:ident, $metric_id:expr, $value:expr, $event_codes:expr $(,)?) => {{
let status = $cobalt_proxy.$method_name($metric_id, $value, $event_codes).await;
match status {
Ok(Ok(())) => Ok(()),
Ok(Err(e)) => Err(format_err!("Failed logging metric: {}, error: {:?}", $metric_id, e)),
Err(e) => Err(format_err!("Failed logging metric: {}, error: {}", $metric_id, e)),
}
}};
}
macro_rules! log_cobalt_1dot1_batch {
($cobalt_proxy:expr, $events:expr, $context:expr $(,)?) => {{
let status = $cobalt_proxy.log_metric_events($events).await;
match status {
Ok(Ok(())) => Ok(()),
Ok(Err(e)) => Err(format_err!(
"Failed logging batch metrics, context: {}, error: {:?}",
$context,
e
)),
Err(e) => Err(format_err!(
"Failed logging batch metrics, context: {}, error: {}",
$context,
e
)),
}
}};
}
const INSPECT_SCAN_EVENTS_LIMIT: usize = 7;
const INSPECT_CONNECT_EVENTS_LIMIT: usize = 7;
const INSPECT_DISCONNECT_EVENTS_LIMIT: usize = 7;
const INSPECT_EXTERNAL_DISCONNECT_EVENTS_LIMIT: usize = 2;
/// Inspect node with properties queried by external entities.
/// Do not change or remove existing properties that are still used.
pub struct ExternalInspectNode {
node: InspectNode,
disconnect_events: Mutex<BoundedListNode>,
}
impl ExternalInspectNode {
pub fn new(node: InspectNode) -> Self {
let disconnect_events = node.create_child("disconnect_events");
Self {
node,
disconnect_events: Mutex::new(BoundedListNode::new(
disconnect_events,
INSPECT_EXTERNAL_DISCONNECT_EVENTS_LIMIT,
)),
}
}
}
/// Duration without signal before we determine driver as unresponsive
const UNRESPONSIVE_FLAG_MIN_DURATION: zx::Duration = zx::Duration::from_seconds(60);
pub struct Telemetry {
monitor_svc_proxy: fidl_fuchsia_wlan_device_service::DeviceMonitorProxy,
new_cobalt_1dot1_proxy: CreateMetricsLoggerFn,
connection_state: ConnectionState,
last_checked_connection_state: fasync::Time,
stats_logger: StatsLogger,
// Inspect properties/nodes that telemetry hangs onto
inspect_node: InspectNode,
get_iface_stats_fail_count: UintProperty,
scan_events_node: Mutex<AutoPersist<BoundedListNode>>,
connect_events_node: Mutex<AutoPersist<BoundedListNode>>,
disconnect_events_node: Mutex<AutoPersist<BoundedListNode>>,
external_inspect_node: ExternalInspectNode,
// Auto-persistence on various client stats counters
auto_persist_client_stats_counters: AutoPersist<()>,
// Storage for recalling what experiments are currently active.
experiments: experiment::Experiments,
// For keeping track of how long client connections were enabled when turning client
// connections on and off.
last_enabled_client_connections: Option<fasync::Time>,
// For keeping track of how long client connections were disabled when turning client
// connections off and on again. None if a command to turn off client connections has never
// been sent or if client connections are on.
last_disabled_client_connections: Option<fasync::Time>,
}
impl Telemetry {
pub fn new(
telemetry_sender: TelemetrySender,
monitor_svc_proxy: fidl_fuchsia_wlan_device_service::DeviceMonitorProxy,
cobalt_1dot1_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
new_cobalt_1dot1_proxy: CreateMetricsLoggerFn,
inspect_node: InspectNode,
external_inspect_node: InspectNode,
persistence_req_sender: auto_persist::PersistenceReqSender,
) -> Self {
let stats_logger = StatsLogger::new(cobalt_1dot1_proxy, &inspect_node);
inspect_record_connection_status(&inspect_node, telemetry_sender.clone());
let get_iface_stats_fail_count = inspect_node.create_uint("get_iface_stats_fail_count", 0);
let scan_events = inspect_node.create_child("scan_events");
let connect_events = inspect_node.create_child("connect_events");
let disconnect_events = inspect_node.create_child("disconnect_events");
let external_inspect_node = ExternalInspectNode::new(external_inspect_node);
inspect_record_external_data(&external_inspect_node, telemetry_sender);
Self {
monitor_svc_proxy,
new_cobalt_1dot1_proxy,
connection_state: ConnectionState::Idle(IdleState { connect_start_time: None }),
last_checked_connection_state: fasync::Time::now(),
stats_logger,
inspect_node,
get_iface_stats_fail_count,
scan_events_node: Mutex::new(AutoPersist::new(
BoundedListNode::new(scan_events, INSPECT_SCAN_EVENTS_LIMIT),
"wlancfg-scan-events",
persistence_req_sender.clone(),
)),
connect_events_node: Mutex::new(AutoPersist::new(
BoundedListNode::new(connect_events, INSPECT_CONNECT_EVENTS_LIMIT),
"wlancfg-connect-events",
persistence_req_sender.clone(),
)),
disconnect_events_node: Mutex::new(AutoPersist::new(
BoundedListNode::new(disconnect_events, INSPECT_DISCONNECT_EVENTS_LIMIT),
"wlancfg-disconnect-events",
persistence_req_sender.clone(),
)),
external_inspect_node,
auto_persist_client_stats_counters: AutoPersist::new(
(),
"wlancfg-client-stats-counters",
persistence_req_sender.clone(),
),
experiments: experiment::Experiments::new(),
last_enabled_client_connections: None,
last_disabled_client_connections: None,
}
}
pub async fn handle_periodic_telemetry(&mut self) {
let now = fasync::Time::now();
let duration = now - self.last_checked_connection_state;
self.stats_logger.log_stat(StatOp::AddTotalDuration(duration)).await;
self.stats_logger.log_queued_stats().await;
match &mut self.connection_state {
ConnectionState::Idle(..) => (),
ConnectionState::Connected(state) => {
self.stats_logger.log_stat(StatOp::AddConnectedDuration(duration)).await;
if let Some(proxy) = &state.telemetry_proxy {
match proxy
.get_counter_stats()
.on_timeout(GET_IFACE_STATS_TIMEOUT, || {
warn!("Timed out waiting for counter stats");
Ok(Err(zx::Status::TIMED_OUT.into_raw()))
})
.await
{
Ok(Ok(stats)) => {
*state.num_consecutive_get_counter_stats_failures.get_mut() = 0;
if let Some(prev_counters) = state.prev_counters.as_ref() {
diff_and_log_counters(
&mut self.stats_logger,
prev_counters,
&stats,
duration,
)
.await;
}
let _prev = state.prev_counters.replace(stats);
}
error => {
info!("Failed to get interface stats: {:?}", error);
let _ = self.get_iface_stats_fail_count.add(1);
*state.num_consecutive_get_counter_stats_failures.get_mut() += 1;
// Safe to unwrap: If we've exceeded 63 bits of consecutive failures,
// we have other things to worry about.
self.stats_logger
.log_consecutive_counter_stats_failures(
(*state.num_consecutive_get_counter_stats_failures)
.try_into()
.unwrap(),
)
.await;
let _ = state.prev_counters.take();
}
}
}
let unresponsive_signal_ind =
now - state.last_signal_report > UNRESPONSIVE_FLAG_MIN_DURATION;
let mut is_driver_unresponsive = state.is_driver_unresponsive.get_mut();
if unresponsive_signal_ind != *is_driver_unresponsive {
*is_driver_unresponsive = unresponsive_signal_ind;
if unresponsive_signal_ind {
warn!("driver unresponsive due to missing signal report");
}
}
}
ConnectionState::Disconnected(state) => {
self.stats_logger.log_stat(StatOp::AddDowntimeDuration(duration)).await;
if let Some(prev) = state.latest_no_saved_neighbor_time.take() {
let duration = now - prev;
state.accounted_no_saved_neighbor_duration += duration;
self.stats_logger
.log_stat(StatOp::AddDowntimeNoSavedNeighborDuration(duration))
.await;
state.latest_no_saved_neighbor_time = Some(now);
}
}
}
self.last_checked_connection_state = now;
}
pub async fn handle_telemetry_event(&mut self, event: TelemetryEvent) {
let now = fasync::Time::now();
match event {
TelemetryEvent::QueryStatus { sender } => {
let info = match &self.connection_state {
ConnectionState::Idle(..) => ConnectionStateInfo::Idle,
ConnectionState::Disconnected(..) => ConnectionStateInfo::Disconnected,
ConnectionState::Connected(state) => ConnectionStateInfo::Connected {
iface_id: state.iface_id,
ap_state: state.ap_state.clone(),
telemetry_proxy: state.telemetry_proxy.clone(),
},
};
let _result = sender.send(QueryStatusResult { connection_state: info });
}
TelemetryEvent::StartEstablishConnection { reset_start_time } => match &mut self
.connection_state
{
ConnectionState::Idle(IdleState { connect_start_time })
| ConnectionState::Disconnected(DisconnectedState { connect_start_time, .. }) => {
if reset_start_time || connect_start_time.is_none() {
let _prev = connect_start_time.replace(now);
}
}
ConnectionState::Connected(state) => {
// When in connected state, only set the start time if `reset_start_time` is
// true because it indicates the user triggers the new connect action.
if reset_start_time {
let _prev = state.new_connect_start_time.replace(now);
}
}
},
TelemetryEvent::ClearEstablishConnectionStartTime => match &mut self.connection_state {
ConnectionState::Idle(state) => {
let _start_time = state.connect_start_time.take();
}
ConnectionState::Disconnected(state) => {
let _start_time = state.connect_start_time.take();
}
ConnectionState::Connected(state) => {
let _start_time = state.new_connect_start_time.take();
}
},
TelemetryEvent::ActiveScanRequested { num_ssids_requested } => {
self.stats_logger
.log_active_scan_requested_cobalt_metrics(num_ssids_requested)
.await
}
TelemetryEvent::ActiveScanRequestedViaApi { num_ssids_requested } => {
self.stats_logger
.log_active_scan_requested_via_api_cobalt_metrics(num_ssids_requested)
.await
}
TelemetryEvent::NetworkSelectionDecision {
network_selection_type,
num_candidates,
selected_count,
} => {
self.stats_logger
.log_network_selection_metrics(
&mut self.connection_state,
network_selection_type,
num_candidates,
selected_count,
)
.await;
}
TelemetryEvent::ConnectResult {
iface_id,
policy_connect_reason,
result,
multiple_bss_candidates,
ap_state,
network_is_likely_hidden,
} => {
let connect_start_time = match &self.connection_state {
ConnectionState::Idle(state) => state.connect_start_time,
ConnectionState::Disconnected(state) => state.connect_start_time,
ConnectionState::Connected(..) => {
warn!("Received ConnectResult event while still connected");
None
}
};
self.stats_logger
.report_connect_result(
policy_connect_reason,
result.code,
multiple_bss_candidates,
&ap_state,
connect_start_time,
)
.await;
self.stats_logger.log_stat(StatOp::AddConnectAttemptsCount).await;
if result.code == fidl_ieee80211::StatusCode::Success {
self.log_connect_event_inspect(&ap_state, multiple_bss_candidates);
self.stats_logger.log_stat(StatOp::AddConnectSuccessfulCount).await;
self.stats_logger
.log_device_connected_cobalt_metrics(
multiple_bss_candidates,
&ap_state,
network_is_likely_hidden,
)
.await;
if let ConnectionState::Disconnected(state) = &self.connection_state {
if state.latest_no_saved_neighbor_time.is_some() {
warn!("'No saved neighbor' flag still set even though connected");
}
self.stats_logger.queue_stat_op(StatOp::AddDowntimeDuration(
now - self.last_checked_connection_state,
));
let total_downtime = now - state.disconnected_since;
if total_downtime < state.accounted_no_saved_neighbor_duration {
warn!(
"Total downtime is less than no-saved-neighbor duration. \
Total downtime: {:?}, No saved neighbor duration: {:?}",
total_downtime, state.accounted_no_saved_neighbor_duration
)
}
let adjusted_downtime = max(
total_downtime - state.accounted_no_saved_neighbor_duration,
0.seconds(),
);
self.stats_logger
.log_downtime_cobalt_metrics(adjusted_downtime, &state.disconnect_info)
.await;
let disconnect_source = state.disconnect_info.disconnect_source;
self.stats_logger
.log_reconnect_cobalt_metrics(total_downtime, disconnect_source)
.await;
}
// Log successful post-recovery connection attempt if relevant.
if let Some(recovery_reason) =
self.stats_logger.recovery_record.connect_failure.take()
{
self.stats_logger
.log_post_recovery_result(recovery_reason, RecoveryOutcome::Success)
.await
}
let telemetry_proxy = match fidl::endpoints::create_proxy() {
Ok((proxy, server)) => {
match self.monitor_svc_proxy.get_sme_telemetry(iface_id, server).await {
Ok(Ok(())) => Some(proxy),
Ok(Err(e)) => {
error!("Request for SME telemetry for iface {} completed with error {}. No telemetry will be captured.", iface_id, e);
None
}
Err(e) => {
error!("Failed to request SME telemetry for iface {} with error {}. No telemetry will be captured.", iface_id, e);
None
}
}
}
Err(e) => {
error!("Failed to create SME telemetry channel with error {}. No telemetry will be captured.", e);
None
}
};
self.connection_state = ConnectionState::Connected(ConnectedState {
iface_id,
new_connect_start_time: None,
prev_counters: None,
multiple_bss_candidates,
ap_state,
network_is_likely_hidden,
// We have not received a signal report yet, but since this is used as
// indicator for whether driver is still responsive, set it to the
// connection start time for now.
last_signal_report: now,
num_consecutive_get_counter_stats_failures: InspectableU64::new(
0,
&self.inspect_node,
"num_consecutive_get_counter_stats_failures",
),
is_driver_unresponsive: InspectableBool::new(
false,
&self.inspect_node,
"is_driver_unresponsive",
),
telemetry_proxy,
});
self.last_checked_connection_state = now;
} else if !result.is_credential_rejected {
// In the case where the connection failed for a reason other than a credential
// mismatch, log a connection failure occurrence metric.
self.stats_logger.log_connection_failure().await;
// Log failed post-recovery connection attempt if relevant.
if let Some(recovery_reason) =
self.stats_logger.recovery_record.connect_failure.take()
{
self.stats_logger
.log_post_recovery_result(recovery_reason, RecoveryOutcome::Failure)
.await
}
}
}
TelemetryEvent::Disconnected { track_subsequent_downtime, info } => {
self.log_disconnect_event_inspect(&info);
self.stats_logger
.log_stat(StatOp::AddDisconnectCount(info.disconnect_source))
.await;
self.stats_logger
.log_pre_disconnect_score_deltas(
info.connected_duration,
info.connection_scores.clone(),
)
.await;
let duration = now - self.last_checked_connection_state;
match &self.connection_state {
ConnectionState::Connected(state) => {
self.stats_logger
.log_disconnect_cobalt_metrics(&info, state.multiple_bss_candidates)
.await;
self.stats_logger.queue_stat_op(StatOp::AddConnectedDuration(duration));
// Log device connected to AP metrics right now in case we have not logged it
// to Cobalt yet today.
self.stats_logger
.log_device_connected_cobalt_metrics(
state.multiple_bss_candidates,
&state.ap_state,
state.network_is_likely_hidden,
)
.await;
// Log metrics if connection had a short duration.
if info.connected_duration < METRICS_SHORT_CONNECT_DURATION {
self.stats_logger
.log_short_duration_connection_metrics(
info.connection_scores.clone(),
info.disconnect_source,
info.previous_connect_reason,
)
.await;
}
}
_ => {
warn!("Received disconnect event while not connected. Metric may not be logged");
}
}
let connect_start_time = if info.is_sme_reconnecting {
// If `is_sme_reconnecting` is true, we already know that the process of
// establishing connection is already started at the moment of disconnect,
// so set the connect_start_time to now.
Some(now)
} else {
match &self.connection_state {
ConnectionState::Connected(state) => state.new_connect_start_time,
_ => None,
}
};
self.connection_state = if track_subsequent_downtime {
ConnectionState::Disconnected(DisconnectedState {
disconnected_since: now,
disconnect_info: info,
connect_start_time,
// We assume that there's a saved neighbor in vicinity until proven
// otherwise from scan result.
latest_no_saved_neighbor_time: None,
accounted_no_saved_neighbor_duration: 0.seconds(),
})
} else {
ConnectionState::Idle(IdleState { connect_start_time })
};
self.last_checked_connection_state = now;
}
TelemetryEvent::OnSignalReport { ind, rssi_velocity } => {
if let ConnectionState::Connected(state) = &mut self.connection_state {
state.ap_state.tracked.signal.rssi_dbm = ind.rssi_dbm;
state.ap_state.tracked.signal.snr_db = ind.snr_db;
state.last_signal_report = now;
self.stats_logger.log_signal_report_metrics(ind.rssi_dbm, rssi_velocity).await;
}
}
TelemetryEvent::OnChannelSwitched { info } => {
if let ConnectionState::Connected(state) = &mut self.connection_state {
state.ap_state.tracked.channel.primary = info.new_channel;
self.stats_logger
.log_device_connected_channel_cobalt_metrics(info.new_channel)
.await;
}
}
TelemetryEvent::RoamingScan => {
self.stats_logger.log_roaming_scan_metrics().await;
}
TelemetryEvent::WouldRoamConnect => {
self.stats_logger.log_would_roam_connect().await;
}
TelemetryEvent::SavedNetworkCount {
saved_network_count,
config_count_per_saved_network,
} => {
self.stats_logger
.log_saved_network_counts(saved_network_count, config_count_per_saved_network)
.await;
}
TelemetryEvent::NetworkSelectionScanInterval { time_since_last_scan } => {
self.stats_logger.log_network_selection_scan_interval(time_since_last_scan).await;
}
TelemetryEvent::ConnectionSelectionScanResults {
saved_network_count,
bss_count_per_saved_network,
saved_network_count_found_by_active_scan,
} => {
self.stats_logger
.log_connection_selection_scan_results(
saved_network_count,
bss_count_per_saved_network,
saved_network_count_found_by_active_scan,
)
.await;
}
TelemetryEvent::StartClientConnectionsRequest => {
let now = fasync::Time::now();
if self.last_enabled_client_connections.is_none() {
self.last_enabled_client_connections = Some(now);
}
if let Some(disabled_time) = self.last_disabled_client_connections {
let disabled_duration = now - disabled_time;
self.stats_logger.log_start_client_connections_request(disabled_duration).await
}
self.last_disabled_client_connections = None;
}
TelemetryEvent::StopClientConnectionsRequest => {
let now = fasync::Time::now();
// Do not change the time if the request to turn off connections comes in when
// client connections are already stopped.
if self.last_disabled_client_connections.is_none() {
self.last_disabled_client_connections = Some(fasync::Time::now());
}
if let Some(enabled_time) = self.last_enabled_client_connections {
let enabled_duration = now - enabled_time;
self.stats_logger.log_stop_client_connections_request(enabled_duration).await
}
self.last_enabled_client_connections = None;
}
TelemetryEvent::StopAp { enabled_duration } => {
self.stats_logger.log_stop_ap_cobalt_metrics(enabled_duration).await
}
TelemetryEvent::UpdateExperiment { experiment } => {
self.experiments.update_experiment(experiment);
let active_experiments = self.experiments.get_experiment_ids();
let cobalt_1dot1_proxy =
match (self.new_cobalt_1dot1_proxy)(active_experiments).await {
Ok(proxy) => proxy,
Err(e) => {
warn!("{}", e);
return;
}
};
self.stats_logger.replace_cobalt_proxy(cobalt_1dot1_proxy);
}
TelemetryEvent::IfaceCreationResult(result) => {
self.stats_logger.log_iface_creation_result(result).await;
}
TelemetryEvent::IfaceDestructionResult(result) => {
self.stats_logger.log_iface_destruction_result(result).await;
}
TelemetryEvent::StartApResult(result) => {
self.stats_logger.log_ap_start_result(result).await;
}
TelemetryEvent::ScanRequestFulfillmentTime { duration, reason } => {
self.stats_logger.log_scan_request_fulfillment_time(duration, reason).await;
}
TelemetryEvent::ScanQueueStatistics { fulfilled_requests, remaining_requests } => {
self.stats_logger
.log_scan_queue_statistics(fulfilled_requests, remaining_requests)
.await;
}
TelemetryEvent::BssSelectionResult {
reason,
scored_candidates,
selected_candidate,
} => {
self.stats_logger
.log_bss_selection_metrics(reason, scored_candidates, selected_candidate)
.await
}
TelemetryEvent::PostConnectionScores { connect_time, score_at_connect, scores } => {
self.stats_logger
.log_post_connection_score_deltas(connect_time, score_at_connect, scores)
.await;
}
TelemetryEvent::ScanEvent { inspect_data, scan_defects } => {
self.log_scan_event_inspect(inspect_data);
self.stats_logger.log_scan_issues(scan_defects).await;
}
TelemetryEvent::LongDurationConnectionScoreAverage { scores } => {
self.stats_logger
.log_connection_score_average(
metrics::ConnectionScoreAverageMetricDimensionDuration::LongDuration as u32,
scores,
)
.await;
}
TelemetryEvent::RecoveryEvent { reason } => {
self.stats_logger.log_recovery_occurrence(reason).await;
}
TelemetryEvent::GetTimeSeries { sender } => {
let _result = sender.send(Arc::clone(&self.stats_logger.time_series_stats));
}
}
}
pub fn log_scan_event_inspect(&self, scan_event_info: ScanEventInspectData) {
if !scan_event_info.unknown_protection_ies.is_empty() {
inspect_log!(self.scan_events_node.lock().get_mut(), {
unknown_protection_ies: InspectList(&scan_event_info.unknown_protection_ies)
});
}
}
pub fn log_connect_event_inspect(
&self,
ap_state: &client::types::ApState,
multiple_bss_candidates: bool,
) {
inspect_log!(self.connect_events_node.lock().get_mut(), {
multiple_bss_candidates: multiple_bss_candidates,
network: {
bssid: ap_state.original().bssid.to_string(),
ssid: ap_state.original().ssid.to_string(),
rssi_dbm: ap_state.tracked.signal.rssi_dbm,
snr_db: ap_state.tracked.signal.snr_db,
},
});
}
pub fn log_disconnect_event_inspect(&self, info: &DisconnectInfo) {
inspect_log!(self.disconnect_events_node.lock().get_mut(), {
connected_duration: info.connected_duration.into_nanos(),
disconnect_source: info.disconnect_source.inspect_string(),
network: {
rssi_dbm: info.ap_state.tracked.signal.rssi_dbm,
snr_db: info.ap_state.tracked.signal.snr_db,
bssid: info.ap_state.original().bssid.to_string(),
ssid: info.ap_state.original().ssid.to_string(),
protection: format!("{:?}", info.ap_state.original().protection()),
channel: format!("{}", info.ap_state.tracked.channel),
ht_cap?: info.ap_state.original().raw_ht_cap().map(|cap| InspectBytes(cap.bytes)),
vht_cap?: info.ap_state.original().raw_vht_cap().map(|cap| InspectBytes(cap.bytes)),
wsc?: info.ap_state.original().probe_resp_wsc().as_ref().map(|wsc| make_inspect_loggable!(
device_name: String::from_utf8_lossy(&wsc.device_name[..]).to_string(),
manufacturer: String::from_utf8_lossy(&wsc.manufacturer[..]).to_string(),
model_name: String::from_utf8_lossy(&wsc.model_name[..]).to_string(),
model_number: String::from_utf8_lossy(&wsc.model_number[..]).to_string(),
)),
is_wmm_assoc: info.ap_state.original().find_wmm_param().is_some(),
wmm_param?: info.ap_state.original().find_wmm_param().map(|bytes| InspectBytes(bytes)),
}
});
inspect_log!(self.external_inspect_node.disconnect_events.lock(), {
// Flatten the reason code for external consumer as their reason code metric
// cannot easily be adjusted to accept an additional dimension.
flattened_reason_code: info.disconnect_source.flattened_reason_code(),
locally_initiated: info.disconnect_source.locally_initiated(),
network: {
channel: {
primary: info.ap_state.tracked.channel.primary,
},
},
});
}
pub async fn log_daily_cobalt_metrics(&mut self) {
self.stats_logger.log_daily_cobalt_metrics().await;
if let ConnectionState::Connected(state) = &self.connection_state {
self.stats_logger
.log_device_connected_cobalt_metrics(
state.multiple_bss_candidates,
&state.ap_state,
state.network_is_likely_hidden,
)
.await;
}
}
pub async fn signal_hr_passed(&mut self) {
self.stats_logger.handle_hr_passed().await;
}
pub async fn persist_client_stats_counters(&mut self) {
let _auto_persist_guard = self.auto_persist_client_stats_counters.get_mut();
}
}
// Convert float to an integer in "ten thousandth" unit
// Example: 0.02f64 (i.e. 2%) -> 200 per ten thousand
fn float_to_ten_thousandth(value: f64) -> i64 {
(value * 10000f64) as i64
}
fn round_to_nearest_second(duration: zx::Duration) -> i64 {
const MILLIS_PER_SEC: i64 = 1000;
let millis = duration.into_millis();
let rounded_portion = if millis % MILLIS_PER_SEC >= 500 { 1 } else { 0 };
millis / MILLIS_PER_SEC + rounded_portion
}
pub async fn connect_to_metrics_logger_factory(
) -> Result<fidl_fuchsia_metrics::MetricEventLoggerFactoryProxy, Error> {
let cobalt_1dot1_svc = fuchsia_component::client::connect_to_protocol::<
fidl_fuchsia_metrics::MetricEventLoggerFactoryMarker,
>()
.context("failed to connect to metrics service")?;
Ok(cobalt_1dot1_svc)
}
// Communicates with the MetricEventLoggerFactory service to create a MetricEventLoggerProxy for
// the caller.
pub async fn create_metrics_logger(
factory_proxy: &fidl_fuchsia_metrics::MetricEventLoggerFactoryProxy,
experiment_ids: Option<Vec<u32>>,
) -> Result<fidl_fuchsia_metrics::MetricEventLoggerProxy, Error> {
let (cobalt_1dot1_proxy, cobalt_1dot1_server) =
fidl::endpoints::create_proxy::<fidl_fuchsia_metrics::MetricEventLoggerMarker>()
.context("failed to create MetricEventLoggerMarker endponts")?;
let project_spec = fidl_fuchsia_metrics::ProjectSpec {
customer_id: None, // defaults to fuchsia
project_id: Some(metrics::PROJECT_ID),
..Default::default()
};
let experiment_ids = match experiment_ids {
Some(experiment_ids) => experiment_ids,
None => experiment::default_experiments(),
};
let status = factory_proxy
.create_metric_event_logger_with_experiments(
&project_spec,
&experiment_ids,
cobalt_1dot1_server,
)
.await
.context("failed to create metrics event logger")?;
match status {
Ok(_) => Ok(cobalt_1dot1_proxy),
Err(err) => Err(format_err!("failed to create metrics event logger: {:?}", err)),
}
}
const HIGH_PACKET_DROP_RATE_THRESHOLD: f64 = 0.02;
const VERY_HIGH_PACKET_DROP_RATE_THRESHOLD: f64 = 0.05;
const DEVICE_LOW_CONNECTION_SUCCESS_RATE_THRESHOLD: f64 = 0.1;
async fn diff_and_log_counters(
stats_logger: &mut StatsLogger,
prev: &fidl_fuchsia_wlan_stats::IfaceCounterStats,
current: &fidl_fuchsia_wlan_stats::IfaceCounterStats,
duration: zx::Duration,
) {
let tx_total = current.tx_total - prev.tx_total;
let tx_drop = current.tx_drop - prev.tx_drop;
let rx_total = current.rx_unicast_total - prev.rx_unicast_total;
let rx_drop = current.rx_unicast_drop - prev.rx_unicast_drop;
let tx_drop_rate = if tx_total > 0 { tx_drop as f64 / tx_total as f64 } else { 0f64 };
let rx_drop_rate = if rx_total > 0 { rx_drop as f64 / rx_total as f64 } else { 0f64 };
stats_logger
.log_stat(StatOp::AddRxTxPacketCounters {
rx_unicast_total: rx_total,
rx_unicast_drop: rx_drop,
tx_total,
tx_drop,
})
.await;
if tx_drop_rate > HIGH_PACKET_DROP_RATE_THRESHOLD {
stats_logger.log_stat(StatOp::AddTxHighPacketDropDuration(duration)).await;
}
if rx_drop_rate > HIGH_PACKET_DROP_RATE_THRESHOLD {
stats_logger.log_stat(StatOp::AddRxHighPacketDropDuration(duration)).await;
}
if tx_drop_rate > VERY_HIGH_PACKET_DROP_RATE_THRESHOLD {
stats_logger.log_stat(StatOp::AddTxVeryHighPacketDropDuration(duration)).await;
}
if rx_drop_rate > VERY_HIGH_PACKET_DROP_RATE_THRESHOLD {
stats_logger.log_stat(StatOp::AddRxVeryHighPacketDropDuration(duration)).await;
}
if rx_total == 0 {
stats_logger.log_stat(StatOp::AddNoRxDuration(duration)).await;
}
}
struct StatsLogger {
cobalt_1dot1_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
time_series_stats: Arc<Mutex<TimeSeriesStats>>,
last_1d_stats: Arc<Mutex<WindowedStats<StatCounters>>>,
last_7d_stats: Arc<Mutex<WindowedStats<StatCounters>>>,
/// Stats aggregated for each day and then logged into Cobalt.
/// As these stats are more detailed than `last_1d_stats`, we do not track per-hour
/// windowed stats in order to reduce space and heap allocation. Instead, these stats
/// are logged to Cobalt once every 24 hours and then cleared. Additionally, these
/// are not logged into Inspect.
last_1d_detailed_stats: DailyDetailedStats,
stat_ops: Vec<StatOp>,
hr_tick: u32,
rssi_velocity_hist: HashMap<u32, fidl_fuchsia_metrics::HistogramBucket>,
rssi_hist: HashMap<u32, fidl_fuchsia_metrics::HistogramBucket>,
recovery_record: RecoveryRecord,
throttled_error_logger: ThrottledErrorLogger,
// Inspect nodes
_time_series_inspect_node: LazyNode,
_1d_counters_inspect_node: LazyNode,
_7d_counters_inspect_node: LazyNode,
}
impl StatsLogger {
pub fn new(
cobalt_1dot1_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
inspect_node: &InspectNode,
) -> Self {
let time_series_stats = Arc::new(Mutex::new(TimeSeriesStats::new()));
let last_1d_stats = Arc::new(Mutex::new(WindowedStats::new(24)));
let last_7d_stats = Arc::new(Mutex::new(WindowedStats::new(7)));
let _time_series_inspect_node = inspect_time_series::inspect_create_stats(
inspect_node,
"time_series",
Arc::clone(&time_series_stats),
);
let _1d_counters_inspect_node =
inspect_create_counters(inspect_node, "1d_counters", Arc::clone(&last_1d_stats));
let _7d_counters_inspect_node =
inspect_create_counters(inspect_node, "7d_counters", Arc::clone(&last_7d_stats));
Self {
cobalt_1dot1_proxy,
time_series_stats,
last_1d_stats,
last_7d_stats,
last_1d_detailed_stats: DailyDetailedStats::new(),
stat_ops: vec![],
hr_tick: 0,
rssi_velocity_hist: HashMap::new(),
rssi_hist: HashMap::new(),
recovery_record: RecoveryRecord::new(),
throttled_error_logger: ThrottledErrorLogger::new(
MINUTES_BETWEEN_COBALT_SYSLOG_WARNINGS,
),
_1d_counters_inspect_node,
_7d_counters_inspect_node,
_time_series_inspect_node,
}
}
/// Replace Cobalt proxy with a new one. Used when the Cobalt proxy instance has to
/// be recreated to update an experiment.
pub fn replace_cobalt_proxy(
&mut self,
cobalt_1dot1_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
) {
self.cobalt_1dot1_proxy = cobalt_1dot1_proxy;
}
async fn log_stat(&mut self, stat_op: StatOp) {
self.log_time_series(&stat_op);
self.log_stat_counters(stat_op);
}
fn log_time_series(&mut self, stat_op: &StatOp) {
match stat_op {
StatOp::AddTotalDuration(duration) => {
self.time_series_stats
.lock()
.total_duration_sec
.log_value(&(round_to_nearest_second(*duration) as i32));
}
StatOp::AddConnectedDuration(duration) => {
self.time_series_stats
.lock()
.connected_duration_sec
.log_value(&(round_to_nearest_second(*duration) as i32));
}
StatOp::AddConnectAttemptsCount => {
self.time_series_stats.lock().connect_attempt_count.log_value(&1u32);
}
StatOp::AddConnectSuccessfulCount => {
self.time_series_stats.lock().connect_successful_count.log_value(&1u32);
}
StatOp::AddDisconnectCount(..) => {
self.time_series_stats.lock().disconnect_count.log_value(&1u32);
}
StatOp::AddRxTxPacketCounters {
rx_unicast_total,
rx_unicast_drop,
tx_total,
tx_drop,
} => {
self.time_series_stats
.lock()
.rx_unicast_total_count
.log_value(&(*rx_unicast_total as u32));
self.time_series_stats
.lock()
.rx_unicast_drop_count
.log_value(&(*rx_unicast_drop as u32));
self.time_series_stats.lock().tx_total_count.log_value(&(*tx_total as u32));
self.time_series_stats.lock().tx_drop_count.log_value(&(*tx_drop as u32));
}
StatOp::AddNoRxDuration(duration) => {
self.time_series_stats
.lock()
.no_rx_duration_sec
.log_value(&(round_to_nearest_second(*duration) as i32));
}
StatOp::AddDowntimeDuration(..)
| StatOp::AddDowntimeNoSavedNeighborDuration(..)
| StatOp::AddTxHighPacketDropDuration(..)
| StatOp::AddRxHighPacketDropDuration(..)
| StatOp::AddTxVeryHighPacketDropDuration(..)
| StatOp::AddRxVeryHighPacketDropDuration(..) => (),
}
}
fn log_stat_counters(&mut self, stat_op: StatOp) {
let zero = StatCounters::default();
let addition = match stat_op {
StatOp::AddTotalDuration(duration) => StatCounters { total_duration: duration, ..zero },
StatOp::AddConnectedDuration(duration) => {
StatCounters { connected_duration: duration, ..zero }
}
StatOp::AddDowntimeDuration(duration) => {
StatCounters { downtime_duration: duration, ..zero }
}
StatOp::AddDowntimeNoSavedNeighborDuration(duration) => {
StatCounters { downtime_no_saved_neighbor_duration: duration, ..zero }
}
StatOp::AddConnectAttemptsCount => StatCounters { connect_attempts_count: 1, ..zero },
StatOp::AddConnectSuccessfulCount => {
StatCounters { connect_successful_count: 1, ..zero }
}
StatOp::AddDisconnectCount(disconnect_source) => match disconnect_source {
fidl_sme::DisconnectSource::User(reason) => {
if is_roam_disconnect(reason) {
StatCounters { disconnect_count: 1, roaming_disconnect_count: 1, ..zero }
} else {
StatCounters { disconnect_count: 1, ..zero }
}
}
fidl_sme::DisconnectSource::Mlme(_) | fidl_sme::DisconnectSource::Ap(_) => {
StatCounters { disconnect_count: 1, non_roaming_disconnect_count: 1, ..zero }
}
},
StatOp::AddTxHighPacketDropDuration(duration) => {
StatCounters { tx_high_packet_drop_duration: duration, ..zero }
}
StatOp::AddRxHighPacketDropDuration(duration) => {
StatCounters { rx_high_packet_drop_duration: duration, ..zero }
}
StatOp::AddTxVeryHighPacketDropDuration(duration) => {
StatCounters { tx_very_high_packet_drop_duration: duration, ..zero }
}
StatOp::AddRxVeryHighPacketDropDuration(duration) => {
StatCounters { rx_very_high_packet_drop_duration: duration, ..zero }
}
StatOp::AddNoRxDuration(duration) => StatCounters { no_rx_duration: duration, ..zero },
StatOp::AddRxTxPacketCounters { .. } => StatCounters { ..zero },
};
if addition != zero {
self.last_1d_stats.lock().saturating_add(&addition);
self.last_7d_stats.lock().saturating_add(&addition);
}
}
// Queue stat operation to be logged later. This allows the caller to control the timing of
// when stats are logged. This ensures that various counters are not inconsistent with each
// other because one is logged early and the other one later.
fn queue_stat_op(&mut self, stat_op: StatOp) {
self.stat_ops.push(stat_op);
}
async fn log_queued_stats(&mut self) {
while let Some(stat_op) = self.stat_ops.pop() {
self.log_stat(stat_op).await;
}
}
async fn report_connect_result(
&mut self,
policy_connect_reason: Option<client::types::ConnectReason>,
code: fidl_ieee80211::StatusCode,
multiple_bss_candidates: bool,
ap_state: &client::types::ApState,
connect_start_time: Option<fasync::Time>,
) {
self.log_establish_connection_cobalt_metrics(
policy_connect_reason,
code,
multiple_bss_candidates,
ap_state,
connect_start_time,
)
.await;
*self.last_1d_detailed_stats.connect_attempts_status.entry(code).or_insert(0) += 1;
let is_multi_bss_dim = convert::convert_is_multi_bss(multiple_bss_candidates);
self.last_1d_detailed_stats
.connect_per_is_multi_bss
.entry(is_multi_bss_dim)
.or_insert(ConnectAttemptsCounter::default())
.increment(code);
let security_type_dim = convert::convert_security_type(&ap_state.original().protection());
self.last_1d_detailed_stats
.connect_per_security_type
.entry(security_type_dim)
.or_insert(ConnectAttemptsCounter::default())
.increment(code);
self.last_1d_detailed_stats
.connect_per_primary_channel
.entry(ap_state.tracked.channel.primary)
.or_insert(ConnectAttemptsCounter::default())
.increment(code);
let channel_band_dim = convert::convert_channel_band(ap_state.tracked.channel.primary);
self.last_1d_detailed_stats
.connect_per_channel_band
.entry(channel_band_dim)
.or_insert(ConnectAttemptsCounter::default())
.increment(code);
let rssi_bucket_dim = convert::convert_rssi_bucket(ap_state.tracked.signal.rssi_dbm);
self.last_1d_detailed_stats
.connect_per_rssi_bucket
.entry(rssi_bucket_dim)
.or_insert(ConnectAttemptsCounter::default())
.increment(code);
let snr_bucket_dim = convert::convert_snr_bucket(ap_state.tracked.signal.snr_db);
self.last_1d_detailed_stats
.connect_per_snr_bucket
.entry(snr_bucket_dim)
.or_insert(ConnectAttemptsCounter::default())
.increment(code);
}
async fn log_daily_cobalt_metrics(&mut self) {
self.log_daily_1d_cobalt_metrics().await;
self.log_daily_7d_cobalt_metrics().await;
self.log_daily_detailed_cobalt_metrics().await;
}
async fn log_daily_1d_cobalt_metrics(&mut self) {
let mut metric_events = vec![];
let c = self.last_1d_stats.lock().windowed_stat(None);
let uptime_ratio = c.connected_duration.into_seconds() as f64
/ (c.connected_duration + c.adjusted_downtime()).into_seconds() as f64;
if uptime_ratio.is_finite() {
metric_events.push(MetricEvent {
metric_id: metrics::CONNECTED_UPTIME_RATIO_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(uptime_ratio)),
});
}
let connected_dur_in_day = c.connected_duration.into_seconds() as f64 / (24 * 3600) as f64;
let dpdc_ratio = c.disconnect_count as f64 / connected_dur_in_day;
if dpdc_ratio.is_finite() {
metric_events.push(MetricEvent {
metric_id: metrics::DISCONNECT_PER_DAY_CONNECTED_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(dpdc_ratio)),
});
}
let roam_dpdc_ratio = c.roaming_disconnect_count as f64 / connected_dur_in_day;
if roam_dpdc_ratio.is_finite() {
metric_events.push(MetricEvent {
metric_id: metrics::ROAMING_DISCONNECT_PER_DAY_CONNECTED_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(roam_dpdc_ratio)),
});
}
let non_roam_dpdc_ratio = c.non_roaming_disconnect_count as f64 / connected_dur_in_day;
if non_roam_dpdc_ratio.is_finite() {
metric_events.push(MetricEvent {
metric_id: metrics::NON_ROAMING_DISCONNECT_PER_DAY_CONNECTED_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
non_roam_dpdc_ratio,
)),
});
}
let high_rx_drop_time_ratio = c.rx_high_packet_drop_duration.into_seconds() as f64
/ c.connected_duration.into_seconds() as f64;
if high_rx_drop_time_ratio.is_finite() {
metric_events.push(MetricEvent {
metric_id: metrics::TIME_RATIO_WITH_HIGH_RX_PACKET_DROP_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
high_rx_drop_time_ratio,
)),
});
}
let high_tx_drop_time_ratio = c.tx_high_packet_drop_duration.into_seconds() as f64
/ c.connected_duration.into_seconds() as f64;
if high_tx_drop_time_ratio.is_finite() {
metric_events.push(MetricEvent {
metric_id: metrics::TIME_RATIO_WITH_HIGH_TX_PACKET_DROP_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
high_tx_drop_time_ratio,
)),
});
}
let very_high_rx_drop_time_ratio = c.rx_very_high_packet_drop_duration.into_seconds()
as f64
/ c.connected_duration.into_seconds() as f64;
if very_high_rx_drop_time_ratio.is_finite() {
metric_events.push(MetricEvent {
metric_id: metrics::TIME_RATIO_WITH_VERY_HIGH_RX_PACKET_DROP_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
very_high_rx_drop_time_ratio,
)),
});
}
let very_high_tx_drop_time_ratio = c.tx_very_high_packet_drop_duration.into_seconds()
as f64
/ c.connected_duration.into_seconds() as f64;
if very_high_tx_drop_time_ratio.is_finite() {
metric_events.push(MetricEvent {
metric_id: metrics::TIME_RATIO_WITH_VERY_HIGH_TX_PACKET_DROP_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
very_high_tx_drop_time_ratio,
)),
});
}
let no_rx_time_ratio =
c.no_rx_duration.into_seconds() as f64 / c.connected_duration.into_seconds() as f64;
if no_rx_time_ratio.is_finite() {
metric_events.push(MetricEvent {
metric_id: metrics::TIME_RATIO_WITH_NO_RX_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
no_rx_time_ratio,
)),
});
}
let connection_success_rate = c.connection_success_rate();
if connection_success_rate.is_finite() {
metric_events.push(MetricEvent {
metric_id: metrics::CONNECTION_SUCCESS_RATE_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
connection_success_rate,
)),
});
}
self.throttled_error_logger.throttle_error(log_cobalt_1dot1_batch!(
self.cobalt_1dot1_proxy,
&metric_events,
"log_daily_1d_cobalt_metrics",
));
}
async fn log_daily_7d_cobalt_metrics(&mut self) {
let c = self.last_7d_stats.lock().windowed_stat(None);
let connected_dur_in_day = c.connected_duration.into_seconds() as f64 / (24 * 3600) as f64;
let dpdc_ratio = c.disconnect_count as f64 / connected_dur_in_day;
if dpdc_ratio.is_finite() {
let mut metric_events = vec![];
metric_events.push(MetricEvent {
metric_id: metrics::DISCONNECT_PER_DAY_CONNECTED_7D_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(dpdc_ratio)),
});
self.throttled_error_logger.throttle_error(log_cobalt_1dot1_batch!(
self.cobalt_1dot1_proxy,
&metric_events,
"log_daily_7d_cobalt_metrics",
));
}
}
async fn log_daily_detailed_cobalt_metrics(&mut self) {
let mut metric_events = vec![];
let c = self.last_1d_stats.lock().windowed_stat(None);
if c.connection_success_rate().is_finite() {
let device_low_connection_success =
c.connection_success_rate() < DEVICE_LOW_CONNECTION_SUCCESS_RATE_THRESHOLD;
for (status_code, count) in &self.last_1d_detailed_stats.connect_attempts_status {
metric_events.push(MetricEvent {
metric_id: if device_low_connection_success {
metrics::CONNECT_ATTEMPT_ON_BAD_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID
} else {
metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID
},
event_codes: vec![*status_code as u32],
payload: MetricEventPayload::Count(*count),
});
}
for (is_multi_bss_dim, counters) in
&self.last_1d_detailed_stats.connect_per_is_multi_bss
{
let success_rate = counters.success as f64 / counters.total as f64;
metric_events.push(MetricEvent {
metric_id:
metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID,
event_codes: vec![*is_multi_bss_dim as u32],
payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
success_rate,
)),
});
}
for (security_type_dim, counters) in
&self.last_1d_detailed_stats.connect_per_security_type
{
let success_rate = counters.success as f64 / counters.total as f64;
metric_events.push(MetricEvent {
metric_id:
metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID,
event_codes: vec![*security_type_dim as u32],
payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
success_rate,
)),
});
}
for (primary_channel, counters) in
&self.last_1d_detailed_stats.connect_per_primary_channel
{
let success_rate = counters.success as f64 / counters.total as f64;
metric_events.push(MetricEvent {
metric_id:
metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
event_codes: vec![*primary_channel as u32],
payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
success_rate,
)),
});
}
for (channel_band_dim, counters) in
&self.last_1d_detailed_stats.connect_per_channel_band
{
let success_rate = counters.success as f64 / counters.total as f64;
metric_events.push(MetricEvent {
metric_id:
metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
event_codes: vec![*channel_band_dim as u32],
payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
success_rate,
)),
});
}
for (rssi_bucket_dim, counters) in &self.last_1d_detailed_stats.connect_per_rssi_bucket
{
let success_rate = counters.success as f64 / counters.total as f64;
metric_events.push(MetricEvent {
metric_id:
metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_RSSI_BUCKET_METRIC_ID,
event_codes: vec![*rssi_bucket_dim as u32],
payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
success_rate,
)),
});
}
for (snr_bucket_dim, counters) in &self.last_1d_detailed_stats.connect_per_snr_bucket {
let success_rate = counters.success as f64 / counters.total as f64;
metric_events.push(MetricEvent {
metric_id:
metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SNR_BUCKET_METRIC_ID,
event_codes: vec![*snr_bucket_dim as u32],
payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
success_rate,
)),
});
}
}
self.throttled_error_logger.throttle_error(log_cobalt_1dot1_batch!(
self.cobalt_1dot1_proxy,
&metric_events,
"log_daily_detailed_cobalt_metrics",
));
}
async fn handle_hr_passed(&mut self) {
self.log_hourly_fleetwise_quality_cobalt_metrics().await;
self.hr_tick = (self.hr_tick + 1) % 24;
self.last_1d_stats.lock().slide_window();
if self.hr_tick == 0 {
self.last_7d_stats.lock().slide_window();
self.last_1d_detailed_stats = DailyDetailedStats::new();
}
self.log_hourly_rssi_histogram_metrics().await;
}
// Send out the RSSI and RSSI velocity metrics that have been collected over the last hour.
async fn log_hourly_rssi_histogram_metrics(&mut self) {
let rssi_buckets: Vec<_> = self.rssi_hist.values().copied().collect();
self.throttled_error_logger.throttle_error(log_cobalt_1dot1!(
self.cobalt_1dot1_proxy,
log_integer_histogram,
metrics::CONNECTION_RSSI_METRIC_ID,
&rssi_buckets,
&[],
));
self.rssi_hist.clear();
let velocity_buckets: Vec<_> = self.rssi_velocity_hist.values().copied().collect();
self.throttled_error_logger.throttle_error(log_cobalt_1dot1!(
self.cobalt_1dot1_proxy,
log_integer_histogram,
metrics::RSSI_VELOCITY_METRIC_ID,
&velocity_buckets,
&[],
));
self.rssi_velocity_hist.clear();
}
async fn log_hourly_fleetwise_quality_cobalt_metrics(&mut self) {
let mut metric_events = vec![];
// Get stats from the last hour
let c = self.last_1d_stats.lock().windowed_stat(Some(1));
let total_wlan_uptime = c.connected_duration + c.adjusted_downtime();
// Log the durations calculated in the last hour
metric_events.push(MetricEvent {
metric_id: metrics::TOTAL_WLAN_UPTIME_NEAR_SAVED_NETWORK_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(total_wlan_uptime.into_micros()),
});
metric_events.push(MetricEvent {
metric_id: metrics::TOTAL_CONNECTED_UPTIME_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(c.connected_duration.into_micros()),
});
metric_events.push(MetricEvent {
metric_id: metrics::TOTAL_TIME_WITH_HIGH_RX_PACKET_DROP_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(c.rx_high_packet_drop_duration.into_micros()),
});
metric_events.push(MetricEvent {
metric_id: metrics::TOTAL_TIME_WITH_HIGH_TX_PACKET_DROP_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(c.tx_high_packet_drop_duration.into_micros()),
});
metric_events.push(MetricEvent {
metric_id: metrics::TOTAL_TIME_WITH_VERY_HIGH_RX_PACKET_DROP_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(
c.rx_very_high_packet_drop_duration.into_micros(),
),
});
metric_events.push(MetricEvent {
metric_id: metrics::TOTAL_TIME_WITH_VERY_HIGH_TX_PACKET_DROP_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(
c.tx_very_high_packet_drop_duration.into_micros(),
),
});
metric_events.push(MetricEvent {
metric_id: metrics::TOTAL_TIME_WITH_NO_RX_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(c.no_rx_duration.into_micros()),
});
self.throttled_error_logger.throttle_error(log_cobalt_1dot1_batch!(
self.cobalt_1dot1_proxy,
&metric_events,
"log_hourly_fleetwise_quality_cobalt_metrics",
));
}
async fn log_disconnect_cobalt_metrics(
&mut self,
disconnect_info: &DisconnectInfo,
multiple_bss_candidates: bool,
) {
let mut metric_events = vec![];
let policy_disconnect_reason_dim = {
use metrics::PolicyDisconnectionMigratedMetricDimensionReason::*;
match &disconnect_info.disconnect_source {
fidl_sme::DisconnectSource::User(reason) => match reason {
fidl_sme::UserDisconnectReason::Unknown => Unknown,
fidl_sme::UserDisconnectReason::FailedToConnect => FailedToConnect,
fidl_sme::UserDisconnectReason::FidlConnectRequest => FidlConnectRequest,
fidl_sme::UserDisconnectReason::FidlStopClientConnectionsRequest => {
FidlStopClientConnectionsRequest
}
fidl_sme::UserDisconnectReason::ProactiveNetworkSwitch => {
ProactiveNetworkSwitch
}
fidl_sme::UserDisconnectReason::DisconnectDetectedFromSme => {
DisconnectDetectedFromSme
}
fidl_sme::UserDisconnectReason::RegulatoryRegionChange => {
RegulatoryRegionChange
}
fidl_sme::UserDisconnectReason::Startup => Startup,
fidl_sme::UserDisconnectReason::NetworkUnsaved => NetworkUnsaved,
fidl_sme::UserDisconnectReason::NetworkConfigUpdated => NetworkConfigUpdated,
fidl_sme::UserDisconnectReason::WlanstackUnitTesting
| fidl_sme::UserDisconnectReason::WlanSmeUnitTesting
| fidl_sme::UserDisconnectReason::WlanServiceUtilTesting
| fidl_sme::UserDisconnectReason::WlanDevTool
| fidl_sme::UserDisconnectReason::Recovery => Unknown,
},
fidl_sme::DisconnectSource::Ap(..) | fidl_sme::DisconnectSource::Mlme(..) => {
DisconnectDetectedFromSme
}
}
};
metric_events.push(MetricEvent {
metric_id: metrics::POLICY_DISCONNECTION_MIGRATED_METRIC_ID,
event_codes: vec![policy_disconnect_reason_dim as u32],
payload: MetricEventPayload::Count(1),
});
metric_events.push(MetricEvent {
metric_id: metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::Count(1),
});
let device_uptime_dim = {
use metrics::DisconnectBreakdownByDeviceUptimeMetricDimensionDeviceUptime::*;
match fasync::Time::now() - fasync::Time::from_nanos(0) {
x if x < 1.hour() => LessThan1Hour,
x if x < 3.hours() => LessThan3Hours,
x if x < 12.hours() => LessThan12Hours,
x if x < 24.hours() => LessThan1Day,
x if x < 48.hours() => LessThan2Days,
_ => AtLeast2Days,
}
};
metric_events.push(MetricEvent {
metric_id: metrics::DISCONNECT_BREAKDOWN_BY_DEVICE_UPTIME_METRIC_ID,
event_codes: vec![device_uptime_dim as u32],
payload: MetricEventPayload::Count(1),
});
let connected_duration_dim = {
use metrics::DisconnectBreakdownByConnectedDurationMetricDimensionConnectedDuration::*;
match disconnect_info.connected_duration {
x if x < 30.seconds() => LessThan30Seconds,
x if x < 5.minutes() => LessThan5Minutes,
x if x < 1.hour() => LessThan1Hour,
x if x < 6.hours() => LessThan6Hours,
x if x < 24.hours() => LessThan24Hours,
_ => AtLeast24Hours,
}
};
metric_events.push(MetricEvent {
metric_id: metrics::DISCONNECT_BREAKDOWN_BY_CONNECTED_DURATION_METRIC_ID,
event_codes: vec![connected_duration_dim as u32],
payload: MetricEventPayload::Count(1),
});
let disconnect_source_dim =
convert::convert_disconnect_source(&disconnect_info.disconnect_source);
metric_events.push(MetricEvent {
metric_id: metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID,
event_codes: vec![
disconnect_info.disconnect_source.cobalt_reason_code() as u32,
disconnect_source_dim as u32,
],
payload: MetricEventPayload::Count(1),
});
metric_events.push(MetricEvent {
metric_id: metrics::DISCONNECT_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
event_codes: vec![disconnect_info.ap_state.tracked.channel.primary as u32],
payload: MetricEventPayload::Count(1),
});
let channel_band_dim =
convert::convert_channel_band(disconnect_info.ap_state.tracked.channel.primary);
metric_events.push(MetricEvent {
metric_id: metrics::DISCONNECT_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
event_codes: vec![channel_band_dim as u32],
payload: MetricEventPayload::Count(1),
});
let is_multi_bss_dim = convert::convert_is_multi_bss(multiple_bss_candidates);
metric_events.push(MetricEvent {
metric_id: metrics::DISCONNECT_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID,
event_codes: vec![is_multi_bss_dim as u32],
payload: MetricEventPayload::Count(1),
});
let security_type_dim =
convert::convert_security_type(&disconnect_info.ap_state.original().protection());
metric_events.push(MetricEvent {
metric_id: metrics::DISCONNECT_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID,
event_codes: vec![security_type_dim as u32],
payload: MetricEventPayload::Count(1),
});
// Log connect duration for roaming, non-roaming, not including manual disconnects
// triggered by a user.
let duration_minutes = disconnect_info.connected_duration.into_minutes();
if let fidl_sme::DisconnectSource::User(reason) = disconnect_info.disconnect_source {
// Log duration for roaming/total disconnects if the disconnect is for roaming, but not
// if it is another user reason such as an unsaved network.
if is_roam_disconnect(reason) {
metric_events.push(MetricEvent {
metric_id: metrics::CONNECTED_DURATION_BEFORE_ROAMING_DISCONNECT_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(duration_minutes),
});
metric_events.push(MetricEvent {
metric_id: metrics::NETWORK_ROAMING_DISCONNECT_COUNTS_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::Count(1),
});
}
} else {
metric_events.push(MetricEvent {
metric_id: metrics::CONNECTED_DURATION_BEFORE_NON_ROAMING_DISCONNECT_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(duration_minutes),
});
metric_events.push(MetricEvent {
metric_id: metrics::NETWORK_NON_ROAMING_DISCONNECT_COUNTS_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::Count(1),
});
}
metric_events.push(MetricEvent {
metric_id: metrics::CONNECTED_DURATION_BEFORE_DISCONNECT_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(duration_minutes),
});
metric_events.push(MetricEvent {
metric_id: metrics::NETWORK_DISCONNECT_COUNTS_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::Count(1),
});
if disconnect_info.disconnect_source
== fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::FidlConnectRequest)
{
metric_events.push(MetricEvent {
metric_id: metrics::MANUAL_NETWORK_CHANGE_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::Count(1),
});
}
self.throttled_error_logger.throttle_error(log_cobalt_1dot1_batch!(
self.cobalt_1dot1_proxy,
&metric_events,
"log_disconnect_cobalt_metrics",
));
}
async fn log_active_scan_requested_cobalt_metrics(&mut self, num_ssids_requested: usize) {
use metrics::ActiveScanRequestedForNetworkSelectionMigratedMetricDimensionActiveScanSsidsRequested as ActiveScanSsidsRequested;
let active_scan_ssids_requested_dim = match num_ssids_requested {
0 => ActiveScanSsidsRequested::Zero,
1 => ActiveScanSsidsRequested::One,
2..=4 => ActiveScanSsidsRequested::TwoToFour,
5..=10 => ActiveScanSsidsRequested::FiveToTen,
11..=20 => ActiveScanSsidsRequested::ElevenToTwenty,
21..=50 => ActiveScanSsidsRequested::TwentyOneToFifty,
51..=100 => ActiveScanSsidsRequested::FiftyOneToOneHundred,
101.. => ActiveScanSsidsRequested::OneHundredAndOneOrMore,
};
self.throttled_error_logger.throttle_error(log_cobalt_1dot1!(
self.cobalt_1dot1_proxy,
log_occurrence,
metrics::ACTIVE_SCAN_REQUESTED_FOR_NETWORK_SELECTION_MIGRATED_METRIC_ID,
1,
&[active_scan_ssids_requested_dim as u32],
));
}
async fn log_active_scan_requested_via_api_cobalt_metrics(
&mut self,
num_ssids_requested: usize,
) {
use metrics::ActiveScanRequestedForPolicyApiMetricDimensionActiveScanSsidsRequested as ActiveScanSsidsRequested;
let active_scan_ssids_requested_dim = match num_ssids_requested {
0 => ActiveScanSsidsRequested::Zero,
1 => ActiveScanSsidsRequested::One,
2..=4 => ActiveScanSsidsRequested::TwoToFour,
5..=10 => ActiveScanSsidsRequested::FiveToTen,
11..=20 => ActiveScanSsidsRequested::ElevenToTwenty,
21..=50 => ActiveScanSsidsRequested::TwentyOneToFifty,
51..=100 => ActiveScanSsidsRequested::FiftyOneToOneHundred,
101.. => ActiveScanSsidsRequested::OneHundredAndOneOrMore,
};
self.throttled_error_logger.throttle_error(log_cobalt_1dot1!(
self.cobalt_1dot1_proxy,
log_occurrence,
metrics::ACTIVE_SCAN_REQUESTED_FOR_POLICY_API_METRIC_ID,
1,
&[active_scan_ssids_requested_dim as u32],
));
}
async fn log_saved_network_counts(
&mut self,
saved_network_count: usize,
config_count_per_saved_network: Vec<usize>,
) {
let mut metric_events = vec![];
// Count the total number of saved networks
use metrics::SavedNetworksMigratedMetricDimensionSavedNetworks as SavedNetworksCount;
let num_networks = match saved_network_count {
0 => SavedNetworksCount::Zero,
1 => SavedNetworksCount::One,
2..=4 => SavedNetworksCount::TwoToFour,
5..=40 => SavedNetworksCount::FiveToForty,
41..=500 => SavedNetworksCount::FortyToFiveHundred,
501.. => SavedNetworksCount::FiveHundredAndOneOrMore,
};
metric_events.push(MetricEvent {
metric_id: metrics::SAVED_NETWORKS_MIGRATED_METRIC_ID,
event_codes: vec![num_networks as u32],
payload: MetricEventPayload::Count(1),
});
// Count the number of configs for each saved network
use metrics::SavedConfigurationsForSavedNetworkMigratedMetricDimensionSavedConfigurations as ConfigCountDimension;
for config_count in config_count_per_saved_network {
let num_configs = match config_count {
0 => ConfigCountDimension::Zero,
1 => ConfigCountDimension::One,
2..=4 => ConfigCountDimension::TwoToFour,
5..=40 => ConfigCountDimension::FiveToForty,
41..=500 => ConfigCountDimension::FortyToFiveHundred,
501.. => ConfigCountDimension::FiveHundredAndOneOrMore,
};
metric_events.push(MetricEvent {
metric_id: metrics::SAVED_CONFIGURATIONS_FOR_SAVED_NETWORK_MIGRATED_METRIC_ID,
event_codes: vec![num_configs as u32],
payload: MetricEventPayload::Count(1),
});
}
self.throttled_error_logger.throttle_error(log_cobalt_1dot1_batch!(
self.cobalt_1dot1_proxy,
&metric_events,
"log_saved_network_counts",
));
}
async fn log_network_selection_scan_interval(&mut self, time_since_last_scan: zx::Duration) {
self.throttled_error_logger.throttle_error(log_cobalt_1dot1!(
self.cobalt_1dot1_proxy,
log_integer,
metrics::LAST_SCAN_AGE_WHEN_SCAN_REQUESTED_MIGRATED_METRIC_ID,
time_since_last_scan.into_micros(),
&[],
));
}
async fn log_connection_selection_scan_results(
&mut self,
saved_network_count: usize,
bss_count_per_saved_network: Vec<usize>,
saved_network_count_found_by_active_scan: usize,
) {
let mut metric_events = vec![];
use metrics::SavedNetworkInScanResultMigratedMetricDimensionBssCount as BssCount;
for bss_count in bss_count_per_saved_network {
// Record how many BSSs are visible in the scan results for this saved network.
let bss_count_metric = match bss_count {
0 => BssCount::Zero, // The ::Zero enum exists, but we shouldn't get a scan result with no BSS
1 => BssCount::One,
2..=4 => BssCount::TwoToFour,
5..=10 => BssCount::FiveToTen,
11..=20 => BssCount::ElevenToTwenty,
21.. => BssCount::TwentyOneOrMore,
};
metric_events.push(MetricEvent {
metric_id: metrics::SAVED_NETWORK_IN_SCAN_RESULT_MIGRATED_METRIC_ID,
event_codes: vec![bss_count_metric as u32],
payload: MetricEventPayload::Count(1),
});
}
use metrics::ScanResultsReceivedMigratedMetricDimensionSavedNetworksCount as SavedNetworkCount;
let saved_network_count_metric = match saved_network_count {
0 => SavedNetworkCount::Zero,
1 => SavedNetworkCount::One,
2..=4 => SavedNetworkCount::TwoToFour,
5..=20 => SavedNetworkCount::FiveToTwenty,
21..=40 => SavedNetworkCount::TwentyOneToForty,
41.. => SavedNetworkCount::FortyOneOrMore,
};
metric_events.push(MetricEvent {
metric_id: metrics::SCAN_RESULTS_RECEIVED_MIGRATED_METRIC_ID,
event_codes: vec![saved_network_count_metric as u32],
payload: MetricEventPayload::Count(1),
});
use metrics::SavedNetworkInScanResultWithActiveScanMigratedMetricDimensionActiveScanSsidsObserved as ActiveScanSsidsObserved;
let actively_scanned_networks_metrics = match saved_network_count_found_by_active_scan {
0 => ActiveScanSsidsObserved::Zero,
1 => ActiveScanSsidsObserved::One,
2..=4 => ActiveScanSsidsObserved::TwoToFour,
5..=10 => ActiveScanSsidsObserved::FiveToTen,
11..=20 => ActiveScanSsidsObserved::ElevenToTwenty,
21..=50 => ActiveScanSsidsObserved::TwentyOneToFifty,
51..=100 => ActiveScanSsidsObserved::FiftyOneToOneHundred,
101.. => ActiveScanSsidsObserved::OneHundredAndOneOrMore,
};
metric_events.push(MetricEvent {
metric_id: metrics::SAVED_NETWORK_IN_SCAN_RESULT_WITH_ACTIVE_SCAN_MIGRATED_METRIC_ID,
event_codes: vec![actively_scanned_networks_metrics as u32],
payload: MetricEventPayload::Count(1),
});
self.throttled_error_logger.throttle_error(log_cobalt_1dot1_batch!(
self.cobalt_1dot1_proxy,
&metric_events,
"log_connection_selection_scan_results",
));
}
async fn log_establish_connection_cobalt_metrics(
&mut self,
policy_connect_reason: Option<client::types::ConnectReason>,
code: fidl_ieee80211::StatusCode,
multiple_bss_candidates: bool,
ap_state: &client::types::ApState,
connect_start_time: Option<fasync::Time>,
) {
let metric_events = self.build_establish_connection_cobalt_metrics(
policy_connect_reason,
code,
multiple_bss_candidates,
ap_state,
connect_start_time,
);
self.throttled_error_logger.throttle_error(log_cobalt_1dot1_batch!(
self.cobalt_1dot1_proxy,
&metric_events,
"log_establish_connection_cobalt_metrics",
));
}
fn build_establish_connection_cobalt_metrics(
&mut self,
policy_connect_reason: Option<client::types::ConnectReason>,
code: fidl_ieee80211::StatusCode,
multiple_bss_candidates: bool,
ap_state: &client::types::ApState,
connect_start_time: Option<fasync::Time>,
) -> Vec<MetricEvent> {
let mut metric_events = vec![];
if let Some(policy_connect_reason) = policy_connect_reason {
metric_events.push(MetricEvent {
metric_id: metrics::POLICY_CONNECTION_ATTEMPT_MIGRATED_METRIC_ID,
event_codes: vec![policy_connect_reason as u32],
payload: MetricEventPayload::Count(1),
});
// Also log non-retry connect attempts without dimension
match policy_connect_reason {
metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::FidlConnectRequest
| metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::ProactiveNetworkSwitch
| metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::IdleInterfaceAutoconnect
| metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::NewSavedNetworkAutoconnect => {
metric_events.push(MetricEvent {
metric_id: metrics::POLICY_CONNECTION_ATTEMPTS_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::Count(1),
});
}
metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::RetryAfterDisconnectDetected
| metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::RetryAfterFailedConnectAttempt
| metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::RegulatoryChangeReconnect => (),
}
}
metric_events.push(MetricEvent {
metric_id: metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
event_codes: vec![code as u32],
payload: MetricEventPayload::Count(1),
});
if code != fidl_ieee80211::StatusCode::Success {
return metric_events;
}
match connect_start_time {
Some(start_time) => {
let user_wait_time = fasync::Time::now() - start_time;
let user_wait_time_dim = convert::convert_user_wait_time(user_wait_time);
metric_events.push(MetricEvent {
metric_id: metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID,
event_codes: vec![user_wait_time_dim as u32],
payload: MetricEventPayload::Count(1),
});
}
None => warn!(
"Metric for user wait time on connect is not logged because \
the start time is not populated"
),
}
let is_multi_bss_dim = convert::convert_is_multi_bss(multiple_bss_candidates);
metric_events.push(MetricEvent {
metric_id: metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID,
event_codes: vec![is_multi_bss_dim as u32],
payload: MetricEventPayload::Count(1),
});
let security_type_dim = convert::convert_security_type(&ap_state.original().protection());
metric_events.push(MetricEvent {
metric_id: metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID,
event_codes: vec![security_type_dim as u32],
payload: MetricEventPayload::Count(1),
});
metric_events.push(MetricEvent {
metric_id: metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
event_codes: vec![ap_state.tracked.channel.primary as u32],
payload: MetricEventPayload::Count(1),
});
let channel_band_dim = convert::convert_channel_band(ap_state.tracked.channel.primary);
metric_events.push(MetricEvent {
metric_id: metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
event_codes: vec![channel_band_dim as u32],
payload: MetricEventPayload::Count(1),
});
let oui = ap_state.original().bssid.to_oui_uppercase("");
metric_events.push(MetricEvent {
metric_id: metrics::SUCCESSFUL_CONNECT_PER_OUI_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::StringValue(oui),
});
metric_events
}
async fn log_downtime_cobalt_metrics(
&mut self,
downtime: zx::Duration,
disconnect_info: &DisconnectInfo,
) {
let disconnect_source_dim =
convert::convert_disconnect_source(&disconnect_info.disconnect_source);
self.throttled_error_logger.throttle_error(log_cobalt_1dot1!(
self.cobalt_1dot1_proxy,
log_integer,
metrics::DOWNTIME_BREAKDOWN_BY_DISCONNECT_REASON_METRIC_ID,
downtime.into_micros(),
&[
disconnect_info.disconnect_source.cobalt_reason_code() as u32,
disconnect_source_dim as u32
],
));
}
async fn log_reconnect_cobalt_metrics(
&mut self,
reconnect_duration: zx::Duration,
disconnect_reason: fidl_sme::DisconnectSource,
) {
let mut metric_events = vec![];
let reconnect_duration_dim = {
use metrics::ConnectivityWlanMetricDimensionReconnectDuration::*;
match reconnect_duration {
x if x < 100.millis() => LessThan100Milliseconds,
x if x < 1.second() => LessThan1Second,
x if x < 5.seconds() => LessThan5Seconds,
x if x < 30.seconds() => LessThan30Seconds,
_ => AtLeast30Seconds,
}
};
metric_events.push(MetricEvent {
metric_id: metrics::RECONNECT_BREAKDOWN_BY_DURATION_METRIC_ID,
event_codes: vec![reconnect_duration_dim as u32],
payload: MetricEventPayload::Count(1),
});
// Log the reconnect time for roaming or for unexpected disconnects such as lost signal or
// connection terminated by AP.
if let fidl_sme::DisconnectSource::User(reason) = disconnect_reason {
if is_roam_disconnect(reason) {
metric_events.push(MetricEvent {
metric_id: metrics::ROAMING_RECONNECT_DURATION_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(reconnect_duration.into_micros()),
});
}
} else {
// The other disconnect sources are AP and MLME, which are all considered unexpected.
metric_events.push(MetricEvent {
metric_id: metrics::NON_ROAMING_RECONNECT_DURATION_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(reconnect_duration.into_micros()),
});
}
self.throttled_error_logger.throttle_error(log_cobalt_1dot1_batch!(
self.cobalt_1dot1_proxy,
&metric_events,
"log_reconnect_cobalt_metrics",
));
}
/// Metrics to log when device first connects to an AP, and periodically afterward
/// (at least once a day) if the device is still connected to the AP.
async fn log_device_connected_cobalt_metrics(
&mut self,
multiple_bss_candidates: bool,
ap_state: &client::types::ApState,
network_is_likely_hidden: bool,
) {
let mut metric_events = vec![];
metric_events.push(MetricEvent {
metric_id: metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::Count(1),
});
let security_type_dim = convert::convert_security_type(&ap_state.original().protection());
metric_events.push(MetricEvent {
metric_id: metrics::CONNECTED_NETWORK_SECURITY_TYPE_METRIC_ID,
event_codes: vec![security_type_dim as u32],
payload: MetricEventPayload::Count(1),
});
if ap_state.original().supports_uapsd() {
metric_events.push(MetricEvent {
metric_id: metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_APSD_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::Count(1),
});
}
if let Some(rm_enabled_cap) = ap_state.original().rm_enabled_cap() {
if rm_enabled_cap.link_measurement_enabled() {
metric_events.push(MetricEvent {
metric_id:
metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_LINK_MEASUREMENT_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::Count(1),
});
}
if rm_enabled_cap.neighbor_report_enabled() {
metric_events.push(MetricEvent {
metric_id:
metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_NEIGHBOR_REPORT_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::Count(1),
});
}
}
if ap_state.original().supports_ft() {
metric_events.push(MetricEvent {
metric_id: metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_FT_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::Count(1),
});
}
if let Some(cap) = ap_state.original().ext_cap().and_then(|cap| cap.ext_caps_octet_3) {
if cap.bss_transition() {
metric_events.push(MetricEvent {
metric_id: metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_BSS_TRANSITION_MANAGEMENT_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::Count(1),
});
}
}
let is_multi_bss_dim = convert::convert_is_multi_bss(multiple_bss_candidates);
metric_events.push(MetricEvent {
metric_id: metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID,
event_codes: vec![is_multi_bss_dim as u32],
payload: MetricEventPayload::Count(1),
});
let oui = ap_state.original().bssid.to_oui_uppercase("");
metric_events.push(MetricEvent {
metric_id: metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::StringValue(oui.clone()),
});
append_device_connected_channel_cobalt_metrics(
&mut metric_events,
ap_state.tracked.channel.primary,
);
if network_is_likely_hidden {
metric_events.push(MetricEvent {
metric_id: metrics::CONNECT_TO_LIKELY_HIDDEN_NETWORK_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::Count(1),
});
}
self.throttled_error_logger.throttle_error(log_cobalt_1dot1_batch!(
self.cobalt_1dot1_proxy,
&metric_events,
"log_device_connected_cobalt_metrics",
));
}
async fn log_device_connected_channel_cobalt_metrics(&mut self, primary_channel: u8) {
let mut metric_events = vec![];
append_device_connected_channel_cobalt_metrics(&mut metric_events, primary_channel);
self.throttled_error_logger.throttle_error(log_cobalt_1dot1_batch!(
self.cobalt_1dot1_proxy,
&metric_events,
"log_device_connected_channel_cobalt_metrics",
));
}
async fn log_roaming_scan_metrics(&mut self) {
self.throttled_error_logger.throttle_error(log_cobalt_1dot1!(
self.cobalt_1dot1_proxy,
log_occurrence,
metrics::POLICY_PROACTIVE_ROAMING_SCAN_COUNTS_METRIC_ID,
1,
&[],
));
}
/// Log metrics that will be used to analyze when roaming would happen before roams are
/// enabled. This doesn't effect general disconnect metrics, including ones that include roam
/// event codes.
async fn log_would_roam_connect(&mut self) {
self.throttled_error_logger.throttle_error(log_cobalt_1dot1!(
self.cobalt_1dot1_proxy,
log_occurrence,
metrics::NETWORK_ROAMING_DISCONNECT_COUNTS_METRIC_ID,
1,
&[],
));
}
async fn log_start_client_connections_request(&mut self, disabled_duration: zx::Duration) {
if disabled_duration < USER_RESTART_TIME_THRESHOLD {
self.throttled_error_logger.throttle_error(log_cobalt_1dot1!(
self.cobalt_1dot1_proxy,
log_occurrence,
metrics::CLIENT_CONNECTIONS_STOP_AND_START_METRIC_ID,
1,
&[],
));
}
}
async fn log_stop_client_connections_request(&mut self, enabled_duration: zx::Duration) {
self.throttled_error_logger.throttle_error(log_cobalt_1dot1!(
self.cobalt_1dot1_proxy,
log_integer,
metrics::CLIENT_CONNECTIONS_ENABLED_DURATION_MIGRATED_METRIC_ID,
enabled_duration.into_micros(),
&[],
));
}
async fn log_stop_ap_cobalt_metrics(&mut self, enabled_duration: zx::Duration) {
self.throttled_error_logger.throttle_error(log_cobalt_1dot1!(
self.cobalt_1dot1_proxy,
log_integer,
metrics::ACCESS_POINT_ENABLED_DURATION_MIGRATED_METRIC_ID,
enabled_duration.into_micros(),
&[],
));
}
async fn log_signal_report_metrics(&mut self, rssi: i8, rssi_velocity: f64) {
// The range of the RSSI histogram is -128 to 0 with bucket size 1. The buckets are:
// bucket 0: reserved for underflow, although not possible with i8
// bucket 1: -128
// bucket 2: -127
// ...
// bucket 129: 0
// bucket 130: overflow (1 and above)
let index = min(130, rssi as i16 + 129) as u32;
let entry = self
.rssi_hist
.entry(index)
.or_insert(fidl_fuchsia_metrics::HistogramBucket { index, count: 0 });
entry.count = entry.count + 1;
// Add the count to the RSSI velocity histogram, which will be periodically logged.
// The histogram range is -10 to 10, and index 0 is reserved for values below -10. For
// example, RSSI velocity -10 should map to index 1 and velocity 0 should map to index 11.
const RSSI_VELOCITY_MIN_IDX: f64 = 0.0;
const RSSI_VELOCITY_MAX_IDX: f64 = 22.0;
const RSSI_VELOCITY_HIST_OFFSET: f64 = 11.0;
let index = (rssi_velocity + RSSI_VELOCITY_HIST_OFFSET)
.clamp(RSSI_VELOCITY_MIN_IDX, RSSI_VELOCITY_MAX_IDX) as u32;
let entry = self
.rssi_velocity_hist
.entry(index)
.or_insert(fidl_fuchsia_metrics::HistogramBucket { index, count: 0 });
entry.count = entry.count + 1;
}
async fn log_iface_creation_result(&mut self, result: Result<(), ()>) {
if result.is_err() {
self.throttled_error_logger.throttle_error(log_cobalt_1dot1!(
self.cobalt_1dot1_proxy,
log_occurrence,
metrics::INTERFACE_CREATION_FAILURE_METRIC_ID,
1,
&[]
))
}
if let Some(reason) = self.recovery_record.create_iface_failure.take() {
match result {
Ok(()) => self.log_post_recovery_result(reason, RecoveryOutcome::Success).await,
Err(()) => self.log_post_recovery_result(reason, RecoveryOutcome::Failure).await,
}
}
}
async fn log_iface_destruction_result(&mut self, result: Result<(), ()>) {
if result.is_err() {
self.throttled_error_logger.throttle_error(log_cobalt_1dot1!(
self.cobalt_1dot1_proxy,
log_occurrence,
metrics::INTERFACE_DESTRUCTION_FAILURE_METRIC_ID,
1,
&[]
))
}
if let Some(reason) = self.recovery_record.destroy_iface_failure.take() {
match result {
Ok(()) => self.log_post_recovery_result(reason, RecoveryOutcome::Success).await,
Err(()) => self.log_post_recovery_result(reason, RecoveryOutcome::Failure).await,
}
}
}
async fn log_scan_issues(&mut self, issues: Vec<ScanIssue>) {
// If this is a scan result following a recovery intervention, judge whether or not the
// recovery mechanism was successful.
if let Some(reason) = self.recovery_record.scan_failure.take() {
let outcome = match issues.contains(&ScanIssue::ScanFailure) {
true => RecoveryOutcome::Failure,
false => RecoveryOutcome::Success,
};
self.log_post_recovery_result(reason, outcome).await;
}
if let Some(reason) = self.recovery_record.scan_cancellation.take() {
let outcome = match issues.contains(&ScanIssue::AbortedScan) {
true => RecoveryOutcome::Failure,
false => RecoveryOutcome::Success,
};
self.log_post_recovery_result(reason, outcome).await;
}
if let Some(reason) = self.recovery_record.scan_results_empty.take() {
let outcome = match issues.contains(&ScanIssue::EmptyScanResults) {
true => RecoveryOutcome::Failure,
false => RecoveryOutcome::Success,
};
self.log_post_recovery_result(reason, outcome).await;
}
// Log general occurrence metrics for any observed defects
for issue in issues {
self.throttled_error_logger.throttle_error(log_cobalt_1dot1!(
self.cobalt_1dot1_proxy,
log_occurrence,
issue.as_metric_id(),
1,
&[]
))
}
}
async fn log_connection_failure(&mut self) {
self.throttled_error_logger.throttle_error(log_cobalt_1dot1!(
self.cobalt_1dot1_proxy,
log_occurrence,
metrics::CONNECTION_FAILURES_METRIC_ID,
1,
&[]
))
}
async fn log_ap_start_result(&mut self, result: Result<(), ()>) {
if result.is_err() {
self.throttled_error_logger.throttle_error(log_cobalt_1dot1!(
self.cobalt_1dot1_proxy,
log_occurrence,
metrics::AP_START_FAILURE_METRIC_ID,
1,
&[]
))
}
if let Some(reason) = self.recovery_record.start_ap_failure.take() {
match result {
Ok(()) => self.log_post_recovery_result(reason, RecoveryOutcome::Success).await,
Err(()) => self.log_post_recovery_result(reason, RecoveryOutcome::Failure).await,
}
}
}
async fn log_scan_request_fulfillment_time(
&mut self,
duration: zx::Duration,
reason: client::scan::ScanReason,
) {
let fulfillment_time_dim = {
use metrics::ConnectivityWlanMetricDimensionScanFulfillmentTime::*;
match duration.into_millis() {
..=0_000 => Unknown,
1..=1_000 => LessThanOneSecond,
1_001..=2_000 => LessThanTwoSeconds,
2_001..=3_000 => LessThanThreeSeconds,
3_001..=5_000 => LessThanFiveSeconds,
5_001..=8_000 => LessThanEightSeconds,
8_001..=13_000 => LessThanThirteenSeconds,
13_001..=21_000 => LessThanTwentyOneSeconds,
21_001..=34_000 => LessThanThirtyFourSeconds,
34_001..=55_000 => LessThanFiftyFiveSeconds,
55_001.. => MoreThanFiftyFiveSeconds,
}
};
let reason_dim = {
use client::scan::ScanReason;
use metrics::ConnectivityWlanMetricDimensionScanReason::*;
match reason {
ScanReason::ClientRequest => ClientRequest,
ScanReason::NetworkSelection => NetworkSelection,
ScanReason::BssSelection => BssSelection,
ScanReason::BssSelectionAugmentation => BssSelectionAugmentation,
ScanReason::RoamSearch => ProactiveRoaming,
}
};
self.throttled_error_logger.throttle_error(log_cobalt_1dot1!(
self.cobalt_1dot1_proxy,
log_occurrence,
metrics::SUCCESSFUL_SCAN_REQUEST_FULFILLMENT_TIME_METRIC_ID,
1,
&[fulfillment_time_dim as u32, reason_dim as u32],
))
}
async fn log_scan_queue_statistics(
&mut self,
fulfilled_requests: usize,
remaining_requests: usize,
) {
let fulfilled_requests_dim = {
use metrics::ConnectivityWlanMetricDimensionScanRequestsFulfilled::*;
match fulfilled_requests {
0 => Zero,
1 => One,
2 => Two,
3 => Three,
4 => Four,
5..=9 => FiveToNine,
10.. => TenOrMore,
}
};
let remaining_requests_dim = {
use metrics::ConnectivityWlanMetricDimensionScanRequestsRemaining::*;
match remaining_requests {
0 => Zero,
1 => One,
2 => Two,
3 => Three,
4 => Four,
5..=9 => FiveToNine,
10..=14 => TenToFourteen,
15.. => FifteenOrMore,
}
};
self.throttled_error_logger.throttle_error(log_cobalt_1dot1!(
self.cobalt_1dot1_proxy,
log_occurrence,
metrics::SCAN_QUEUE_STATISTICS_AFTER_COMPLETED_SCAN_METRIC_ID,
1,
&[fulfilled_requests_dim as u32, remaining_requests_dim as u32],
))
}
async fn log_consecutive_counter_stats_failures(&mut self, count: i64) {
self.throttled_error_logger.throttle_error(log_cobalt_1dot1!(
self.cobalt_1dot1_proxy,
log_integer,
metrics::CONSECUTIVE_COUNTER_STATS_FAILURES_METRIC_ID,
count,
&[]
))
}
/// Helper function used to log post-connect and pre-disconnect average score delta metrics. The
/// calculated score delta is the difference between the average of `scores` and the provided
/// baseline score.
async fn log_average_delta_metric(
&mut self,
metric_id: u32,
scores: Vec<TimestampedConnectionScore>,
baseline_score: u8,
time_dimension: u32,
) {
if scores.is_empty() {
warn!("Scores list for time dimension {:?} is empty.", time_dimension);
return;
}
let score_dimension = {
// This dimension is the same for post-connect and pre-disconnect, representing the
// first and last recorded score, respectively.
use metrics::AverageScoreDeltaAfterConnectionByInitialScoreMetricDimensionInitialScore::*;
match baseline_score {
u8::MIN..=20 => _0To20,
21..=40 => _21To40,
41..=60 => _41To60,
61..=80 => _61To80,
81..=u8::MAX => _81To100,
}
};
let baseline_score = baseline_score as u32;
// Using saturating arithmetic to ensure overflow panics are impossible. In practice,
// integers for this metric should not be remotely near overflowing.
let avg = baseline_score.saturating_add(
scores.iter().fold(0u32, |sum, TimestampedConnectionScore { score, .. }| {
sum.saturating_add(*score as u32)
}),
) / (scores.len() + 1) as u32;
let delta = (avg as i64).saturating_sub(baseline_score as i64);
self.throttled_error_logger.throttle_error(log_cobalt_1dot1!(
&self.cobalt_1dot1_proxy,
log_integer,
metric_id,
delta,
&[score_dimension as u32, time_dimension],
));
}
async fn log_post_connection_score_deltas(
&mut self,
connect_time: fasync::Time,
score_at_connect: u8,
scores: HistoricalList<TimestampedConnectionScore>,
) {
// The following time ranges are 100ms longer than the corresponding duration dimensions.
// Scores should be logged every 1 second, but the extra time provides a buffer reports are
// not perfectly periodic.
use metrics::AverageScoreDeltaAfterConnectionByInitialScoreMetricDimensionTimeSinceConnect as DurationDimension;
self.log_average_delta_metric(
metrics::AVERAGE_SCORE_DELTA_AFTER_CONNECTION_BY_INITIAL_SCORE_METRIC_ID,
scores.get_between(connect_time, connect_time + zx::Duration::from_millis(1100)),
score_at_connect,
DurationDimension::OneSecond as u32,
)
.await;
self.log_average_delta_metric(
metrics::AVERAGE_SCORE_DELTA_AFTER_CONNECTION_BY_INITIAL_SCORE_METRIC_ID,
scores.get_between(connect_time, connect_time + zx::Duration::from_millis(5100)),
score_at_connect,
DurationDimension::FiveSeconds as u32,
)
.await;
self.log_average_delta_metric(
metrics::AVERAGE_SCORE_DELTA_AFTER_CONNECTION_BY_INITIAL_SCORE_METRIC_ID,
scores.get_between(connect_time, connect_time + zx::Duration::from_millis(10100)),
score_at_connect,
DurationDimension::TenSeconds as u32,
)
.await;
self.log_average_delta_metric(
metrics::AVERAGE_SCORE_DELTA_AFTER_CONNECTION_BY_INITIAL_SCORE_METRIC_ID,
scores.get_between(connect_time, connect_time + zx::Duration::from_millis(30100)),
score_at_connect,
DurationDimension::ThirtySeconds as u32,
)
.await;
}
async fn log_pre_disconnect_score_deltas(
&mut self,
connect_duration: zx::Duration,
mut scores: HistoricalList<TimestampedConnectionScore>,
) {
// The following time ranges are 100ms longer than the corresponding duration dimensions.
// Scores should be logged every 1 second, but the extra time provides a buffer reports are
// not perfectly periodic.
use metrics::AverageScoreDeltaBeforeDisconnectByFinalScoreMetricDimensionTimeUntilDisconnect as DurationDimension;
if connect_duration >= AVERAGE_SCORE_DELTA_MINIMUM_DURATION {
// Get the last recorded score before the disconnect occurs.
if let Some(TimestampedConnectionScore { score: final_score, time: final_score_time }) =
scores.0.pop_back()
{
self.log_average_delta_metric(
metrics::AVERAGE_SCORE_DELTA_BEFORE_DISCONNECT_BY_FINAL_SCORE_METRIC_ID,
scores.get_recent(final_score_time - zx::Duration::from_millis(1100)),
final_score,
DurationDimension::OneSecond as u32,
)
.await;
self.log_average_delta_metric(
metrics::AVERAGE_SCORE_DELTA_BEFORE_DISCONNECT_BY_FINAL_SCORE_METRIC_ID,
scores.get_recent(final_score_time - zx::Duration::from_millis(5100)),
final_score,
DurationDimension::FiveSeconds as u32,
)
.await;
self.log_average_delta_metric(
metrics::AVERAGE_SCORE_DELTA_BEFORE_DISCONNECT_BY_FINAL_SCORE_METRIC_ID,
scores.get_recent(final_score_time - zx::Duration::from_millis(10100)),
final_score,
DurationDimension::TenSeconds as u32,
)
.await;
self.log_average_delta_metric(
metrics::AVERAGE_SCORE_DELTA_BEFORE_DISCONNECT_BY_FINAL_SCORE_METRIC_ID,
scores.get_recent(final_score_time - zx::Duration::from_millis(30100)),
final_score,
DurationDimension::ThirtySeconds as u32,
)
.await;
} else {
warn!("Past scores list is unexpectedly empty");
}
}
}
async fn log_short_duration_connection_metrics(
&mut self,
scores: HistoricalList<TimestampedConnectionScore>,
disconnect_source: fidl_sme::DisconnectSource,
previous_connect_reason: client::types::ConnectReason,
) {
self.log_connection_score_average(
metrics::ConnectionScoreAverageMetricDimensionDuration::ShortDuration as u32,
scores.get_before(fasync::Time::now()),
)
.await;
// Logs user requested connection during short duration connection, which indicates that we
// did not successfully select the user's preferred connection.
match disconnect_source {
fidl_sme::DisconnectSource::User(
fidl_sme::UserDisconnectReason::FidlConnectRequest,
)
| fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::NetworkUnsaved) => {
let metric_events = vec![
MetricEvent {
metric_id: metrics::POLICY_FIDL_CONNECTION_ATTEMPTS_DURING_SHORT_CONNECTION_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::Count(1),
},
MetricEvent {
metric_id: metrics::POLICY_FIDL_CONNECTION_ATTEMPTS_DURING_SHORT_CONNECTION_DETAILED_METRIC_ID,
event_codes: vec![previous_connect_reason as u32],
payload: MetricEventPayload::Count(1),
}
];
self.throttled_error_logger.throttle_error(log_cobalt_1dot1_batch!(
self.cobalt_1dot1_proxy,
&metric_events,
"log_short_duration_connection_metrics",
));
}
_ => {}
}
}
async fn log_network_selection_metrics(
&mut self,
connection_state: &mut ConnectionState,
network_selection_type: NetworkSelectionType,
num_candidates: Result<usize, ()>,
selected_count: usize,
) {
let now = fasync::Time::now();
let mut metric_events = vec![];
metric_events.push(MetricEvent {
metric_id: metrics::NETWORK_SELECTION_COUNT_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::Count(1),
});
match num_candidates {
Ok(n) if n > 0 => {
// Saved neighbors are seen, so clear the "no saved neighbor" flag. Account
// for any untracked time to the `downtime_no_saved_neighbor_duration`
// counter.
if let ConnectionState::Disconnected(state) = connection_state {
if let Some(prev) = state.latest_no_saved_neighbor_time.take() {
let duration = now - prev;
state.accounted_no_saved_neighbor_duration += duration;
self.queue_stat_op(StatOp::AddDowntimeNoSavedNeighborDuration(duration));
}
}
if network_selection_type == NetworkSelectionType::Undirected {
// Log number of selected networks if a network was not specified.
metric_events.push(MetricEvent {
metric_id: metrics::NUM_NETWORKS_SELECTED_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(selected_count as i64),
});
}
}
Ok(0) if network_selection_type == NetworkSelectionType::Undirected => {
// No saved neighbor is seen. If "no saved neighbor" flag isn't set, then
// set it to the current time. Otherwise, do nothing because the telemetry
// loop will account for untracked downtime during periodic telemetry run.
if let ConnectionState::Disconnected(state) = connection_state {
if state.latest_no_saved_neighbor_time.is_none() {
state.latest_no_saved_neighbor_time = Some(now);
}
}
}
_ => (),
}
self.throttled_error_logger.throttle_error(log_cobalt_1dot1_batch!(
self.cobalt_1dot1_proxy,
&metric_events,
"log_network_selection_metrics",
));
}
async fn log_bss_selection_metrics(
&mut self,
reason: client::types::ConnectReason,
mut scored_candidates: Vec<(client::types::ScannedCandidate, i16)>,
selected_candidate: Option<(client::types::ScannedCandidate, i16)>,
) {
let mut metric_events = vec![];
// Record dimensionless BSS selection count
metric_events.push(MetricEvent {
metric_id: metrics::BSS_SELECTION_COUNT_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::Count(1),
});
// Record detailed BSS selection count
metric_events.push(MetricEvent {
metric_id: metrics::BSS_SELECTION_COUNT_DETAILED_METRIC_ID,
event_codes: vec![reason as u32],
payload: MetricEventPayload::Count(1),
});
// Record dimensionless number of BSS candidates
metric_events.push(MetricEvent {
metric_id: metrics::NUM_BSS_CONSIDERED_IN_SELECTION_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(scored_candidates.len() as i64),
});
// Record detailed number of BSS candidates
metric_events.push(MetricEvent {
metric_id: metrics::NUM_BSS_CONSIDERED_IN_SELECTION_DETAILED_METRIC_ID,
event_codes: vec![reason as u32],
payload: MetricEventPayload::IntegerValue(scored_candidates.len() as i64),
});
if !scored_candidates.is_empty() {
let (mut best_score_2g, mut best_score_5g) = (None, None);
let mut unique_networks = HashSet::new();
for (candidate, score) in &scored_candidates {
// Record candidate's score
metric_events.push(MetricEvent {
metric_id: metrics::BSS_CANDIDATE_SCORE_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(*score as i64),
});
let _ = unique_networks.insert(&candidate.network);
if candidate.bss.channel.is_2ghz() {
best_score_2g = best_score_2g.or(Some(*score)).map(|s| max(s, *score));
} else {
best_score_5g = best_score_5g.or(Some(*score)).map(|s| max(s, *score));
}
}
// Record number of unique networks in bss selection. This differs from number of
// networks selected, since some actions may bypass network selection (e.g. proactive
// roaming)
metric_events.push(MetricEvent {
metric_id: metrics::NUM_NETWORKS_REPRESENTED_IN_BSS_SELECTION_METRIC_ID,
event_codes: vec![reason as u32],
payload: MetricEventPayload::IntegerValue(unique_networks.len() as i64),
});
if let Some((_, score)) = selected_candidate {
// Record selected candidate's score
metric_events.push(MetricEvent {
metric_id: metrics::SELECTED_BSS_SCORE_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(score as i64),
});
// Record runner-up candidate's score, iff there were multiple candidates and the
// selected candidate is the top scoring candidate (or tied in score)
scored_candidates.sort_by_key(|(_, score)| Reverse(*score));
if scored_candidates.len() > 1 && score == scored_candidates[0].1 {
let delta = score - scored_candidates[1].1;
metric_events.push(MetricEvent {
metric_id: metrics::RUNNER_UP_CANDIDATE_SCORE_DELTA_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue(delta as i64),
});
}
}
let ghz_event_code =
if let (Some(score_2g), Some(score_5g)) = (best_score_2g, best_score_5g) {
// Record delta between best 5GHz and best 2.4GHz candidates
metric_events.push(MetricEvent {
metric_id: metrics::BEST_CANDIDATES_GHZ_SCORE_DELTA_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::IntegerValue((score_5g - score_2g) as i64),
});
metrics::ConnectivityWlanMetricDimensionBands::MultiBand
} else if best_score_2g.is_some() {
metrics::ConnectivityWlanMetricDimensionBands::Band2Dot4Ghz
} else {
metrics::ConnectivityWlanMetricDimensionBands::Band5Ghz
};
metric_events.push(MetricEvent {
metric_id: metrics::GHZ_BANDS_AVAILABLE_IN_BSS_SELECTION_METRIC_ID,
event_codes: vec![ghz_event_code as u32],
payload: MetricEventPayload::Count(1),
});
}
self.throttled_error_logger.throttle_error(log_cobalt_1dot1_batch!(
self.cobalt_1dot1_proxy,
&metric_events,
"log_bss_selection_cobalt_metrics",
));
}
async fn log_connection_score_average(
&mut self,
duration_dim: u32,
scores: Vec<TimestampedConnectionScore>,
) {
if !scores.is_empty() {
self.throttled_error_logger.throttle_error(log_cobalt_1dot1!(
self.cobalt_1dot1_proxy,
log_integer,
metrics::CONNECTION_SCORE_AVERAGE_METRIC_ID,
scores.iter().map(|t| t.score as i64).sum::<i64>() / scores.len() as i64,
&[duration_dim],
));
} else {
warn!("Connection score list is unexpectedly empty.");
}
}
async fn log_recovery_occurrence(&mut self, reason: RecoveryReason) {
self.recovery_record.record_recovery_attempt(reason);
let dimension = match reason {
RecoveryReason::CreateIfaceFailure(_) => {
metrics::RecoveryOccurrenceMetricDimensionReason::InterfaceCreationFailure
}
RecoveryReason::DestroyIfaceFailure(_) => {
metrics::RecoveryOccurrenceMetricDimensionReason::InterfaceDestructionFailure
}
RecoveryReason::ConnectFailure(_) => {
metrics::RecoveryOccurrenceMetricDimensionReason::ClientConnectionFailure
}
RecoveryReason::StartApFailure(_) => {
metrics::RecoveryOccurrenceMetricDimensionReason::ApStartFailure
}
RecoveryReason::ScanFailure(_) => {
metrics::RecoveryOccurrenceMetricDimensionReason::ScanFailure
}
RecoveryReason::ScanCancellation(_) => {
metrics::RecoveryOccurrenceMetricDimensionReason::ScanCancellation
}
RecoveryReason::ScanResultsEmpty(_) => {
metrics::RecoveryOccurrenceMetricDimensionReason::ScanResultsEmpty
}
};
self.throttled_error_logger.throttle_error(log_cobalt_1dot1!(
self.cobalt_1dot1_proxy,
log_occurrence,
metrics::RECOVERY_OCCURRENCE_METRIC_ID,
1,
&[dimension.as_event_code()],
))
}
async fn log_post_recovery_result(&mut self, reason: RecoveryReason, outcome: RecoveryOutcome) {
async fn log_post_recovery_metric(
throttled_error_logger: &mut ThrottledErrorLogger,
proxy: &mut fidl_fuchsia_metrics::MetricEventLoggerProxy,
metric_id: u32,
event_codes: &[u32],
) {
throttled_error_logger.throttle_error(log_cobalt_1dot1!(
proxy,
log_occurrence,
metric_id,
1,
event_codes,
))
}
match reason {
RecoveryReason::CreateIfaceFailure(_) => {
log_post_recovery_metric(
&mut self.throttled_error_logger,
&mut self.cobalt_1dot1_proxy,
metrics::INTERFACE_CREATION_RECOVERY_OUTCOME_METRIC_ID,
&[outcome.as_event_code()],
)
.await;
}
RecoveryReason::DestroyIfaceFailure(_) => {
log_post_recovery_metric(
&mut self.throttled_error_logger,
&mut self.cobalt_1dot1_proxy,
metrics::INTERFACE_DESTRUCTION_RECOVERY_OUTCOME_METRIC_ID,
&[outcome.as_event_code()],
)
.await;
}
RecoveryReason::ConnectFailure(mechanism) => {
log_post_recovery_metric(
&mut self.throttled_error_logger,
&mut self.cobalt_1dot1_proxy,
metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
&[outcome.as_event_code(), mechanism.as_event_code()],
)
.await;
}
RecoveryReason::StartApFailure(mechanism) => {
log_post_recovery_metric(
&mut self.throttled_error_logger,
&mut self.cobalt_1dot1_proxy,
metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
&[outcome.as_event_code(), mechanism.as_event_code()],
)
.await;
}
RecoveryReason::ScanFailure(mechanism) => {
log_post_recovery_metric(
&mut self.throttled_error_logger,
&mut self.cobalt_1dot1_proxy,
metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
&[outcome.as_event_code(), mechanism.as_event_code()],
)
.await;
}
RecoveryReason::ScanCancellation(mechanism) => {
log_post_recovery_metric(
&mut self.throttled_error_logger,
&mut self.cobalt_1dot1_proxy,
metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
&[outcome.as_event_code(), mechanism.as_event_code()],
)
.await;
}
RecoveryReason::ScanResultsEmpty(mechanism) => {
log_post_recovery_metric(
&mut self.throttled_error_logger,
&mut self.cobalt_1dot1_proxy,
metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
&[outcome.as_event_code(), mechanism.as_event_code()],
)
.await;
}
}
}
}
// If metrics cannot be reported for extended periods of time, logging new metrics will fail and
// the error messages tend to clutter up the logs. This container limits the rate at which such
// potentially noisy logs are reported. Duplicate error messages are aggregated periodically
// reported.
struct ThrottledErrorLogger {
time_of_last_log: fasync::Time,
suppressed_errors: HashMap<String, usize>,
minutes_between_reports: i64,
}
impl ThrottledErrorLogger {
fn new(minutes_between_reports: i64) -> Self {
Self {
time_of_last_log: fasync::Time::from_nanos(0),
suppressed_errors: HashMap::new(),
minutes_between_reports,
}
}
fn throttle_error(&mut self, result: Result<(), Error>) {
if let Err(e) = result {
// If sufficient time has passed since the last time a cobalt error was logged, report
// the number of cobalt errors that have been encountered.
let curr_time = fasync::Time::now();
let time_since_last_log = curr_time - self.time_of_last_log;
if time_since_last_log.into_minutes() > self.minutes_between_reports {
warn!("{}", e.to_string());
if !self.suppressed_errors.is_empty() {
for (log, count) in self.suppressed_errors.iter() {
warn!("Suppressed {} instances: {}", count, log);
}
self.suppressed_errors.clear();
}
self.time_of_last_log = curr_time;
} else {
// If not enough time has passed since the last warning log, just update the record
// of cobalt errors so that they can be reported later.
let error_string = e.to_string();
let count = self.suppressed_errors.entry(error_string).or_default();
*count += 1;
}
}
}
}
fn append_device_connected_channel_cobalt_metrics(
metric_events: &mut Vec<MetricEvent>,
primary_channel: u8,
) {
metric_events.push(MetricEvent {
metric_id: metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
event_codes: vec![primary_channel as u32],
payload: MetricEventPayload::Count(1),
});
let channel_band_dim = convert::convert_channel_band(primary_channel);
metric_events.push(MetricEvent {
metric_id: metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
event_codes: vec![channel_band_dim as u32],
payload: MetricEventPayload::Count(1),
});
}
// Return whether the user disconnect reason indicates that the disconnect was for a roam.
// This enumerates all enum options in order to cause a compile error if the enum changes.
fn is_roam_disconnect(reason: fidl_sme::UserDisconnectReason) -> bool {
use fidl_sme::UserDisconnectReason;
match reason {
UserDisconnectReason::ProactiveNetworkSwitch => true,
UserDisconnectReason::Unknown
| UserDisconnectReason::FailedToConnect
| UserDisconnectReason::FidlStopClientConnectionsRequest
| UserDisconnectReason::DisconnectDetectedFromSme
| UserDisconnectReason::RegulatoryRegionChange
| UserDisconnectReason::Startup
| UserDisconnectReason::NetworkUnsaved
| UserDisconnectReason::NetworkConfigUpdated
| UserDisconnectReason::FidlConnectRequest
| UserDisconnectReason::WlanstackUnitTesting
| UserDisconnectReason::WlanSmeUnitTesting
| UserDisconnectReason::WlanServiceUtilTesting
| UserDisconnectReason::WlanDevTool
| UserDisconnectReason::Recovery => false,
}
}
enum StatOp {
AddTotalDuration(zx::Duration),
AddConnectedDuration(zx::Duration),
AddDowntimeDuration(zx::Duration),
// Downtime with no saved network in vicinity
AddDowntimeNoSavedNeighborDuration(zx::Duration),
AddConnectAttemptsCount,
AddConnectSuccessfulCount,
AddDisconnectCount(fidl_sme::DisconnectSource),
AddTxHighPacketDropDuration(zx::Duration),
AddRxHighPacketDropDuration(zx::Duration),
AddTxVeryHighPacketDropDuration(zx::Duration),
AddRxVeryHighPacketDropDuration(zx::Duration),
AddNoRxDuration(zx::Duration),
AddRxTxPacketCounters {
rx_unicast_total: u64,
rx_unicast_drop: u64,
tx_total: u64,
tx_drop: u64,
},
}
#[derive(Clone, Default, PartialEq)]
struct StatCounters {
total_duration: zx::Duration,
connected_duration: zx::Duration,
downtime_duration: zx::Duration,
downtime_no_saved_neighbor_duration: zx::Duration,
connect_attempts_count: u64,
connect_successful_count: u64,
disconnect_count: u64,
roaming_disconnect_count: u64,
non_roaming_disconnect_count: u64,
tx_high_packet_drop_duration: zx::Duration,
rx_high_packet_drop_duration: zx::Duration,
tx_very_high_packet_drop_duration: zx::Duration,
rx_very_high_packet_drop_duration: zx::Duration,
no_rx_duration: zx::Duration,
}
impl StatCounters {
fn adjusted_downtime(&self) -> zx::Duration {
max(0.seconds(), self.downtime_duration - self.downtime_no_saved_neighbor_duration)
}
fn connection_success_rate(&self) -> f64 {
self.connect_successful_count as f64 / self.connect_attempts_count as f64
}
}
// `Add` implementation is required to implement `SaturatingAdd` down below.
impl Add for StatCounters {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
total_duration: self.total_duration + other.total_duration,
connected_duration: self.connected_duration + other.connected_duration,
downtime_duration: self.downtime_duration + other.downtime_duration,
downtime_no_saved_neighbor_duration: self.downtime_no_saved_neighbor_duration
+ other.downtime_no_saved_neighbor_duration,
connect_attempts_count: self.connect_attempts_count + other.connect_attempts_count,
connect_successful_count: self.connect_successful_count
+ other.connect_successful_count,
disconnect_count: self.disconnect_count + other.disconnect_count,
roaming_disconnect_count: self.roaming_disconnect_count
+ other.roaming_disconnect_count,
non_roaming_disconnect_count: self.non_roaming_disconnect_count
+ other.non_roaming_disconnect_count,
tx_high_packet_drop_duration: self.tx_high_packet_drop_duration
+ other.tx_high_packet_drop_duration,
rx_high_packet_drop_duration: self.rx_high_packet_drop_duration
+ other.rx_high_packet_drop_duration,
tx_very_high_packet_drop_duration: self.tx_very_high_packet_drop_duration
+ other.tx_very_high_packet_drop_duration,
rx_very_high_packet_drop_duration: self.rx_very_high_packet_drop_duration
+ other.rx_very_high_packet_drop_duration,
no_rx_duration: self.no_rx_duration + other.no_rx_duration,
}
}
}
impl SaturatingAdd for StatCounters {
fn saturating_add(&self, v: &Self) -> Self {
Self {
total_duration: zx::Duration::from_nanos(
self.total_duration.into_nanos().saturating_add(v.total_duration.into_nanos()),
),
connected_duration: zx::Duration::from_nanos(
self.connected_duration
.into_nanos()
.saturating_add(v.connected_duration.into_nanos()),
),
downtime_duration: zx::Duration::from_nanos(
self.downtime_duration
.into_nanos()
.saturating_add(v.downtime_duration.into_nanos()),
),
downtime_no_saved_neighbor_duration: zx::Duration::from_nanos(
self.downtime_no_saved_neighbor_duration
.into_nanos()
.saturating_add(v.downtime_no_saved_neighbor_duration.into_nanos()),
),
connect_attempts_count: self
.connect_attempts_count
.saturating_add(v.connect_attempts_count),
connect_successful_count: self
.connect_successful_count
.saturating_add(v.connect_successful_count),
disconnect_count: self.disconnect_count.saturating_add(v.disconnect_count),
roaming_disconnect_count: self
.roaming_disconnect_count
.saturating_add(v.roaming_disconnect_count),
non_roaming_disconnect_count: self
.non_roaming_disconnect_count
.saturating_add(v.non_roaming_disconnect_count),
tx_high_packet_drop_duration: zx::Duration::from_nanos(
self.tx_high_packet_drop_duration
.into_nanos()
.saturating_add(v.tx_high_packet_drop_duration.into_nanos()),
),
rx_high_packet_drop_duration: zx::Duration::from_nanos(
self.rx_high_packet_drop_duration
.into_nanos()
.saturating_add(v.rx_high_packet_drop_duration.into_nanos()),
),
tx_very_high_packet_drop_duration: zx::Duration::from_nanos(
self.tx_very_high_packet_drop_duration
.into_nanos()
.saturating_add(v.tx_very_high_packet_drop_duration.into_nanos()),
),
rx_very_high_packet_drop_duration: zx::Duration::from_nanos(
self.rx_very_high_packet_drop_duration
.into_nanos()
.saturating_add(v.rx_very_high_packet_drop_duration.into_nanos()),
),
no_rx_duration: zx::Duration::from_nanos(
self.no_rx_duration.into_nanos().saturating_add(v.no_rx_duration.into_nanos()),
),
}
}
}
#[derive(Debug)]
struct DailyDetailedStats {
connect_attempts_status: HashMap<fidl_ieee80211::StatusCode, u64>,
connect_per_is_multi_bss: HashMap<
metrics::SuccessfulConnectBreakdownByIsMultiBssMetricDimensionIsMultiBss,
ConnectAttemptsCounter,
>,
connect_per_security_type: HashMap<
metrics::SuccessfulConnectBreakdownBySecurityTypeMetricDimensionSecurityType,
ConnectAttemptsCounter,
>,
connect_per_primary_channel: HashMap<u8, ConnectAttemptsCounter>,
connect_per_channel_band: HashMap<
metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand,
ConnectAttemptsCounter,
>,
connect_per_rssi_bucket:
HashMap<metrics::ConnectivityWlanMetricDimensionRssiBucket, ConnectAttemptsCounter>,
connect_per_snr_bucket:
HashMap<metrics::ConnectivityWlanMetricDimensionSnrBucket, ConnectAttemptsCounter>,
}
impl DailyDetailedStats {
pub fn new() -> Self {
Self {
connect_attempts_status: HashMap::new(),
connect_per_is_multi_bss: HashMap::new(),
connect_per_security_type: HashMap::new(),
connect_per_primary_channel: HashMap::new(),
connect_per_channel_band: HashMap::new(),
connect_per_rssi_bucket: HashMap::new(),
connect_per_snr_bucket: HashMap::new(),
}
}
}
#[derive(Debug, Default, Copy, Clone, PartialEq)]
struct ConnectAttemptsCounter {
success: u64,
total: u64,
}
impl ConnectAttemptsCounter {
fn increment(&mut self, code: fidl_ieee80211::StatusCode) {
self.total += 1;
if code == fidl_ieee80211::StatusCode::Success {
self.success += 1;
}
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::util::testing::{
create_inspect_persistence_channel, generate_random_bss, generate_random_channel,
generate_random_scanned_candidate,
},
diagnostics_assertions::{
AnyBoolProperty, AnyNumericProperty, AnyStringProperty, NonZeroUintProperty,
},
fidl::endpoints::create_proxy_and_stream,
fidl_fuchsia_metrics::{MetricEvent, MetricEventLoggerRequest, MetricEventPayload},
fidl_fuchsia_wlan_common as fidl_common,
futures::{stream::FusedStream, task::Poll, TryStreamExt},
ieee80211_testutils::{BSSID_REGEX, SSID_REGEX},
rand::Rng,
regex::Regex,
std::{
collections::VecDeque,
pin::{pin, Pin},
},
test_case::test_case,
wlan_common::{
assert_variant,
bss::BssDescription,
channel::{Cbw, Channel},
ie::IeType,
random_bss_description,
test_utils::fake_stas::IesOverrides,
},
};
const STEP_INCREMENT: zx::Duration = zx::Duration::from_seconds(1);
const IFACE_ID: u16 = 1;
// Macro rule for testing Inspect data tree. When we query for Inspect data, the LazyNode
// will make a stats query req that we need to respond to in order to unblock the test.
macro_rules! assert_data_tree_with_respond_blocking_req {
($test_helper:expr, $test_fut:expr, $($rest:tt)+) => {{
use {
fuchsia_inspect::reader, diagnostics_assertions::assert_data_tree,
};
let inspector = $test_helper.inspector.clone();
let read_fut = reader::read(&inspector);
let mut read_fut = pin!(read_fut);
loop {
match $test_helper.exec.run_until_stalled(&mut read_fut) {
Poll::Pending => {
// Run telemetry test future so it can respond to QueryStatus request,
// while clearing out any potentially blocking Cobalt events
$test_helper.drain_cobalt_events(&mut $test_fut);
// Manually respond to iface stats request
if let Some(telemetry_svc_stream) = &mut $test_helper.telemetry_svc_stream {
if !telemetry_svc_stream.is_terminated() {
respond_iface_histogram_stats_req(
&mut $test_helper.exec,
telemetry_svc_stream,
);
}
}
}
Poll::Ready(result) => {
let hierarchy = result.expect("failed to get hierarchy");
assert_data_tree!(hierarchy, $($rest)+);
break
}
}
}
}}
}
#[fuchsia::test]
fn test_detect_driver_unresponsive_signal_ind() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
is_driver_unresponsive: false,
}
});
test_helper.advance_by(
UNRESPONSIVE_FLAG_MIN_DURATION - TELEMETRY_QUERY_INTERVAL,
test_fut.as_mut(),
);
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
is_driver_unresponsive: false,
}
});
// Send a signal, which resets timing information for determining driver unresponsiveness
let ind = fidl_internal::SignalReportIndication { rssi_dbm: -40, snr_db: 30 };
test_helper
.telemetry_sender
.send(TelemetryEvent::OnSignalReport { ind, rssi_velocity: 1.0 });
test_helper.advance_by(UNRESPONSIVE_FLAG_MIN_DURATION, test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
is_driver_unresponsive: false,
}
});
// On the next telemetry interval, driver is recognized as unresponsive
test_helper.advance_by(TELEMETRY_QUERY_INTERVAL, test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
is_driver_unresponsive: true,
}
});
}
#[fuchsia::test]
fn test_logging_num_consecutive_get_counter_stats_failures() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.set_counter_stats_resp(Box::new(|| Err(zx::sys::ZX_ERR_TIMED_OUT)));
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
num_consecutive_get_counter_stats_failures: 0u64,
}
});
test_helper.advance_by(TELEMETRY_QUERY_INTERVAL * 20i64, test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
num_consecutive_get_counter_stats_failures: 20u64,
}
});
// Expect that Cobalt has been notified.
test_helper.drain_cobalt_events(&mut test_fut);
let logged_metrics =
test_helper.get_logged_metrics(metrics::CONSECUTIVE_COUNTER_STATS_FAILURES_METRIC_ID);
assert_eq!(logged_metrics.len(), 20);
assert_eq!(
logged_metrics[19].payload,
fidl_fuchsia_metrics::MetricEventPayload::IntegerValue(20)
);
}
#[fuchsia::test]
fn test_log_connect_event_correct_shape() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
connect_events: {
"0": {
"@time": AnyNumericProperty,
multiple_bss_candidates: AnyBoolProperty,
network: {
bssid: &*BSSID_REGEX,
ssid: &*SSID_REGEX,
rssi_dbm: AnyNumericProperty,
snr_db: AnyNumericProperty,
}
}
}
}
});
}
#[fuchsia::test]
fn test_log_connection_status_correct_shape() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
connection_status: contains {
status_string: AnyStringProperty,
connected_network: contains {
rssi_dbm: AnyNumericProperty,
snr_db: AnyNumericProperty,
bssid: &*BSSID_REGEX,
ssid: &*SSID_REGEX,
protection: AnyStringProperty,
channel: AnyStringProperty,
is_wmm_assoc: AnyBoolProperty,
}
}
}
});
}
#[fuchsia::test]
fn test_log_disconnect_event_correct_shape() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
track_subsequent_downtime: false,
info: fake_disconnect_info(),
});
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
external: contains {
stats: contains {
disconnect_events: {
"0": {
"@time": AnyNumericProperty,
flattened_reason_code: AnyNumericProperty,
locally_initiated: AnyBoolProperty,
network: {
channel: {
primary: AnyNumericProperty,
}
}
}
}
}
},
stats: contains {
disconnect_events: {
"0": {
"@time": AnyNumericProperty,
connected_duration: AnyNumericProperty,
disconnect_source: Regex::new("^source: [^,]+, reason: [^,]+(?:, mlme_event_name: [^,]+)?$").unwrap(),
network: contains {
rssi_dbm: AnyNumericProperty,
snr_db: AnyNumericProperty,
bssid: &*BSSID_REGEX,
ssid: &*SSID_REGEX,
protection: AnyStringProperty,
channel: AnyStringProperty,
is_wmm_assoc: AnyBoolProperty,
}
}
}
}
});
}
#[fuchsia::test]
fn test_stat_cycles() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(24.hours() - TELEMETRY_QUERY_INTERVAL, test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
total_duration: (24.hours() - TELEMETRY_QUERY_INTERVAL).into_nanos(),
connected_duration: (24.hours() - TELEMETRY_QUERY_INTERVAL).into_nanos(),
},
"7d_counters": contains {
total_duration: (24.hours() - TELEMETRY_QUERY_INTERVAL).into_nanos(),
connected_duration: (24.hours() - TELEMETRY_QUERY_INTERVAL).into_nanos(),
},
}
});
test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
// The first hour window is now discarded, so it only shows 23 hours
// of total and connected duration.
total_duration: 23.hours().into_nanos(),
connected_duration: 23.hours().into_nanos(),
},
"7d_counters": contains {
total_duration: 24.hours().into_nanos(),
connected_duration: 24.hours().into_nanos(),
},
}
});
test_helper.advance_by(2.hours(), test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
total_duration: 23.hours().into_nanos(),
connected_duration: 23.hours().into_nanos(),
},
"7d_counters": contains {
total_duration: 26.hours().into_nanos(),
connected_duration: 26.hours().into_nanos(),
},
}
});
// Disconnect now
let info = fake_disconnect_info();
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: false, info });
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(8.hours(), test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
total_duration: 23.hours().into_nanos(),
// Now the 1d connected counter should decrease
connected_duration: 15.hours().into_nanos(),
},
"7d_counters": contains {
total_duration: 34.hours().into_nanos(),
connected_duration: 26.hours().into_nanos(),
},
}
});
// The 7d counters do not decrease before the 7th day
test_helper.advance_by(14.hours(), test_fut.as_mut());
test_helper.advance_by((5 * 24).hours() - TELEMETRY_QUERY_INTERVAL, test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
total_duration: (24.hours() - TELEMETRY_QUERY_INTERVAL).into_nanos(),
connected_duration: 0i64,
},
"7d_counters": contains {
total_duration: ((7 * 24).hours() - TELEMETRY_QUERY_INTERVAL).into_nanos(),
connected_duration: 26.hours().into_nanos(),
},
}
});
// On the 7th day, the first window is removed (24 hours of duration is deducted)
test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
total_duration: 23.hours().into_nanos(),
connected_duration: 0i64,
},
"7d_counters": contains {
total_duration: (6 * 24).hours().into_nanos(),
connected_duration: 2.hours().into_nanos(),
},
}
});
}
#[fuchsia::test]
fn test_daily_detailed_stat_cycles() {
let (mut test_helper, mut test_fut) = setup_test();
for _ in 0..10 {
test_helper.send_connected_event(random_bss_description!(Wpa2));
}
test_helper.advance_by(24.hours(), test_fut.as_mut());
// On 1st day, 10 successful connects, so verify metric is logged with count of 10.
let status_codes = test_helper.get_logged_metrics(
metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
);
assert_eq!(status_codes.len(), 1);
assert_eq!(status_codes[0].event_codes, vec![fidl_ieee80211::StatusCode::Success as u32]);
assert_eq!(status_codes[0].payload, MetricEventPayload::Count(10));
test_helper.cobalt_events.clear();
test_helper.send_connected_event(random_bss_description!(Wpa2));
test_helper.advance_by(24.hours(), test_fut.as_mut());
// On 2nd day, 1 successful connect, so verify metric is logged with count of 1.
let status_codes = test_helper.get_logged_metrics(
metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
);
assert_eq!(status_codes.len(), 1);
assert_eq!(status_codes[0].event_codes, vec![fidl_ieee80211::StatusCode::Success as u32]);
assert_eq!(status_codes[0].payload, MetricEventPayload::Count(1));
}
#[fuchsia::test]
fn test_total_duration_counters() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.advance_by(30.minutes(), test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
total_duration: 30.minutes().into_nanos(),
},
"7d_counters": contains {
total_duration: 30.minutes().into_nanos(),
},
}
});
test_helper.advance_by(30.minutes(), test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
total_duration: 1.hour().into_nanos(),
},
"7d_counters": contains {
total_duration: 1.hour().into_nanos(),
},
}
});
}
#[fuchsia::test]
fn test_total_duration_time_series() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.advance_by(25.seconds(), test_fut.as_mut());
let time_series = test_helper.get_time_series(&mut test_fut);
let total_duration_sec: Vec<_> =
time_series.lock().total_duration_sec.minutely_iter().map(|v| *v).collect();
assert_eq!(total_duration_sec, vec![15]);
test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
let total_duration_sec: Vec<_> =
time_series.lock().total_duration_sec.minutely_iter().map(|v| *v).collect();
assert_eq!(total_duration_sec, vec![30]);
}
/// This test is to verify that after a `TelemetryEvent::UpdateExperiment`,
/// the `1d_counters` and `7d_counters` in Inspect LazyNode are still valid,
/// ensuring that the regression from https://fxbug.dev/42071769 is not introduced
#[fuchsia::test]
fn test_counters_after_update_experiment() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.telemetry_sender.send(TelemetryEvent::UpdateExperiment {
experiment: experiment::ExperimentUpdate::Power(
fidl_common::PowerSaveType::PsModeLowPower,
),
});
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(1.minute(), test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
total_duration: 1.minute().into_nanos(),
},
"7d_counters": contains {
total_duration: 1.minute().into_nanos(),
},
}
});
}
#[fuchsia::test]
fn test_counters_when_idle() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.advance_by(30.minutes(), test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
connected_duration: 0i64,
downtime_duration: 0i64,
downtime_no_saved_neighbor_duration: 0i64,
},
"7d_counters": contains {
connected_duration: 0i64,
downtime_duration: 0i64,
downtime_no_saved_neighbor_duration: 0i64,
},
}
});
test_helper.advance_by(30.minutes(), test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
connected_duration: 0i64,
downtime_duration: 0i64,
downtime_no_saved_neighbor_duration: 0i64,
},
"7d_counters": contains {
connected_duration: 0i64,
downtime_duration: 0i64,
downtime_no_saved_neighbor_duration: 0i64,
},
}
});
}
#[fuchsia::test]
fn test_connected_counters_increase_when_connected() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(30.minutes(), test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
connected_duration: 30.minutes().into_nanos(),
downtime_duration: 0i64,
downtime_no_saved_neighbor_duration: 0i64,
},
"7d_counters": contains {
connected_duration: 30.minutes().into_nanos(),
downtime_duration: 0i64,
downtime_no_saved_neighbor_duration: 0i64,
},
}
});
test_helper.advance_by(30.minutes(), test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
connected_duration: 1.hour().into_nanos(),
downtime_duration: 0i64,
downtime_no_saved_neighbor_duration: 0i64,
},
"7d_counters": contains {
connected_duration: 1.hour().into_nanos(),
downtime_duration: 0i64,
downtime_no_saved_neighbor_duration: 0i64,
},
}
});
}
#[fuchsia::test]
fn test_downtime_counter() {
let (mut test_helper, mut test_fut) = setup_test();
// Disconnect but not track downtime. Downtime counter should not increase.
let info = fake_disconnect_info();
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: false, info });
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(10.minutes(), test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
connected_duration: 0i64,
downtime_duration: 0i64,
downtime_no_saved_neighbor_duration: 0i64,
},
"7d_counters": contains {
connected_duration: 0i64,
downtime_duration: 0i64,
downtime_no_saved_neighbor_duration: 0i64,
},
}
});
// Disconnect and track downtime. Downtime counter should now increase
let info = fake_disconnect_info();
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: true, info });
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(15.minutes(), test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
connected_duration: 0i64,
downtime_duration: 15.minutes().into_nanos(),
downtime_no_saved_neighbor_duration: 0i64,
},
"7d_counters": contains {
connected_duration: 0i64,
downtime_duration: 15.minutes().into_nanos(),
downtime_no_saved_neighbor_duration: 0i64,
},
}
});
}
#[fuchsia::test]
fn test_counters_connect_then_disconnect() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(5.seconds(), test_fut.as_mut());
// Disconnect but not track downtime. Downtime counter should not increase.
let info = fake_disconnect_info();
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: true, info });
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
// The 5 seconds connected duration is not accounted for yet.
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
connected_duration: 0i64,
downtime_duration: 0i64,
downtime_no_saved_neighbor_duration: 0i64,
},
"7d_counters": contains {
connected_duration: 0i64,
downtime_duration: 0i64,
downtime_no_saved_neighbor_duration: 0i64,
},
}
});
// At next telemetry checkpoint, `test_fut` updates the connected and downtime durations.
let downtime_start = fasync::Time::now();
test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
connected_duration: 5.seconds().into_nanos(),
downtime_duration: (fasync::Time::now() - downtime_start).into_nanos(),
downtime_no_saved_neighbor_duration: 0i64,
},
"7d_counters": contains {
connected_duration: 5.seconds().into_nanos(),
downtime_duration: (fasync::Time::now() - downtime_start).into_nanos(),
downtime_no_saved_neighbor_duration: 0i64,
},
}
});
}
#[fuchsia::test]
fn test_downtime_no_saved_neighbor_duration_counter() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.send_connected_event(random_bss_description!(Wpa2));
test_helper.drain_cobalt_events(&mut test_fut);
// Disconnect and track downtime.
let info = fake_disconnect_info();
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: true, info });
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(5.seconds(), test_fut.as_mut());
// Indicate that there's no saved neighbor in vicinity
test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
network_selection_type: NetworkSelectionType::Undirected,
num_candidates: Ok(0),
selected_count: 0,
});
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
connected_duration: 0i64,
downtime_duration: TELEMETRY_QUERY_INTERVAL.into_nanos(),
downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL - 5.seconds()).into_nanos(),
},
"7d_counters": contains {
connected_duration: 0i64,
downtime_duration: TELEMETRY_QUERY_INTERVAL.into_nanos(),
downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL - 5.seconds()).into_nanos(),
},
}
});
test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
connected_duration: 0i64,
downtime_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL*2 - 5.seconds()).into_nanos(),
},
"7d_counters": contains {
connected_duration: 0i64,
downtime_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL*2 - 5.seconds()).into_nanos(),
},
}
});
test_helper.advance_by(5.seconds(), test_fut.as_mut());
// Indicate that saved neighbor has been found
test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
network_selection_type: NetworkSelectionType::Undirected,
num_candidates: Ok(1),
selected_count: 0,
});
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
// `downtime_no_saved_neighbor_duration` counter is not updated right away.
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
connected_duration: 0i64,
downtime_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL*2 - 5.seconds()).into_nanos(),
},
"7d_counters": contains {
connected_duration: 0i64,
downtime_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL*2 - 5.seconds()).into_nanos(),
},
}
});
// At the next checkpoint, both downtime counters are updated together.
test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
connected_duration: 0i64,
downtime_duration: (TELEMETRY_QUERY_INTERVAL * 3).into_nanos(),
downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
},
"7d_counters": contains {
connected_duration: 0i64,
downtime_duration: (TELEMETRY_QUERY_INTERVAL * 3).into_nanos(),
downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
},
}
});
// Disconnect but don't track downtime
let info = fake_disconnect_info();
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: false, info });
// Indicate that there's no saved neighbor in vicinity
test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
network_selection_type: NetworkSelectionType::Undirected,
num_candidates: Ok(0),
selected_count: 0,
});
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
// However, this time neither of the downtime counters should be incremented
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
connected_duration: 0i64,
downtime_duration: (TELEMETRY_QUERY_INTERVAL * 3).into_nanos(),
downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
},
"7d_counters": contains {
connected_duration: 0i64,
downtime_duration: (TELEMETRY_QUERY_INTERVAL * 3).into_nanos(),
downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
},
}
});
}
#[fuchsia::test]
fn test_log_connect_attempt_counters() {
let (mut test_helper, mut test_fut) = setup_test();
// Send 10 failed connect results, then 1 successful.
for i in 0..10 {
let event = TelemetryEvent::ConnectResult {
iface_id: IFACE_ID,
policy_connect_reason: Some(
client::types::ConnectReason::RetryAfterFailedConnectAttempt,
),
result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
multiple_bss_candidates: true,
ap_state: random_bss_description!(Wpa1).into(),
network_is_likely_hidden: false,
};
test_helper.telemetry_sender.send(event);
// Verify that the connection failure has been logged.
test_helper.drain_cobalt_events(&mut test_fut);
let logged_metrics =
test_helper.get_logged_metrics(metrics::CONNECTION_FAILURES_METRIC_ID);
assert_eq!(logged_metrics.len(), i + 1);
}
test_helper.send_connected_event(random_bss_description!(Wpa2));
test_helper.drain_cobalt_events(&mut test_fut);
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
connect_attempts_count: 11u64,
connect_successful_count: 1u64,
},
"7d_counters": contains {
connect_attempts_count: 11u64,
connect_successful_count: 1u64,
},
}
});
}
#[fuchsia::test]
fn test_log_connect_attempt_time_series() {
let (mut test_helper, mut test_fut) = setup_test();
// Send 10 failed connect results, then 1 successful.
for i in 0..10 {
let event = TelemetryEvent::ConnectResult {
iface_id: IFACE_ID,
policy_connect_reason: Some(
client::types::ConnectReason::RetryAfterFailedConnectAttempt,
),
result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
multiple_bss_candidates: true,
ap_state: random_bss_description!(Wpa1).into(),
network_is_likely_hidden: false,
};
test_helper.telemetry_sender.send(event);
// Verify that the connection failure has been logged.
test_helper.drain_cobalt_events(&mut test_fut);
let logged_metrics =
test_helper.get_logged_metrics(metrics::CONNECTION_FAILURES_METRIC_ID);
assert_eq!(logged_metrics.len(), i + 1);
}
test_helper.send_connected_event(random_bss_description!(Wpa2));
test_helper.drain_cobalt_events(&mut test_fut);
let time_series = test_helper.get_time_series(&mut test_fut);
let connect_attempt_count: Vec<_> =
time_series.lock().connect_attempt_count.minutely_iter().map(|v| *v).collect();
let connect_successful_count: Vec<_> =
time_series.lock().connect_successful_count.minutely_iter().map(|v| *v).collect();
assert_eq!(connect_attempt_count, vec![11]);
assert_eq!(connect_successful_count, vec![1]);
}
#[fuchsia::test]
fn test_disconnect_count_counter() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
disconnect_count: 0u64,
roaming_disconnect_count: 0u64,
},
"7d_counters": contains {
disconnect_count: 0u64,
roaming_disconnect_count: 0u64,
},
}
});
// Send a non-roaming disconnect
let info = DisconnectInfo {
disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
reason_code: fidl_ieee80211::ReasonCode::StaLeaving,
mlme_event_name: fidl_sme::DisconnectMlmeEventName::DisassociateIndication,
}),
..fake_disconnect_info()
};
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: true, info });
test_helper.drain_cobalt_events(&mut test_fut);
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
disconnect_count: 1u64,
roaming_disconnect_count: 0u64,
non_roaming_non_user_disconnect_count: 1u64,
},
"7d_counters": contains {
disconnect_count: 1u64,
roaming_disconnect_count: 0u64,
non_roaming_non_user_disconnect_count: 1u64,
},
}
});
let info = DisconnectInfo {
disconnect_source: fidl_sme::DisconnectSource::User(
fidl_sme::UserDisconnectReason::Startup,
),
..fake_disconnect_info()
};
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: false, info });
test_helper.drain_cobalt_events(&mut test_fut);
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
disconnect_count: 2u64,
roaming_disconnect_count: 0u64,
non_roaming_non_user_disconnect_count: 1u64,
},
"7d_counters": contains {
disconnect_count: 2u64,
roaming_disconnect_count: 0u64,
non_roaming_non_user_disconnect_count: 1u64,
},
}
});
// Send a roaming disconnect
let info = DisconnectInfo {
disconnect_source: fidl_sme::DisconnectSource::User(
fidl_sme::UserDisconnectReason::ProactiveNetworkSwitch,
),
..fake_disconnect_info()
};
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: false, info });
test_helper.drain_cobalt_events(&mut test_fut);
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
"1d_counters": contains {
disconnect_count: 3u64,
roaming_disconnect_count: 1u64,
non_roaming_non_user_disconnect_count: 1u64,
},
"7d_counters": contains {
disconnect_count: 3u64,
roaming_disconnect_count: 1u64,
non_roaming_non_user_disconnect_count: 1u64,
},
}
});
}
#[fuchsia::test]
fn test_disconnect_count_time_series() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
let time_series = test_helper.get_time_series(&mut test_fut);
let disconnect_count: Vec<_> =
time_series.lock().disconnect_count.minutely_iter().map(|v| *v).collect();
assert_eq!(disconnect_count, vec![0]);
let info = DisconnectInfo {
disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
reason_code: fidl_ieee80211::ReasonCode::StaLeaving,
mlme_event_name: fidl_sme::DisconnectMlmeEventName::DisassociateIndication,
}),
..fake_disconnect_info()
};
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: true, info });
test_helper.drain_cobalt_events(&mut test_fut);
let time_series = test_helper.get_time_series(&mut test_fut);
let disconnect_count: Vec<_> =
time_series.lock().disconnect_count.minutely_iter().map(|v| *v).collect();
assert_eq!(disconnect_count, vec![1]);
}
#[fuchsia::test]
fn test_connected_duration_time_series() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(90.seconds(), test_fut.as_mut());
let time_series = test_helper.get_time_series(&mut test_fut);
let connected_duration_sec: Vec<_> =
time_series.lock().connected_duration_sec.minutely_iter().map(|v| *v).collect();
assert_eq!(connected_duration_sec, vec![45, 45]);
}
#[fuchsia::test]
fn test_rx_tx_counters_no_issue() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(1.hour(), test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
get_iface_stats_fail_count: 0u64,
"1d_counters": contains {
tx_high_packet_drop_duration: 0i64,
rx_high_packet_drop_duration: 0i64,
tx_very_high_packet_drop_duration: 0i64,
rx_very_high_packet_drop_duration: 0i64,
no_rx_duration: 0i64,
},
"7d_counters": contains {
tx_high_packet_drop_duration: 0i64,
rx_high_packet_drop_duration: 0i64,
tx_very_high_packet_drop_duration: 0i64,
rx_very_high_packet_drop_duration: 0i64,
no_rx_duration: 0i64,
},
}
});
}
#[fuchsia::test]
fn test_tx_high_packet_drop_duration_counters() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.set_counter_stats_resp(Box::new(|| {
let seed = fasync::Time::now().into_nanos() as u64;
Ok(fidl_fuchsia_wlan_stats::IfaceCounterStats {
tx_total: 10 * seed,
tx_drop: 3 * seed,
..fake_iface_counter_stats(seed)
})
}));
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(1.hour(), test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
get_iface_stats_fail_count: 0u64,
"1d_counters": contains {
// Deduct 15 seconds beecause there isn't packet counter to diff against in
// the first interval of telemetry
tx_high_packet_drop_duration: (1.hour() - TELEMETRY_QUERY_INTERVAL).into_nanos(),
rx_high_packet_drop_duration: 0i64,
tx_very_high_packet_drop_duration: (1.hour() - TELEMETRY_QUERY_INTERVAL).into_nanos(),
rx_very_high_packet_drop_duration: 0i64,
no_rx_duration: 0i64,
},
"7d_counters": contains {
tx_high_packet_drop_duration: (1.hour() - TELEMETRY_QUERY_INTERVAL).into_nanos(),
rx_high_packet_drop_duration: 0i64,
tx_very_high_packet_drop_duration: (1.hour() - TELEMETRY_QUERY_INTERVAL).into_nanos(),
rx_very_high_packet_drop_duration: 0i64,
no_rx_duration: 0i64,
},
}
});
}
#[fuchsia::test]
fn test_rx_high_packet_drop_duration_counters() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.set_counter_stats_resp(Box::new(|| {
let seed = fasync::Time::now().into_nanos() as u64;
Ok(fidl_fuchsia_wlan_stats::IfaceCounterStats {
rx_unicast_total: 10 * seed,
rx_unicast_drop: 3 * seed,
..fake_iface_counter_stats(seed)
})
}));
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(1.hour(), test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
get_iface_stats_fail_count: 0u64,
"1d_counters": contains {
// Deduct 15 seconds beecause there isn't packet counter to diff against in
// the first interval of telemetry
rx_high_packet_drop_duration: (1.hour() - TELEMETRY_QUERY_INTERVAL).into_nanos(),
tx_high_packet_drop_duration: 0i64,
rx_very_high_packet_drop_duration: (1.hour() - TELEMETRY_QUERY_INTERVAL).into_nanos(),
tx_very_high_packet_drop_duration: 0i64,
no_rx_duration: 0i64,
},
"7d_counters": contains {
rx_high_packet_drop_duration: (1.hour() - TELEMETRY_QUERY_INTERVAL).into_nanos(),
tx_high_packet_drop_duration: 0i64,
rx_very_high_packet_drop_duration: (1.hour() - TELEMETRY_QUERY_INTERVAL).into_nanos(),
tx_very_high_packet_drop_duration: 0i64,
no_rx_duration: 0i64,
},
}
});
}
#[fuchsia::test]
fn test_rx_tx_high_but_not_very_high_packet_drop_duration_counters() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.set_counter_stats_resp(Box::new(|| {
let seed = fasync::Time::now().into_nanos() as u64;
Ok(fidl_fuchsia_wlan_stats::IfaceCounterStats {
// 3% drop rate would be high, but not very high
rx_unicast_total: 100 * seed,
rx_unicast_drop: 3 * seed,
tx_total: 100 * seed,
tx_drop: 3 * seed,
..fake_iface_counter_stats(seed)
})
}));
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(1.hour(), test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
get_iface_stats_fail_count: 0u64,
"1d_counters": contains {
// Deduct 15 seconds beecause there isn't packet counter to diff against in
// the first interval of telemetry
rx_high_packet_drop_duration: (1.hour() - TELEMETRY_QUERY_INTERVAL).into_nanos(),
tx_high_packet_drop_duration: (1.hour() - TELEMETRY_QUERY_INTERVAL).into_nanos(),
// Very high drop rate counters should still be 0
rx_very_high_packet_drop_duration: 0i64,
tx_very_high_packet_drop_duration: 0i64,
no_rx_duration: 0i64,
},
"7d_counters": contains {
rx_high_packet_drop_duration: (1.hour() - TELEMETRY_QUERY_INTERVAL).into_nanos(),
tx_high_packet_drop_duration: (1.hour() - TELEMETRY_QUERY_INTERVAL).into_nanos(),
rx_very_high_packet_drop_duration: 0i64,
tx_very_high_packet_drop_duration: 0i64,
no_rx_duration: 0i64,
},
}
});
}
#[fuchsia::test]
fn test_rx_tx_packet_time_series() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.set_counter_stats_resp(Box::new(|| {
let seed = (fasync::Time::now() - fasync::Time::from_nanos(0i64)).into_seconds() as u64;
Ok(fidl_fuchsia_wlan_stats::IfaceCounterStats {
rx_unicast_total: 100 * seed,
rx_unicast_drop: 3 * seed,
tx_total: 10 * seed,
tx_drop: 2 * seed,
..fake_iface_counter_stats(seed)
})
}));
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(2.minutes(), test_fut.as_mut());
let time_series = test_helper.get_time_series(&mut test_fut);
let rx_unicast_drop_count: Vec<_> =
time_series.lock().rx_unicast_drop_count.minutely_iter().map(|v| *v).collect();
let rx_unicast_total_count: Vec<_> =
time_series.lock().rx_unicast_total_count.minutely_iter().map(|v| *v).collect();
let tx_drop_count: Vec<_> =
time_series.lock().tx_drop_count.minutely_iter().map(|v| *v).collect();
let tx_total_count: Vec<_> =
time_series.lock().tx_total_count.minutely_iter().map(|v| *v).collect();
// Note: Packets from the first 15 seconds are not accounted because we
// we did not take packet measurement at 0th second mark.
// Additionally, the count for 45th-60th second mark is logged
// at the 60th mark, which is considered to be part of the second
// window.
assert_eq!(rx_unicast_drop_count, vec![90, 180, 45]);
assert_eq!(rx_unicast_total_count, vec![3000, 6000, 1500]);
assert_eq!(tx_drop_count, vec![60, 120, 30]);
assert_eq!(tx_total_count, vec![300, 600, 150]);
}
#[fuchsia::test]
fn test_no_rx_duration_counters() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.set_counter_stats_resp(Box::new(|| {
let seed = fasync::Time::now().into_nanos() as u64;
Ok(fidl_fuchsia_wlan_stats::IfaceCounterStats {
rx_unicast_total: 10,
..fake_iface_counter_stats(seed)
})
}));
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(1.hour(), test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
get_iface_stats_fail_count: 0u64,
"1d_counters": contains {
// Deduct 15 seconds beecause there isn't packet counter to diff against in
// the first interval of telemetry
no_rx_duration: (1.hour() - TELEMETRY_QUERY_INTERVAL).into_nanos(),
rx_high_packet_drop_duration: 0i64,
tx_high_packet_drop_duration: 0i64,
rx_very_high_packet_drop_duration: 0i64,
tx_very_high_packet_drop_duration: 0i64,
},
"7d_counters": contains {
no_rx_duration: (1.hour() - TELEMETRY_QUERY_INTERVAL).into_nanos(),
rx_high_packet_drop_duration: 0i64,
tx_high_packet_drop_duration: 0i64,
rx_very_high_packet_drop_duration: 0i64,
tx_very_high_packet_drop_duration: 0i64,
},
}
});
}
#[fuchsia::test]
fn test_no_rx_duration_time_series() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.set_counter_stats_resp(Box::new(|| {
let seed = fasync::Time::now().into_nanos() as u64;
Ok(fidl_fuchsia_wlan_stats::IfaceCounterStats {
rx_unicast_total: 10,
..fake_iface_counter_stats(seed)
})
}));
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(150.seconds(), test_fut.as_mut());
let time_series = test_helper.get_time_series(&mut test_fut);
let no_rx_duration_sec: Vec<_> =
time_series.lock().no_rx_duration_sec.minutely_iter().map(|v| *v).collect();
assert_eq!(no_rx_duration_sec, vec![30, 60, 45]);
}
#[fuchsia::test]
fn test_get_iface_stats_fail() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.set_counter_stats_resp(Box::new(|| Err(zx::sys::ZX_ERR_NOT_SUPPORTED)));
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(1.hour(), test_fut.as_mut());
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
stats: contains {
get_iface_stats_fail_count: NonZeroUintProperty,
"1d_counters": contains {
no_rx_duration: 0i64,
rx_high_packet_drop_duration: 0i64,
tx_high_packet_drop_duration: 0i64,
rx_very_high_packet_drop_duration: 0i64,
tx_very_high_packet_drop_duration: 0i64,
},
"7d_counters": contains {
no_rx_duration: 0i64,
rx_high_packet_drop_duration: 0i64,
tx_high_packet_drop_duration: 0i64,
rx_very_high_packet_drop_duration: 0i64,
tx_very_high_packet_drop_duration: 0i64,
},
}
});
}
#[fuchsia::test]
fn test_log_signal_histograms_inspect() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.send_connected_event(random_bss_description!(Wpa2));
test_helper.drain_cobalt_events(&mut test_fut);
// Default iface stats responder in `test_helper` already mock these histograms.
assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
external: contains {
stats: contains {
connection_status: contains {
histograms: {
antenna0_2Ghz: {
antenna_index: 0u64,
antenna_freq: "2Ghz",
snr_histogram: vec![30i64, 999],
snr_invalid_samples: 11u64,
noise_floor_histogram: vec![-55i64, 999],
noise_floor_invalid_samples: 44u64,
rssi_histogram: vec![-25i64, 999],
rssi_invalid_samples: 55u64,
},
antenna1_5Ghz: {
antenna_index: 1u64,
antenna_freq: "5Ghz",
rx_rate_histogram: vec![100i64, 1500],
rx_rate_invalid_samples: 33u64,
},
}
}
}
}
});
}
#[fuchsia::test]
fn test_log_daily_uptime_ratio_cobalt_metric() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(12.hours(), test_fut.as_mut());
let info = fake_disconnect_info();
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: true, info });
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(6.hours(), test_fut.as_mut());
// Indicate that there's no saved neighbor in vicinity
test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
network_selection_type: NetworkSelectionType::Undirected,
num_candidates: Ok(0),
selected_count: 0,
});
test_helper.advance_by(6.hours(), test_fut.as_mut());
let uptime_ratios =
test_helper.get_logged_metrics(metrics::CONNECTED_UPTIME_RATIO_METRIC_ID);
assert_eq!(uptime_ratios.len(), 1);
// 12 hours of uptime, 6 hours of adjusted downtime => 66.66% uptime
assert_eq!(uptime_ratios[0].payload, MetricEventPayload::IntegerValue(6666));
}
/// Send a random connect event and 4 hours later send a disconnect with the specified
/// disconnect source.
fn connect_and_disconnect_with_source(
test_helper: &mut TestHelper,
mut test_fut: Pin<&mut impl Future<Output = ()>>,
disconnect_source: fidl_sme::DisconnectSource,
) {
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(4.hours(), test_fut.as_mut());
let info = DisconnectInfo { disconnect_source, ..fake_disconnect_info() };
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: true, info });
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.drain_cobalt_events(&mut test_fut);
}
#[fuchsia::test]
fn test_log_daily_disconnect_per_day_connected_cobalt_metric() {
let (mut test_helper, mut test_fut) = setup_test();
// Send 1 roaming, 1 non-roaming, and 1 user disconnect with the device connected for a
// total of 12 hours. The user disconnect is counted toward total disconnects but not for
// roaming or non-roaming disconnects.
let non_roaming_source = fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
reason_code: fidl_ieee80211::ReasonCode::LeavingNetworkDeauth,
mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
});
connect_and_disconnect_with_source(&mut test_helper, test_fut.as_mut(), non_roaming_source);
let roaming_source = fidl_sme::DisconnectSource::User(
fidl_sme::UserDisconnectReason::ProactiveNetworkSwitch,
);
connect_and_disconnect_with_source(&mut test_helper, test_fut.as_mut(), roaming_source);
let user_source = fidl_sme::DisconnectSource::User(
fidl_sme::UserDisconnectReason::FidlStopClientConnectionsRequest,
);
connect_and_disconnect_with_source(&mut test_helper, test_fut.as_mut(), user_source);
test_helper.advance_by(12.hours(), test_fut.as_mut());
let dpdc_ratios =
test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
assert_eq!(dpdc_ratios.len(), 1);
// 3 disconnects, 0.5 day connected => 6 disconnects per day connected
assert_eq!(dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(60_000));
// 1 roaming disconnect, 0.5 day connected => 2 roaming disconnects per day connected
let non_roam_dpdc_ratios = test_helper
.get_logged_metrics(metrics::NON_ROAMING_DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
assert_eq!(non_roam_dpdc_ratios.len(), 1);
assert_eq!(non_roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(20_000));
let roam_dpdc_ratios =
test_helper.get_logged_metrics(metrics::ROAMING_DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
assert_eq!(roam_dpdc_ratios.len(), 1);
assert_eq!(roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(20_000));
let dpdc_ratios_7d =
test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_7D_METRIC_ID);
assert_eq!(dpdc_ratios_7d.len(), 1);
assert_eq!(dpdc_ratios_7d[0].payload, MetricEventPayload::IntegerValue(60_000));
// Clear record of logged Cobalt events
test_helper.cobalt_events.clear();
// Connect for another 1 day to dilute the 7d ratio
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(24.hours(), test_fut.as_mut());
// No disconnect in the last day, so the 1d ratio would be 0.
let dpdc_ratios =
test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
assert_eq!(dpdc_ratios.len(), 1);
assert_eq!(dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(0));
let non_roam_dpdc_ratios = test_helper
.get_logged_metrics(metrics::NON_ROAMING_DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
assert_eq!(non_roam_dpdc_ratios.len(), 1);
assert_eq!(non_roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(0));
let dpdc_ratios_7d =
test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_7D_METRIC_ID);
assert_eq!(dpdc_ratios_7d.len(), 1);
// 3 disconnects, 1.5 day connected => 2 disconnects per day connected
// (which equals 20,000 in TenThousandth unit)
assert_eq!(dpdc_ratios_7d[0].payload, MetricEventPayload::IntegerValue(20_000));
let roam_dpdc_ratios =
test_helper.get_logged_metrics(metrics::ROAMING_DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
assert_eq!(roam_dpdc_ratios.len(), 1);
assert_eq!(roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(0));
}
#[fuchsia::test]
fn test_log_daily_roaming_disconnect_per_day_connected_cobalt_metric() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(12.hours(), test_fut.as_mut());
let info = DisconnectInfo {
disconnect_source: fidl_sme::DisconnectSource::User(
fidl_sme::UserDisconnectReason::ProactiveNetworkSwitch,
),
..fake_disconnect_info()
};
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: true, info });
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(12.hours(), test_fut.as_mut());
let dpdc_ratios =
test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
assert_eq!(dpdc_ratios.len(), 1);
// 1 disconnect, 0.5 day connected => 2 disconnects per day connected
// (which equals 20_0000 in TenThousandth unit)
assert_eq!(dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(20_000));
let non_roam_dpdc_ratios = test_helper
.get_logged_metrics(metrics::NON_ROAMING_DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
assert_eq!(non_roam_dpdc_ratios.len(), 1);
assert_eq!(non_roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(0));
let roam_dpdc_ratios =
test_helper.get_logged_metrics(metrics::ROAMING_DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
assert_eq!(roam_dpdc_ratios.len(), 1);
assert_eq!(roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(20_000));
}
#[fuchsia::test]
fn test_log_daily_disconnect_per_day_connected_cobalt_metric_device_high_disconnect() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(1.hours(), test_fut.as_mut());
let info = fake_disconnect_info();
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: true, info });
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(23.hours(), test_fut.as_mut());
}
#[fuchsia::test]
fn test_log_daily_rx_tx_ratio_cobalt_metrics() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.set_counter_stats_resp(Box::new(|| {
let seed = fasync::Time::now().into_nanos() as u64 / 1000_000_000;
Ok(fidl_fuchsia_wlan_stats::IfaceCounterStats {
tx_total: 10 * seed,
// TX drop rate stops increasing at 1 hour + TELEMETRY_QUERY_INTERVAL mark.
// Because the first TELEMETRY_QUERY_INTERVAL doesn't count when
// computing counters, this leads to 3 hour of high TX drop rate.
tx_drop: 3 * min(
seed,
(3.hours() + TELEMETRY_QUERY_INTERVAL).into_seconds() as u64,
),
// RX total stops increasing at 23 hour mark
rx_unicast_total: 10 * min(seed, 23.hours().into_seconds() as u64),
// RX drop rate stops increasing at 4 hour + TELEMETRY_QUERY_INTERVAL mark.
rx_unicast_drop: 3 * min(
seed,
(4.hours() + TELEMETRY_QUERY_INTERVAL).into_seconds() as u64,
),
..fake_iface_counter_stats(seed)
})
}));
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(24.hours(), test_fut.as_mut());
let high_rx_drop_time_ratios =
test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_HIGH_RX_PACKET_DROP_METRIC_ID);
// 4 hours of high RX drop rate, 24 hours connected => 16.66% duration
assert_eq!(high_rx_drop_time_ratios.len(), 1);
assert_eq!(high_rx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(1666));
let high_tx_drop_time_ratios =
test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_HIGH_TX_PACKET_DROP_METRIC_ID);
// 3 hours of high RX drop rate, 24 hours connected => 12.48% duration
assert_eq!(high_tx_drop_time_ratios.len(), 1);
assert_eq!(high_tx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(1250));
let very_high_rx_drop_time_ratios = test_helper
.get_logged_metrics(metrics::TIME_RATIO_WITH_VERY_HIGH_RX_PACKET_DROP_METRIC_ID);
assert_eq!(very_high_rx_drop_time_ratios.len(), 1);
assert_eq!(
very_high_rx_drop_time_ratios[0].payload,
MetricEventPayload::IntegerValue(1666)
);
let very_high_tx_drop_time_ratios = test_helper
.get_logged_metrics(metrics::TIME_RATIO_WITH_VERY_HIGH_TX_PACKET_DROP_METRIC_ID);
assert_eq!(very_high_tx_drop_time_ratios.len(), 1);
assert_eq!(
very_high_tx_drop_time_ratios[0].payload,
MetricEventPayload::IntegerValue(1250)
);
// 1 hour of no RX, 24 hours connected => 4.16% duration
let no_rx_time_ratios =
test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_NO_RX_METRIC_ID);
assert_eq!(no_rx_time_ratios.len(), 1);
assert_eq!(no_rx_time_ratios[0].payload, MetricEventPayload::IntegerValue(416));
}
#[fuchsia::test]
fn test_log_daily_rx_tx_ratio_cobalt_metrics_zero() {
// This test is to verify that when the RX/TX ratios are 0 (there's no issue), we still
// log to Cobalt.
let (mut test_helper, mut test_fut) = setup_test();
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(24.hours(), test_fut.as_mut());
let high_rx_drop_time_ratios =
test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_HIGH_RX_PACKET_DROP_METRIC_ID);
assert_eq!(high_rx_drop_time_ratios.len(), 1);
assert_eq!(high_rx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
let high_tx_drop_time_ratios =
test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_HIGH_TX_PACKET_DROP_METRIC_ID);
assert_eq!(high_tx_drop_time_ratios.len(), 1);
assert_eq!(high_tx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
let very_high_rx_drop_time_ratios = test_helper
.get_logged_metrics(metrics::TIME_RATIO_WITH_VERY_HIGH_RX_PACKET_DROP_METRIC_ID);
assert_eq!(very_high_rx_drop_time_ratios.len(), 1);
assert_eq!(very_high_rx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
let very_high_tx_drop_time_ratios = test_helper
.get_logged_metrics(metrics::TIME_RATIO_WITH_VERY_HIGH_TX_PACKET_DROP_METRIC_ID);
assert_eq!(very_high_tx_drop_time_ratios.len(), 1);
assert_eq!(very_high_tx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
let no_rx_time_ratios =
test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_NO_RX_METRIC_ID);
assert_eq!(no_rx_time_ratios.len(), 1);
assert_eq!(no_rx_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
}
#[fuchsia::test]
fn test_log_daily_establish_connection_metrics() {
let (mut test_helper, mut test_fut) = setup_test();
// Send 10 failed connect results, then 1 successful.
for _ in 0..10 {
let event = TelemetryEvent::ConnectResult {
iface_id: IFACE_ID,
policy_connect_reason: Some(
client::types::ConnectReason::RetryAfterFailedConnectAttempt,
),
result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
multiple_bss_candidates: true,
ap_state: random_bss_description!(Wpa1).into(),
network_is_likely_hidden: true,
};
test_helper.telemetry_sender.send(event);
}
test_helper.send_connected_event(random_bss_description!(Wpa2));
test_helper.advance_by(24.hours(), test_fut.as_mut());
let connection_success_rate =
test_helper.get_logged_metrics(metrics::CONNECTION_SUCCESS_RATE_METRIC_ID);
assert_eq!(connection_success_rate.len(), 1);
// 1 successful, 11 total attempts => 9.09% success rate
assert_eq!(connection_success_rate[0].payload, MetricEventPayload::IntegerValue(909));
}
#[fuchsia::test]
fn test_log_hourly_fleetwide_uptime_cobalt_metrics() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(1.hour(), test_fut.as_mut());
let total_wlan_uptime_durs =
test_helper.get_logged_metrics(metrics::TOTAL_WLAN_UPTIME_NEAR_SAVED_NETWORK_METRIC_ID);
assert_eq!(total_wlan_uptime_durs.len(), 1);
assert_eq!(
total_wlan_uptime_durs[0].payload,
MetricEventPayload::IntegerValue(1.hour().into_micros())
);
let connected_durs =
test_helper.get_logged_metrics(metrics::TOTAL_CONNECTED_UPTIME_METRIC_ID);
assert_eq!(connected_durs.len(), 1);
assert_eq!(
connected_durs[0].payload,
MetricEventPayload::IntegerValue(1.hour().into_micros())
);
// Clear record of logged Cobalt events
test_helper.cobalt_events.clear();
test_helper.advance_by(30.minutes(), test_fut.as_mut());
let info = fake_disconnect_info();
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: true, info });
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(15.minutes(), test_fut.as_mut());
// Indicate that there's no saved neighbor in vicinity
test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
network_selection_type: NetworkSelectionType::Undirected,
num_candidates: Ok(0),
selected_count: 0,
});
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(15.minutes(), test_fut.as_mut());
let total_wlan_uptime_durs =
test_helper.get_logged_metrics(metrics::TOTAL_WLAN_UPTIME_NEAR_SAVED_NETWORK_METRIC_ID);
assert_eq!(total_wlan_uptime_durs.len(), 1);
// 30 minutes connected uptime + 15 minutes downtime near saved network
assert_eq!(
total_wlan_uptime_durs[0].payload,
MetricEventPayload::IntegerValue(45.minutes().into_micros())
);
let connected_durs =
test_helper.get_logged_metrics(metrics::TOTAL_CONNECTED_UPTIME_METRIC_ID);
assert_eq!(connected_durs.len(), 1);
assert_eq!(
connected_durs[0].payload,
MetricEventPayload::IntegerValue(30.minutes().into_micros())
);
}
#[fuchsia::test]
fn test_log_hourly_fleetwide_rx_tx_cobalt_metrics() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.set_counter_stats_resp(Box::new(|| {
let seed = fasync::Time::now().into_nanos() as u64 / 1000_000_000;
Ok(fidl_fuchsia_wlan_stats::IfaceCounterStats {
tx_total: 10 * seed,
// TX drop rate stops increasing at 10 min + TELEMETRY_QUERY_INTERVAL mark.
// Because the first TELEMETRY_QUERY_INTERVAL doesn't count when
// computing counters, this leads to 10 min of high TX drop rate.
tx_drop: 3 * min(
seed,
(10.minutes() + TELEMETRY_QUERY_INTERVAL).into_seconds() as u64,
),
// RX total stops increasing at 45 min mark
rx_unicast_total: 10 * min(seed, 45.minutes().into_seconds() as u64),
// RX drop rate stops increasing at 20 min + TELEMETRY_QUERY_INTERVAL mark.
rx_unicast_drop: 3 * min(
seed,
(20.minutes() + TELEMETRY_QUERY_INTERVAL).into_seconds() as u64,
),
..fake_iface_counter_stats(seed)
})
}));
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(1.hour(), test_fut.as_mut());
let rx_high_drop_durs =
test_helper.get_logged_metrics(metrics::TOTAL_TIME_WITH_HIGH_RX_PACKET_DROP_METRIC_ID);
assert_eq!(rx_high_drop_durs.len(), 1);
assert_eq!(
rx_high_drop_durs[0].payload,
MetricEventPayload::IntegerValue(20.minutes().into_micros())
);
let tx_high_drop_durs =
test_helper.get_logged_metrics(metrics::TOTAL_TIME_WITH_HIGH_TX_PACKET_DROP_METRIC_ID);
assert_eq!(tx_high_drop_durs.len(), 1);
assert_eq!(
tx_high_drop_durs[0].payload,
MetricEventPayload::IntegerValue(10.minutes().into_micros())
);
let rx_very_high_drop_durs = test_helper
.get_logged_metrics(metrics::TOTAL_TIME_WITH_VERY_HIGH_RX_PACKET_DROP_METRIC_ID);
assert_eq!(rx_very_high_drop_durs.len(), 1);
assert_eq!(
rx_very_high_drop_durs[0].payload,
MetricEventPayload::IntegerValue(20.minutes().into_micros())
);
let tx_very_high_drop_durs = test_helper
.get_logged_metrics(metrics::TOTAL_TIME_WITH_VERY_HIGH_TX_PACKET_DROP_METRIC_ID);
assert_eq!(tx_very_high_drop_durs.len(), 1);
assert_eq!(
tx_very_high_drop_durs[0].payload,
MetricEventPayload::IntegerValue(10.minutes().into_micros())
);
let no_rx_durs = test_helper.get_logged_metrics(metrics::TOTAL_TIME_WITH_NO_RX_METRIC_ID);
assert_eq!(no_rx_durs.len(), 1);
assert_eq!(
no_rx_durs[0].payload,
MetricEventPayload::IntegerValue(15.minutes().into_micros())
);
}
#[fuchsia::test]
fn test_log_rssi_and_velocity_hourly() {
let (mut test_helper, mut test_fut) = setup_test();
// RSSI velocity is only logged if in the connected state.
test_helper.send_connected_event(random_bss_description!(Wpa2));
// Send some RSSI velocities
let rssi_velocity_1 = -2.0;
let rssi_velocity_2 = 2.0;
let ind_1 = fidl_internal::SignalReportIndication { rssi_dbm: -50, snr_db: 30 };
let ind_2 = fidl_internal::SignalReportIndication { rssi_dbm: -61, snr_db: 40 };
test_helper
.telemetry_sender
.send(TelemetryEvent::OnSignalReport { ind: ind_1, rssi_velocity: rssi_velocity_1 });
test_helper
.telemetry_sender
.send(TelemetryEvent::OnSignalReport { ind: ind_1, rssi_velocity: rssi_velocity_2 });
test_helper
.telemetry_sender
.send(TelemetryEvent::OnSignalReport { ind: ind_2, rssi_velocity: rssi_velocity_2 });
// After an hour has passed, the RSSI velocity should be logged to cobalt
test_helper.advance_by(1.hour(), test_fut.as_mut());
test_helper.drain_cobalt_events(&mut test_fut);
let metrics = test_helper.get_logged_metrics(metrics::RSSI_VELOCITY_METRIC_ID);
assert_eq!(metrics.len(), 1);
assert_variant!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => {
// RSSI velocity in [-2,-1) maps to bucket 9 and velocity in [2,3) maps to bucket 13.
assert_eq!(buckets.len(), 2);
assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket{index: 9, count: 1}));
assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket{index: 13, count: 2}));
});
let metrics = test_helper.get_logged_metrics(metrics::CONNECTION_RSSI_METRIC_ID);
assert_eq!(metrics.len(), 1);
assert_variant!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => {
assert_eq!(buckets.len(), 2);
assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket{index: 79, count: 2}));
assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket{index: 68, count: 1}));
});
test_helper.clear_cobalt_events();
// Send another different RSSI velocity
let rssi_velocity_3 = 3.0;
let ind_3 = fidl_internal::SignalReportIndication { rssi_dbm: -75, snr_db: 30 };
test_helper
.telemetry_sender
.send(TelemetryEvent::OnSignalReport { ind: ind_3, rssi_velocity: rssi_velocity_3 });
test_helper.advance_by(1.hour(), test_fut.as_mut());
// Check that the previously logged values are not logged again, and the new value is
// logged.
test_helper.drain_cobalt_events(&mut test_fut);
let metrics = test_helper.get_logged_metrics(metrics::CONNECTION_RSSI_METRIC_ID);
assert_eq!(metrics.len(), 1);
let buckets =
assert_variant!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => buckets);
assert_eq!(buckets.len(), 1);
assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 54, count: 1 }));
let metrics = test_helper.get_logged_metrics(metrics::RSSI_VELOCITY_METRIC_ID);
assert_eq!(metrics.len(), 1);
assert_eq!(
metrics[0].payload,
MetricEventPayload::Histogram(vec![fidl_fuchsia_metrics::HistogramBucket {
index: 14,
count: 1
}])
);
}
#[fuchsia::test]
fn test_log_rssi_and_velocity_histogram_bounds() {
let (mut test_helper, mut test_fut) = setup_test();
// RSSI velocity is only logged if in the connected state.
test_helper.send_connected_event(random_bss_description!(Wpa2));
// Send some RSSI velocities in the underflow and overflow buckets
// -128 is the lowest bucket and nothing can be in the underflow bucket because -129
// can't be expressed by i8.
let ind_min = fidl_internal::SignalReportIndication { rssi_dbm: -128, snr_db: 30 };
// 0 is the highest histogram bucket and 1 and above are in the overflow bucket.
let ind_max = fidl_internal::SignalReportIndication { rssi_dbm: 0, snr_db: 30 };
let ind_overflow_1 = fidl_internal::SignalReportIndication { rssi_dbm: 1, snr_db: 30 };
let ind_overflow_2 = fidl_internal::SignalReportIndication { rssi_dbm: 127, snr_db: 30 };
// Send the telemetry events. -10 is the min velocity bucket and 10 is the max.
test_helper
.telemetry_sender
.send(TelemetryEvent::OnSignalReport { ind: ind_min, rssi_velocity: -11.0 });
test_helper
.telemetry_sender
.send(TelemetryEvent::OnSignalReport { ind: ind_min, rssi_velocity: -15.0 });
test_helper
.telemetry_sender
.send(TelemetryEvent::OnSignalReport { ind: ind_min, rssi_velocity: 11.0 });
test_helper
.telemetry_sender
.send(TelemetryEvent::OnSignalReport { ind: ind_max, rssi_velocity: 20.0 });
test_helper
.telemetry_sender
.send(TelemetryEvent::OnSignalReport { ind: ind_overflow_1, rssi_velocity: -10.0 });
test_helper
.telemetry_sender
.send(TelemetryEvent::OnSignalReport { ind: ind_overflow_2, rssi_velocity: 10.0 });
test_helper.advance_by(1.hour(), test_fut.as_mut());
// Check that the min, max, underflow, and overflow buckets are used correctly.
test_helper.drain_cobalt_events(&mut test_fut);
// Check RSSI values
let metrics = test_helper.get_logged_metrics(metrics::CONNECTION_RSSI_METRIC_ID);
assert_eq!(metrics.len(), 1);
let buckets =
assert_variant!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => buckets);
assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 1, count: 3 }));
assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 129, count: 1 }));
assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 130, count: 2 }));
// Check RSSI velocity values
let metrics = test_helper.get_logged_metrics(metrics::RSSI_VELOCITY_METRIC_ID);
assert_eq!(metrics.len(), 1);
let buckets =
assert_variant!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => buckets);
// RSSI velocity below -10 maps to underflow bucket, and 11 or above maps to overflow.
assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 1, count: 1 }));
assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 21, count: 1 }));
assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 0, count: 2 }));
assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 22, count: 2 }));
}
#[fuchsia::test]
fn test_log_short_duration_connection_metrics() {
let (mut test_helper, mut test_fut) = setup_test();
let now = fasync::Time::now();
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
let channel = generate_random_channel();
let ap_state = random_bss_description!(Wpa2, channel: channel).into();
let mut connection_scores = HistoricalList::new(5);
connection_scores.add(TimestampedConnectionScore::new(10, now));
connection_scores.add(TimestampedConnectionScore::new(20, now));
connection_scores.add(TimestampedConnectionScore::new(30, now));
// Log disconnect with reason FidlConnectRequest during short duration
let info = DisconnectInfo {
connected_duration: METRICS_SHORT_CONNECT_DURATION - 1.second(),
disconnect_source: fidl_sme::DisconnectSource::User(
fidl_sme::UserDisconnectReason::FidlConnectRequest,
),
ap_state,
connection_scores,
..fake_disconnect_info()
};
test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
track_subsequent_downtime: true,
info: info.clone(),
});
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
// Log disconnect with reason NetworkUnsaved during short duration
let info = DisconnectInfo {
disconnect_source: fidl_sme::DisconnectSource::User(
fidl_sme::UserDisconnectReason::NetworkUnsaved,
),
..info
};
test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
track_subsequent_downtime: true,
info: info.clone(),
});
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
// Log disconnect with reason NetworkUnsaved during longer duration connection
let info = DisconnectInfo {
connected_duration: METRICS_SHORT_CONNECT_DURATION + 1.second(),
..info
};
test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
track_subsequent_downtime: true,
info: info.clone(),
});
test_helper.drain_cobalt_events(&mut test_fut);
let logged_metrics = test_helper.get_logged_metrics(
metrics::POLICY_FIDL_CONNECTION_ATTEMPTS_DURING_SHORT_CONNECTION_METRIC_ID,
);
assert_eq!(logged_metrics.len(), 2);
let logged_metrics = test_helper.get_logged_metrics(
metrics::POLICY_FIDL_CONNECTION_ATTEMPTS_DURING_SHORT_CONNECTION_DETAILED_METRIC_ID,
);
assert_eq!(logged_metrics.len(), 2);
assert_eq!(logged_metrics[0].event_codes, vec![info.previous_connect_reason as u32]);
let logged_metrics =
test_helper.get_logged_metrics(metrics::CONNECTION_SCORE_AVERAGE_METRIC_ID);
assert_eq!(logged_metrics.len(), 2);
assert_eq!(
logged_metrics[0].event_codes,
vec![metrics::ConnectionScoreAverageMetricDimensionDuration::ShortDuration as u32]
);
assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(20));
}
#[fuchsia::test]
fn test_log_disconnect_cobalt_metrics() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.advance_by(3.hours(), test_fut.as_mut());
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(5.hours(), test_fut.as_mut());
let primary_channel = 8;
let channel = Channel::new(primary_channel, Cbw::Cbw20);
let ap_state = random_bss_description!(Wpa2, channel: channel).into();
let info = DisconnectInfo {
connected_duration: 5.hours(),
disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
reason_code: fidl_ieee80211::ReasonCode::LeavingNetworkDeauth,
mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
}),
ap_state,
..fake_disconnect_info()
};
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: true, info });
test_helper.drain_cobalt_events(&mut test_fut);
let policy_disconnection_reasons =
test_helper.get_logged_metrics(metrics::POLICY_DISCONNECTION_MIGRATED_METRIC_ID);
assert_eq!(policy_disconnection_reasons.len(), 1);
assert_eq!(policy_disconnection_reasons[0].payload, MetricEventPayload::Count(1));
assert_eq!(
policy_disconnection_reasons[0].event_codes,
vec![client::types::DisconnectReason::DisconnectDetectedFromSme as u32]
);
let disconnect_counts =
test_helper.get_logged_metrics(metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID);
assert_eq!(disconnect_counts.len(), 1);
assert_eq!(disconnect_counts[0].payload, MetricEventPayload::Count(1));
let breakdowns_by_device_uptime = test_helper
.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_DEVICE_UPTIME_METRIC_ID);
assert_eq!(breakdowns_by_device_uptime.len(), 1);
assert_eq!(breakdowns_by_device_uptime[0].event_codes, vec![
metrics::DisconnectBreakdownByDeviceUptimeMetricDimensionDeviceUptime::LessThan12Hours as u32,
]);
assert_eq!(breakdowns_by_device_uptime[0].payload, MetricEventPayload::Count(1));
let breakdowns_by_connected_duration = test_helper
.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_CONNECTED_DURATION_METRIC_ID);
assert_eq!(breakdowns_by_connected_duration.len(), 1);
assert_eq!(breakdowns_by_connected_duration[0].event_codes, vec![
metrics::DisconnectBreakdownByConnectedDurationMetricDimensionConnectedDuration::LessThan6Hours as u32,
]);
assert_eq!(breakdowns_by_connected_duration[0].payload, MetricEventPayload::Count(1));
let breakdowns_by_reason =
test_helper.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID);
assert_eq!(breakdowns_by_reason.len(), 1);
assert_eq!(
breakdowns_by_reason[0].event_codes,
vec![3u32, metrics::ConnectivityWlanMetricDimensionDisconnectSource::Mlme as u32,]
);
assert_eq!(breakdowns_by_reason[0].payload, MetricEventPayload::Count(1));
let breakdowns_by_channel = test_helper
.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID);
assert_eq!(breakdowns_by_channel.len(), 1);
assert_eq!(breakdowns_by_channel[0].event_codes, vec![channel.primary as u32]);
assert_eq!(breakdowns_by_channel[0].payload, MetricEventPayload::Count(1));
let breakdowns_by_channel_band =
test_helper.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID);
assert_eq!(breakdowns_by_channel_band.len(), 1);
assert_eq!(
breakdowns_by_channel_band[0].event_codes,
vec![
metrics::DisconnectBreakdownByChannelBandMetricDimensionChannelBand::Band2Dot4Ghz
as u32
]
);
assert_eq!(breakdowns_by_channel_band[0].payload, MetricEventPayload::Count(1));
let breakdowns_by_is_multi_bss =
test_helper.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID);
assert_eq!(breakdowns_by_is_multi_bss.len(), 1);
assert_eq!(
breakdowns_by_is_multi_bss[0].event_codes,
vec![metrics::DisconnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::Yes as u32]
);
assert_eq!(breakdowns_by_is_multi_bss[0].payload, MetricEventPayload::Count(1));
let breakdowns_by_security_type = test_helper
.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID);
assert_eq!(breakdowns_by_security_type.len(), 1);
assert_eq!(
breakdowns_by_security_type[0].event_codes,
vec![
metrics::DisconnectBreakdownBySecurityTypeMetricDimensionSecurityType::Wpa2Personal
as u32
]
);
assert_eq!(breakdowns_by_security_type[0].payload, MetricEventPayload::Count(1));
// Check that non-roaming and total disconnects are logged but not a roaming disconnect.
let roam_connected_duration = test_helper
.get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_ROAMING_DISCONNECT_METRIC_ID);
assert_eq!(roam_connected_duration.len(), 0);
let non_roam_connected_duration = test_helper.get_logged_metrics(
metrics::CONNECTED_DURATION_BEFORE_NON_ROAMING_DISCONNECT_METRIC_ID,
);
assert_eq!(non_roam_connected_duration.len(), 1);
assert_eq!(non_roam_connected_duration[0].payload, MetricEventPayload::IntegerValue(300));
let total_connected_duration =
test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_DISCONNECT_METRIC_ID);
assert_eq!(total_connected_duration.len(), 1);
assert_eq!(total_connected_duration[0].payload, MetricEventPayload::IntegerValue(300));
let user_network_change_counts =
test_helper.get_logged_metrics(metrics::MANUAL_NETWORK_CHANGE_METRIC_ID);
assert!(user_network_change_counts.is_empty());
let roam_disconnect_counts =
test_helper.get_logged_metrics(metrics::NETWORK_ROAMING_DISCONNECT_COUNTS_METRIC_ID);
assert!(roam_disconnect_counts.is_empty());
let non_roam_disconnect_counts = test_helper
.get_logged_metrics(metrics::NETWORK_NON_ROAMING_DISCONNECT_COUNTS_METRIC_ID);
assert_eq!(non_roam_disconnect_counts.len(), 1);
assert_eq!(non_roam_disconnect_counts[0].payload, MetricEventPayload::Count(1));
let total_disconnect_counts =
test_helper.get_logged_metrics(metrics::NETWORK_DISCONNECT_COUNTS_METRIC_ID);
assert_eq!(total_disconnect_counts.len(), 1);
assert_eq!(total_disconnect_counts[0].payload, MetricEventPayload::Count(1));
}
#[fuchsia::test]
fn test_log_roam_disconnect_cobalt_metrics() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.advance_by(3.hours(), test_fut.as_mut());
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
const DUR_MIN: i64 = 125;
test_helper.advance_by(DUR_MIN.minutes(), test_fut.as_mut());
// Send a disconnect event.
let info = DisconnectInfo {
connected_duration: DUR_MIN.minutes(),
disconnect_source: fidl_sme::DisconnectSource::User(
fidl_sme::UserDisconnectReason::ProactiveNetworkSwitch,
),
..fake_disconnect_info()
};
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: true, info });
test_helper.drain_cobalt_events(&mut test_fut);
// Check that connected durations for roam and total disconnects are logged, and not for
// a non-roaming disconnect.
let roam_connected_duration = test_helper
.get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_ROAMING_DISCONNECT_METRIC_ID);
assert_eq!(roam_connected_duration.len(), 1);
assert_eq!(roam_connected_duration[0].payload, MetricEventPayload::IntegerValue(DUR_MIN));
let non_roam_connected_duration = test_helper.get_logged_metrics(
metrics::CONNECTED_DURATION_BEFORE_NON_ROAMING_DISCONNECT_METRIC_ID,
);
assert_eq!(non_roam_connected_duration.len(), 0);
let total_connected_duration =
test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_DISCONNECT_METRIC_ID);
assert_eq!(total_connected_duration.len(), 1);
assert_eq!(total_connected_duration[0].payload, MetricEventPayload::IntegerValue(DUR_MIN));
// Check that a metric count is not logged for a manual network switch.
let user_network_change_counts =
test_helper.get_logged_metrics(metrics::MANUAL_NETWORK_CHANGE_METRIC_ID);
assert!(user_network_change_counts.is_empty());
// Check that a roam and total disconnect is logged, and not a non-roaming disconnect.
let roam_disconnect_counts =
test_helper.get_logged_metrics(metrics::NETWORK_ROAMING_DISCONNECT_COUNTS_METRIC_ID);
assert_eq!(roam_disconnect_counts.len(), 1);
assert_eq!(roam_disconnect_counts[0].payload, MetricEventPayload::Count(1));
let non_roam_disconnect_counts = test_helper
.get_logged_metrics(metrics::NETWORK_NON_ROAMING_DISCONNECT_COUNTS_METRIC_ID);
assert!(non_roam_disconnect_counts.is_empty());
let total_disconnect_counts =
test_helper.get_logged_metrics(metrics::NETWORK_DISCONNECT_COUNTS_METRIC_ID);
assert_eq!(total_disconnect_counts.len(), 1);
assert_eq!(total_disconnect_counts[0].payload, MetricEventPayload::Count(1));
}
#[fuchsia::test]
fn test_log_user_disconnect_cobalt_metrics() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.advance_by(3.hours(), test_fut.as_mut());
test_helper.send_connected_event(random_bss_description!(Wpa2));
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
const DUR_MIN: i64 = 250;
test_helper.advance_by(DUR_MIN.minutes(), test_fut.as_mut());
// Send a disconnect event.
let info = DisconnectInfo {
connected_duration: DUR_MIN.minutes(),
disconnect_source: fidl_sme::DisconnectSource::User(
fidl_sme::UserDisconnectReason::FidlConnectRequest,
),
..fake_disconnect_info()
};
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: true, info });
test_helper.drain_cobalt_events(&mut test_fut);
// Check that a count was logged for a disconnect from a user requested network change.
let user_network_change_counts =
test_helper.get_logged_metrics(metrics::MANUAL_NETWORK_CHANGE_METRIC_ID);
assert_eq!(user_network_change_counts.len(), 1);
assert_eq!(user_network_change_counts[0].payload, MetricEventPayload::Count(1));
// Check that nothing was logged for roaming and non-roaming disconnects.
let roam_connected_duration = test_helper
.get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_ROAMING_DISCONNECT_METRIC_ID);
assert_eq!(roam_connected_duration.len(), 0);
let non_roam_connected_duration = test_helper.get_logged_metrics(
metrics::CONNECTED_DURATION_BEFORE_NON_ROAMING_DISCONNECT_METRIC_ID,
);
assert_eq!(non_roam_connected_duration.len(), 0);
let roam_disconnect_counts =
test_helper.get_logged_metrics(metrics::NETWORK_ROAMING_DISCONNECT_COUNTS_METRIC_ID);
assert!(roam_disconnect_counts.is_empty());
let non_roam_disconnect_counts = test_helper
.get_logged_metrics(metrics::NETWORK_NON_ROAMING_DISCONNECT_COUNTS_METRIC_ID);
assert!(non_roam_disconnect_counts.is_empty());
// Check that a connected duration and a count were logged for overall disconnects.
let total_connected_duration =
test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_DISCONNECT_METRIC_ID);
assert_eq!(total_connected_duration.len(), 1);
assert_eq!(total_connected_duration[0].payload, MetricEventPayload::IntegerValue(DUR_MIN));
let total_disconnect_counts =
test_helper.get_logged_metrics(metrics::NETWORK_DISCONNECT_COUNTS_METRIC_ID);
assert_eq!(total_disconnect_counts.len(), 1);
assert_eq!(total_disconnect_counts[0].payload, MetricEventPayload::Count(1));
}
#[fuchsia::test]
fn test_log_saved_networks_count() {
let (mut test_helper, mut test_fut) = setup_test();
let event = TelemetryEvent::SavedNetworkCount {
saved_network_count: 4,
config_count_per_saved_network: vec![1, 1],
};
test_helper.telemetry_sender.send(event);
test_helper.drain_cobalt_events(&mut test_fut);
let saved_networks_count =
test_helper.get_logged_metrics(metrics::SAVED_NETWORKS_MIGRATED_METRIC_ID);
assert_eq!(saved_networks_count.len(), 1);
assert_eq!(
saved_networks_count[0].event_codes,
vec![metrics::SavedNetworksMigratedMetricDimensionSavedNetworks::TwoToFour as u32]
);
let config_count = test_helper
.get_logged_metrics(metrics::SAVED_CONFIGURATIONS_FOR_SAVED_NETWORK_MIGRATED_METRIC_ID);
assert_eq!(config_count.len(), 2);
assert_eq!(
config_count[0].event_codes,
vec![metrics::SavedConfigurationsForSavedNetworkMigratedMetricDimensionSavedConfigurations::One as u32]
);
assert_eq!(
config_count[1].event_codes,
vec![metrics::SavedConfigurationsForSavedNetworkMigratedMetricDimensionSavedConfigurations::One as u32]
);
}
#[fuchsia::test]
fn test_log_network_selection_scan_interval() {
let (mut test_helper, mut test_fut) = setup_test();
let duration = zx::Duration::from_seconds(rand::thread_rng().gen_range(0..100));
let event = TelemetryEvent::NetworkSelectionScanInterval { time_since_last_scan: duration };
test_helper.telemetry_sender.send(event);
test_helper.drain_cobalt_events(&mut test_fut);
let last_scan_age = test_helper
.get_logged_metrics(metrics::LAST_SCAN_AGE_WHEN_SCAN_REQUESTED_MIGRATED_METRIC_ID);
assert_eq!(last_scan_age.len(), 1);
assert_eq!(
last_scan_age[0].payload,
fidl_fuchsia_metrics::MetricEventPayload::IntegerValue(duration.into_micros())
);
}
#[fuchsia::test]
fn test_log_connection_selection_scan_results() {
let (mut test_helper, mut test_fut) = setup_test();
let event = TelemetryEvent::ConnectionSelectionScanResults {
saved_network_count: 4,
saved_network_count_found_by_active_scan: 1,
bss_count_per_saved_network: vec![10, 10],
};
test_helper.telemetry_sender.send(event);
test_helper.drain_cobalt_events(&mut test_fut);
let saved_networks_count =
test_helper.get_logged_metrics(metrics::SCAN_RESULTS_RECEIVED_MIGRATED_METRIC_ID);
assert_eq!(saved_networks_count.len(), 1);
assert_eq!(
saved_networks_count[0].event_codes,
vec![
metrics::ScanResultsReceivedMigratedMetricDimensionSavedNetworksCount::TwoToFour
as u32
]
);
let active_scanned_network = test_helper.get_logged_metrics(
metrics::SAVED_NETWORK_IN_SCAN_RESULT_WITH_ACTIVE_SCAN_MIGRATED_METRIC_ID,
);
assert_eq!(active_scanned_network.len(), 1);
assert_eq!(
active_scanned_network[0].event_codes,
vec![metrics::SavedNetworkInScanResultWithActiveScanMigratedMetricDimensionActiveScanSsidsObserved::One as u32]
);
let bss_count = test_helper
.get_logged_metrics(metrics::SAVED_NETWORK_IN_SCAN_RESULT_MIGRATED_METRIC_ID);
assert_eq!(bss_count.len(), 2);
assert_eq!(
bss_count[0].event_codes,
vec![
metrics::SavedNetworkInScanResultMigratedMetricDimensionBssCount::FiveToTen as u32
]
);
assert_eq!(
bss_count[1].event_codes,
vec![
metrics::SavedNetworkInScanResultMigratedMetricDimensionBssCount::FiveToTen as u32
]
);
}
#[fuchsia::test]
fn test_log_establish_connection_cobalt_metrics() {
let (mut test_helper, mut test_fut) = setup_test();
let primary_channel = 8;
let channel = Channel::new(primary_channel, Cbw::Cbw20);
let ap_state = random_bss_description!(Wpa2,
bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
channel: channel,
rssi_dbm: -50,
snr_db: 25,
)
.into();
let event = TelemetryEvent::ConnectResult {
iface_id: IFACE_ID,
policy_connect_reason: Some(client::types::ConnectReason::FidlConnectRequest),
result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
multiple_bss_candidates: true,
ap_state,
network_is_likely_hidden: true,
};
test_helper.telemetry_sender.send(event);
test_helper.drain_cobalt_events(&mut test_fut);
let policy_connect_reasons =
test_helper.get_logged_metrics(metrics::POLICY_CONNECTION_ATTEMPT_MIGRATED_METRIC_ID);
assert_eq!(policy_connect_reasons.len(), 1);
assert_eq!(
policy_connect_reasons[0].event_codes,
vec![client::types::ConnectReason::FidlConnectRequest as u32]
);
assert_eq!(policy_connect_reasons[0].payload, MetricEventPayload::Count(1));
let breakdowns_by_status_code = test_helper
.get_logged_metrics(metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID);
assert_eq!(breakdowns_by_status_code.len(), 1);
assert_eq!(
breakdowns_by_status_code[0].event_codes,
vec![fidl_ieee80211::StatusCode::Success as u32]
);
assert_eq!(breakdowns_by_status_code[0].payload, MetricEventPayload::Count(1));
let breakdowns_by_user_wait_time = test_helper
.get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
// TelemetryEvent::StartEstablishConnection is never sent, so connect start time is never
// tracked, hence this metric is not logged.
assert_eq!(breakdowns_by_user_wait_time.len(), 0);
let breakdowns_by_is_multi_bss = test_helper
.get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID);
assert_eq!(breakdowns_by_is_multi_bss.len(), 1);
assert_eq!(
breakdowns_by_is_multi_bss[0].event_codes,
vec![
metrics::SuccessfulConnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::Yes
as u32
]
);
assert_eq!(breakdowns_by_is_multi_bss[0].payload, MetricEventPayload::Count(1));
let breakdowns_by_security_type = test_helper
.get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID);
assert_eq!(breakdowns_by_security_type.len(), 1);
assert_eq!(
breakdowns_by_security_type[0].event_codes,
vec![
metrics::SuccessfulConnectBreakdownBySecurityTypeMetricDimensionSecurityType::Wpa2Personal
as u32
]
);
assert_eq!(breakdowns_by_security_type[0].payload, MetricEventPayload::Count(1));
let breakdowns_by_channel = test_helper
.get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID);
assert_eq!(breakdowns_by_channel.len(), 1);
assert_eq!(breakdowns_by_channel[0].event_codes, vec![primary_channel as u32]);
assert_eq!(breakdowns_by_channel[0].payload, MetricEventPayload::Count(1));
let breakdowns_by_channel_band = test_helper
.get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID);
assert_eq!(breakdowns_by_channel_band.len(), 1);
assert_eq!(breakdowns_by_channel_band[0].event_codes, vec![
metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band2Dot4Ghz as u32
]);
assert_eq!(breakdowns_by_channel_band[0].payload, MetricEventPayload::Count(1));
let per_oui = test_helper.get_logged_metrics(metrics::SUCCESSFUL_CONNECT_PER_OUI_METRIC_ID);
assert_eq!(per_oui.len(), 1);
assert_eq!(per_oui[0].payload, MetricEventPayload::StringValue("00F620".to_string()));
let fidl_connect_count =
test_helper.get_logged_metrics(metrics::POLICY_CONNECTION_ATTEMPTS_METRIC_ID);
assert_eq!(fidl_connect_count.len(), 1);
assert_eq!(fidl_connect_count[0].payload, MetricEventPayload::Count(1));
let network_is_likely_hidden =
test_helper.get_logged_metrics(metrics::CONNECT_TO_LIKELY_HIDDEN_NETWORK_METRIC_ID);
assert_eq!(network_is_likely_hidden.len(), 1);
assert_eq!(network_is_likely_hidden[0].payload, MetricEventPayload::Count(1));
}
#[fuchsia::test]
fn test_log_connect_attempt_breakdown_by_failed_status_code() {
let (mut test_helper, mut test_fut) = setup_test();
let event = TelemetryEvent::ConnectResult {
iface_id: IFACE_ID,
policy_connect_reason: None,
result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedCapabilitiesMismatch),
multiple_bss_candidates: true,
ap_state: random_bss_description!(Wpa2).into(),
network_is_likely_hidden: true,
};
test_helper.telemetry_sender.send(event);
test_helper.drain_cobalt_events(&mut test_fut);
let breakdowns_by_status_code = test_helper
.get_logged_metrics(metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID);
assert_eq!(breakdowns_by_status_code.len(), 1);
assert_eq!(
breakdowns_by_status_code[0].event_codes,
vec![fidl_ieee80211::StatusCode::RefusedCapabilitiesMismatch as u32]
);
}
#[fuchsia::test]
fn test_log_establish_connection_status_code_cobalt_metrics_normal_device() {
let (mut test_helper, mut test_fut) = setup_test();
for _ in 0..3 {
let event = TelemetryEvent::ConnectResult {
iface_id: IFACE_ID,
policy_connect_reason: Some(
client::types::ConnectReason::RetryAfterFailedConnectAttempt,
),
result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
multiple_bss_candidates: true,
ap_state: random_bss_description!(Wpa1).into(),
network_is_likely_hidden: true,
};
test_helper.telemetry_sender.send(event);
}
test_helper.send_connected_event(random_bss_description!(Wpa2));
test_helper.advance_by(24.hours(), test_fut.as_mut());
let status_codes = test_helper.get_logged_metrics(
metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
);
assert_eq!(status_codes.len(), 2);
assert_eq_cobalt_events(
status_codes,
vec![
MetricEvent {
metric_id:
metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
event_codes: vec![fidl_ieee80211::StatusCode::Success as u32],
payload: MetricEventPayload::Count(1),
},
MetricEvent {
metric_id:
metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
event_codes: vec![fidl_ieee80211::StatusCode::RefusedReasonUnspecified as u32],
payload: MetricEventPayload::Count(3),
},
],
);
}
#[fuchsia::test]
fn test_log_establish_connection_status_code_cobalt_metrics_bad_device() {
let (mut test_helper, mut test_fut) = setup_test();
for _ in 0..10 {
let event = TelemetryEvent::ConnectResult {
iface_id: IFACE_ID,
policy_connect_reason: Some(
client::types::ConnectReason::RetryAfterFailedConnectAttempt,
),
result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
multiple_bss_candidates: true,
ap_state: random_bss_description!(Wpa1).into(),
network_is_likely_hidden: true,
};
test_helper.telemetry_sender.send(event);
}
test_helper.send_connected_event(random_bss_description!(Wpa2));
test_helper.advance_by(24.hours(), test_fut.as_mut());
let status_codes = test_helper.get_logged_metrics(
metrics::CONNECT_ATTEMPT_ON_BAD_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
);
assert_eq!(status_codes.len(), 2);
assert_eq_cobalt_events(
status_codes,
vec![
MetricEvent {
metric_id:
metrics::CONNECT_ATTEMPT_ON_BAD_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
event_codes: vec![fidl_ieee80211::StatusCode::Success as u32],
payload: MetricEventPayload::Count(1),
},
MetricEvent {
metric_id:
metrics::CONNECT_ATTEMPT_ON_BAD_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
event_codes: vec![fidl_ieee80211::StatusCode::RefusedReasonUnspecified as u32],
payload: MetricEventPayload::Count(10),
},
],
);
}
#[fuchsia::test]
fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_no_reset() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper
.telemetry_sender
.send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
test_helper.advance_by(2.seconds(), test_fut.as_mut());
test_helper
.telemetry_sender
.send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
test_helper.advance_by(4.seconds(), test_fut.as_mut());
test_helper.send_connected_event(random_bss_description!(Wpa2));
test_helper.drain_cobalt_events(&mut test_fut);
let breakdowns_by_user_wait_time = test_helper
.get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
assert_eq!(breakdowns_by_user_wait_time.len(), 1);
assert_eq!(
breakdowns_by_user_wait_time[0].event_codes,
// Both the 2 seconds and 4 seconds since the first StartEstablishConnection
// should be counted.
vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan8Seconds as u32]
);
}
#[fuchsia::test]
fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_with_reset() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper
.telemetry_sender
.send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
test_helper.advance_by(2.seconds(), test_fut.as_mut());
test_helper
.telemetry_sender
.send(TelemetryEvent::StartEstablishConnection { reset_start_time: true });
test_helper.advance_by(4.seconds(), test_fut.as_mut());
test_helper.send_connected_event(random_bss_description!(Wpa2));
test_helper.drain_cobalt_events(&mut test_fut);
let breakdowns_by_user_wait_time = test_helper
.get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
assert_eq!(breakdowns_by_user_wait_time.len(), 1);
assert_eq!(
breakdowns_by_user_wait_time[0].event_codes,
// Only the 4 seconds after the last StartEstablishConnection should be counted.
vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan5Seconds as u32]
);
}
#[fuchsia::test]
fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_with_clear() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper
.telemetry_sender
.send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
test_helper.advance_by(10.seconds(), test_fut.as_mut());
test_helper.telemetry_sender.send(TelemetryEvent::ClearEstablishConnectionStartTime);
test_helper.advance_by(30.seconds(), test_fut.as_mut());
test_helper
.telemetry_sender
.send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
test_helper.advance_by(2.seconds(), test_fut.as_mut());
test_helper.send_connected_event(random_bss_description!(Wpa2));
test_helper.drain_cobalt_events(&mut test_fut);
let breakdowns_by_user_wait_time = test_helper
.get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
assert_eq!(breakdowns_by_user_wait_time.len(), 1);
assert_eq!(
breakdowns_by_user_wait_time[0].event_codes,
// Only the 2 seconds after the last StartEstablishConnection should be counted.
vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan3Seconds as u32]
);
}
#[test_case(
(true, random_bss_description!(Wpa2)),
(false, random_bss_description!(Wpa2)),
metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID,
metrics::SuccessfulConnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::Yes as u32,
metrics::SuccessfulConnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::No as u32;
"breakdown_by_is_multi_bss"
)]
#[test_case(
(false, random_bss_description!(Wpa1)),
(false, random_bss_description!(Wpa2)),
metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID,
metrics::SuccessfulConnectBreakdownBySecurityTypeMetricDimensionSecurityType::Wpa1 as u32,
metrics::SuccessfulConnectBreakdownBySecurityTypeMetricDimensionSecurityType::Wpa2Personal as u32;
"breakdown_by_security_type"
)]
#[test_case(
(false, random_bss_description!(Wpa2, channel: Channel::new(6, Cbw::Cbw20))),
(false, random_bss_description!(Wpa2, channel: Channel::new(157, Cbw::Cbw40))),
metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
6,
157;
"breakdown_by_primary_channel"
)]
#[test_case(
(false, random_bss_description!(Wpa2, channel: Channel::new(6, Cbw::Cbw20))),
(false, random_bss_description!(Wpa2, channel: Channel::new(157, Cbw::Cbw40))),
metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band2Dot4Ghz as u32,
metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band5Ghz as u32;
"breakdown_by_channel_band"
)]
#[test_case(
(false, random_bss_description!(Wpa2, rssi_dbm: -79)),
(false, random_bss_description!(Wpa2, rssi_dbm: -40)),
metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_RSSI_BUCKET_METRIC_ID,
metrics::ConnectivityWlanMetricDimensionRssiBucket::From79To77 as u32,
metrics::ConnectivityWlanMetricDimensionRssiBucket::From50To35 as u32;
"breakdown_by_rssi_bucket"
)]
#[test_case(
(false, random_bss_description!(Wpa2, snr_db: 11)),
(false, random_bss_description!(Wpa2, snr_db: 35)),
metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SNR_BUCKET_METRIC_ID,
metrics::ConnectivityWlanMetricDimensionSnrBucket::From11To15 as u32,
metrics::ConnectivityWlanMetricDimensionSnrBucket::From26To40 as u32;
"breakdown_by_snr_bucket"
)]
#[fuchsia::test(add_test_attr = false)]
fn test_log_daily_connect_success_rate_breakdown_cobalt_metrics(
first_connect_result_params: (bool, BssDescription),
second_connect_result_params: (bool, BssDescription),
metric_id: u32,
event_code_1: u32,
event_code_2: u32,
) {
let (mut test_helper, mut test_fut) = setup_test();
for i in 0..3 {
let code = if i == 0 {
fidl_ieee80211::StatusCode::Success
} else {
fidl_ieee80211::StatusCode::RefusedReasonUnspecified
};
let event = TelemetryEvent::ConnectResult {
iface_id: IFACE_ID,
policy_connect_reason: Some(
client::types::ConnectReason::RetryAfterFailedConnectAttempt,
),
result: fake_connect_result(code),
multiple_bss_candidates: first_connect_result_params.0,
ap_state: first_connect_result_params.1.clone().into(),
network_is_likely_hidden: true,
};
test_helper.telemetry_sender.send(event);
}
for i in 0..2 {
let code = if i == 0 {
fidl_ieee80211::StatusCode::Success
} else {
fidl_ieee80211::StatusCode::RefusedReasonUnspecified
};
let event = TelemetryEvent::ConnectResult {
iface_id: IFACE_ID,
policy_connect_reason: Some(
client::types::ConnectReason::RetryAfterFailedConnectAttempt,
),
result: fake_connect_result(code),
multiple_bss_candidates: second_connect_result_params.0,
ap_state: second_connect_result_params.1.clone().into(),
network_is_likely_hidden: true,
};
test_helper.telemetry_sender.send(event);
}
test_helper.advance_by(24.hours(), test_fut.as_mut());
let metrics = test_helper.get_logged_metrics(metric_id);
assert_eq!(metrics.len(), 2);
assert_eq_cobalt_events(
metrics,
vec![
MetricEvent {
metric_id,
event_codes: vec![event_code_1],
payload: MetricEventPayload::IntegerValue(3333), // 1/3 = 33.33%
},
MetricEvent {
metric_id,
event_codes: vec![event_code_2],
payload: MetricEventPayload::IntegerValue(5000), // 1/2 = 50.00%
},
],
);
}
#[fuchsia::test]
fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_while_connected() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.send_connected_event(random_bss_description!(Wpa2));
test_helper.drain_cobalt_events(&mut test_fut);
test_helper.cobalt_events.clear();
test_helper
.telemetry_sender
.send(TelemetryEvent::StartEstablishConnection { reset_start_time: true });
test_helper.advance_by(2.seconds(), test_fut.as_mut());
let info = fake_disconnect_info();
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: false, info });
test_helper.advance_by(4.seconds(), test_fut.as_mut());
test_helper.send_connected_event(random_bss_description!(Wpa2));
test_helper.drain_cobalt_events(&mut test_fut);
let breakdowns_by_user_wait_time = test_helper
.get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
assert_eq!(breakdowns_by_user_wait_time.len(), 1);
assert_eq!(
breakdowns_by_user_wait_time[0].event_codes,
// Both the 2 seconds and 4 seconds since the first StartEstablishConnection
// should be counted.
vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan8Seconds as u32]
);
}
#[fuchsia::test]
fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_with_clear_while_connected(
) {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.send_connected_event(random_bss_description!(Wpa2));
test_helper.drain_cobalt_events(&mut test_fut);
test_helper.cobalt_events.clear();
test_helper
.telemetry_sender
.send(TelemetryEvent::StartEstablishConnection { reset_start_time: true });
test_helper.telemetry_sender.send(TelemetryEvent::ClearEstablishConnectionStartTime);
let info = fake_disconnect_info();
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: false, info });
test_helper.advance_by(2.seconds(), test_fut.as_mut());
test_helper
.telemetry_sender
.send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
test_helper.advance_by(4.seconds(), test_fut.as_mut());
test_helper.send_connected_event(random_bss_description!(Wpa2));
test_helper.drain_cobalt_events(&mut test_fut);
let breakdowns_by_user_wait_time = test_helper
.get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
assert_eq!(breakdowns_by_user_wait_time.len(), 1);
assert_eq!(
breakdowns_by_user_wait_time[0].event_codes,
// Only the 4 seconds after the last StartEstablishConnection should be counted.
vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan5Seconds as u32]
);
}
#[fuchsia::test]
fn test_log_establish_connection_cobalt_metrics_user_wait_time_logged_for_sme_reconnecting() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.send_connected_event(random_bss_description!(Wpa2));
test_helper.drain_cobalt_events(&mut test_fut);
test_helper.cobalt_events.clear();
let info = DisconnectInfo { is_sme_reconnecting: true, ..fake_disconnect_info() };
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: false, info });
test_helper.advance_by(2.seconds(), test_fut.as_mut());
test_helper.send_connected_event(random_bss_description!(Wpa2));
test_helper.drain_cobalt_events(&mut test_fut);
let breakdowns_by_user_wait_time = test_helper
.get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
assert_eq!(breakdowns_by_user_wait_time.len(), 1);
assert_eq!(
breakdowns_by_user_wait_time[0].event_codes,
vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan3Seconds as u32]
);
}
#[fuchsia::test]
fn test_log_downtime_cobalt_metrics() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.send_connected_event(random_bss_description!(Wpa2));
test_helper.drain_cobalt_events(&mut test_fut);
let info = DisconnectInfo {
disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
reason_code: fidl_ieee80211::ReasonCode::LeavingNetworkDeauth,
mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
}),
..fake_disconnect_info()
};
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: true, info });
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(42.minutes(), test_fut.as_mut());
// Indicate that there's no saved neighbor in vicinity
test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
network_selection_type: NetworkSelectionType::Undirected,
num_candidates: Ok(0),
selected_count: 0,
});
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(5.minutes(), test_fut.as_mut());
// Indicate that there's some saved neighbor in vicinity
test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
network_selection_type: NetworkSelectionType::Undirected,
num_candidates: Ok(5),
selected_count: 1,
});
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(7.minutes(), test_fut.as_mut());
// Reconnect
test_helper.send_connected_event(random_bss_description!(Wpa2));
test_helper.drain_cobalt_events(&mut test_fut);
let breakdowns_by_reason = test_helper
.get_logged_metrics(metrics::DOWNTIME_BREAKDOWN_BY_DISCONNECT_REASON_METRIC_ID);
assert_eq!(breakdowns_by_reason.len(), 1);
assert_eq!(
breakdowns_by_reason[0].event_codes,
vec![3u32, metrics::ConnectivityWlanMetricDimensionDisconnectSource::Mlme as u32,]
);
assert_eq!(
breakdowns_by_reason[0].payload,
MetricEventPayload::IntegerValue(49.minutes().into_micros())
);
}
#[fuchsia::test]
fn test_log_reconnect_cobalt_metrics() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.send_connected_event(random_bss_description!(Wpa2));
test_helper.drain_cobalt_events(&mut test_fut);
let info = DisconnectInfo {
disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
reason_code: fidl_ieee80211::ReasonCode::LeavingNetworkDeauth,
mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
}),
..fake_disconnect_info()
};
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: true, info });
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(3.seconds(), test_fut.as_mut());
// Reconnect
test_helper.send_connected_event(random_bss_description!(Wpa2));
test_helper.drain_cobalt_events(&mut test_fut);
let metrics =
test_helper.get_logged_metrics(metrics::RECONNECT_BREAKDOWN_BY_DURATION_METRIC_ID);
assert_eq!(metrics.len(), 1);
assert_eq!(
metrics[0].event_codes,
vec![
metrics::ConnectivityWlanMetricDimensionReconnectDuration::LessThan5Seconds as u32
]
);
assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
// Verify the reconnect duration was logged for an unexpected disconnect and not a roam.
// 3 seconds would be sent as 3,000,000 microseconds.
let metrics =
test_helper.get_logged_metrics(metrics::NON_ROAMING_RECONNECT_DURATION_METRIC_ID);
assert_eq!(metrics.len(), 1);
assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(3_000_000));
assert_eq!(
test_helper.get_logged_metrics(metrics::ROAMING_RECONNECT_DURATION_METRIC_ID).len(),
0
);
// Send a disconnect and reconnect for a proactive network switch.
test_helper.clear_cobalt_events();
let info = DisconnectInfo {
disconnect_source: fidl_sme::DisconnectSource::User(
fidl_sme::UserDisconnectReason::ProactiveNetworkSwitch,
),
..fake_disconnect_info()
};
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: true, info });
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
let downtime = 5_000_000;
test_helper.advance_by(downtime.micros(), test_fut.as_mut());
// Reconnect and verify that a roam reconnect time is logged and a non-roam reconnect time
// is not logged.
test_helper.send_connected_event(random_bss_description!(Wpa2));
test_helper.drain_cobalt_events(&mut test_fut);
let roam_reconnect =
test_helper.get_logged_metrics(metrics::ROAMING_RECONNECT_DURATION_METRIC_ID);
assert_eq!(roam_reconnect.len(), 1);
assert_eq!(roam_reconnect[0].payload, MetricEventPayload::IntegerValue(downtime));
let non_roam_reconnect =
test_helper.get_logged_metrics(metrics::NON_ROAMING_RECONNECT_DURATION_METRIC_ID);
assert_eq!(non_roam_reconnect.len(), 0);
}
#[fuchsia::test]
fn test_log_device_connected_cobalt_metrics() {
let (mut test_helper, mut test_fut) = setup_test();
let wmm_info = vec![0x80]; // U-APSD enabled
#[rustfmt::skip]
let rm_enabled_capabilities = vec![
0x03, // link measurement and neighbor report enabled
0x00, 0x00, 0x00, 0x00,
];
#[rustfmt::skip]
let ext_capabilities = vec![
0x04, 0x00,
0x08, // BSS transition supported
0x00, 0x00, 0x00, 0x00, 0x40
];
let bss_description = random_bss_description!(Wpa2,
channel: Channel::new(157, Cbw::Cbw40),
ies_overrides: IesOverrides::new()
.remove(IeType::WMM_PARAM)
.set(IeType::WMM_INFO, wmm_info)
.set(IeType::RM_ENABLED_CAPABILITIES, rm_enabled_capabilities)
.set(IeType::MOBILITY_DOMAIN, vec![0x00; 3])
.set(IeType::EXT_CAPABILITIES, ext_capabilities),
bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
);
test_helper.send_connected_event(bss_description);
test_helper.drain_cobalt_events(&mut test_fut);
let num_devices_connected =
test_helper.get_logged_metrics(metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID);
assert_eq!(num_devices_connected.len(), 1);
assert_eq!(num_devices_connected[0].payload, MetricEventPayload::Count(1));
let connected_security_type =
test_helper.get_logged_metrics(metrics::CONNECTED_NETWORK_SECURITY_TYPE_METRIC_ID);
assert_eq!(connected_security_type.len(), 1);
assert_eq!(
connected_security_type[0].event_codes,
vec![
metrics::ConnectedNetworkSecurityTypeMetricDimensionSecurityType::Wpa2Personal
as u32
]
);
assert_eq!(connected_security_type[0].payload, MetricEventPayload::Count(1));
let connected_apsd = test_helper
.get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_APSD_METRIC_ID);
assert_eq!(connected_apsd.len(), 1);
assert_eq!(connected_apsd[0].payload, MetricEventPayload::Count(1));
let connected_link_measurement = test_helper.get_logged_metrics(
metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_LINK_MEASUREMENT_METRIC_ID,
);
assert_eq!(connected_link_measurement.len(), 1);
assert_eq!(connected_link_measurement[0].payload, MetricEventPayload::Count(1));
let connected_neighbor_report = test_helper.get_logged_metrics(
metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_NEIGHBOR_REPORT_METRIC_ID,
);
assert_eq!(connected_neighbor_report.len(), 1);
assert_eq!(connected_neighbor_report[0].payload, MetricEventPayload::Count(1));
let connected_ft = test_helper
.get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_FT_METRIC_ID);
assert_eq!(connected_ft.len(), 1);
assert_eq!(connected_ft[0].payload, MetricEventPayload::Count(1));
let connected_bss_transition_mgmt = test_helper.get_logged_metrics(
metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_BSS_TRANSITION_MANAGEMENT_METRIC_ID,
);
assert_eq!(connected_bss_transition_mgmt.len(), 1);
assert_eq!(connected_bss_transition_mgmt[0].payload, MetricEventPayload::Count(1));
let breakdown_by_is_multi_bss = test_helper.get_logged_metrics(
metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID,
);
assert_eq!(breakdown_by_is_multi_bss.len(), 1);
assert_eq!(
breakdown_by_is_multi_bss[0].event_codes,
vec![
metrics::SuccessfulConnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::Yes
as u32
]
);
assert_eq!(breakdown_by_is_multi_bss[0].payload, MetricEventPayload::Count(1));
let breakdown_by_primary_channel = test_helper.get_logged_metrics(
metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
);
assert_eq!(breakdown_by_primary_channel.len(), 1);
assert_eq!(breakdown_by_primary_channel[0].event_codes, vec![157]);
assert_eq!(breakdown_by_primary_channel[0].payload, MetricEventPayload::Count(1));
let breakdown_by_channel_band = test_helper.get_logged_metrics(
metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
);
assert_eq!(breakdown_by_channel_band.len(), 1);
assert_eq!(
breakdown_by_channel_band[0].event_codes,
vec![
metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band5Ghz
as u32
]
);
assert_eq!(breakdown_by_channel_band[0].payload, MetricEventPayload::Count(1));
let ap_oui_connected =
test_helper.get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID);
assert_eq!(ap_oui_connected.len(), 1);
assert_eq!(
ap_oui_connected[0].payload,
MetricEventPayload::StringValue("00F620".to_string())
);
let network_is_likely_hidden =
test_helper.get_logged_metrics(metrics::CONNECT_TO_LIKELY_HIDDEN_NETWORK_METRIC_ID);
assert_eq!(network_is_likely_hidden.len(), 1);
assert_eq!(network_is_likely_hidden[0].payload, MetricEventPayload::Count(1));
}
#[fuchsia::test]
fn test_log_device_connected_cobalt_metrics_ap_features_not_supported() {
let (mut test_helper, mut test_fut) = setup_test();
let bss_description = random_bss_description!(Wpa2,
ies_overrides: IesOverrides::new()
.remove(IeType::WMM_PARAM)
.remove(IeType::WMM_INFO)
.remove(IeType::RM_ENABLED_CAPABILITIES)
.remove(IeType::MOBILITY_DOMAIN)
.remove(IeType::EXT_CAPABILITIES)
);
test_helper.send_connected_event(bss_description);
test_helper.drain_cobalt_events(&mut test_fut);
let connected_apsd = test_helper
.get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_APSD_METRIC_ID);
assert_eq!(connected_apsd.len(), 0);
let connected_link_measurement = test_helper.get_logged_metrics(
metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_LINK_MEASUREMENT_METRIC_ID,
);
assert_eq!(connected_link_measurement.len(), 0);
let connected_neighbor_report = test_helper.get_logged_metrics(
metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_NEIGHBOR_REPORT_METRIC_ID,
);
assert_eq!(connected_neighbor_report.len(), 0);
let connected_ft = test_helper
.get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_FT_METRIC_ID);
assert_eq!(connected_ft.len(), 0);
let connected_bss_transition_mgmt = test_helper.get_logged_metrics(
metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_BSS_TRANSITION_MANAGEMENT_METRIC_ID,
);
assert_eq!(connected_bss_transition_mgmt.len(), 0);
}
#[test_case(metrics::CONNECT_TO_LIKELY_HIDDEN_NETWORK_METRIC_ID, None; "connect_to_likely_hidden_network")]
#[test_case(metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID, None; "number_of_connected_devices")]
#[test_case(metrics::CONNECTED_NETWORK_SECURITY_TYPE_METRIC_ID, None; "breakdown_by_security_type")]
#[test_case(metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID, None; "breakdown_by_is_multi_bss")]
#[test_case(metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID, None; "breakdown_by_primary_channel")]
#[test_case(metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID, None; "breakdown_by_channel_band")]
#[test_case(metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID,
Some(vec![
MetricEvent {
metric_id: metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID,
event_codes: vec![],
payload: MetricEventPayload::StringValue("00F620".to_string()),
},
]); "number_of_devices_connected_to_specific_oui")]
#[fuchsia::test(add_test_attr = false)]
fn test_log_device_connected_cobalt_metrics_on_disconnect_and_periodically(
metric_id: u32,
payload: Option<Vec<MetricEvent>>,
) {
let (mut test_helper, mut test_fut) = setup_test();
let bss_description = random_bss_description!(Wpa2,
bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
);
test_helper.send_connected_event(bss_description);
test_helper.drain_cobalt_events(&mut test_fut);
test_helper.cobalt_events.clear();
test_helper.advance_by(24.hours(), test_fut.as_mut());
// Verify that after 24 hours has passed, metric is logged at least once because
// device is still connected
let metrics = test_helper.get_logged_metrics(metric_id);
assert!(!metrics.is_empty());
if let Some(payload) = payload {
assert_eq_cobalt_events(metrics, payload)
}
test_helper.cobalt_events.clear();
let info = fake_disconnect_info();
test_helper
.telemetry_sender
.send(TelemetryEvent::Disconnected { track_subsequent_downtime: false, info });
test_helper.drain_cobalt_events(&mut test_fut);
// Verify that on disconnect, device connected metric is also logged.
let metrics = test_helper.get_logged_metrics(metric_id);
assert_eq!(metrics.len(), 1);
}
#[fuchsia::test]
fn test_log_device_connected_cobalt_metrics_on_channel_switched() {
let (mut test_helper, mut test_fut) = setup_test();
let bss_description = random_bss_description!(Wpa2,
channel: Channel::new(4, Cbw::Cbw20),
);
test_helper.send_connected_event(bss_description);
test_helper.drain_cobalt_events(&mut test_fut);
let breakdown_by_primary_channel = test_helper.get_logged_metrics(
metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
);
assert_eq!(breakdown_by_primary_channel.len(), 1);
assert_eq!(breakdown_by_primary_channel[0].event_codes, vec![4]);
assert_eq!(breakdown_by_primary_channel[0].payload, MetricEventPayload::Count(1));
let breakdown_by_channel_band = test_helper.get_logged_metrics(
metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
);
assert_eq!(breakdown_by_channel_band.len(), 1);
assert_eq!(
breakdown_by_channel_band[0].event_codes,
vec![
metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band2Dot4Ghz
as u32
]
);
assert_eq!(breakdown_by_channel_band[0].payload, MetricEventPayload::Count(1));
// Clear out existing Cobalt metrics
test_helper.cobalt_events.clear();
test_helper.telemetry_sender.send(TelemetryEvent::OnChannelSwitched {
info: fidl_internal::ChannelSwitchInfo { new_channel: 157 },
});
test_helper.drain_cobalt_events(&mut test_fut);
// On channel switched, device connected metrics for the new channel and channel band
// are logged.
let breakdown_by_primary_channel = test_helper.get_logged_metrics(
metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
);
assert_eq!(breakdown_by_primary_channel.len(), 1);
assert_eq!(breakdown_by_primary_channel[0].event_codes, vec![157]);
assert_eq!(breakdown_by_primary_channel[0].payload, MetricEventPayload::Count(1));
let breakdown_by_channel_band = test_helper.get_logged_metrics(
metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
);
assert_eq!(breakdown_by_channel_band.len(), 1);
assert_eq!(
breakdown_by_channel_band[0].event_codes,
vec![
metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band5Ghz
as u32
]
);
assert_eq!(breakdown_by_channel_band[0].payload, MetricEventPayload::Count(1));
}
#[fuchsia::test]
fn test_active_scan_requested_metric() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper
.telemetry_sender
.send(TelemetryEvent::ActiveScanRequested { num_ssids_requested: 4 });
test_helper.drain_cobalt_events(&mut test_fut);
let metrics = test_helper.get_logged_metrics(
metrics::ACTIVE_SCAN_REQUESTED_FOR_NETWORK_SELECTION_MIGRATED_METRIC_ID,
);
assert_eq!(metrics.len(), 1);
assert_eq!(metrics[0].event_codes, vec![metrics::ActiveScanRequestedForNetworkSelectionMigratedMetricDimensionActiveScanSsidsRequested::TwoToFour as u32]);
assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
}
#[fuchsia::test]
fn test_log_device_performed_roaming_scan() {
let (mut test_helper, mut test_fut) = setup_test();
// Send a roaming scan event
test_helper.telemetry_sender.send(TelemetryEvent::RoamingScan);
test_helper.drain_cobalt_events(&mut test_fut);
// Check that the event was logged to cobalt.
let metrics =
test_helper.get_logged_metrics(metrics::POLICY_PROACTIVE_ROAMING_SCAN_COUNTS_METRIC_ID);
assert_eq!(metrics.len(), 1);
assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
}
#[fuchsia::test]
fn test_connection_enabled_duration_metric() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.advance_by(10.seconds(), test_fut.as_mut());
test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
test_helper.drain_cobalt_events(&mut test_fut);
let metrics = test_helper
.get_logged_metrics(metrics::CLIENT_CONNECTIONS_ENABLED_DURATION_MIGRATED_METRIC_ID);
assert_eq!(metrics.len(), 1);
assert_eq!(
metrics[0].payload,
MetricEventPayload::IntegerValue(10.seconds().into_micros())
);
}
#[fuchsia::test]
fn test_restart_metric_start_client_connections_request_sent_first() {
let (mut test_helper, mut test_fut) = setup_test();
// Send a start client connections event and then a stop and start corresponding to a
// restart. The first start client connections should not count for the metric.
test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
test_helper.advance_by(2.seconds(), test_fut.as_mut());
test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
test_helper.advance_by(1.seconds(), test_fut.as_mut());
test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
// Check that exactly 1 restart client connections event was logged to cobalt.
test_helper.drain_cobalt_events(&mut test_fut);
let metrics =
test_helper.get_logged_metrics(metrics::CLIENT_CONNECTIONS_STOP_AND_START_METRIC_ID);
assert_eq!(metrics.len(), 1);
assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
}
#[fuchsia::test]
fn test_restart_metric_stop_client_connections_request_sent_first() {
let (mut test_helper, mut test_fut) = setup_test();
// Send stop and start events corresponding to restarting client connections.
test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
test_helper.advance_by(3.seconds(), test_fut.as_mut());
test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
// Check that 1 restart client connection event has been logged to cobalt.
test_helper.drain_cobalt_events(&mut test_fut);
let metrics =
test_helper.get_logged_metrics(metrics::CLIENT_CONNECTIONS_STOP_AND_START_METRIC_ID);
assert_eq!(metrics.len(), 1);
assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
// Stop and start client connections quickly again.
test_helper.advance_by(20.seconds(), test_fut.as_mut());
test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
test_helper.advance_by(1.seconds(), test_fut.as_mut());
test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
// Check that 1 more event has been logged.
test_helper.drain_cobalt_events(&mut test_fut);
let metrics =
test_helper.get_logged_metrics(metrics::CLIENT_CONNECTIONS_STOP_AND_START_METRIC_ID);
assert_eq!(metrics.len(), 2);
assert_eq!(metrics[1].payload, MetricEventPayload::Count(1));
}
#[fuchsia::test]
fn test_restart_metric_stop_client_connections_request_long_time_not_counted() {
let (mut test_helper, mut test_fut) = setup_test();
// Send a stop and start with some time in between, then a quick stop and start.
test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
test_helper.advance_by(30.seconds(), test_fut.as_mut());
test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
test_helper.advance_by(2.seconds(), test_fut.as_mut());
// Check that a restart was not logged since some time passed between requests.
test_helper.drain_cobalt_events(&mut test_fut);
let metrics =
test_helper.get_logged_metrics(metrics::CLIENT_CONNECTIONS_STOP_AND_START_METRIC_ID);
assert!(metrics.is_empty());
// Send another stop and start that do correspond to a restart.
test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
test_helper.advance_by(1.seconds(), test_fut.as_mut());
test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
// Check that exactly 1 restart client connections event was logged to cobalt.
test_helper.drain_cobalt_events(&mut test_fut);
let metrics =
test_helper.get_logged_metrics(metrics::CLIENT_CONNECTIONS_STOP_AND_START_METRIC_ID);
assert_eq!(metrics.len(), 1);
assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
}
#[fuchsia::test]
fn test_restart_metric_extra_stop_client_connections_ignored() {
let (mut test_helper, mut test_fut) = setup_test();
// Stop client connections well before starting it again.
test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
test_helper.advance_by(10.seconds(), test_fut.as_mut());
// Send another stop client connections shortly before a start request. The second request
// should should not cause a metric to be logged, since connections were already off.
test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
test_helper.advance_by(1.seconds(), test_fut.as_mut());
test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
test_helper.drain_cobalt_events(&mut test_fut);
let metrics =
test_helper.get_logged_metrics(metrics::CLIENT_CONNECTIONS_STOP_AND_START_METRIC_ID);
assert!(metrics.is_empty());
}
#[fuchsia::test]
fn test_stop_ap_metric() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper
.telemetry_sender
.send(TelemetryEvent::StopAp { enabled_duration: 50.seconds() });
test_helper.drain_cobalt_events(&mut test_fut);
let metrics = test_helper
.get_logged_metrics(metrics::ACCESS_POINT_ENABLED_DURATION_MIGRATED_METRIC_ID);
assert_eq!(metrics.len(), 1);
assert_eq!(
metrics[0].payload,
MetricEventPayload::IntegerValue(50.seconds().into_micros())
);
}
#[fuchsia::test]
fn test_data_persistence_called_every_five_minutes() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.advance_by(5.minutes(), test_fut.as_mut());
let tags = test_helper.get_persistence_reqs();
assert!(tags.contains(&"wlancfg-client-stats-counters".to_string()), "tags: {:?}", tags);
test_helper.send_connected_event(random_bss_description!(Wpa2));
test_helper.advance_by(5.minutes(), test_fut.as_mut());
let tags = test_helper.get_persistence_reqs();
assert!(tags.contains(&"wlancfg-client-stats-counters".to_string()), "tags: {:?}", tags);
}
#[derive(PartialEq)]
enum CreateMetricsLoggerFailureMode {
None,
FactoryRequest,
ApiFailure,
}
#[test_case(None, CreateMetricsLoggerFailureMode::None)]
#[test_case(None, CreateMetricsLoggerFailureMode::FactoryRequest)]
#[test_case(None, CreateMetricsLoggerFailureMode::ApiFailure)]
#[test_case(Some(vec![123]), CreateMetricsLoggerFailureMode::None)]
#[test_case(Some(vec![123]), CreateMetricsLoggerFailureMode::FactoryRequest)]
#[test_case(Some(vec![123]), CreateMetricsLoggerFailureMode::ApiFailure)]
#[fuchsia::test]
fn test_create_metrics_logger(
experiment_id: Option<Vec<u32>>,
failure_mode: CreateMetricsLoggerFailureMode,
) {
let mut exec = fasync::TestExecutor::new();
let (factory_proxy, mut factory_stream) = fidl::endpoints::create_proxy_and_stream::<
fidl_fuchsia_metrics::MetricEventLoggerFactoryMarker,
>()
.expect("failed to create proxy and stream.");
let fut = create_metrics_logger(&factory_proxy, experiment_id.clone());
let mut fut = pin!(fut);
// First, test the case where the factory service cannot be reached and expect an error.
if failure_mode == CreateMetricsLoggerFailureMode::FactoryRequest {
drop(factory_stream);
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(Err(_)));
return;
}
// If the test case is intended to allow the factory service to be contacted, run the
// request future until stalled.
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Depending on whether or not we specified an experiment ID, we expect different API
// requests.
let request = exec.run_until_stalled(&mut factory_stream.next());
let expected_experiments = match experiment_id {
Some(experiment_ids) => experiment_ids,
None => experiment::default_experiments(),
};
assert_variant!(
request,
Poll::Ready(Some(Ok(fidl_fuchsia_metrics::MetricEventLoggerFactoryRequest::CreateMetricEventLoggerWithExperiments {
project_spec: fidl_fuchsia_metrics::ProjectSpec {
customer_id: None,
project_id: Some(metrics::PROJECT_ID),
..
},
experiment_ids,
responder,
..
}))) => {
assert_eq!(experiment_ids, expected_experiments);
match failure_mode {
CreateMetricsLoggerFailureMode::FactoryRequest => panic!("The factory request failure should have been handled already."),
CreateMetricsLoggerFailureMode::None => responder.send(Ok(())).expect("failed to send response"),
CreateMetricsLoggerFailureMode::ApiFailure => responder.send(Err(fidl_fuchsia_metrics::Error::InvalidArguments)).expect("failed to send response"),
}
}
);
// The future should run to completion and the output will vary depending on the specified
// failure mode.
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(result) => {
match failure_mode {
CreateMetricsLoggerFailureMode::FactoryRequest => panic!("The factory request failure should have been handled already."),
CreateMetricsLoggerFailureMode::None => assert_variant!(result, Ok(_)),
CreateMetricsLoggerFailureMode::ApiFailure => assert_variant!(result, Err(_))
}
});
}
#[fuchsia::test]
fn test_log_iface_creation_failure() {
let (mut test_helper, mut test_fut) = setup_test();
// Send a notification that interface creation has failed.
test_helper.telemetry_sender.send(TelemetryEvent::IfaceCreationResult(Err(())));
// Run the telemetry loop until it stalls.
assert_variant!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
// Expect that Cobalt has been notified of the interface creation failure.
test_helper.drain_cobalt_events(&mut test_fut);
let logged_metrics =
test_helper.get_logged_metrics(metrics::INTERFACE_CREATION_FAILURE_METRIC_ID);
assert_eq!(logged_metrics.len(), 1);
}
#[fuchsia::test]
fn test_log_iface_destruction_failure() {
let (mut test_helper, mut test_fut) = setup_test();
// Send a notification that interface creation has failed.
test_helper.telemetry_sender.send(TelemetryEvent::IfaceDestructionResult(Err(())));
// Run the telemetry loop until it stalls.
assert_variant!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
// Expect that Cobalt has been notified of the interface creation failure.
test_helper.drain_cobalt_events(&mut test_fut);
let logged_metrics =
test_helper.get_logged_metrics(metrics::INTERFACE_DESTRUCTION_FAILURE_METRIC_ID);
assert_eq!(logged_metrics.len(), 1);
}
#[test_case(ScanIssue::ScanFailure, metrics::CLIENT_SCAN_FAILURE_METRIC_ID)]
#[test_case(ScanIssue::AbortedScan, metrics::ABORTED_SCAN_METRIC_ID)]
#[test_case(ScanIssue::EmptyScanResults, metrics::EMPTY_SCAN_RESULTS_METRIC_ID)]
#[fuchsia::test(add_test_attr = false)]
fn test_scan_defect_metrics(scan_issue: ScanIssue, expected_metric_id: u32) {
let (mut test_helper, mut test_fut) = setup_test();
let event = TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData::new(),
scan_defects: vec![scan_issue],
};
// Send a notification that interface creation has failed.
test_helper.telemetry_sender.send(event);
// Run the telemetry loop until it stalls.
assert_variant!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
// Expect that Cobalt has been notified of the metric
test_helper.drain_cobalt_events(&mut test_fut);
let logged_metrics = test_helper.get_logged_metrics(expected_metric_id);
assert_eq!(logged_metrics.len(), 1);
}
#[fuchsia::test]
fn test_log_ap_start_failure() {
let (mut test_helper, mut test_fut) = setup_test();
// Send a notification that starting the AP has failed.
test_helper.telemetry_sender.send(TelemetryEvent::StartApResult(Err(())));
// Run the telemetry loop until it stalls.
assert_variant!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
// Expect that Cobalt has been notified of the AP start failure.
test_helper.drain_cobalt_events(&mut test_fut);
let logged_metrics = test_helper.get_logged_metrics(metrics::AP_START_FAILURE_METRIC_ID);
assert_eq!(logged_metrics.len(), 1);
}
#[test_case(
RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset),
metrics::RecoveryOccurrenceMetricDimensionReason::InterfaceCreationFailure ;
"log recovery event for iface creation failure"
)]
#[test_case(
RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset),
metrics::RecoveryOccurrenceMetricDimensionReason::InterfaceDestructionFailure ;
"log recovery event for iface destruction failure"
)]
#[test_case(
RecoveryReason::ConnectFailure(ClientRecoveryMechanism::Disconnect),
metrics::RecoveryOccurrenceMetricDimensionReason::ClientConnectionFailure ;
"log recovery event for connect failure"
)]
#[test_case(
RecoveryReason::StartApFailure(ApRecoveryMechanism::StopAp),
metrics::RecoveryOccurrenceMetricDimensionReason::ApStartFailure ;
"log recovery event for start AP failure"
)]
#[test_case(
RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect),
metrics::RecoveryOccurrenceMetricDimensionReason::ScanFailure ;
"log recovery event for scan failure"
)]
#[test_case(
RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect),
metrics::RecoveryOccurrenceMetricDimensionReason::ScanCancellation ;
"log recovery event for scan cancellation"
)]
#[test_case(
RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect),
metrics::RecoveryOccurrenceMetricDimensionReason::ScanResultsEmpty ;
"log recovery event for empty scan results"
)]
#[fuchsia::test(add_test_attr = false)]
fn test_log_recovery_occurrence(
reason: RecoveryReason,
expected_dimension: metrics::RecoveryOccurrenceMetricDimensionReason,
) {
let (mut test_helper, mut test_fut) = setup_test();
// Send the recovery event metric.
test_helper.telemetry_sender.send(TelemetryEvent::RecoveryEvent { reason });
// Run the telemetry loop until it stalls.
assert_variant!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
// Expect that Cobalt has been notified of the recovery event
assert_variant!(
test_helper.exec.run_until_stalled(&mut test_helper.cobalt_1dot1_stream.next()),
Poll::Ready(Some(Ok(fidl_fuchsia_metrics::MetricEventLoggerRequest::LogOccurrence {
metric_id, event_codes, responder, ..
}))) => {
assert_eq!(metric_id, metrics::RECOVERY_OCCURRENCE_METRIC_ID);
assert_eq!(event_codes, vec![expected_dimension.as_event_code()]);
assert!(responder.send(Ok(())).is_ok());
});
}
#[test_case(
RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset),
RecoveryOutcome::Success,
metrics::INTERFACE_CREATION_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32] ;
"create iface fixed by resetting PHY"
)]
#[test_case(
RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset),
RecoveryOutcome::Failure,
metrics::INTERFACE_CREATION_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Failure as u32] ;
"create iface not fixed by resetting PHY"
)]
#[test_case(
RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset),
RecoveryOutcome::Success,
metrics::INTERFACE_DESTRUCTION_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32] ;
"destroy iface fixed by resetting PHY"
)]
#[test_case(
RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset),
RecoveryOutcome::Failure,
metrics::INTERFACE_DESTRUCTION_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Failure as u32] ;
"destroy iface not fixed by resetting PHY"
)]
#[test_case(
RecoveryReason::ConnectFailure(ClientRecoveryMechanism::Disconnect),
RecoveryOutcome::Success,
metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
"connect works after disconnecting"
)]
#[test_case(
RecoveryReason::ConnectFailure(ClientRecoveryMechanism::DestroyIface),
RecoveryOutcome::Success,
metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
"connect works after destroying iface"
)]
#[test_case(
RecoveryReason::ConnectFailure(ClientRecoveryMechanism::PhyReset),
RecoveryOutcome::Success,
metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
"connect works after resetting PHY"
)]
#[test_case(
RecoveryReason::ConnectFailure(ClientRecoveryMechanism::Disconnect),
RecoveryOutcome::Failure,
metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
"connect still fails after disconnecting"
)]
#[test_case(
RecoveryReason::ConnectFailure(ClientRecoveryMechanism::DestroyIface),
RecoveryOutcome::Failure,
metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
"connect still fails after destroying iface"
)]
#[test_case(
RecoveryReason::ConnectFailure(ClientRecoveryMechanism::PhyReset),
RecoveryOutcome::Failure,
metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
"connect still fails after resetting PHY"
)]
#[test_case(
RecoveryReason::StartApFailure(ApRecoveryMechanism::StopAp),
RecoveryOutcome::Success,
metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ApRecoveryMechanism::StopAp as u32] ;
"start AP works after stopping AP"
)]
#[test_case(
RecoveryReason::StartApFailure(ApRecoveryMechanism::DestroyIface),
RecoveryOutcome::Success,
metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ApRecoveryMechanism::DestroyIface as u32] ;
"start AP works after destroying iface"
)]
#[test_case(
RecoveryReason::StartApFailure(ApRecoveryMechanism::ResetPhy),
RecoveryOutcome::Success,
metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ApRecoveryMechanism::ResetPhy as u32] ;
"start AP works after resetting PHY"
)]
#[test_case(
RecoveryReason::StartApFailure(ApRecoveryMechanism::StopAp),
RecoveryOutcome::Failure,
metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Failure as u32, ApRecoveryMechanism::StopAp as u32] ;
"start AP still fails after stopping AP"
)]
#[test_case(
RecoveryReason::StartApFailure(ApRecoveryMechanism::DestroyIface),
RecoveryOutcome::Failure,
metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Failure as u32, ApRecoveryMechanism::DestroyIface as u32] ;
"start AP still fails after destroying iface"
)]
#[test_case(
RecoveryReason::StartApFailure(ApRecoveryMechanism::ResetPhy),
RecoveryOutcome::Failure,
metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Failure as u32, ApRecoveryMechanism::ResetPhy as u32] ;
"start AP still fails after resetting PHY"
)]
#[test_case(
RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect),
RecoveryOutcome::Success,
metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
"scan works after disconnecting"
)]
#[test_case(
RecoveryReason::ScanFailure(ClientRecoveryMechanism::DestroyIface),
RecoveryOutcome::Success,
metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
"scan works after destroying iface"
)]
#[test_case(
RecoveryReason::ScanFailure(ClientRecoveryMechanism::PhyReset),
RecoveryOutcome::Success,
metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
"scan works after resetting PHY"
)]
#[test_case(
RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect),
RecoveryOutcome::Failure,
metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
"scan still fails after disconnecting"
)]
#[test_case(
RecoveryReason::ScanFailure(ClientRecoveryMechanism::DestroyIface),
RecoveryOutcome::Failure,
metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
"scan still fails after destroying iface"
)]
#[test_case(
RecoveryReason::ScanFailure(ClientRecoveryMechanism::PhyReset),
RecoveryOutcome::Failure,
metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
"scan still fails after resetting PHY"
)]
#[test_case(
RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect),
RecoveryOutcome::Success,
metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
"scan is no longer cancelled after disconnecting"
)]
#[test_case(
RecoveryReason::ScanCancellation(ClientRecoveryMechanism::DestroyIface),
RecoveryOutcome::Success,
metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
"scan is no longer cancelled after destroying iface"
)]
#[test_case(
RecoveryReason::ScanCancellation(ClientRecoveryMechanism::PhyReset),
RecoveryOutcome::Success,
metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
"scan is no longer cancelled after resetting PHY"
)]
#[test_case(
RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect),
RecoveryOutcome::Failure,
metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
"scan is still cancelled after disconnect"
)]
#[test_case(
RecoveryReason::ScanCancellation(ClientRecoveryMechanism::DestroyIface),
RecoveryOutcome::Failure,
metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
"scan is still cancelled after destroying iface"
)]
#[test_case(
RecoveryReason::ScanCancellation(ClientRecoveryMechanism::PhyReset),
RecoveryOutcome::Failure,
metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
"scan is still cancelled after resetting PHY"
)]
#[test_case(
RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect),
RecoveryOutcome::Success,
metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
"scan results not empty after disconnect"
)]
#[test_case(
RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::DestroyIface),
RecoveryOutcome::Success,
metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
"scan results not empty after destroy iface"
)]
#[test_case(
RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::PhyReset),
RecoveryOutcome::Success,
metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
"scan results not empty after PHY reset"
)]
#[test_case(
RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect),
RecoveryOutcome::Failure,
metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
"scan results still empty after disconnect"
)]
#[test_case(
RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::DestroyIface),
RecoveryOutcome::Failure,
metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
"scan results still empty after destroy iface"
)]
#[test_case(
RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::PhyReset),
RecoveryOutcome::Failure,
metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
"scan results still empty after PHY reset"
)]
#[fuchsia::test(add_test_attr = false)]
fn test_log_post_recovery_result(
reason: RecoveryReason,
outcome: RecoveryOutcome,
expected_metric_id: u32,
expected_event_codes: Vec<u32>,
) {
let mut exec = fasync::TestExecutor::new();
// Construct a StatsLogger
let (cobalt_1dot1_proxy, mut cobalt_1dot1_stream) =
create_proxy_and_stream::<fidl_fuchsia_metrics::MetricEventLoggerMarker>()
.expect("failed to create MetricsEventLogger proxy");
let inspector = Inspector::default();
let inspect_node = inspector.root().create_child("stats");
let mut stats_logger = StatsLogger::new(cobalt_1dot1_proxy, &inspect_node);
// Log the test telemetry event.
let fut = stats_logger.log_post_recovery_result(reason, outcome);
let mut fut = pin!(fut);
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
// Verify the metric that was emitted.
assert_variant!(
exec.run_until_stalled(&mut cobalt_1dot1_stream.next()),
Poll::Ready(Some(Ok(fidl_fuchsia_metrics::MetricEventLoggerRequest::LogOccurrence {
metric_id, event_codes, responder, ..
}))) => {
assert_eq!(metric_id, expected_metric_id);
assert_eq!(event_codes, expected_event_codes);
assert!(responder.send(Ok(())).is_ok());
});
// The future should complete.
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
}
#[fuchsia::test]
fn test_post_recovery_connect_success() {
let (mut test_helper, mut test_fut) = setup_test();
// Send the recovery event metric.
let reason = RecoveryReason::ConnectFailure(ClientRecoveryMechanism::PhyReset);
let event = TelemetryEvent::RecoveryEvent { reason };
test_helper.telemetry_sender.send(event);
// Run the telemetry loop until it stalls.
assert_variant!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
// Expect that Cobalt has been notified of the recovery event
test_helper.drain_cobalt_events(&mut test_fut);
let logged_metrics = test_helper.get_logged_metrics(metrics::RECOVERY_OCCURRENCE_METRIC_ID);
assert_eq!(logged_metrics.len(), 1);
// Verify the reason dimension.
assert_eq!(
logged_metrics[0].event_codes,
vec![metrics::RecoveryOccurrenceMetricDimensionReason::ClientConnectionFailure
.as_event_code()]
);
// Send a successful connect result.
test_helper.telemetry_sender.send(TelemetryEvent::ConnectResult {
iface_id: IFACE_ID,
policy_connect_reason: Some(
client::types::ConnectReason::RetryAfterFailedConnectAttempt,
),
result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
multiple_bss_candidates: true,
ap_state: random_bss_description!(Wpa1).into(),
network_is_likely_hidden: false,
});
// Run the telemetry loop until it stalls.
assert_variant!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
// Verify the connect post-recovery success metric was logged.
test_helper.drain_cobalt_events(&mut test_fut);
let logged_metrics =
test_helper.get_logged_metrics(metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID);
assert_eq!(
logged_metrics[0].event_codes,
vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32]
);
// Verify a subsequent connect result does not cause another metric to be logged.
test_helper.telemetry_sender.send(TelemetryEvent::ConnectResult {
iface_id: IFACE_ID,
policy_connect_reason: Some(
client::types::ConnectReason::RetryAfterFailedConnectAttempt,
),
result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
multiple_bss_candidates: true,
ap_state: random_bss_description!(Wpa1).into(),
network_is_likely_hidden: false,
});
assert_variant!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.cobalt_events = Vec::new();
test_helper.drain_cobalt_events(&mut test_fut);
let logged_metrics =
test_helper.get_logged_metrics(metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID);
assert!(logged_metrics.is_empty());
}
#[fuchsia::test]
fn test_post_recovery_connect_failure() {
let (mut test_helper, mut test_fut) = setup_test();
// Send the recovery event metric.
let reason = RecoveryReason::ConnectFailure(ClientRecoveryMechanism::PhyReset);
let event = TelemetryEvent::RecoveryEvent { reason };
test_helper.telemetry_sender.send(event);
// Run the telemetry loop until it stalls.
assert_variant!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
// Expect that Cobalt has been notified of the recovery event
test_helper.drain_cobalt_events(&mut test_fut);
let logged_metrics = test_helper.get_logged_metrics(metrics::RECOVERY_OCCURRENCE_METRIC_ID);
assert_eq!(logged_metrics.len(), 1);
// Verify the reason dimension.
assert_eq!(
logged_metrics[0].event_codes,
vec![metrics::RecoveryOccurrenceMetricDimensionReason::ClientConnectionFailure
.as_event_code()]
);
// Send a failed connect result.
test_helper.telemetry_sender.send(TelemetryEvent::ConnectResult {
iface_id: IFACE_ID,
policy_connect_reason: Some(
client::types::ConnectReason::RetryAfterFailedConnectAttempt,
),
result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
multiple_bss_candidates: true,
ap_state: random_bss_description!(Wpa1).into(),
network_is_likely_hidden: false,
});
// Run the telemetry loop until it stalls.
assert_variant!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
// Verify the connect post-recovery failure metric was logged.
test_helper.drain_cobalt_events(&mut test_fut);
let logged_metrics =
test_helper.get_logged_metrics(metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID);
assert_eq!(
logged_metrics[0].event_codes,
vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32]
);
// Verify a subsequent connect result does not cause another metric to be logged.
test_helper.telemetry_sender.send(TelemetryEvent::ConnectResult {
iface_id: IFACE_ID,
policy_connect_reason: Some(
client::types::ConnectReason::RetryAfterFailedConnectAttempt,
),
result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
multiple_bss_candidates: true,
ap_state: random_bss_description!(Wpa1).into(),
network_is_likely_hidden: false,
});
assert_variant!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.cobalt_events = Vec::new();
test_helper.drain_cobalt_events(&mut test_fut);
let logged_metrics =
test_helper.get_logged_metrics(metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID);
assert!(logged_metrics.is_empty());
}
fn test_generic_post_recovery_event(
recovery_event: TelemetryEvent,
post_recovery_event: TelemetryEvent,
duplicate_check_event: TelemetryEvent,
expected_metric_id: u32,
dimensions: Vec<u32>,
) {
let (mut test_helper, mut test_fut) = setup_test();
// Send the recovery event metric
test_helper.telemetry_sender.send(recovery_event);
assert_variant!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
// Send the post-recovery result metric
test_helper.telemetry_sender.send(post_recovery_event);
assert_variant!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
// Get the metric that was logged and verify that it was constructed properly.
test_helper.drain_cobalt_events(&mut test_fut);
let logged_metrics = test_helper.get_logged_metrics(expected_metric_id);
assert_eq!(logged_metrics.len(), 1);
assert_eq!(logged_metrics[0].event_codes, dimensions);
// Re-send the result metric and verify that nothing new was logged.
test_helper.cobalt_events = Vec::new();
test_helper.telemetry_sender.send(duplicate_check_event);
assert_variant!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
let logged_metrics = test_helper.get_logged_metrics(expected_metric_id);
assert!(logged_metrics.is_empty());
}
#[test_case(
TelemetryEvent::RecoveryEvent {
reason: RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect)
},
TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
scan_defects: vec![]
},
TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
scan_defects: vec![]
},
metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
"Scan succeeds after recovery with no other defects"
)]
#[test_case(
TelemetryEvent::RecoveryEvent {
reason: RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect)
},
TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
scan_defects: vec![ScanIssue::ScanFailure]
},
TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
scan_defects: vec![ScanIssue::ScanFailure]
},
metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
"Scan still fails following recovery"
)]
#[test_case(
TelemetryEvent::RecoveryEvent {
reason: RecoveryReason::ScanFailure(ClientRecoveryMechanism::DestroyIface)
},
TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
scan_defects: vec![ScanIssue::AbortedScan]
},
TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
scan_defects: vec![ScanIssue::AbortedScan]
},
metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
"Scan succeeds after recovery but the scan was cancelled"
)]
#[test_case(
TelemetryEvent::RecoveryEvent {
reason: RecoveryReason::ScanFailure(ClientRecoveryMechanism::PhyReset)
},
TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
scan_defects: vec![ScanIssue::EmptyScanResults]
},
TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
scan_defects: vec![ScanIssue::EmptyScanResults]
},
metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
"Scan succeeds after recovery but the results are empty"
)]
#[test_case(
TelemetryEvent::RecoveryEvent {
reason: RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect)
},
TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
scan_defects: vec![]
},
TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
scan_defects: vec![]
},
metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
"Scan no longer cancelled after recovery"
)]
#[test_case(
TelemetryEvent::RecoveryEvent {
reason: RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect)
},
TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
scan_defects: vec![ScanIssue::ScanFailure]
},
TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
scan_defects: vec![ScanIssue::ScanFailure]
},
metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
"Scan not cancelled after recovery but fails instead"
)]
#[test_case(
TelemetryEvent::RecoveryEvent {
reason: RecoveryReason::ScanCancellation(ClientRecoveryMechanism::DestroyIface)
},
TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
scan_defects: vec![ScanIssue::AbortedScan]
},
TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
scan_defects: vec![ScanIssue::AbortedScan]
},
metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
"Scan still cancelled after recovery"
)]
#[test_case(
TelemetryEvent::RecoveryEvent {
reason: RecoveryReason::ScanCancellation(ClientRecoveryMechanism::PhyReset)
},
TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
scan_defects: vec![ScanIssue::EmptyScanResults]
},
TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
scan_defects: vec![ScanIssue::EmptyScanResults]
},
metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
"Scan not cancelled after recovery but results are empty"
)]
#[test_case(
TelemetryEvent::RecoveryEvent {
reason: RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect)
},
TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
scan_defects: vec![]
},
TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
scan_defects: vec![]
},
metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
"Scan results not empty after recovery and no other errors"
)]
#[test_case(
TelemetryEvent::RecoveryEvent {
reason: RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect)
},
TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
scan_defects: vec![ScanIssue::ScanFailure]
},
TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
scan_defects: vec![ScanIssue::ScanFailure]
},
metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
"Scan results no longer empty after recovery, but scan fails"
)]
#[test_case(
TelemetryEvent::RecoveryEvent {
reason: RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::DestroyIface)
},
TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
scan_defects: vec![ScanIssue::AbortedScan]
},
TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
scan_defects: vec![ScanIssue::AbortedScan]
},
metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
"Scan results not empty after recovery but scan is cancelled"
)]
#[test_case(
TelemetryEvent::RecoveryEvent {
reason: RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::PhyReset)
},
TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
scan_defects: vec![ScanIssue::EmptyScanResults]
},
TelemetryEvent::ScanEvent {
inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
scan_defects: vec![ScanIssue::EmptyScanResults]
},
metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
"Scan results still empty after recovery"
)]
#[fuchsia::test(add_test_attr = false)]
fn test_post_recovery_scan_metrics(
recovery_event: TelemetryEvent,
post_recovery_event: TelemetryEvent,
duplicate_check_event: TelemetryEvent,
expected_metric_id: u32,
dimensions: Vec<u32>,
) {
test_generic_post_recovery_event(
recovery_event,
post_recovery_event,
duplicate_check_event,
expected_metric_id,
dimensions,
);
}
#[test_case(
TelemetryEvent::RecoveryEvent {
reason: RecoveryReason::StartApFailure(ApRecoveryMechanism::ResetPhy)
},
TelemetryEvent::StartApResult(Err(())),
TelemetryEvent::StartApResult(Err(())),
metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Failure as u32, ApRecoveryMechanism::ResetPhy as u32] ;
"start AP still does not work after recovery"
)]
#[test_case(
TelemetryEvent::RecoveryEvent {
reason: RecoveryReason::StartApFailure(ApRecoveryMechanism::ResetPhy)
},
TelemetryEvent::StartApResult(Ok(())),
TelemetryEvent::StartApResult(Ok(())),
metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32, ApRecoveryMechanism::ResetPhy as u32] ;
"start AP works after recovery"
)]
#[fuchsia::test(add_test_attr = false)]
fn test_post_recovery_start_ap(
recovery_event: TelemetryEvent,
post_recovery_event: TelemetryEvent,
duplicate_check_event: TelemetryEvent,
expected_metric_id: u32,
dimensions: Vec<u32>,
) {
test_generic_post_recovery_event(
recovery_event,
post_recovery_event,
duplicate_check_event,
expected_metric_id,
dimensions,
);
}
#[test_case(
TelemetryEvent::RecoveryEvent {
reason: RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset)
},
TelemetryEvent::IfaceCreationResult(Err(())),
TelemetryEvent::IfaceCreationResult(Err(())),
metrics::INTERFACE_CREATION_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Failure as u32] ;
"create iface still does not work after recovery"
)]
#[test_case(
TelemetryEvent::RecoveryEvent {
reason: RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset)
},
TelemetryEvent::IfaceCreationResult(Ok(())),
TelemetryEvent::IfaceCreationResult(Ok(())),
metrics::INTERFACE_CREATION_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32] ;
"create iface works after recovery"
)]
#[fuchsia::test(add_test_attr = false)]
fn test_post_recovery_create_iface(
recovery_event: TelemetryEvent,
post_recovery_event: TelemetryEvent,
duplicate_check_event: TelemetryEvent,
expected_metric_id: u32,
dimensions: Vec<u32>,
) {
test_generic_post_recovery_event(
recovery_event,
post_recovery_event,
duplicate_check_event,
expected_metric_id,
dimensions,
);
}
#[test_case(
TelemetryEvent::RecoveryEvent {
reason: RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset)
},
TelemetryEvent::IfaceDestructionResult(Err(())),
TelemetryEvent::IfaceDestructionResult(Err(())),
metrics::INTERFACE_DESTRUCTION_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Failure as u32] ;
"destroy iface does not work after recovery"
)]
#[test_case(
TelemetryEvent::RecoveryEvent {
reason: RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset)
},
TelemetryEvent::IfaceDestructionResult(Ok(())),
TelemetryEvent::IfaceDestructionResult(Ok(())),
metrics::INTERFACE_DESTRUCTION_RECOVERY_OUTCOME_METRIC_ID,
vec![RecoveryOutcome::Success as u32] ;
"destroy iface works after recovery"
)]
#[fuchsia::test(add_test_attr = false)]
fn test_post_recovery_destroy_iface(
recovery_event: TelemetryEvent,
post_recovery_event: TelemetryEvent,
duplicate_check_event: TelemetryEvent,
expected_metric_id: u32,
dimensions: Vec<u32>,
) {
test_generic_post_recovery_event(
recovery_event,
post_recovery_event,
duplicate_check_event,
expected_metric_id,
dimensions,
);
}
#[fuchsia::test]
fn test_log_scan_request_fulfillment_time() {
let (mut test_helper, mut test_fut) = setup_test();
// Send a scan fulfillment duration
let duration = zx::Duration::from_seconds(15);
test_helper.telemetry_sender.send(TelemetryEvent::ScanRequestFulfillmentTime {
duration,
reason: client::scan::ScanReason::ClientRequest,
});
// Run the telemetry loop until it stalls.
assert_variant!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
// Expect that Cobalt has been notified of the scan fulfillment metric
test_helper.drain_cobalt_events(&mut test_fut);
let logged_metrics = test_helper
.get_logged_metrics(metrics::SUCCESSFUL_SCAN_REQUEST_FULFILLMENT_TIME_METRIC_ID);
assert_eq!(logged_metrics.len(), 1);
assert_eq!(
logged_metrics[0].event_codes,
vec![
metrics::ConnectivityWlanMetricDimensionScanFulfillmentTime::LessThanTwentyOneSeconds as u32,
metrics::ConnectivityWlanMetricDimensionScanReason::ClientRequest as u32
]
);
}
#[fuchsia::test]
fn test_log_scan_queue_statistics() {
let (mut test_helper, mut test_fut) = setup_test();
// Send a scan queue report
test_helper.telemetry_sender.send(TelemetryEvent::ScanQueueStatistics {
fulfilled_requests: 4,
remaining_requests: 12,
});
// Run the telemetry loop until it stalls.
assert_variant!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
// Expect that Cobalt has been notified of the scan queue metrics
test_helper.drain_cobalt_events(&mut test_fut);
let logged_metrics = test_helper
.get_logged_metrics(metrics::SCAN_QUEUE_STATISTICS_AFTER_COMPLETED_SCAN_METRIC_ID);
assert_eq!(logged_metrics.len(), 1);
assert_eq!(
logged_metrics[0].event_codes,
vec![
metrics::ConnectivityWlanMetricDimensionScanRequestsFulfilled::Four as u32,
metrics::ConnectivityWlanMetricDimensionScanRequestsRemaining::TenToFourteen as u32
]
);
}
#[fuchsia::test]
fn test_log_post_connection_score_deltas() {
let (mut test_helper, mut test_fut) = setup_test();
let connect_time = fasync::Time::from_nanos(1_000_000_000);
let scores_deque: VecDeque<TimestampedConnectionScore> = VecDeque::from_iter([
// One second after connection
TimestampedConnectionScore::new(90, connect_time + zx::Duration::from_millis(500)),
TimestampedConnectionScore::new(85, connect_time + zx::Duration::from_seconds(1)),
// Five seconds after connection
TimestampedConnectionScore::new(60, connect_time + zx::Duration::from_seconds(2)),
TimestampedConnectionScore::new(55, connect_time + zx::Duration::from_seconds(3)),
TimestampedConnectionScore::new(50, connect_time + zx::Duration::from_seconds(5)),
// Ten seconds after connection
TimestampedConnectionScore::new(50, connect_time + zx::Duration::from_seconds(6)),
TimestampedConnectionScore::new(45, connect_time + zx::Duration::from_seconds(8)),
TimestampedConnectionScore::new(40, connect_time + zx::Duration::from_seconds(9)),
TimestampedConnectionScore::new(35, connect_time + zx::Duration::from_seconds(10)),
// Thirty seconds after connection
TimestampedConnectionScore::new(30, connect_time + zx::Duration::from_seconds(11)),
TimestampedConnectionScore::new(20, connect_time + zx::Duration::from_seconds(20)),
TimestampedConnectionScore::new(10, connect_time + zx::Duration::from_seconds(30)),
// More than thirty seconds after connection
TimestampedConnectionScore::new(100, connect_time + zx::Duration::from_seconds(31)),
]);
let scores = HistoricalList { 0: scores_deque };
let score_at_connect: u8 = 80;
test_helper.telemetry_sender.send(TelemetryEvent::PostConnectionScores {
connect_time: connect_time,
score_at_connect,
scores,
});
test_helper.drain_cobalt_events(&mut test_fut);
let logged_metrics = test_helper.get_logged_metrics(
metrics::AVERAGE_SCORE_DELTA_AFTER_CONNECTION_BY_INITIAL_SCORE_METRIC_ID,
);
use metrics::AverageScoreDeltaAfterConnectionByInitialScoreMetricDimensionInitialScore::*;
use metrics::AverageScoreDeltaAfterConnectionByInitialScoreMetricDimensionTimeSinceConnect as DurationDimension;
// Logged metrics for one, five, ten, and thirty seconds.
assert_eq!(logged_metrics.len(), 4);
// Verify one second average delta
assert_eq!(
logged_metrics[0].event_codes,
vec![_61To80 as u32, DurationDimension::OneSecond as u32]
);
assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(5));
// Verify five second average delta
assert_eq!(
logged_metrics[1].event_codes,
vec![_61To80 as u32, DurationDimension::FiveSeconds as u32]
);
assert_eq!(logged_metrics[1].payload, MetricEventPayload::IntegerValue(-10));
// Verify ten second average delta
assert_eq!(
logged_metrics[2].event_codes,
vec![_61To80 as u32, DurationDimension::TenSeconds as u32]
);
assert_eq!(logged_metrics[2].payload, MetricEventPayload::IntegerValue(-21));
// Verify thirty second average delta
assert_eq!(
logged_metrics[3].event_codes,
vec![_61To80 as u32, DurationDimension::ThirtySeconds as u32]
);
assert_eq!(logged_metrics[3].payload, MetricEventPayload::IntegerValue(-30));
}
#[fuchsia::test]
fn test_log_pre_disconnect_score_deltas() {
let (mut test_helper, mut test_fut) = setup_test();
let final_score_time = fasync::Time::from_nanos(31_000_000_000);
let scores_deque: VecDeque<TimestampedConnectionScore> = VecDeque::from_iter([
// More than thirty seconds before last recorded score
TimestampedConnectionScore::new(100, final_score_time - zx::Duration::from_seconds(31)),
// Thirty seconds before last recorded score
TimestampedConnectionScore::new(10, final_score_time - zx::Duration::from_seconds(30)),
TimestampedConnectionScore::new(20, final_score_time - zx::Duration::from_seconds(20)),
TimestampedConnectionScore::new(30, final_score_time - zx::Duration::from_seconds(11)),
// Ten seconds before last recorded score
TimestampedConnectionScore::new(35, final_score_time - zx::Duration::from_seconds(10)),
TimestampedConnectionScore::new(40, final_score_time - zx::Duration::from_seconds(9)),
TimestampedConnectionScore::new(45, final_score_time - zx::Duration::from_seconds(8)),
TimestampedConnectionScore::new(50, final_score_time - zx::Duration::from_seconds(6)),
// Five seconds before last recorded score
TimestampedConnectionScore::new(50, final_score_time - zx::Duration::from_seconds(5)),
TimestampedConnectionScore::new(55, final_score_time - zx::Duration::from_seconds(3)),
TimestampedConnectionScore::new(60, final_score_time - zx::Duration::from_seconds(2)),
// One second before last recorded score
TimestampedConnectionScore::new(85, final_score_time - zx::Duration::from_seconds(1)),
TimestampedConnectionScore::new(90, final_score_time - zx::Duration::from_millis(500)),
// Last recorded score
TimestampedConnectionScore::new(80, final_score_time),
]);
let scores = HistoricalList { 0: scores_deque };
// Record a disconnect for a connection meeting the minimum required duration.
let disconnect_info = DisconnectInfo {
connected_duration: AVERAGE_SCORE_DELTA_MINIMUM_DURATION,
connection_scores: scores,
..fake_disconnect_info()
};
test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
track_subsequent_downtime: false,
info: disconnect_info,
});
test_helper.drain_cobalt_events(&mut test_fut);
let logged_metrics = test_helper.get_logged_metrics(
metrics::AVERAGE_SCORE_DELTA_BEFORE_DISCONNECT_BY_FINAL_SCORE_METRIC_ID,
);
use metrics::AverageScoreDeltaBeforeDisconnectByFinalScoreMetricDimensionFinalScore::*;
use metrics::AverageScoreDeltaBeforeDisconnectByFinalScoreMetricDimensionTimeUntilDisconnect as DurationDimension;
// Logged metrics for one, five, ten, and thirty seconds.
assert_eq!(logged_metrics.len(), 4);
// Verify one second average delta
assert_eq!(
logged_metrics[0].event_codes,
vec![_61To80 as u32, DurationDimension::OneSecond as u32]
);
assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(5));
// Verify five second average delta
assert_eq!(
logged_metrics[1].event_codes,
vec![_61To80 as u32, DurationDimension::FiveSeconds as u32]
);
assert_eq!(logged_metrics[1].payload, MetricEventPayload::IntegerValue(-10));
// Verify ten second average delta
assert_eq!(
logged_metrics[2].event_codes,
vec![_61To80 as u32, DurationDimension::TenSeconds as u32]
);
assert_eq!(logged_metrics[2].payload, MetricEventPayload::IntegerValue(-21));
// Verify thirty second average delta
assert_eq!(
logged_metrics[3].event_codes,
vec![_61To80 as u32, DurationDimension::ThirtySeconds as u32]
);
assert_eq!(logged_metrics[3].payload, MetricEventPayload::IntegerValue(-30));
// Record a disconnect shorter than the minimum required duration
let disconnect_info = DisconnectInfo {
connected_duration: AVERAGE_SCORE_DELTA_MINIMUM_DURATION
- zx::Duration::from_seconds(1),
..fake_disconnect_info()
};
test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
track_subsequent_downtime: false,
info: disconnect_info,
});
test_helper.drain_cobalt_events(&mut test_fut);
// No additional metrics should be logged.
let logged_metrics = test_helper.get_logged_metrics(
metrics::AVERAGE_SCORE_DELTA_BEFORE_DISCONNECT_BY_FINAL_SCORE_METRIC_ID,
);
assert_eq!(logged_metrics.len(), 4);
}
#[fuchsia::test]
fn test_log_network_selection_metrics() {
let (mut test_helper, mut test_fut) = setup_test();
// Send network selection event
test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
network_selection_type: NetworkSelectionType::Undirected,
num_candidates: Ok(3),
selected_count: 2,
});
// Run the telemetry loop until it stalls.
assert_variant!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.drain_cobalt_events(&mut test_fut);
// Verify the network selection is counted
let logged_metrics =
test_helper.get_logged_metrics(metrics::NETWORK_SELECTION_COUNT_METRIC_ID);
assert_eq!(logged_metrics.len(), 1);
assert_eq!(logged_metrics[0].payload, MetricEventPayload::Count(1));
// Verify the number of selected candidates is recorded
let logged_metrics =
test_helper.get_logged_metrics(metrics::NUM_NETWORKS_SELECTED_METRIC_ID);
assert_eq!(logged_metrics.len(), 1);
assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(2));
// Send a network selection metric where there were 0 candidates.
test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
network_selection_type: NetworkSelectionType::Undirected,
num_candidates: Ok(0),
selected_count: 0,
});
// Run the telemetry loop until it stalls.
assert_variant!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
test_helper.drain_cobalt_events(&mut test_fut);
// Verify the network selection is counted
let logged_metrics =
test_helper.get_logged_metrics(metrics::NETWORK_SELECTION_COUNT_METRIC_ID);
assert_eq!(logged_metrics.len(), 2);
// The number of selected networks should not be recorded, since there were no candidates
// to select from
let logged_metrics =
test_helper.get_logged_metrics(metrics::NUM_NETWORKS_SELECTED_METRIC_ID);
assert_eq!(logged_metrics.len(), 1);
}
#[fuchsia::test]
fn test_log_bss_selection_metrics() {
let (mut test_helper, mut test_fut) = setup_test();
// Send BSS selection result event with 3 candidate, multi-bss, one selected
let selected_candidate_2g = client::types::ScannedCandidate {
bss: client::types::Bss {
channel: client::types::WlanChan::new(1, wlan_common::channel::Cbw::Cbw20),
..generate_random_bss()
},
..generate_random_scanned_candidate()
};
let candidate_2g = client::types::ScannedCandidate {
bss: client::types::Bss {
channel: client::types::WlanChan::new(1, wlan_common::channel::Cbw::Cbw20),
..generate_random_bss()
},
..generate_random_scanned_candidate()
};
let candidate_5g = client::types::ScannedCandidate {
bss: client::types::Bss {
channel: client::types::WlanChan::new(36, wlan_common::channel::Cbw::Cbw40),
..generate_random_bss()
},
..generate_random_scanned_candidate()
};
let scored_candidates =
vec![(selected_candidate_2g.clone(), 70), (candidate_2g, 60), (candidate_5g, 50)];
test_helper.telemetry_sender.send(TelemetryEvent::BssSelectionResult {
reason: client::types::ConnectReason::FidlConnectRequest,
scored_candidates: scored_candidates.clone(),
selected_candidate: Some((selected_candidate_2g, 70)),
});
test_helper.drain_cobalt_events(&mut test_fut);
let fidl_connect_event_code = vec![
metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::FidlConnectRequest
as u32,
];
// Check that the BSS selection occurrence metrics are logged
let logged_metrics = test_helper.get_logged_metrics(metrics::BSS_SELECTION_COUNT_METRIC_ID);
assert_eq!(logged_metrics.len(), 1);
assert_eq!(logged_metrics[0].event_codes, Vec::<u32>::new());
assert_eq!(logged_metrics[0].payload, MetricEventPayload::Count(1));
let logged_metrics =
test_helper.get_logged_metrics(metrics::BSS_SELECTION_COUNT_DETAILED_METRIC_ID);
assert_eq!(logged_metrics.len(), 1);
assert_eq!(logged_metrics[0].event_codes, fidl_connect_event_code);
assert_eq!(logged_metrics[0].payload, MetricEventPayload::Count(1));
// Check that the candidate count metrics are logged
let logged_metrics =
test_helper.get_logged_metrics(metrics::NUM_BSS_CONSIDERED_IN_SELECTION_METRIC_ID);
assert_eq!(logged_metrics.len(), 1);
assert_eq!(logged_metrics[0].event_codes, Vec::<u32>::new());
assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(3));
let logged_metrics = test_helper
.get_logged_metrics(metrics::NUM_BSS_CONSIDERED_IN_SELECTION_DETAILED_METRIC_ID);
assert_eq!(logged_metrics.len(), 1);
assert_eq!(logged_metrics[0].event_codes, fidl_connect_event_code);
assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(3));
// Check that all candidate scores are logged
let logged_metrics = test_helper.get_logged_metrics(metrics::BSS_CANDIDATE_SCORE_METRIC_ID);
assert_eq!(logged_metrics.len(), 3);
for i in 0..3 {
assert_eq!(
logged_metrics[i].payload,
MetricEventPayload::IntegerValue(scored_candidates[i].1 as i64)
)
}
// Check that unique network count is logged
let logged_metrics = test_helper
.get_logged_metrics(metrics::NUM_NETWORKS_REPRESENTED_IN_BSS_SELECTION_METRIC_ID);
assert_eq!(logged_metrics.len(), 1);
assert_eq!(logged_metrics[0].event_codes, fidl_connect_event_code);
assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(3));
// Check that selected candidate score is logged
let logged_metrics = test_helper.get_logged_metrics(metrics::SELECTED_BSS_SCORE_METRIC_ID);
assert_eq!(logged_metrics.len(), 1);
assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(70));
// Check that runner-up score delta is logged
let logged_metrics =
test_helper.get_logged_metrics(metrics::RUNNER_UP_CANDIDATE_SCORE_DELTA_METRIC_ID);
assert_eq!(logged_metrics.len(), 1);
assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(10));
// Check that GHz score delta is logged
let logged_metrics =
test_helper.get_logged_metrics(metrics::BEST_CANDIDATES_GHZ_SCORE_DELTA_METRIC_ID);
assert_eq!(logged_metrics.len(), 1);
assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(-20));
// Check that GHz bands present in selection is logged
let logged_metrics =
test_helper.get_logged_metrics(metrics::GHZ_BANDS_AVAILABLE_IN_BSS_SELECTION_METRIC_ID);
assert_eq!(logged_metrics.len(), 1);
assert_eq!(
logged_metrics[0].event_codes,
vec![metrics::GhzBandsAvailableInBssSelectionMetricDimensionBands::MultiBand as u32]
);
assert_eq!(logged_metrics[0].payload, MetricEventPayload::Count(1));
}
#[fuchsia::test]
fn test_log_bss_selection_metrics_none_selected() {
let (mut test_helper, mut test_fut) = setup_test();
test_helper.telemetry_sender.send(TelemetryEvent::BssSelectionResult {
reason: client::types::ConnectReason::FidlConnectRequest,
scored_candidates: vec![],
selected_candidate: None,
});
test_helper.drain_cobalt_events(&mut test_fut);
// Check that only the BSS selection occurrence and candidate count metrics are recorded
assert!(!test_helper.get_logged_metrics(metrics::BSS_SELECTION_COUNT_METRIC_ID).is_empty());
assert!(!test_helper
.get_logged_metrics(metrics::BSS_SELECTION_COUNT_DETAILED_METRIC_ID)
.is_empty());
assert!(!test_helper
.get_logged_metrics(metrics::NUM_BSS_CONSIDERED_IN_SELECTION_METRIC_ID)
.is_empty());
assert!(!test_helper
.get_logged_metrics(metrics::NUM_BSS_CONSIDERED_IN_SELECTION_DETAILED_METRIC_ID)
.is_empty());
assert!(test_helper.get_logged_metrics(metrics::BSS_CANDIDATE_SCORE_METRIC_ID).is_empty());
assert!(test_helper
.get_logged_metrics(metrics::NUM_NETWORKS_REPRESENTED_IN_BSS_SELECTION_METRIC_ID)
.is_empty());
assert!(test_helper
.get_logged_metrics(metrics::RUNNER_UP_CANDIDATE_SCORE_DELTA_METRIC_ID)
.is_empty());
assert!(test_helper
.get_logged_metrics(metrics::NUM_NETWORKS_REPRESENTED_IN_BSS_SELECTION_METRIC_ID)
.is_empty());
assert!(test_helper
.get_logged_metrics(metrics::BEST_CANDIDATES_GHZ_SCORE_DELTA_METRIC_ID)
.is_empty());
assert!(test_helper
.get_logged_metrics(metrics::GHZ_BANDS_AVAILABLE_IN_BSS_SELECTION_METRIC_ID)
.is_empty());
}
#[fuchsia::test]
fn test_log_bss_selection_metrics_runner_up_delta_not_recorded() {
let (mut test_helper, mut test_fut) = setup_test();
let scored_candidates = vec![
(generate_random_scanned_candidate(), 90),
(generate_random_scanned_candidate(), 60),
(generate_random_scanned_candidate(), 50),
];
test_helper.telemetry_sender.send(TelemetryEvent::BssSelectionResult {
reason: client::types::ConnectReason::FidlConnectRequest,
scored_candidates: scored_candidates,
// Report that the selected candidate was not the highest scoring candidate.
selected_candidate: Some((generate_random_scanned_candidate(), 60)),
});
test_helper.drain_cobalt_events(&mut test_fut);
// No delta metric should be recorded
assert!(test_helper
.get_logged_metrics(metrics::RUNNER_UP_CANDIDATE_SCORE_DELTA_METRIC_ID)
.is_empty());
}
#[fuchsia::test]
fn test_log_connection_score_average_long_duration() {
let (mut test_helper, mut test_fut) = setup_test();
let now = fasync::Time::now();
let scores = vec![
TimestampedConnectionScore::new(10, now),
TimestampedConnectionScore::new(20, now),
TimestampedConnectionScore::new(30, now),
TimestampedConnectionScore::new(40, now),
TimestampedConnectionScore::new(50, now),
];
test_helper
.telemetry_sender
.send(TelemetryEvent::LongDurationConnectionScoreAverage { scores });
test_helper.drain_cobalt_events(&mut test_fut);
let logged_metrics =
test_helper.get_logged_metrics(metrics::CONNECTION_SCORE_AVERAGE_METRIC_ID);
assert_eq!(logged_metrics.len(), 1);
assert_eq!(
logged_metrics[0].event_codes,
vec![metrics::ConnectionScoreAverageMetricDimensionDuration::LongDuration as u32]
);
assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(30));
// Ensure an empty score list would not cause an arithmetic error.
test_helper
.telemetry_sender
.send(TelemetryEvent::LongDurationConnectionScoreAverage { scores: vec![] });
test_helper.drain_cobalt_events(&mut test_fut);
assert_eq!(
test_helper.get_logged_metrics(metrics::CONNECTION_SCORE_AVERAGE_METRIC_ID).len(),
1
);
}
struct TestHelper {
telemetry_sender: TelemetrySender,
inspector: Inspector,
monitor_svc_stream: fidl_fuchsia_wlan_device_service::DeviceMonitorRequestStream,
telemetry_svc_stream: Option<fidl_fuchsia_wlan_sme::TelemetryRequestStream>,
cobalt_1dot1_stream: fidl_fuchsia_metrics::MetricEventLoggerRequestStream,
persistence_stream: mpsc::Receiver<String>,
counter_stats_resp:
Option<Box<dyn Fn() -> fidl_fuchsia_wlan_sme::TelemetryGetCounterStatsResult>>,
/// As requests to Cobalt are responded to via `self.drain_cobalt_events()`,
/// their payloads are drained to this HashMap
cobalt_events: Vec<MetricEvent>,
// Note: keep the executor field last in the struct so it gets dropped last.
exec: fasync::TestExecutor,
}
impl TestHelper {
/// Advance executor until stalled.
/// This function will also reply to any ongoing requests to establish an iface
/// telemetry channel.
fn advance_test_fut<T>(
&mut self,
test_fut: &mut (impl Future<Output = T> + Unpin),
) -> Poll<T> {
let result = self.exec.run_until_stalled(test_fut);
if let Poll::Ready(Some(Ok(req))) =
self.exec.run_until_stalled(&mut self.monitor_svc_stream.next())
{
match req {
fidl_fuchsia_wlan_device_service::DeviceMonitorRequest::GetSmeTelemetry {
iface_id,
telemetry_server,
responder,
} => {
assert_eq!(iface_id, IFACE_ID);
let telemetry_stream = telemetry_server
.into_stream()
.expect("Failed to create telemetry stream");
responder.send(Ok(())).expect("Failed to respond to telemetry request");
self.telemetry_svc_stream = Some(telemetry_stream);
self.exec.run_until_stalled(test_fut)
}
_ => panic!("Unexpected device monitor request: {:?}", req),
}
} else {
result
}
}
/// Advance executor by `duration`.
/// This function repeatedly advances the executor by 1 second, triggering
/// any expired timers and running the test_fut, until `duration` is reached.
fn advance_by(
&mut self,
duration: zx::Duration,
mut test_fut: Pin<&mut impl Future<Output = ()>>,
) {
assert_eq!(
duration.into_nanos() % STEP_INCREMENT.into_nanos(),
0,
"duration {:?} is not divisible by STEP_INCREMENT",
duration,
);
const_assert_eq!(
TELEMETRY_QUERY_INTERVAL.into_nanos() % STEP_INCREMENT.into_nanos(),
0
);
for _i in 0..(duration.into_nanos() / STEP_INCREMENT.into_nanos()) {
self.exec.set_fake_time(fasync::Time::after(STEP_INCREMENT));
let _ = self.exec.wake_expired_timers();
assert_eq!(self.advance_test_fut(&mut test_fut), Poll::Pending);
if let Some(telemetry_svc_stream) = &mut self.telemetry_svc_stream {
if !telemetry_svc_stream.is_terminated() {
respond_iface_counter_stats_req(
&mut self.exec,
telemetry_svc_stream,
&self.counter_stats_resp,
);
}
}
// Respond to any potential Cobalt request, draining their payloads to
// `self.cobalt_events`.
self.drain_cobalt_events(&mut test_fut);
assert_eq!(self.advance_test_fut(&mut test_fut), Poll::Pending);
}
}
fn set_counter_stats_resp(
&mut self,
counter_stats_resp: Box<
dyn Fn() -> fidl_fuchsia_wlan_sme::TelemetryGetCounterStatsResult,
>,
) {
let _ = self.counter_stats_resp.replace(counter_stats_resp);
}
/// Advance executor by some duration until the next time `test_fut` handles periodic
/// telemetry. This uses `self.advance_by` underneath.
///
/// This function assumes that executor starts test_fut at time 0 (which should be true
/// if TestHelper is created from `setup_test()`)
fn advance_to_next_telemetry_checkpoint(
&mut self,
test_fut: Pin<&mut impl Future<Output = ()>>,
) {
let now = fasync::Time::now();
let remaining_interval = TELEMETRY_QUERY_INTERVAL.into_nanos()
- (now.into_nanos() % TELEMETRY_QUERY_INTERVAL.into_nanos());
self.advance_by(zx::Duration::from_nanos(remaining_interval), test_fut)
}
/// Continually execute the future and respond to any incoming Cobalt request with Ok.
/// Append each metric request payload into `self.cobalt_events`.
fn drain_cobalt_events(&mut self, test_fut: &mut (impl Future + Unpin)) {
let mut made_progress = true;
while made_progress {
let _result = self.advance_test_fut(test_fut);
made_progress = false;
while let Poll::Ready(Some(Ok(req))) =
self.exec.run_until_stalled(&mut self.cobalt_1dot1_stream.next())
{
self.cobalt_events.append(&mut req.respond_to_metric_req(Ok(())));
made_progress = true;
}
}
}
fn get_logged_metrics(&self, metric_id: u32) -> Vec<MetricEvent> {
self.cobalt_events.iter().filter(|ev| ev.metric_id == metric_id).cloned().collect()
}
fn send_connected_event(&mut self, ap_state: impl Into<client::types::ApState>) {
let event = TelemetryEvent::ConnectResult {
iface_id: IFACE_ID,
policy_connect_reason: Some(
client::types::ConnectReason::RetryAfterFailedConnectAttempt,
),
result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
multiple_bss_candidates: true,
ap_state: ap_state.into(),
network_is_likely_hidden: true,
};
self.telemetry_sender.send(event);
}
// Empty the cobalt metrics can be stored so that future checks on cobalt metrics can
// ignore previous values.
fn clear_cobalt_events(&mut self) {
self.cobalt_events = Vec::new();
}
fn get_persistence_reqs(&mut self) -> Vec<String> {
let mut persistence_reqs = vec![];
loop {
match self.persistence_stream.try_next() {
Ok(Some(tag)) => persistence_reqs.push(tag),
_ => return persistence_reqs,
}
}
}
fn get_time_series(
&mut self,
test_fut: &mut (impl Future<Output = ()> + Unpin),
) -> Arc<Mutex<TimeSeriesStats>> {
let (sender, mut receiver) = oneshot::channel();
self.telemetry_sender.send(TelemetryEvent::GetTimeSeries { sender });
assert_variant!(self.advance_test_fut(test_fut), Poll::Pending);
self.drain_cobalt_events(test_fut);
assert_variant!(receiver.try_recv(), Ok(Some(stats)) => stats)
}
}
fn respond_iface_counter_stats_req(
executor: &mut fasync::TestExecutor,
telemetry_svc_stream: &mut fidl_fuchsia_wlan_sme::TelemetryRequestStream,
counter_stats_resp: &Option<
Box<dyn Fn() -> fidl_fuchsia_wlan_sme::TelemetryGetCounterStatsResult>,
>,
) {
let telemetry_svc_req_fut = telemetry_svc_stream.try_next();
let mut telemetry_svc_req_fut = pin!(telemetry_svc_req_fut);
if let Poll::Ready(Ok(Some(request))) =
executor.run_until_stalled(&mut telemetry_svc_req_fut)
{
match request {
fidl_fuchsia_wlan_sme::TelemetryRequest::GetCounterStats { responder } => {
let resp = match &counter_stats_resp {
Some(get_resp) => get_resp(),
None => {
let seed = fasync::Time::now().into_nanos() as u64;
Ok(fake_iface_counter_stats(seed))
}
};
responder
.send(resp.as_ref().map_err(|e| *e))
.expect("expect sending GetCounterStats response to succeed");
}
_ => {
panic!("unexpected request: {:?}", request);
}
}
}
}
fn respond_iface_histogram_stats_req(
executor: &mut fasync::TestExecutor,
telemetry_svc_stream: &mut fidl_fuchsia_wlan_sme::TelemetryRequestStream,
) {
let telemetry_svc_req_fut = telemetry_svc_stream.try_next();
let mut telemetry_svc_req_fut = pin!(telemetry_svc_req_fut);
if let Poll::Ready(Ok(Some(request))) =
executor.run_until_stalled(&mut telemetry_svc_req_fut)
{
match request {
fidl_fuchsia_wlan_sme::TelemetryRequest::GetHistogramStats { responder } => {
responder
.send(Ok(&fake_iface_histogram_stats()))
.expect("expect sending GetHistogramStats response to succeed");
}
_ => {
panic!("unexpected request: {:?}", request);
}
}
}
}
/// Assert two set of Cobalt MetricEvent equal, disregarding the order
#[track_caller]
fn assert_eq_cobalt_events(
mut left: Vec<fidl_fuchsia_metrics::MetricEvent>,
mut right: Vec<fidl_fuchsia_metrics::MetricEvent>,
) {
left.sort_by(metric_event_cmp);
right.sort_by(metric_event_cmp);
assert_eq!(left, right);
}
fn metric_event_cmp(
left: &fidl_fuchsia_metrics::MetricEvent,
right: &fidl_fuchsia_metrics::MetricEvent,
) -> std::cmp::Ordering {
match left.metric_id.cmp(&right.metric_id) {
std::cmp::Ordering::Equal => match left.event_codes.len().cmp(&right.event_codes.len())
{
std::cmp::Ordering::Equal => (),
ordering => return ordering,
},
ordering => return ordering,
}
for i in 0..left.event_codes.len() {
match left.event_codes[i].cmp(&right.event_codes[i]) {
std::cmp::Ordering::Equal => (),
ordering => return ordering,
}
}
match (&left.payload, &right.payload) {
(MetricEventPayload::Count(v1), MetricEventPayload::Count(v2)) => v1.cmp(v2),
(MetricEventPayload::IntegerValue(v1), MetricEventPayload::IntegerValue(v2)) => {
v1.cmp(v2)
}
(MetricEventPayload::StringValue(v1), MetricEventPayload::StringValue(v2)) => {
v1.cmp(v2)
}
(MetricEventPayload::Histogram(_), MetricEventPayload::Histogram(_)) => {
unimplemented!()
}
_ => unimplemented!(),
}
}
trait CobaltExt {
// Respond to MetricEventLoggerRequest and extract its MetricEvent
fn respond_to_metric_req(
self,
result: Result<(), fidl_fuchsia_metrics::Error>,
) -> Vec<fidl_fuchsia_metrics::MetricEvent>;
}
impl CobaltExt for MetricEventLoggerRequest {
fn respond_to_metric_req(
self,
result: Result<(), fidl_fuchsia_metrics::Error>,
) -> Vec<fidl_fuchsia_metrics::MetricEvent> {
match self {
Self::LogOccurrence { metric_id, count, event_codes, responder } => {
assert!(responder.send(result).is_ok());
vec![MetricEvent {
metric_id,
event_codes,
payload: MetricEventPayload::Count(count),
}]
}
Self::LogInteger { metric_id, value, event_codes, responder } => {
assert!(responder.send(result).is_ok());
vec![MetricEvent {
metric_id,
event_codes,
payload: MetricEventPayload::IntegerValue(value),
}]
}
Self::LogIntegerHistogram { metric_id, histogram, event_codes, responder } => {
assert!(responder.send(result).is_ok());
vec![MetricEvent {
metric_id,
event_codes,
payload: MetricEventPayload::Histogram(histogram),
}]
}
Self::LogString { metric_id, string_value, event_codes, responder } => {
assert!(responder.send(result).is_ok());
vec![MetricEvent {
metric_id,
event_codes,
payload: MetricEventPayload::StringValue(string_value),
}]
}
Self::LogMetricEvents { events, responder } => {
assert!(responder.send(result).is_ok());
events
}
}
}
}
fn setup_test() -> (TestHelper, Pin<Box<impl Future<Output = ()>>>) {
let mut exec = fasync::TestExecutor::new_with_fake_time();
exec.set_fake_time(fasync::Time::from_nanos(0));
let (monitor_svc_proxy, monitor_svc_stream) =
create_proxy_and_stream::<fidl_fuchsia_wlan_device_service::DeviceMonitorMarker>()
.expect("failed to create DeviceMonitor proxy");
let (cobalt_1dot1_proxy, cobalt_1dot1_stream) =
create_proxy_and_stream::<fidl_fuchsia_metrics::MetricEventLoggerMarker>()
.expect("failed to create MetricsEventLogger proxy");
let inspector = Inspector::default();
let inspect_node = inspector.root().create_child("stats");
let external_inspect_node = inspector.root().create_child("external");
let (persistence_req_sender, persistence_stream) = create_inspect_persistence_channel();
let (telemetry_sender, test_fut) = serve_telemetry(
monitor_svc_proxy,
cobalt_1dot1_proxy.clone(),
Box::new(move |_experiments| {
let cobalt_1dot1_proxy = cobalt_1dot1_proxy.clone();
async move { Ok(cobalt_1dot1_proxy) }.boxed()
}),
inspect_node,
external_inspect_node.create_child("stats"),
persistence_req_sender,
);
inspector.root().record(external_inspect_node);
let mut test_fut = Box::pin(test_fut);
assert_eq!(exec.run_until_stalled(&mut test_fut), Poll::Pending);
let test_helper = TestHelper {
telemetry_sender,
inspector,
monitor_svc_stream,
telemetry_svc_stream: None,
cobalt_1dot1_stream,
persistence_stream,
counter_stats_resp: None,
cobalt_events: vec![],
exec,
};
(test_helper, test_fut)
}
fn fake_iface_counter_stats(nth_req: u64) -> fidl_fuchsia_wlan_stats::IfaceCounterStats {
fidl_fuchsia_wlan_stats::IfaceCounterStats {
rx_unicast_total: nth_req,
rx_unicast_drop: 0,
rx_multicast: 2 * nth_req,
tx_total: nth_req,
tx_drop: 0,
}
}
fn fake_iface_histogram_stats() -> fidl_fuchsia_wlan_stats::IfaceHistogramStats {
fidl_fuchsia_wlan_stats::IfaceHistogramStats {
noise_floor_histograms: fake_noise_floor_histograms(),
rssi_histograms: fake_rssi_histograms(),
rx_rate_index_histograms: fake_rx_rate_index_histograms(),
snr_histograms: fake_snr_histograms(),
}
}
fn fake_noise_floor_histograms() -> Vec<fidl_fuchsia_wlan_stats::NoiseFloorHistogram> {
vec![fidl_fuchsia_wlan_stats::NoiseFloorHistogram {
hist_scope: fidl_fuchsia_wlan_stats::HistScope::PerAntenna,
antenna_id: Some(Box::new(fidl_fuchsia_wlan_stats::AntennaId {
freq: fidl_fuchsia_wlan_stats::AntennaFreq::Antenna2G,
index: 0,
})),
noise_floor_samples: vec![fidl_fuchsia_wlan_stats::HistBucket {
bucket_index: 200,
num_samples: 999,
}],
invalid_samples: 44,
}]
}
fn fake_rssi_histograms() -> Vec<fidl_fuchsia_wlan_stats::RssiHistogram> {
vec![fidl_fuchsia_wlan_stats::RssiHistogram {
hist_scope: fidl_fuchsia_wlan_stats::HistScope::PerAntenna,
antenna_id: Some(Box::new(fidl_fuchsia_wlan_stats::AntennaId {
freq: fidl_fuchsia_wlan_stats::AntennaFreq::Antenna2G,
index: 0,
})),
rssi_samples: vec![fidl_fuchsia_wlan_stats::HistBucket {
bucket_index: 230,
num_samples: 999,
}],
invalid_samples: 55,
}]
}
fn fake_rx_rate_index_histograms() -> Vec<fidl_fuchsia_wlan_stats::RxRateIndexHistogram> {
vec![
fidl_fuchsia_wlan_stats::RxRateIndexHistogram {
hist_scope: fidl_fuchsia_wlan_stats::HistScope::Station,
antenna_id: None,
rx_rate_index_samples: vec![fidl_fuchsia_wlan_stats::HistBucket {
bucket_index: 99,
num_samples: 1400,
}],
invalid_samples: 22,
},
fidl_fuchsia_wlan_stats::RxRateIndexHistogram {
hist_scope: fidl_fuchsia_wlan_stats::HistScope::PerAntenna,
antenna_id: Some(Box::new(fidl_fuchsia_wlan_stats::AntennaId {
freq: fidl_fuchsia_wlan_stats::AntennaFreq::Antenna5G,
index: 1,
})),
rx_rate_index_samples: vec![fidl_fuchsia_wlan_stats::HistBucket {
bucket_index: 100,
num_samples: 1500,
}],
invalid_samples: 33,
},
]
}
fn fake_snr_histograms() -> Vec<fidl_fuchsia_wlan_stats::SnrHistogram> {
vec![fidl_fuchsia_wlan_stats::SnrHistogram {
hist_scope: fidl_fuchsia_wlan_stats::HistScope::PerAntenna,
antenna_id: Some(Box::new(fidl_fuchsia_wlan_stats::AntennaId {
freq: fidl_fuchsia_wlan_stats::AntennaFreq::Antenna2G,
index: 0,
})),
snr_samples: vec![fidl_fuchsia_wlan_stats::HistBucket {
bucket_index: 30,
num_samples: 999,
}],
invalid_samples: 11,
}]
}
fn fake_disconnect_info() -> DisconnectInfo {
use crate::util::testing::generate_disconnect_info;
let is_sme_reconnecting = false;
let fidl_disconnect_info = generate_disconnect_info(is_sme_reconnecting);
DisconnectInfo {
connected_duration: 6.hours(),
is_sme_reconnecting: fidl_disconnect_info.is_sme_reconnecting,
disconnect_source: fidl_disconnect_info.disconnect_source,
previous_connect_reason: client::types::ConnectReason::IdleInterfaceAutoconnect,
ap_state: random_bss_description!(Wpa2).into(),
connection_scores: HistoricalList::new(8),
}
}
fn fake_connect_result(code: fidl_ieee80211::StatusCode) -> fidl_sme::ConnectResult {
fidl_sme::ConnectResult { code, is_credential_rejected: false, is_reconnect: false }
}
#[fuchsia::test]
fn test_error_throttling() {
let exec = fasync::TestExecutor::new_with_fake_time();
exec.set_fake_time(fasync::Time::from_nanos(0));
let mut error_logger = ThrottledErrorLogger::new(MINUTES_BETWEEN_COBALT_SYSLOG_WARNINGS);
// Set the fake time to 61 minutes past 0 time to ensure that messages will be logged.
exec.set_fake_time(fasync::Time::after(fasync::Duration::from_minutes(
MINUTES_BETWEEN_COBALT_SYSLOG_WARNINGS + 1,
)));
// Log an error and verify that no record of it was retained (ie: the error was emitted
// immediately).
error_logger.throttle_error(Err(format_err!("")));
assert!(!error_logger.suppressed_errors.contains_key(&String::from("")));
// Log another error and verify that the error counter has been incremented.
error_logger.throttle_error(Err(format_err!("")));
assert_eq!(error_logger.suppressed_errors[&String::from("")], 1);
// Advance time again and log another error to verify that the counter resets (ie: log was
// emitted).
exec.set_fake_time(fasync::Time::after(fasync::Duration::from_minutes(
MINUTES_BETWEEN_COBALT_SYSLOG_WARNINGS + 1,
)));
error_logger.throttle_error(Err(format_err!("")));
assert!(!error_logger.suppressed_errors.contains_key(&String::from("")));
// Log another error to verify that the counter begins incrementing again.
error_logger.throttle_error(Err(format_err!("")));
assert_eq!(error_logger.suppressed_errors[&String::from("")], 1);
}
}