| // Copyright 2018 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; |
| |
| use { |
| crate::{device::IfaceMap, inspect, telemetry::convert::*}, |
| fidl_fuchsia_cobalt::HistogramBucket, |
| fidl_fuchsia_wlan_stats as fidl_stats, |
| fidl_fuchsia_wlan_stats::MlmeStats::{ApMlmeStats, ClientMlmeStats}, |
| fuchsia_async as fasync, |
| fuchsia_cobalt::CobaltSender, |
| fuchsia_inspect_contrib::{inspect_log, make_inspect_loggable}, |
| fuchsia_zircon as zx, |
| fuchsia_zircon::DurationNum, |
| futures::prelude::*, |
| futures::stream::FuturesUnordered, |
| log::{error, warn}, |
| parking_lot::Mutex, |
| std::cmp::PartialOrd, |
| std::collections::hash_map::Entry, |
| std::collections::HashMap, |
| std::default::Default, |
| std::ops::Sub, |
| std::sync::Arc, |
| wlan_common::{bss::BssDescriptionExt, format::MacFmt}, |
| wlan_metrics_registry as metrics, |
| wlan_sme::client::{ |
| info::{ |
| ConnectStats, ConnectionMilestone, ConnectionPingInfo, DisconnectInfo, |
| DisconnectSource, ScanStats, |
| }, |
| AssociationFailure, ConnectFailure, ConnectResult, |
| }, |
| }; |
| |
| type StatsRef = Arc<Mutex<fidl_stats::IfaceStats>>; |
| |
| /// How often to request RSSI stats and dispatcher packet counts from MLME. |
| const REPORT_PERIOD_MINUTES: i64 = 1; |
| |
| struct ReconnectInfo { |
| gap_time: zx::Duration, |
| same_ssid: bool, |
| } |
| |
| // Export MLME stats to Cobalt every REPORT_PERIOD_MINUTES. |
| pub async fn report_telemetry_periodically(ifaces_map: Arc<IfaceMap>, mut sender: CobaltSender) { |
| // TODO(fxbug.dev/28800): Make this module resilient to Wlanstack2 downtime. |
| |
| let mut last_reported_stats: HashMap<u16, StatsRef> = HashMap::new(); |
| let mut interval_stream = fasync::Interval::new(REPORT_PERIOD_MINUTES.minutes()); |
| while let Some(_) = interval_stream.next().await { |
| let mut futures = FuturesUnordered::new(); |
| for (id, iface) in ifaces_map.get_snapshot().iter() { |
| let id = *id; |
| let iface = Arc::clone(iface); |
| let fut = iface.stats_sched.get_stats().map(move |r| (id, iface, r)); |
| futures.push(fut); |
| } |
| |
| while let Some((id, _iface, stats_result)) = futures.next().await { |
| match stats_result { |
| Ok(current_stats) => match last_reported_stats.entry(id) { |
| Entry::Vacant(entry) => { |
| entry.insert(current_stats); |
| } |
| Entry::Occupied(mut value) => { |
| let last_stats = value.get_mut(); |
| report_stats(&last_stats.lock(), ¤t_stats.lock(), &mut sender); |
| let _dropped = std::mem::replace(value.get_mut(), current_stats); |
| } |
| }, |
| Err(e) => { |
| last_reported_stats.remove(&id); |
| error!("Failed to get the stats for iface '{}': {}", id, e); |
| } |
| }; |
| } |
| } |
| } |
| |
| fn report_stats( |
| last_stats: &fidl_stats::IfaceStats, |
| current_stats: &fidl_stats::IfaceStats, |
| sender: &mut CobaltSender, |
| ) { |
| report_mlme_stats(&last_stats.mlme_stats, ¤t_stats.mlme_stats, sender); |
| } |
| |
| fn report_mlme_stats( |
| last: &Option<Box<fidl_stats::MlmeStats>>, |
| current: &Option<Box<fidl_stats::MlmeStats>>, |
| sender: &mut CobaltSender, |
| ) { |
| if let (Some(ref last), Some(ref current)) = (last, current) { |
| match (last.as_ref(), current.as_ref()) { |
| (ClientMlmeStats(last), ClientMlmeStats(current)) => { |
| report_client_mlme_stats(&last, ¤t, sender) |
| } |
| (ApMlmeStats(_), ApMlmeStats(_)) => {} |
| _ => error!("Current MLME stats type is different from the last MLME stats type"), |
| }; |
| } |
| } |
| |
| fn report_client_mlme_stats( |
| last_stats: &fidl_stats::ClientMlmeStats, |
| current_stats: &fidl_stats::ClientMlmeStats, |
| sender: &mut CobaltSender, |
| ) { |
| report_rssi_stats( |
| metrics::CLIENT_ASSOC_RSSI_METRIC_ID, |
| &last_stats.assoc_data_rssi, |
| ¤t_stats.assoc_data_rssi, |
| sender, |
| ); |
| report_rssi_stats( |
| metrics::CLIENT_BEACON_RSSI_METRIC_ID, |
| &last_stats.beacon_rssi, |
| ¤t_stats.beacon_rssi, |
| sender, |
| ); |
| } |
| |
| fn report_rssi_stats( |
| rssi_metric_id: u32, |
| last_stats: &fidl_stats::RssiStats, |
| current_stats: &fidl_stats::RssiStats, |
| sender: &mut CobaltSender, |
| ) { |
| // In the internal stats histogram, hist[x] represents the number of frames |
| // with RSSI -x. For the Cobalt representation, buckets from -128 to 0 are |
| // used. When data is sent to Cobalt, the concept of index is utilized. |
| // |
| // Shortly, for Cobalt: |
| // Bucket -128 -> index 0 |
| // Bucket -127 -> index 1 |
| // ... |
| // Bucket 0 -> index 128 |
| // |
| // The for loop below converts the stats internal representation to the |
| // Cobalt representation and prepares the histogram that will be sent. |
| |
| let mut histogram = Vec::new(); |
| for bin in 0..current_stats.hist.len() { |
| let diff = get_diff(last_stats.hist[bin], current_stats.hist[bin]); |
| if diff > 0 { |
| let entry = HistogramBucket { |
| index: (fidl_stats::RSSI_BINS - (bin as u8) - 1).into(), |
| count: diff.into(), |
| }; |
| histogram.push(entry); |
| } |
| } |
| |
| if !histogram.is_empty() { |
| sender.log_int_histogram(rssi_metric_id, (), histogram); |
| } |
| } |
| |
| fn get_diff<T>(last_stat: T, current_stat: T) -> T |
| where |
| T: Sub<Output = T> + PartialOrd + Default, |
| { |
| if current_stat >= last_stat { |
| current_stat - last_stat |
| } else { |
| Default::default() |
| } |
| } |
| |
| pub fn log_scan_stats( |
| sender: &mut CobaltSender, |
| inspect_tree: Arc<inspect::WlanstackTree>, |
| scan_stats: &ScanStats, |
| is_join_scan: bool, |
| ) { |
| let (scan_result_dim, error_code_dim) = convert_scan_result(&scan_stats.result); |
| let scan_type_dim = convert_scan_type(scan_stats.scan_type); |
| let is_join_scan_dim = convert_bool_dim(is_join_scan); |
| let client_state_dim = match scan_stats.scan_start_while_connected { |
| true => metrics::ScanResultMetricDimensionClientState::Connected, |
| false => metrics::ScanResultMetricDimensionClientState::Idle, |
| }; |
| |
| sender.log_event_count( |
| metrics::SCAN_RESULT_METRIC_ID, |
| [ |
| scan_result_dim as u32, |
| scan_type_dim as u32, |
| is_join_scan_dim as u32, |
| client_state_dim as u32, |
| ], |
| // Elapsed time during which the count of event has been gathered. We log 0 since |
| // we don't keep track of this. |
| 0, |
| // Log one count of scan result. |
| 1, |
| ); |
| |
| if let Some(error_code_dim) = error_code_dim { |
| inspect_log!(inspect_tree.client_stats.scan_failures.lock(), {}); |
| sender.log_event_count( |
| metrics::SCAN_FAILURE_METRIC_ID, |
| [ |
| error_code_dim as u32, |
| scan_type_dim as u32, |
| is_join_scan_dim as u32, |
| client_state_dim as u32, |
| ], |
| 0, |
| 1, |
| ) |
| } |
| |
| let scan_time = scan_stats.scan_time().into_micros(); |
| sender.log_elapsed_time(metrics::SCAN_TIME_METRIC_ID, (), scan_time); |
| sender.log_elapsed_time( |
| metrics::SCAN_TIME_PER_RESULT_METRIC_ID, |
| scan_result_dim as u32, |
| scan_time, |
| ); |
| sender.log_elapsed_time( |
| metrics::SCAN_TIME_PER_SCAN_TYPE_METRIC_ID, |
| scan_type_dim as u32, |
| scan_time, |
| ); |
| sender.log_elapsed_time( |
| metrics::SCAN_TIME_PER_JOIN_OR_DISCOVERY_METRIC_ID, |
| is_join_scan_dim as u32, |
| scan_time, |
| ); |
| sender.log_elapsed_time( |
| metrics::SCAN_TIME_PER_CLIENT_STATE_METRIC_ID, |
| client_state_dim as u32, |
| scan_time, |
| ); |
| } |
| |
| pub fn log_connect_stats( |
| sender: &mut CobaltSender, |
| inspect_tree: Arc<inspect::WlanstackTree>, |
| connect_stats: &ConnectStats, |
| ) { |
| if let Some(scan_stats) = connect_stats.join_scan_stats() { |
| log_scan_stats(sender, inspect_tree.clone(), &scan_stats, true); |
| } |
| |
| log_connect_attempts_stats(sender, connect_stats); |
| log_connect_result_stats(sender, connect_stats); |
| log_time_to_connect_stats(sender, connect_stats); |
| let reconnect_info = log_connection_gap_time_stats(sender, connect_stats); |
| |
| if let ConnectResult::Success = connect_stats.result { |
| inspect_log!(inspect_tree.client_stats.connect.lock(), { |
| attempts: connect_stats.attempts, |
| reconnect_info?: reconnect_info.map(|info| make_inspect_loggable!({ |
| gap_time: info.gap_time.into_nanos(), |
| same_ssid: info.same_ssid, |
| })), |
| }); |
| } |
| } |
| |
| fn log_connect_attempts_stats(sender: &mut CobaltSender, connect_stats: &ConnectStats) { |
| // Only log attempts for successful connect. If connect is not successful, or if the expected |
| // fields for successful connect attempts are not there, early return. |
| match connect_stats.result { |
| ConnectResult::Success => (), |
| _ => return, |
| } |
| let is_multi_bss = match &connect_stats.scan_end_stats { |
| Some(stats) => stats.bss_count > 1, |
| None => return, |
| }; |
| let bss = match &connect_stats.candidate_network { |
| Some(bss) => bss, |
| None => return, |
| }; |
| |
| use metrics::ConnectionSuccessWithAttemptsBreakdownMetricDimensionAttempts::*; |
| let attempts_dim = match connect_stats.attempts { |
| 0 => { |
| warn!("unexpected 0 attempts in connect stats"); |
| return; |
| } |
| 1 => One, |
| 2 => Two, |
| 3 => Three, |
| 4 => Four, |
| 5 => Five, |
| _ => MoreThanFive, |
| }; |
| let is_multi_bss_dim = convert_bool_dim(is_multi_bss); |
| let protection_dim = convert_protection(&bss.get_protection()); |
| let channel_band_dim = convert_channel_band(bss.chan.primary); |
| |
| sender.log_event_count( |
| metrics::CONNECTION_ATTEMPTS_METRIC_ID, |
| (), // no dimension |
| 0, |
| connect_stats.attempts as i64, |
| ); |
| sender.log_event_count( |
| metrics::CONNECTION_SUCCESS_WITH_ATTEMPTS_BREAKDOWN_METRIC_ID, |
| [ |
| attempts_dim as u32, |
| is_multi_bss_dim as u32, |
| protection_dim as u32, |
| channel_band_dim as u32, |
| ], |
| 0, |
| 1, |
| ); |
| } |
| |
| fn log_connect_result_stats(sender: &mut CobaltSender, connect_stats: &ConnectStats) { |
| let oui = connect_stats.candidate_network.as_ref().map(|bss| bss.bssid.to_oui_uppercase("")); |
| let result_dim = convert_connection_result(&connect_stats.result); |
| sender.with_component().log_event_count::<_, String, _>( |
| metrics::CONNECTION_RESULT_METRIC_ID, |
| result_dim as u32, |
| oui.clone(), |
| 0, |
| 1, |
| ); |
| |
| if let ConnectResult::Failed(failure) = &connect_stats.result { |
| let fail_at_dim = convert_to_fail_at_dim(failure); |
| let timeout_dim = convert_bool_dim(failure.is_timeout()); |
| sender.with_component().log_event_count::<_, String, _>( |
| metrics::CONNECTION_FAILURE_METRIC_ID, |
| [fail_at_dim as u32, timeout_dim as u32], |
| oui.clone(), |
| 0, |
| 1, |
| ); |
| |
| if let ConnectFailure::SelectNetworkFailure(select_network_failure) = failure { |
| let error_reason_dim = convert_select_network_failure(&select_network_failure); |
| sender.with_component().log_event_count::<_, String, _>( |
| metrics::NETWORK_SELECTION_FAILURE_METRIC_ID, |
| error_reason_dim as u32, |
| oui, |
| 0, |
| 1, |
| ); |
| } |
| } |
| |
| // For the remaining metrics, we expect scan result and candidate network to have been found |
| let is_multi_bss = match &connect_stats.scan_end_stats { |
| Some(stats) => stats.bss_count > 1, |
| None => return, |
| }; |
| let bss = match &connect_stats.candidate_network { |
| Some(bss) => bss, |
| None => return, |
| }; |
| let oui = bss.bssid.to_oui_uppercase(""); |
| |
| let is_multi_bss_dim = convert_bool_dim(is_multi_bss); |
| let protection_dim = convert_protection(&bss.get_protection()); |
| let channel_band_dim = convert_channel_band(bss.chan.primary); |
| let rssi_dim = convert_rssi(bss.rssi_dbm); |
| let snr_dim = convert_snr(bss.snr_db); |
| sender.with_component().log_event_count( |
| metrics::CONNECTION_RESULT_POST_NETWORK_SELECTION_METRIC_ID, |
| [ |
| result_dim as u32, |
| is_multi_bss_dim as u32, |
| protection_dim as u32, |
| channel_band_dim as u32, |
| ], |
| oui.clone(), |
| 0, |
| 1, |
| ); |
| sender.with_component().log_event_count( |
| metrics::CONNECTION_RESULT_PER_RSSI_METRIC_ID, |
| [result_dim as u32, rssi_dim as u32], |
| oui.clone(), |
| 0, |
| 1, |
| ); |
| sender.with_component().log_event_count( |
| metrics::CONNECTION_RESULT_PER_SNR_METRIC_ID, |
| [result_dim as u32, snr_dim as u32], |
| oui.clone(), |
| 0, |
| 1, |
| ); |
| |
| match &connect_stats.result { |
| ConnectResult::Failed(failure) => match failure { |
| ConnectFailure::AuthenticationFailure(code) => { |
| let error_code_dim = convert_auth_error_code(*code); |
| sender.with_component().log_event_count( |
| metrics::AUTHENTICATION_FAILURE_METRIC_ID, |
| [ |
| error_code_dim as u32, |
| is_multi_bss_dim as u32, |
| channel_band_dim as u32, |
| protection_dim as u32, |
| ], |
| oui.clone(), |
| 0, |
| 1, |
| ); |
| sender.with_component().log_event_count( |
| metrics::AUTHENTICATION_FAILURE_PER_RSSI_METRIC_ID, |
| [error_code_dim as u32, rssi_dim as u32, channel_band_dim as u32], |
| oui, |
| 0, |
| 1, |
| ); |
| } |
| ConnectFailure::AssociationFailure(AssociationFailure { code, .. }) => { |
| let error_code_dim = convert_assoc_error_code(*code); |
| sender.with_component().log_event_count( |
| metrics::ASSOCIATION_FAILURE_METRIC_ID, |
| [error_code_dim as u32, protection_dim as u32], |
| oui.clone(), |
| 0, |
| 1, |
| ); |
| sender.with_component().log_event_count( |
| metrics::ASSOCIATION_FAILURE_PER_RSSI_METRIC_ID, |
| [error_code_dim as u32, rssi_dim as u32, channel_band_dim as u32], |
| oui, |
| 0, |
| 1, |
| ); |
| } |
| ConnectFailure::EstablishRsnaFailure(..) => { |
| sender.with_component().log_event_count( |
| metrics::ESTABLISH_RSNA_FAILURE_METRIC_ID, |
| protection_dim as u32, |
| oui, |
| 0, |
| 1, |
| ); |
| } |
| // Scan failure is already logged as part of scan stats. |
| // Select network failure is already logged above. |
| _ => (), |
| }, |
| _ => (), |
| } |
| } |
| |
| fn log_time_to_connect_stats(sender: &mut CobaltSender, connect_stats: &ConnectStats) { |
| let connect_result_dim = convert_connection_result(&connect_stats.result); |
| let rssi_dim = connect_stats.candidate_network.as_ref().map(|bss| convert_rssi(bss.rssi_dbm)); |
| |
| let connect_time = connect_stats.connect_time().into_micros(); |
| sender.log_elapsed_time(metrics::CONNECTION_SETUP_TIME_METRIC_ID, (), connect_time); |
| sender.log_elapsed_time( |
| metrics::CONNECTION_SETUP_TIME_PER_RESULT_METRIC_ID, |
| connect_result_dim as u32, |
| connect_time, |
| ); |
| if let Some(connect_time_without_scan) = connect_stats.connect_time_without_scan() { |
| sender.log_elapsed_time( |
| metrics::CONNECTION_SETUP_TIME_WITHOUT_SCAN_METRIC_ID, |
| (), |
| connect_time_without_scan.into_micros(), |
| ); |
| sender.log_elapsed_time( |
| metrics::CONNECTION_SETUP_TIME_WITHOUT_SCAN_PER_RESULT_METRIC_ID, |
| connect_result_dim as u32, |
| connect_time_without_scan.into_micros(), |
| ); |
| if let Some(rssi_dim) = rssi_dim { |
| sender.log_elapsed_time( |
| metrics::CONNECTION_SETUP_TIME_WITHOUT_SCAN_PER_RSSI_METRIC_ID, |
| rssi_dim as u32, |
| connect_time_without_scan.into_micros(), |
| ) |
| } |
| } |
| |
| if let Some(time) = connect_stats.connect_queued_time() { |
| sender.log_elapsed_time(metrics::CONNECTION_QUEUED_TIME_METRIC_ID, (), time.into_micros()); |
| } |
| |
| if let Some(auth_time) = connect_stats.auth_time() { |
| sender.log_elapsed_time( |
| metrics::AUTHENTICATION_TIME_METRIC_ID, |
| (), |
| auth_time.into_micros(), |
| ); |
| if let Some(rssi_dim) = rssi_dim { |
| sender.log_elapsed_time( |
| metrics::AUTHENTICATION_TIME_PER_RSSI_METRIC_ID, |
| rssi_dim as u32, |
| auth_time.into_micros(), |
| ) |
| } |
| } |
| |
| if let Some(assoc_time) = connect_stats.assoc_time() { |
| sender.log_elapsed_time(metrics::ASSOCIATION_TIME_METRIC_ID, (), assoc_time.into_micros()); |
| if let Some(rssi_dim) = rssi_dim { |
| sender.log_elapsed_time( |
| metrics::ASSOCIATION_TIME_PER_RSSI_METRIC_ID, |
| rssi_dim as u32, |
| assoc_time.into_micros(), |
| ) |
| } |
| } |
| |
| if let Some(rsna_time) = connect_stats.rsna_time() { |
| sender.log_elapsed_time( |
| metrics::ESTABLISH_RSNA_TIME_METRIC_ID, |
| (), |
| rsna_time.into_micros(), |
| ); |
| if let Some(rssi_dim) = rssi_dim { |
| sender.log_elapsed_time( |
| metrics::ESTABLISH_RSNA_TIME_PER_RSSI_METRIC_ID, |
| rssi_dim as u32, |
| rsna_time.into_micros(), |
| ) |
| } |
| } |
| } |
| |
| /// If there was a reconnect, log connection gap time stats to Cobalt. Return the duration |
| /// and whether the reconnect was to the same SSID as last connected. |
| fn log_connection_gap_time_stats( |
| sender: &mut CobaltSender, |
| connect_stats: &ConnectStats, |
| ) -> Option<ReconnectInfo> { |
| if connect_stats.result != ConnectResult::Success { |
| return None; |
| } |
| |
| let ssid = match &connect_stats.candidate_network { |
| Some(bss) => &bss.ssid, |
| None => { |
| warn!("No candidate_network in successful connect stats"); |
| return None; |
| } |
| }; |
| |
| let mut reconnect_info = None; |
| if let Some(previous_disconnect_info) = &connect_stats.previous_disconnect_info { |
| let duration = connect_stats.connect_end_at - previous_disconnect_info.disconnect_at; |
| let ssids_dim = if ssid == &previous_disconnect_info.ssid { |
| metrics::ConnectionGapTimeBreakdownMetricDimensionSsids::SameSsid |
| } else { |
| metrics::ConnectionGapTimeBreakdownMetricDimensionSsids::DifferentSsids |
| }; |
| let previous_disconnect_cause_dim = |
| convert_disconnect_source(&previous_disconnect_info.disconnect_source); |
| |
| sender.log_elapsed_time(metrics::CONNECTION_GAP_TIME_METRIC_ID, (), duration.into_micros()); |
| sender.log_elapsed_time( |
| metrics::CONNECTION_GAP_TIME_BREAKDOWN_METRIC_ID, |
| [ssids_dim as u32, previous_disconnect_cause_dim as u32], |
| duration.into_micros(), |
| ); |
| reconnect_info.replace(ReconnectInfo { |
| gap_time: duration, |
| same_ssid: ssid == &previous_disconnect_info.ssid, |
| }); |
| } |
| reconnect_info |
| } |
| |
| pub fn log_connection_ping(sender: &mut CobaltSender, info: &ConnectionPingInfo) { |
| for milestone in ConnectionMilestone::all().iter() { |
| if info.connected_duration() >= milestone.min_duration() { |
| let dur_dim = convert_connected_milestone(milestone); |
| sender.log_event(metrics::CONNECTION_UPTIME_PING_METRIC_ID, dur_dim as u32); |
| } |
| } |
| |
| if info.reaches_new_milestone() { |
| let dur_dim = convert_connected_milestone(&info.get_milestone()); |
| sender.log_event_count( |
| metrics::CONNECTION_COUNT_BY_DURATION_METRIC_ID, |
| dur_dim as u32, |
| 0, |
| 1, |
| ); |
| } |
| } |
| |
| pub fn log_disconnect( |
| sender: &mut CobaltSender, |
| inspect_tree: Arc<inspect::WlanstackTree>, |
| info: &DisconnectInfo, |
| ) { |
| inspect_log!(inspect_tree.client_stats.disconnect.lock(), { |
| connected_duration: info.connected_duration.into_nanos(), |
| last_rssi: info.last_rssi, |
| last_snr: info.last_snr, |
| bssid: info.bssid.to_mac_str(), |
| bssid_hash: inspect_tree.hasher.hash_mac_addr(info.bssid), |
| ssid: String::from_utf8_lossy(&info.ssid[..]).to_string(), |
| ssid_hash: inspect_tree.hasher.hash(&info.ssid[..]), |
| channel: { |
| primary: info.channel.primary, |
| cbw: format!("{:?}", info.channel.cbw.to_fidl().0), |
| secondary80: info.channel.cbw.to_fidl().1, |
| }, |
| reason_code: info.reason_code, |
| disconnect_source: match info.disconnect_source { |
| DisconnectSource::User => "user", |
| DisconnectSource::Mlme => "mlme", |
| DisconnectSource::Ap => "ap", |
| }, |
| time_since_channel_switch?: info.time_since_channel_switch.map(|d| d.into_nanos()), |
| }); |
| |
| if let DisconnectSource::Mlme = info.disconnect_source { |
| use metrics::LostConnectionCountMetricDimensionConnectedTime::*; |
| |
| let duration_dim = match &info.connected_duration { |
| x if x < &1.minutes() => LessThanOneMinute, |
| x if x < &10.minutes() => LessThanTenMinutes, |
| x if x < &30.minutes() => LessThanThirtyMinutes, |
| x if x < &1.hour() => LessThanOneHour, |
| x if x < &3.hours() => LessThanThreeHours, |
| x if x < &6.hours() => LessThanSixHours, |
| _ => AtLeastSixHours, |
| }; |
| let rssi_dim = convert_rssi(info.last_rssi); |
| |
| sender.with_component().log_event_count( |
| metrics::LOST_CONNECTION_COUNT_METRIC_ID, |
| [duration_dim as u32, rssi_dim as u32], |
| info.bssid.to_oui_uppercase(""), |
| 0, |
| 1, |
| ); |
| } |
| |
| use metrics::DisconnectCountBreakdownMetricDimensionConnectedTime::*; |
| use metrics::DisconnectCountBreakdownMetricDimensionDisconnectSource::*; |
| use metrics::DisconnectCountBreakdownMetricDimensionRecentChannelSwitch::*; |
| |
| let connected_time_dim = match &info.connected_duration { |
| x if x < &1.minutes() => LessThanOneMinute, |
| x if x < &10.minutes() => LessThanTenMinutes, |
| x if x < &30.minutes() => LessThanThirtyMinutes, |
| x if x < &1.hour() => LessThanOneHour, |
| x if x < &3.hours() => LessThanThreeHours, |
| x if x < &6.hours() => LessThanSixHours, |
| _ => AtLeastSixHours, |
| }; |
| let disconnect_source_dim = match &info.disconnect_source { |
| DisconnectSource::User => User, |
| DisconnectSource::Mlme => Mlme, |
| DisconnectSource::Ap => Ap, |
| }; |
| let snr_dim = convert_snr(info.last_snr); |
| let recent_channel_switch_dim = match info.time_since_channel_switch.map(|d| d < 1.minutes()) { |
| Some(true) => Yes, |
| _ => No, |
| }; |
| let channel_band_dim = convert_channel_band(info.channel.primary); |
| |
| sender.log_event_count(metrics::DISCONNECT_COUNT_METRIC_ID, (), 0, 1); |
| sender.with_component().log_event_count( |
| metrics::DISCONNECT_COUNT_BREAKDOWN_METRIC_ID, |
| [ |
| connected_time_dim as u32, |
| disconnect_source_dim as u32, |
| snr_dim as u32, |
| recent_channel_switch_dim as u32, |
| channel_band_dim as u32, |
| ], |
| info.bssid.to_oui_uppercase(""), |
| 0, |
| 1, |
| ); |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| crate::{ |
| device::{self, IfaceDevice}, |
| mlme_query_proxy::MlmeQueryProxy, |
| stats_scheduler::{self, StatsRequest}, |
| }, |
| fidl::endpoints::create_proxy, |
| fidl_fuchsia_cobalt::{CobaltEvent, EventPayload}, |
| fidl_fuchsia_wlan_mlme::{self as fidl_mlme, MlmeMarker}, |
| fidl_fuchsia_wlan_stats::{Counter, DispatcherStats, IfaceStats, PacketCounter}, |
| fuchsia_inspect::{assert_inspect_tree, testing::AnyProperty, Inspector}, |
| fuchsia_zircon as zx, |
| futures::channel::mpsc, |
| maplit::hashset, |
| pin_utils::pin_mut, |
| std::collections::HashSet, |
| wlan_common::{ |
| assert_variant, |
| bss::Protection as BssProtection, |
| channel::{Cbw, Channel}, |
| fake_bss, |
| }, |
| wlan_sme::client::{ |
| info::{ |
| ConnectStats, DisconnectInfo, DisconnectSource, PreviousDisconnectInfo, |
| ScanEndStats, ScanResult, ScanStartStats, SupplicantProgress, |
| }, |
| ConnectFailure, ConnectResult, EstablishRsnaFailure, EstablishRsnaFailureReason, |
| SelectNetworkFailure, |
| }, |
| }; |
| |
| const IFACE_ID: u16 = 1; |
| const DURATION_SINCE_LAST_DISCONNECT: zx::Duration = zx::Duration::from_seconds(10); |
| |
| #[test] |
| fn test_report_telemetry_periodically() { |
| let mut exec = fasync::Executor::new().expect("Failed to create an executor"); |
| |
| let (ifaces_map, stats_requests) = fake_iface_map(); |
| let (cobalt_sender, mut cobalt_receiver) = fake_cobalt_sender(); |
| |
| let telemetry_fut = report_telemetry_periodically(Arc::new(ifaces_map), cobalt_sender); |
| pin_mut!(telemetry_fut); |
| |
| // Schedule the first stats request |
| let _ = exec.run_until_stalled(&mut telemetry_fut); |
| assert!(exec.wake_next_timer().is_some()); |
| let _ = exec.run_until_stalled(&mut telemetry_fut); |
| |
| // Provide stats response |
| let mut nth_req = 0; |
| let mut stats_server = stats_requests.for_each(move |req| { |
| nth_req += 1; |
| future::ready(req.reply(fake_iface_stats(nth_req))) |
| }); |
| let _ = exec.run_until_stalled(&mut stats_server); |
| |
| // TODO(fxbug.dev/29730): For some reason, telemetry skips logging the first stats response |
| // Schedule the next stats request |
| let _ = exec.run_until_stalled(&mut telemetry_fut); |
| assert!(exec.wake_next_timer().is_some()); |
| let _ = exec.run_until_stalled(&mut telemetry_fut); |
| |
| // Provide stats response |
| let _ = exec.run_until_stalled(&mut stats_server); |
| |
| // Verify that stats are sent to Cobalt |
| let _ = exec.run_until_stalled(&mut telemetry_fut); |
| let mut expected_metrics = vec![ |
| CobaltEvent { |
| metric_id: metrics::CLIENT_ASSOC_RSSI_METRIC_ID, |
| event_codes: vec![], |
| component: None, |
| payload: EventPayload::IntHistogram(vec![HistogramBucket { index: 128, count: 1 }]), |
| }, |
| CobaltEvent { |
| metric_id: metrics::CLIENT_BEACON_RSSI_METRIC_ID, |
| event_codes: vec![], |
| component: None, |
| payload: EventPayload::IntHistogram(vec![HistogramBucket { index: 128, count: 1 }]), |
| }, |
| ]; |
| while let Ok(Some(event)) = cobalt_receiver.try_next() { |
| let index = expected_metrics.iter().position(|e| *e == event); |
| assert!(index.is_some(), "unexpected event: {:?}", event); |
| expected_metrics.remove(index.unwrap()); |
| } |
| assert!(expected_metrics.is_empty(), "some metrics not logged: {:?}", expected_metrics); |
| } |
| |
| #[test] |
| fn test_log_connect_stats_success() { |
| let (mut cobalt_sender, mut cobalt_receiver) = fake_cobalt_sender(); |
| let inspect_tree = fake_inspect_tree(); |
| log_connect_stats(&mut cobalt_sender, inspect_tree.clone(), &fake_connect_stats()); |
| |
| let mut expected_metrics = hashset! { |
| metrics::CONNECTION_ATTEMPTS_METRIC_ID, |
| metrics::CONNECTION_SUCCESS_WITH_ATTEMPTS_BREAKDOWN_METRIC_ID, |
| metrics::CONNECTION_RESULT_METRIC_ID, |
| metrics::CONNECTION_RESULT_POST_NETWORK_SELECTION_METRIC_ID, |
| metrics::CONNECTION_RESULT_PER_RSSI_METRIC_ID, |
| metrics::CONNECTION_RESULT_PER_SNR_METRIC_ID, |
| metrics::SCAN_RESULT_METRIC_ID, |
| metrics::CONNECTION_SETUP_TIME_METRIC_ID, |
| metrics::CONNECTION_SETUP_TIME_PER_RESULT_METRIC_ID, |
| metrics::CONNECTION_SETUP_TIME_WITHOUT_SCAN_METRIC_ID, |
| metrics::CONNECTION_SETUP_TIME_WITHOUT_SCAN_PER_RESULT_METRIC_ID, |
| metrics::CONNECTION_SETUP_TIME_WITHOUT_SCAN_PER_RSSI_METRIC_ID, |
| metrics::SCAN_TIME_METRIC_ID, |
| metrics::SCAN_TIME_PER_RESULT_METRIC_ID, |
| metrics::SCAN_TIME_PER_SCAN_TYPE_METRIC_ID, |
| metrics::SCAN_TIME_PER_JOIN_OR_DISCOVERY_METRIC_ID, |
| metrics::SCAN_TIME_PER_CLIENT_STATE_METRIC_ID, |
| metrics::AUTHENTICATION_TIME_METRIC_ID, |
| metrics::AUTHENTICATION_TIME_PER_RSSI_METRIC_ID, |
| metrics::ASSOCIATION_TIME_METRIC_ID, |
| metrics::ASSOCIATION_TIME_PER_RSSI_METRIC_ID, |
| metrics::ESTABLISH_RSNA_TIME_METRIC_ID, |
| metrics::ESTABLISH_RSNA_TIME_PER_RSSI_METRIC_ID, |
| metrics::CONNECTION_QUEUED_TIME_METRIC_ID, |
| metrics::CONNECTION_GAP_TIME_METRIC_ID, |
| metrics::CONNECTION_GAP_TIME_BREAKDOWN_METRIC_ID, |
| }; |
| while let Ok(Some(event)) = cobalt_receiver.try_next() { |
| assert!(expected_metrics.contains(&event.metric_id), "unexpected event: {:?}", event); |
| expected_metrics.remove(&event.metric_id); |
| } |
| assert!(expected_metrics.is_empty(), "some metrics not logged: {:?}", expected_metrics); |
| } |
| |
| #[test] |
| fn test_log_connect_stats_scan_failure() { |
| // Note: This mock is not completely correct (e.g. we would not expect time stats for |
| // later steps to be filled out if connect fails at scan), but for our testing |
| // purpose, it's sufficient. The same applies for other connect stats failure |
| // test cases. |
| let connect_stats = ConnectStats { |
| result: ConnectFailure::ScanFailure(fidl_mlme::ScanResultCodes::InvalidArgs).into(), |
| scan_end_stats: Some(ScanEndStats { |
| scan_end_at: now(), |
| result: ScanResult::Failed(fidl_mlme::ScanResultCodes::InvalidArgs), |
| bss_count: 1, |
| }), |
| ..fake_connect_stats() |
| }; |
| |
| let expected_metrics_subset = hashset! { |
| metrics::CONNECTION_RESULT_METRIC_ID, |
| metrics::CONNECTION_FAILURE_METRIC_ID, |
| metrics::SCAN_RESULT_METRIC_ID, |
| metrics::SCAN_FAILURE_METRIC_ID, |
| }; |
| // These metrics are only logged when connection attempt succeeded. |
| let unexpected_metrics = hashset! { |
| metrics::CONNECTION_ATTEMPTS_METRIC_ID, |
| metrics::CONNECTION_SUCCESS_WITH_ATTEMPTS_BREAKDOWN_METRIC_ID, |
| }; |
| test_metric_subset(&connect_stats, expected_metrics_subset, unexpected_metrics); |
| } |
| |
| #[test] |
| fn test_log_connect_stats_select_network_failure() { |
| let connect_stats = ConnectStats { |
| result: SelectNetworkFailure::NoCompatibleNetwork.into(), |
| ..fake_connect_stats() |
| }; |
| let expected_metrics_subset = hashset! { |
| metrics::CONNECTION_RESULT_METRIC_ID, |
| metrics::CONNECTION_FAILURE_METRIC_ID, |
| metrics::NETWORK_SELECTION_FAILURE_METRIC_ID, |
| }; |
| test_metric_subset(&connect_stats, expected_metrics_subset, hashset! {}); |
| } |
| |
| #[test] |
| fn test_log_connect_stats_auth_failure() { |
| let connect_stats = ConnectStats { |
| result: ConnectFailure::AuthenticationFailure( |
| fidl_mlme::AuthenticateResultCodes::Refused, |
| ) |
| .into(), |
| ..fake_connect_stats() |
| }; |
| let expected_metrics_subset = hashset! { |
| metrics::CONNECTION_RESULT_METRIC_ID, |
| metrics::CONNECTION_FAILURE_METRIC_ID, |
| metrics::AUTHENTICATION_FAILURE_METRIC_ID, |
| }; |
| test_metric_subset(&connect_stats, expected_metrics_subset, hashset! {}); |
| } |
| |
| #[test] |
| fn test_log_connect_stats_assoc_failure() { |
| let connect_stats = ConnectStats { |
| result: ConnectFailure::AssociationFailure(AssociationFailure { |
| bss_protection: BssProtection::Open, |
| code: fidl_mlme::AssociateResultCodes::RefusedReasonUnspecified, |
| }) |
| .into(), |
| ..fake_connect_stats() |
| }; |
| let expected_metrics_subset = hashset! { |
| metrics::CONNECTION_RESULT_METRIC_ID, |
| metrics::CONNECTION_FAILURE_METRIC_ID, |
| metrics::ASSOCIATION_FAILURE_METRIC_ID, |
| }; |
| test_metric_subset(&connect_stats, expected_metrics_subset, hashset! {}); |
| } |
| |
| #[test] |
| fn test_log_connect_stats_establish_rsna_failure() { |
| let connect_stats = ConnectStats { |
| result: EstablishRsnaFailure { |
| auth_method: None, |
| reason: EstablishRsnaFailureReason::OverallTimeout, |
| } |
| .into(), |
| ..fake_connect_stats() |
| }; |
| let expected_metrics_subset = hashset! { |
| metrics::CONNECTION_RESULT_METRIC_ID, |
| metrics::CONNECTION_FAILURE_METRIC_ID, |
| metrics::ESTABLISH_RSNA_FAILURE_METRIC_ID, |
| }; |
| test_metric_subset(&connect_stats, expected_metrics_subset, hashset! {}); |
| } |
| |
| #[test] |
| fn test_log_connection_ping() { |
| use metrics::ConnectionCountByDurationMetricDimensionConnectedTime::*; |
| |
| let (mut cobalt_sender, mut cobalt_receiver) = fake_cobalt_sender(); |
| let start = now(); |
| let ping = ConnectionPingInfo::first_connected(start); |
| log_connection_ping(&mut cobalt_sender, &ping); |
| assert_ping_metrics(&mut cobalt_receiver, &[Connected as u32]); |
| assert_variant!(cobalt_receiver.try_next(), Ok(Some(event)) => { |
| assert_eq!(event.metric_id, metrics::CONNECTION_COUNT_BY_DURATION_METRIC_ID); |
| assert_eq!(event.event_codes, vec![Connected as u32]) |
| }); |
| |
| let ping = ping.next_ping(start + 1.minute()); |
| log_connection_ping(&mut cobalt_sender, &ping); |
| assert_ping_metrics(&mut cobalt_receiver, &[Connected as u32, ConnectedOneMinute as u32]); |
| assert_variant!(cobalt_receiver.try_next(), Ok(Some(event)) => { |
| assert_eq!(event.metric_id, metrics::CONNECTION_COUNT_BY_DURATION_METRIC_ID); |
| assert_eq!(event.event_codes, vec![ConnectedOneMinute as u32]) |
| }); |
| |
| let ping = ping.next_ping(start + 3.minutes()); |
| log_connection_ping(&mut cobalt_sender, &ping); |
| assert_ping_metrics(&mut cobalt_receiver, &[Connected as u32, ConnectedOneMinute as u32]); |
| // check that CONNECTION_COUNT_BY_DURATION isn't logged since new milestone is not reached |
| assert_variant!(cobalt_receiver.try_next(), Ok(None) | Err(..)); |
| |
| let ping = ping.next_ping(start + 10.minutes()); |
| log_connection_ping(&mut cobalt_sender, &ping); |
| assert_ping_metrics( |
| &mut cobalt_receiver, |
| &[Connected as u32, ConnectedOneMinute as u32, ConnectedTenMinute as u32], |
| ); |
| assert_variant!(cobalt_receiver.try_next(), Ok(Some(event)) => { |
| assert_eq!(event.metric_id, metrics::CONNECTION_COUNT_BY_DURATION_METRIC_ID); |
| assert_eq!(event.event_codes, vec![ConnectedTenMinute as u32]) |
| }); |
| } |
| |
| fn assert_ping_metrics(cobalt_receiver: &mut mpsc::Receiver<CobaltEvent>, milestones: &[u32]) { |
| for milestone in milestones { |
| assert_variant!(cobalt_receiver.try_next(), Ok(Some(event)) => { |
| assert_eq!(event.metric_id, metrics::CONNECTION_UPTIME_PING_METRIC_ID); |
| assert_eq!(event.event_codes, vec![*milestone]); |
| }); |
| } |
| } |
| |
| #[test] |
| fn test_log_disconnect_initiated_from_mlme() { |
| let (mut cobalt_sender, mut cobalt_receiver) = fake_cobalt_sender(); |
| let inspect_tree = fake_inspect_tree(); |
| let disconnect_info = |
| DisconnectInfo { disconnect_source: DisconnectSource::Mlme, ..fake_disconnect_info() }; |
| log_disconnect(&mut cobalt_sender, inspect_tree.clone(), &disconnect_info); |
| |
| assert_variant!(cobalt_receiver.try_next(), Ok(Some(event)) => { |
| assert_eq!(event.metric_id, metrics::LOST_CONNECTION_COUNT_METRIC_ID); |
| }); |
| assert_variant!(cobalt_receiver.try_next(), Ok(Some(event)) => { |
| assert_eq!(event.metric_id, metrics::DISCONNECT_COUNT_METRIC_ID); |
| }); |
| assert_variant!(cobalt_receiver.try_next(), Ok(Some(event)) => { |
| assert_eq!(event.metric_id, metrics::DISCONNECT_COUNT_BREAKDOWN_METRIC_ID); |
| assert_eq!(event.event_codes, vec![ |
| metrics::DisconnectCountBreakdownMetricDimensionConnectedTime::LessThanOneMinute as u32, |
| metrics::DisconnectCountBreakdownMetricDimensionDisconnectSource::Mlme as u32, |
| metrics::DisconnectCountBreakdownMetricDimensionSnr::From1To10 as u32, |
| metrics::DisconnectCountBreakdownMetricDimensionRecentChannelSwitch::No as u32, |
| metrics::DisconnectCountBreakdownMetricDimensionChannelBand::Band2Dot4Ghz as u32 |
| ]); |
| }); |
| } |
| |
| #[test] |
| fn test_log_disconnect_initiated_from_user_request() { |
| let (mut cobalt_sender, mut cobalt_receiver) = fake_cobalt_sender(); |
| let inspect_tree = fake_inspect_tree(); |
| let disconnect_info = |
| DisconnectInfo { disconnect_source: DisconnectSource::User, ..fake_disconnect_info() }; |
| log_disconnect(&mut cobalt_sender, inspect_tree.clone(), &disconnect_info); |
| |
| assert_variant!(cobalt_receiver.try_next(), Ok(Some(event)) => { |
| assert_eq!(event.metric_id, metrics::DISCONNECT_COUNT_METRIC_ID); |
| }); |
| assert_variant!(cobalt_receiver.try_next(), Ok(Some(event)) => { |
| assert_eq!(event.metric_id, metrics::DISCONNECT_COUNT_BREAKDOWN_METRIC_ID); |
| }); |
| } |
| |
| #[test] |
| fn test_log_disconnect_initiated_from_ap() { |
| let (mut cobalt_sender, mut cobalt_receiver) = fake_cobalt_sender(); |
| let inspect_tree = fake_inspect_tree(); |
| let disconnect_info = |
| DisconnectInfo { disconnect_source: DisconnectSource::Ap, ..fake_disconnect_info() }; |
| log_disconnect(&mut cobalt_sender, inspect_tree.clone(), &disconnect_info); |
| |
| assert_variant!(cobalt_receiver.try_next(), Ok(Some(event)) => { |
| assert_eq!(event.metric_id, metrics::DISCONNECT_COUNT_METRIC_ID); |
| }); |
| assert_variant!(cobalt_receiver.try_next(), Ok(Some(event)) => { |
| assert_eq!(event.metric_id, metrics::DISCONNECT_COUNT_BREAKDOWN_METRIC_ID); |
| }); |
| } |
| |
| #[test] |
| fn test_inspect_log_connect_stats() { |
| let (mut cobalt_sender, _cobalt_receiver) = fake_cobalt_sender(); |
| let inspect_tree = fake_inspect_tree(); |
| |
| let connect_stats = fake_connect_stats(); |
| log_connect_stats(&mut cobalt_sender, inspect_tree.clone(), &connect_stats); |
| |
| assert_inspect_tree!(inspect_tree.inspector, root: contains { |
| client_stats: contains { |
| connect: { |
| "0": { |
| "@time": AnyProperty, |
| attempts: 1u64, |
| reconnect_info: { |
| gap_time: DURATION_SINCE_LAST_DISCONNECT.into_nanos(), |
| same_ssid: true, |
| }, |
| } |
| } |
| } |
| }); |
| } |
| |
| #[test] |
| fn test_inspect_log_disconnect_stats() { |
| let (mut cobalt_sender, _cobalt_receiver) = fake_cobalt_sender(); |
| let inspect_tree = fake_inspect_tree(); |
| |
| let disconnect_info = DisconnectInfo { |
| connected_duration: 30.seconds(), |
| bssid: [1u8; 6], |
| ssid: b"foo".to_vec(), |
| channel: Channel { primary: 1, cbw: Cbw::Cbw20 }, |
| last_rssi: -90, |
| last_snr: 1, |
| reason_code: fidl_mlme::ReasonCode::UnspecifiedReason.into_primitive(), |
| disconnect_source: DisconnectSource::Mlme, |
| time_since_channel_switch: Some(zx::Duration::from_nanos(1337i64)), |
| }; |
| log_disconnect(&mut cobalt_sender, inspect_tree.clone(), &disconnect_info); |
| |
| assert_inspect_tree!(inspect_tree.inspector, root: contains { |
| client_stats: contains { |
| disconnect: { |
| "0": { |
| "@time": AnyProperty, |
| connected_duration: 30.seconds().into_nanos(), |
| bssid: "01:01:01:01:01:01", |
| bssid_hash: AnyProperty, |
| ssid: "foo", |
| ssid_hash: AnyProperty, |
| channel: { |
| primary: 1u64, |
| cbw: "Cbw20", |
| secondary80: 0u64, |
| }, |
| last_rssi: -90i64, |
| last_snr: 1i64, |
| reason_code: 1u64, |
| disconnect_source: "mlme", |
| time_since_channel_switch: 1337i64, |
| } |
| } |
| } |
| }); |
| } |
| |
| #[test] |
| fn test_inspect_log_scan_failures() { |
| let (mut cobalt_sender, _cobalt_receiver) = fake_cobalt_sender(); |
| let inspect_tree = fake_inspect_tree(); |
| |
| let now = now(); |
| let scan_stats = ScanStats { |
| scan_start_at: now - 3.seconds(), |
| scan_end_at: now, |
| scan_type: fidl_mlme::ScanTypes::Active, |
| scan_start_while_connected: true, |
| result: ScanResult::Failed(fidl_mlme::ScanResultCodes::InvalidArgs), |
| bss_count: 5, |
| }; |
| log_scan_stats(&mut cobalt_sender, inspect_tree.clone(), &scan_stats, true); |
| |
| assert_inspect_tree!(inspect_tree.inspector, root: contains { |
| client_stats: contains { |
| scan_failures: { |
| "0": { |
| "@time": AnyProperty, |
| } |
| } |
| } |
| }); |
| } |
| |
| fn test_metric_subset( |
| connect_stats: &ConnectStats, |
| mut expected_metrics_subset: HashSet<u32>, |
| unexpected_metrics: HashSet<u32>, |
| ) { |
| let (mut cobalt_sender, mut cobalt_receiver) = fake_cobalt_sender(); |
| let inspect_tree = fake_inspect_tree(); |
| log_connect_stats(&mut cobalt_sender, inspect_tree.clone(), connect_stats); |
| |
| while let Ok(Some(event)) = cobalt_receiver.try_next() { |
| assert!( |
| !unexpected_metrics.contains(&event.metric_id), |
| "unexpected event: {:?}", |
| event |
| ); |
| if expected_metrics_subset.contains(&event.metric_id) { |
| expected_metrics_subset.remove(&event.metric_id); |
| } |
| } |
| assert!( |
| expected_metrics_subset.is_empty(), |
| "some metrics not logged: {:?}", |
| expected_metrics_subset |
| ); |
| } |
| |
| fn now() -> zx::Time { |
| zx::Time::get_monotonic() |
| } |
| |
| fn fake_connect_stats() -> ConnectStats { |
| let now = now(); |
| ConnectStats { |
| connect_start_at: now, |
| connect_end_at: now, |
| scan_start_stats: Some(ScanStartStats { |
| scan_start_at: now, |
| scan_type: fidl_mlme::ScanTypes::Passive, |
| scan_start_while_connected: false, |
| }), |
| scan_end_stats: Some(ScanEndStats { |
| scan_end_at: now, |
| result: ScanResult::Success, |
| bss_count: 1, |
| }), |
| auth_start_at: Some(now), |
| auth_end_at: Some(now), |
| assoc_start_at: Some(now), |
| assoc_end_at: Some(now), |
| rsna_start_at: Some(now), |
| rsna_end_at: Some(now), |
| supplicant_error: None, |
| supplicant_progress: Some(SupplicantProgress { |
| pmksa_established: true, |
| ptksa_established: true, |
| gtksa_established: true, |
| esssa_established: true, |
| }), |
| num_rsna_key_frame_exchange_timeout: 0, |
| result: ConnectResult::Success, |
| candidate_network: Some(fake_bss!(Open)), |
| attempts: 1, |
| last_ten_failures: vec![], |
| previous_disconnect_info: Some(PreviousDisconnectInfo { |
| ssid: fake_bss!(Open).ssid, |
| disconnect_source: DisconnectSource::User, |
| disconnect_at: now - DURATION_SINCE_LAST_DISCONNECT, |
| }), |
| } |
| } |
| |
| fn fake_iface_stats(nth_req: u64) -> IfaceStats { |
| IfaceStats { |
| dispatcher_stats: DispatcherStats { |
| any_packet: fake_packet_counter(nth_req), |
| mgmt_frame: fake_packet_counter(nth_req), |
| ctrl_frame: fake_packet_counter(nth_req), |
| data_frame: fake_packet_counter(nth_req), |
| }, |
| mlme_stats: Some(Box::new(ClientMlmeStats(fidl_stats::ClientMlmeStats { |
| svc_msg: fake_packet_counter(nth_req), |
| data_frame: fake_packet_counter(nth_req), |
| mgmt_frame: fake_packet_counter(nth_req), |
| tx_frame: fake_packet_counter(nth_req), |
| rx_frame: fake_packet_counter(nth_req), |
| assoc_data_rssi: fake_rssi(nth_req), |
| beacon_rssi: fake_rssi(nth_req), |
| 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_packet_counter(nth_req: u64) -> PacketCounter { |
| PacketCounter { |
| in_: Counter { count: 1 * nth_req, name: "in".to_string() }, |
| out: Counter { count: 2 * nth_req, name: "out".to_string() }, |
| drop: Counter { count: 3 * nth_req, name: "drop".to_string() }, |
| in_bytes: Counter { count: 4 * nth_req, name: "in_bytes".to_string() }, |
| out_bytes: Counter { count: 5 * nth_req, name: "out_bytes".to_string() }, |
| drop_bytes: Counter { count: 6 * nth_req, name: "drop_bytes".to_string() }, |
| } |
| } |
| |
| fn fake_rssi(nth_req: u64) -> fidl_stats::RssiStats { |
| fidl_stats::RssiStats { hist: vec![nth_req] } |
| } |
| |
| fn fake_antenna_id() -> Option<Box<fidl_stats::AntennaId>> { |
| Some(Box::new(fidl_stats::AntennaId { freq: fidl_stats::AntennaFreq::Antenna5G, index: 0 })) |
| } |
| |
| fn fake_noise_floor_histograms() -> Vec<fidl_stats::NoiseFloorHistogram> { |
| vec![fidl_stats::NoiseFloorHistogram { |
| hist_scope: fidl_stats::HistScope::PerAntenna, |
| antenna_id: fake_antenna_id(), |
| // Noise floor bucket_index 165 indicates -90 dBm. |
| noise_floor_samples: vec![fidl_stats::HistBucket { |
| bucket_index: 165, |
| num_samples: 10, |
| }], |
| invalid_samples: 0, |
| }] |
| } |
| |
| fn fake_rssi_histograms() -> Vec<fidl_stats::RssiHistogram> { |
| vec![fidl_stats::RssiHistogram { |
| hist_scope: fidl_stats::HistScope::PerAntenna, |
| antenna_id: fake_antenna_id(), |
| // RSSI bucket_index 225 indicates -30 dBm. |
| rssi_samples: vec![fidl_stats::HistBucket { bucket_index: 225, num_samples: 10 }], |
| invalid_samples: 0, |
| }] |
| } |
| |
| fn fake_rx_rate_index_histograms() -> Vec<fidl_stats::RxRateIndexHistogram> { |
| vec![fidl_stats::RxRateIndexHistogram { |
| hist_scope: fidl_stats::HistScope::PerAntenna, |
| antenna_id: fake_antenna_id(), |
| // Rate bucket_index 74 indicates HT BW40 MCS 14 SGI, which is 802.11n 270 Mb/s. |
| // Rate bucket_index 75 indicates HT BW40 MCS 15 SGI, which is 802.11n 300 Mb/s. |
| rx_rate_index_samples: vec![ |
| fidl_stats::HistBucket { bucket_index: 74, num_samples: 5 }, |
| fidl_stats::HistBucket { bucket_index: 75, num_samples: 5 }, |
| ], |
| invalid_samples: 0, |
| }] |
| } |
| |
| fn fake_snr_histograms() -> Vec<fidl_stats::SnrHistogram> { |
| vec![fidl_stats::SnrHistogram { |
| hist_scope: fidl_stats::HistScope::PerAntenna, |
| antenna_id: fake_antenna_id(), |
| // Signal to noise ratio bucket_index 60 indicates 60 dB. |
| snr_samples: vec![fidl_stats::HistBucket { bucket_index: 60, num_samples: 10 }], |
| invalid_samples: 0, |
| }] |
| } |
| |
| fn fake_disconnect_info() -> DisconnectInfo { |
| DisconnectInfo { |
| connected_duration: 30.seconds(), |
| bssid: [1u8; 6], |
| ssid: b"foo".to_vec(), |
| channel: Channel { primary: 1, cbw: Cbw::Cbw20 }, |
| last_rssi: -90, |
| last_snr: 1, |
| reason_code: fidl_mlme::ReasonCode::UnspecifiedReason.into_primitive(), |
| disconnect_source: DisconnectSource::Mlme, |
| time_since_channel_switch: None, |
| } |
| } |
| |
| fn fake_iface_map() -> (IfaceMap, impl Stream<Item = StatsRequest>) { |
| let (ifaces_map, _watcher) = IfaceMap::new(); |
| let (iface_device, stats_requests) = fake_iface_device(); |
| ifaces_map.insert(IFACE_ID, iface_device); |
| (ifaces_map, stats_requests) |
| } |
| |
| fn fake_iface_device() -> (IfaceDevice, impl Stream<Item = StatsRequest>) { |
| let (sme_sender, _sme_receiver) = mpsc::unbounded(); |
| let (stats_sched, stats_requests) = stats_scheduler::create_scheduler(); |
| let (proxy, _server) = create_proxy::<MlmeMarker>().expect("Error creating proxy"); |
| let mlme_query = MlmeQueryProxy::new(proxy); |
| let (shutdown_sender, _) = mpsc::channel(1); |
| let device_info = fake_device_info(); |
| let iface_device = IfaceDevice { |
| phy_ownership: device::PhyOwnership { phy_id: 0, phy_assigned_id: 0 }, |
| sme_server: device::SmeServer::Client(sme_sender), |
| stats_sched, |
| mlme_query, |
| device_info, |
| shutdown_sender, |
| }; |
| (iface_device, stats_requests) |
| } |
| |
| fn fake_device_info() -> fidl_mlme::DeviceInfo { |
| fidl_mlme::DeviceInfo { |
| role: fidl_mlme::MacRole::Client, |
| bands: vec![], |
| mac_addr: [0xAC; 6], |
| driver_features: vec![], |
| qos_capable: false, |
| } |
| } |
| |
| fn fake_cobalt_sender() -> (CobaltSender, mpsc::Receiver<CobaltEvent>) { |
| const BUFFER_SIZE: usize = 100; |
| let (sender, receiver) = mpsc::channel(BUFFER_SIZE); |
| (CobaltSender::new(sender), receiver) |
| } |
| |
| fn fake_inspect_tree() -> Arc<inspect::WlanstackTree> { |
| let inspector = Inspector::new(); |
| Arc::new(inspect::WlanstackTree::new(inspector)) |
| } |
| } |