1mod convert;
6mod windowed_stats;
7
8use crate::client;
9use crate::client::roaming::lib::{PolicyRoamRequest, RoamReason};
10use crate::mode_management::{Defect, IfaceFailure};
11use crate::telemetry::windowed_stats::WindowedStats;
12use crate::util::historical_list::{HistoricalList, Timestamped};
13use crate::util::pseudo_energy::{EwmaSignalData, RssiVelocity};
14use anyhow::{Context, Error, format_err};
15use cobalt_client::traits::AsEventCode;
16use fidl_fuchsia_metrics::{MetricEvent, MetricEventPayload};
17use fidl_fuchsia_wlan_common as fidl_common;
18use fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211;
19use fidl_fuchsia_wlan_internal as fidl_internal;
20use fidl_fuchsia_wlan_sme as fidl_sme;
21use fuchsia_async::{self as fasync, TimeoutExt};
22use fuchsia_inspect::{
23 ArrayProperty, InspectType, Inspector, LazyNode, Node as InspectNode, NumericProperty,
24 Property, UintProperty,
25};
26use fuchsia_inspect_contrib::inspectable::{InspectableBool, InspectableU64};
27use fuchsia_inspect_contrib::log::{InspectBytes, InspectList};
28use fuchsia_inspect_contrib::nodes::BoundedListNode;
29use fuchsia_inspect_contrib::{inspect_insert, inspect_log, make_inspect_loggable};
30use fuchsia_sync::Mutex;
31use futures::channel::{mpsc, oneshot};
32use futures::{Future, FutureExt, StreamExt, select};
33use log::{error, info, warn};
34use num_traits::SaturatingAdd;
35use static_assertions::const_assert_eq;
36use std::cmp::{Reverse, max, min};
37use std::collections::{HashMap, HashSet};
38use std::ops::Add;
39use std::sync::atomic::{AtomicBool, Ordering};
40use std::sync::{Arc, Once};
41use wlan_common::channel::{Bandwidth, Channel};
42use wlan_metrics_registry as metrics;
43use wlan_telemetry::{ThrottledErrorLogger, TimeoutSource};
44
45const GET_IFACE_STATS_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(5);
47
48pub const METRICS_SHORT_CONNECT_DURATION: zx::MonotonicDuration =
50 zx::MonotonicDuration::from_seconds(90);
51pub const AVERAGE_SCORE_DELTA_MINIMUM_DURATION: zx::MonotonicDuration =
53 zx::MonotonicDuration::from_seconds(30);
54pub const COBALT_REASON_CODE_MAX: u16 = 1000;
56pub const MINUTES_BETWEEN_COBALT_SYSLOG_WARNINGS: i64 = 60;
58pub const EWMA_SMOOTHING_FACTOR_FOR_METRICS: usize = 10;
61
62#[derive(Clone, Debug, PartialEq)]
63pub struct TimestampedConnectionScore {
65 pub score: u8,
66 pub time: fasync::MonotonicInstant,
67}
68impl TimestampedConnectionScore {
69 pub fn new(score: u8, time: fasync::MonotonicInstant) -> Self {
70 Self { score, time }
71 }
72}
73impl Timestamped for TimestampedConnectionScore {
74 fn time(&self) -> fasync::MonotonicInstant {
75 self.time
76 }
77}
78
79#[derive(Clone)]
80#[cfg_attr(test, derive(Debug))]
81pub struct TelemetrySender {
82 sender: Arc<Mutex<mpsc::Sender<TelemetryEvent>>>,
83 sender_is_blocked: Arc<AtomicBool>,
84}
85
86impl TelemetrySender {
87 pub fn new(sender: mpsc::Sender<TelemetryEvent>) -> Self {
88 Self {
89 sender: Arc::new(Mutex::new(sender)),
90 sender_is_blocked: Arc::new(AtomicBool::new(false)),
91 }
92 }
93
94 pub fn send(&self, event: TelemetryEvent) {
96 match self.sender.lock().try_send(event) {
97 Ok(_) => {
98 if self
100 .sender_is_blocked
101 .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
102 .is_ok()
103 {
104 info!("TelemetrySender recovered and resumed sending");
105 }
106 }
107 Err(_) => {
108 if self
110 .sender_is_blocked
111 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
112 .is_ok()
113 {
114 warn!(
115 "TelemetrySender dropped a msg: either buffer is full or no receiver is waiting"
116 );
117 }
118 }
119 }
120 }
121}
122
123#[derive(Clone, Debug, PartialEq)]
124pub struct DisconnectInfo {
125 pub iface_id: u16,
126 pub connected_duration: zx::MonotonicDuration,
127 pub is_sme_reconnecting: bool,
128 pub disconnect_source: fidl_sme::DisconnectSource,
129 pub previous_connect_reason: client::types::ConnectReason,
130 pub ap_state: client::types::ApState,
131 pub signals: HistoricalList<client::types::TimestampedSignal>,
132}
133
134pub trait DisconnectSourceExt {
135 fn inspect_string(&self) -> String;
136 fn flattened_reason_code(&self) -> u32;
137 fn cobalt_reason_code(&self) -> u16;
138 fn locally_initiated(&self) -> bool;
139 fn has_roaming_cause(&self) -> bool;
140}
141
142impl DisconnectSourceExt for fidl_sme::DisconnectSource {
143 fn inspect_string(&self) -> String {
144 match self {
145 fidl_sme::DisconnectSource::User(reason) => {
146 format!("source: user, reason: {reason:?}")
147 }
148 fidl_sme::DisconnectSource::Ap(cause) => format!(
149 "source: ap, reason: {:?}, mlme_event_name: {:?}",
150 cause.reason_code, cause.mlme_event_name
151 ),
152 fidl_sme::DisconnectSource::Mlme(cause) => format!(
153 "source: mlme, reason: {:?}, mlme_event_name: {:?}",
154 cause.reason_code, cause.mlme_event_name
155 ),
156 }
157 }
158
159 fn flattened_reason_code(&self) -> u32 {
164 match self {
165 fidl_sme::DisconnectSource::Ap(cause) => cause.reason_code.into_primitive() as u32,
166 fidl_sme::DisconnectSource::User(reason) => (1u32 << 16) + *reason as u32,
167 fidl_sme::DisconnectSource::Mlme(cause) => {
168 (1u32 << 17) + (cause.reason_code.into_primitive() as u32)
169 }
170 }
171 }
172
173 fn cobalt_reason_code(&self) -> u16 {
174 match self {
175 fidl_sme::DisconnectSource::Ap(cause) => {
177 std::cmp::min(cause.reason_code.into_primitive(), COBALT_REASON_CODE_MAX)
178 }
179 fidl_sme::DisconnectSource::User(reason) => {
180 std::cmp::min(*reason as u16, COBALT_REASON_CODE_MAX)
181 }
182 fidl_sme::DisconnectSource::Mlme(cause) => {
183 std::cmp::min(cause.reason_code.into_primitive(), COBALT_REASON_CODE_MAX)
184 }
185 }
186 }
187
188 fn locally_initiated(&self) -> bool {
189 match self {
190 fidl_sme::DisconnectSource::Ap(..) => false,
191 fidl_sme::DisconnectSource::Mlme(..) | fidl_sme::DisconnectSource::User(..) => true,
192 }
193 }
194
195 fn has_roaming_cause(&self) -> bool {
196 match self {
197 fidl_sme::DisconnectSource::User(_) => false,
198 fidl_sme::DisconnectSource::Ap(cause) | fidl_sme::DisconnectSource::Mlme(cause) => {
199 matches!(
200 cause.mlme_event_name,
201 fidl_sme::DisconnectMlmeEventName::RoamStartIndication
202 | fidl_sme::DisconnectMlmeEventName::RoamResultIndication
203 | fidl_sme::DisconnectMlmeEventName::RoamRequest
204 | fidl_sme::DisconnectMlmeEventName::RoamConfirmation
205 )
206 }
207 }
208 }
209}
210
211#[derive(Debug, PartialEq)]
212pub struct ScanEventInspectData {
213 pub unknown_protection_ies: Vec<String>,
214}
215
216impl Default for ScanEventInspectData {
217 fn default() -> Self {
218 Self::new()
219 }
220}
221
222impl ScanEventInspectData {
223 pub fn new() -> Self {
224 Self { unknown_protection_ies: vec![] }
225 }
226}
227
228#[cfg_attr(test, derive(Debug))]
229pub enum TelemetryEvent {
230 QueryStatus {
232 sender: oneshot::Sender<QueryStatusResult>,
233 },
234 StartEstablishConnection {
236 reset_start_time: bool,
241 },
242 ClearEstablishConnectionStartTime,
244 ActiveScanRequested {
246 num_ssids_requested: usize,
247 },
248 ActiveScanRequestedViaApi {
250 num_ssids_requested: usize,
251 },
252 NetworkSelectionDecision {
254 network_selection_type: NetworkSelectionType,
257 num_candidates: Result<usize, ()>,
264 selected_count: usize,
268 },
269 ConnectResult {
274 iface_id: u16,
275 policy_connect_reason: Option<client::types::ConnectReason>,
276 result: fidl_sme::ConnectResult,
277 multiple_bss_candidates: bool,
278 ap_state: client::types::ApState,
279 network_is_likely_hidden: bool,
280 },
281 PolicyInitiatedRoamResult {
285 iface_id: u16,
286 result: fidl_sme::RoamResult,
287 updated_ap_state: client::types::ApState,
288 original_ap_state: Box<client::types::ApState>,
289 request: Box<PolicyRoamRequest>,
290 request_time: fasync::MonotonicInstant,
291 result_time: fasync::MonotonicInstant,
292 },
293 Disconnected {
298 track_subsequent_downtime: bool,
300 info: Option<DisconnectInfo>,
301 },
302 OnSignalReport {
303 ind: fidl_internal::SignalReportIndication,
304 },
305 OnSignalVelocityUpdate {
306 rssi_velocity: f64,
307 },
308 OnChannelSwitched {
309 info: fidl_internal::ChannelSwitchInfo,
310 },
311 PolicyRoamScan {
314 reasons: Vec<RoamReason>,
315 },
316 PolicyRoamAttempt {
318 request: PolicyRoamRequest,
319 connected_duration: zx::MonotonicDuration,
320 },
321 WouldRoamConnect,
325 SavedNetworkCount {
328 saved_network_count: usize,
329 config_count_per_saved_network: Vec<usize>,
330 },
331 NetworkSelectionScanInterval {
333 time_since_last_scan: zx::MonotonicDuration,
334 },
335 ConnectionSelectionScanResults {
337 saved_network_count: usize,
338 bss_count_per_saved_network: Vec<usize>,
339 saved_network_count_found_by_active_scan: usize,
340 },
341 PostConnectionSignals {
342 connect_time: fasync::MonotonicInstant,
343 signal_at_connect: client::types::Signal,
344 signals: HistoricalList<client::types::TimestampedSignal>,
345 },
346 StartClientConnectionsRequest,
348 StopClientConnectionsRequest,
350 StopAp {
352 enabled_duration: zx::MonotonicDuration,
353 },
354 IfaceCreationResult {
356 role: fidl_common::WlanMacRole,
357 result: Result<u16, ()>,
358 },
359 IfaceDestructionResult {
361 role: fidl_common::WlanMacRole,
362 result: Result<u16, ()>,
363 },
364 StartApResult(Result<(), ()>),
366 ScanRequestFulfillmentTime {
368 duration: zx::MonotonicDuration,
369 reason: client::scan::ScanReason,
370 },
371 ScanQueueStatistics {
373 fulfilled_requests: usize,
374 remaining_requests: usize,
375 },
376 BssSelectionResult {
378 reason: client::types::ConnectReason,
379 scored_candidates: Vec<(client::types::ScannedCandidate, i16)>,
380 selected_candidate: Option<(client::types::ScannedCandidate, i16)>,
381 },
382 ScanEvent {
383 inspect_data: ScanEventInspectData,
384 scan_defects: Vec<ScanIssue>,
385 },
386 LongDurationSignals {
387 signals: Vec<client::types::TimestampedSignal>,
388 },
389 RecoveryEvent {
392 reason: RecoveryReason,
393 },
394 SmeTimeout {
395 source: TimeoutSource,
396 },
397}
398
399#[derive(Clone, Debug)]
400pub struct QueryStatusResult {
401 connection_state: ConnectionStateInfo,
402}
403
404#[derive(Clone, Debug)]
405pub enum ConnectionStateInfo {
406 Idle,
407 Disconnected,
408 Connected {
409 iface_id: u16,
410 ap_state: Box<client::types::ApState>,
411 telemetry_proxy: Option<fidl_fuchsia_wlan_sme::TelemetryProxy>,
412 },
413}
414
415#[derive(Clone, Debug, PartialEq)]
416pub enum NetworkSelectionType {
417 Undirected,
419 Directed,
421}
422
423#[derive(Debug, PartialEq)]
424pub enum ScanIssue {
425 ScanFailure,
426 AbortedScan,
427 EmptyScanResults,
428}
429
430impl ScanIssue {
431 fn as_metric_id(&self) -> u32 {
432 match self {
433 ScanIssue::ScanFailure => metrics::CLIENT_SCAN_FAILURE_METRIC_ID,
434 ScanIssue::AbortedScan => metrics::ABORTED_SCAN_METRIC_ID,
435 ScanIssue::EmptyScanResults => metrics::EMPTY_SCAN_RESULTS_METRIC_ID,
436 }
437 }
438}
439
440pub type ClientRecoveryMechanism = metrics::ConnectivityWlanMetricDimensionClientRecoveryMechanism;
441pub type ApRecoveryMechanism = metrics::ConnectivityWlanMetricDimensionApRecoveryMechanism;
442pub type TimeoutRecoveryMechanism =
443 metrics::ConnectivityWlanMetricDimensionTimeoutRecoveryMechanism;
444
445#[derive(Copy, Clone, Debug, PartialEq)]
446pub enum PhyRecoveryMechanism {
447 PhyReset = 0,
448}
449
450#[derive(Copy, Clone, Debug, PartialEq)]
451pub enum RecoveryReason {
452 CreateIfaceFailure(PhyRecoveryMechanism),
453 DestroyIfaceFailure(PhyRecoveryMechanism),
454 Timeout(TimeoutRecoveryMechanism),
455 ConnectFailure(ClientRecoveryMechanism),
456 StartApFailure(ApRecoveryMechanism),
457 ScanFailure(ClientRecoveryMechanism),
458 ScanCancellation(ClientRecoveryMechanism),
459 ScanResultsEmpty(ClientRecoveryMechanism),
460}
461
462struct RecoveryRecord {
463 scan_failure: Option<RecoveryReason>,
464 scan_cancellation: Option<RecoveryReason>,
465 scan_results_empty: Option<RecoveryReason>,
466 connect_failure: Option<RecoveryReason>,
467 start_ap_failure: Option<RecoveryReason>,
468 create_iface_failure: Option<RecoveryReason>,
469 destroy_iface_failure: Option<RecoveryReason>,
470 timeout: Option<RecoveryReason>,
471}
472
473impl RecoveryRecord {
474 fn new() -> Self {
475 RecoveryRecord {
476 scan_failure: None,
477 scan_cancellation: None,
478 scan_results_empty: None,
479 connect_failure: None,
480 start_ap_failure: None,
481 create_iface_failure: None,
482 destroy_iface_failure: None,
483 timeout: None,
484 }
485 }
486
487 fn record_recovery_attempt(&mut self, reason: RecoveryReason) {
488 match reason {
489 RecoveryReason::ScanFailure(_) => self.scan_failure = Some(reason),
490 RecoveryReason::ScanCancellation(_) => self.scan_cancellation = Some(reason),
491 RecoveryReason::ScanResultsEmpty(_) => self.scan_results_empty = Some(reason),
492 RecoveryReason::ConnectFailure(_) => self.connect_failure = Some(reason),
493 RecoveryReason::StartApFailure(_) => self.start_ap_failure = Some(reason),
494 RecoveryReason::CreateIfaceFailure(_) => self.create_iface_failure = Some(reason),
495 RecoveryReason::DestroyIfaceFailure(_) => self.destroy_iface_failure = Some(reason),
496 RecoveryReason::Timeout(_) => self.timeout = Some(reason),
497 }
498 }
499}
500
501pub type RecoveryOutcome = metrics::ConnectivityWlanMetricDimensionResult;
502
503const TELEMETRY_EVENT_BUFFER_SIZE: usize = 100;
506const TELEMETRY_QUERY_INTERVAL: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(15);
508
509pub fn get_telemetry_config() -> wlan_telemetry::TelemetryConfig {
510 wlan_telemetry::TelemetryConfig {
511 enable_connect_disconnect: true,
512 enable_iface_logger: true,
513 enable_sme_timeout_logger: true,
514 enable_toggle_logger: true,
515 enable_recovery_logger: true,
516 enable_client_iface_counters_logger: true,
517 device_mobility: wlan_telemetry::DeviceMobility::Stationary,
518 ..Default::default()
519 }
520}
521
522pub fn get_cobalt_allowlist() -> wlan_telemetry::CobaltAllowlist {
523 wlan_telemetry::CobaltAllowlist::Only(HashSet::from([
524 metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
525 metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
526 metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID,
527 metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID,
528 metrics::CONNECTED_NETWORK_SECURITY_TYPE_METRIC_ID,
529 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_APSD_METRIC_ID,
530 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_LINK_MEASUREMENT_METRIC_ID,
531 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_NEIGHBOR_REPORT_METRIC_ID,
532 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_FT_METRIC_ID,
533 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_BSS_TRANSITION_MANAGEMENT_METRIC_ID,
534 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
535 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
536 metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID,
537 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_IS_OWE_TRANSITION_METRIC_ID,
538 metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID,
539 metrics::CONNECTED_DURATION_ON_DISCONNECT_METRIC_ID,
540 metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID,
541 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID,
542 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
543 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
544 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_RSSI_BUCKET_METRIC_ID,
545 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SNR_BUCKET_METRIC_ID,
546 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_IS_OWE_TRANSITION_METRIC_ID,
547 metrics::CLIENT_CONNECTION_ENABLED_OCCURRENCE_METRIC_ID,
548 metrics::CLIENT_CONNECTIONS_STOP_AND_START_METRIC_ID,
549 metrics::CLIENT_CONNECTION_ENABLED_DURATION_METRIC_ID,
550 metrics::RECOVERY_OCCURRENCE_2_METRIC_ID,
551 metrics::GET_IFACE_STATS_FAILURE_METRIC_ID,
552 metrics::GET_IFACE_STATS_TIMEOUT_METRIC_ID,
553 metrics::GET_IFACE_STATS_ERROR_IN_RESPONSE_METRIC_ID,
554 metrics::BAD_RX_RATE_METRIC_ID,
555 metrics::RX_UNICAST_PACKETS_METRIC_ID,
556 metrics::BAD_TX_RATE_METRIC_ID,
557 metrics::INTERFACE_CREATION_FAILURE_METRIC_ID,
558 metrics::INTERFACE_DESTRUCTION_FAILURE_METRIC_ID,
559 metrics::SME_OPERATION_TIMEOUT_METRIC_ID,
560 metrics::SME_OPERATION_TIMEOUT_2_METRIC_ID,
561 ]))
562}
563
564pub fn serve_telemetry(
570 monitor_svc_proxy: fidl_fuchsia_wlan_device_service::DeviceMonitorProxy,
571 cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
572 inspect_node: InspectNode,
573 external_inspect_node: InspectNode,
574 defect_sender: mpsc::Sender<Defect>,
575) -> (TelemetrySender, impl Future<Output = ()>) {
576 let (sender, mut receiver) = mpsc::channel::<TelemetryEvent>(TELEMETRY_EVENT_BUFFER_SIZE);
577 let sender = TelemetrySender::new(sender);
578 let cloned_sender = sender.clone();
579 let fut = async move {
580 let mut report_interval_stream = fasync::Interval::new(TELEMETRY_QUERY_INTERVAL);
581 const ONE_MINUTE: zx::MonotonicDuration = zx::MonotonicDuration::from_minutes(1);
582 const_assert_eq!(ONE_MINUTE.into_nanos() % TELEMETRY_QUERY_INTERVAL.into_nanos(), 0);
583 const INTERVAL_TICKS_PER_MINUTE: u64 =
584 (ONE_MINUTE.into_nanos() / TELEMETRY_QUERY_INTERVAL.into_nanos()) as u64;
585 const INTERVAL_TICKS_PER_HR: u64 = INTERVAL_TICKS_PER_MINUTE * 60;
586 const INTERVAL_TICKS_PER_DAY: u64 = INTERVAL_TICKS_PER_HR * 24;
587 let mut interval_tick = 0u64;
588 let lib_telemetry_node = inspect_node.create_child("lib_telemetry");
589 let (lib_telemetry_sender, lib_telemetry_fut) = wlan_telemetry::serve_telemetry(
590 cobalt_proxy.clone(),
591 monitor_svc_proxy.clone(),
592 lib_telemetry_node,
593 "root/client_stats/lib_telemetry",
594 get_telemetry_config(),
595 get_cobalt_allowlist(),
596 );
597 let mut lib_telemetry_fut = Box::pin(lib_telemetry_fut).fuse();
598 let mut telemetry = Telemetry::new(
599 cloned_sender,
600 monitor_svc_proxy,
601 cobalt_proxy,
602 inspect_node,
603 external_inspect_node,
604 defect_sender.clone(),
605 );
606 loop {
607 select! {
608 res = lib_telemetry_fut => {
609 warn!("wlan_telemetry exited unexpectedly: {:?}", res);
610 }
611 event = receiver.next() => {
612 if let Some(event) = event {
613 if let Some(wlan_event) = convert::convert_to_wlan_telemetry_event(&event) {
614 lib_telemetry_sender.send(wlan_event);
615 }
616 telemetry.handle_telemetry_event(event).await;
617 }
618 }
619 _ = report_interval_stream.next() => {
620 telemetry.handle_periodic_telemetry().await;
621
622 interval_tick += 1;
623 if interval_tick.is_multiple_of(INTERVAL_TICKS_PER_DAY) {
624 telemetry.log_daily_cobalt_metrics().await;
625 }
626
627 if interval_tick.is_multiple_of(INTERVAL_TICKS_PER_HR) {
633 telemetry.signal_hr_passed().await;
634 }
635 }
636 }
637 }
638 };
639 (sender, fut)
640}
641
642#[derive(Debug)]
643enum ConnectionState {
644 Idle(IdleState),
646 Connected(Box<ConnectedState>),
647 Disconnected(Box<DisconnectedState>),
648}
649
650#[derive(Debug)]
651struct IdleState {
652 connect_start_time: Option<fasync::MonotonicInstant>,
653}
654
655#[derive(Debug)]
656struct ConnectedState {
657 iface_id: u16,
658 new_connect_start_time: Option<fasync::MonotonicInstant>,
661 prev_connection_stats: Option<fidl_fuchsia_wlan_stats::ConnectionStats>,
662 multiple_bss_candidates: bool,
663 ap_state: Box<client::types::ApState>,
664 network_is_likely_hidden: bool,
665
666 last_signal_report: fasync::MonotonicInstant,
667 num_consecutive_get_counter_stats_failures: InspectableU64,
668 is_driver_unresponsive: InspectableBool,
669
670 telemetry_proxy: Option<fidl_fuchsia_wlan_sme::TelemetryProxy>,
671}
672
673#[derive(Debug)]
674pub struct DisconnectedState {
675 disconnected_since: fasync::MonotonicInstant,
676 disconnect_info: Option<DisconnectInfo>,
677 connect_start_time: Option<fasync::MonotonicInstant>,
678 latest_no_saved_neighbor_time: Option<fasync::MonotonicInstant>,
682 accounted_no_saved_neighbor_duration: zx::MonotonicDuration,
683}
684
685fn inspect_create_counters(
686 inspect_node: &InspectNode,
687 child_name: &str,
688 counters: Arc<Mutex<WindowedStats<StatCounters>>>,
689) -> LazyNode {
690 inspect_node.create_lazy_child(child_name, move || {
691 let counters = Arc::clone(&counters);
692 async move {
693 let inspector = Inspector::default();
694 {
695 let counters_mutex_guard = counters.lock();
696 let counters = counters_mutex_guard.windowed_stat(None);
697 inspect_insert!(inspector.root(), {
698 total_duration: counters.total_duration.into_nanos(),
699 connected_duration: counters.connected_duration.into_nanos(),
700 downtime_duration: counters.downtime_duration.into_nanos(),
701 downtime_no_saved_neighbor_duration: counters.downtime_no_saved_neighbor_duration.into_nanos(),
702 connect_attempts_count: counters.connect_attempts_count,
703 connect_successful_count: counters.connect_successful_count,
704 disconnect_count: counters.disconnect_count,
705 total_non_roam_disconnect_count: counters.total_non_roam_disconnect_count,
706 total_roam_disconnect_count: counters.total_roam_disconnect_count,
707 policy_roam_attempts_count: counters.policy_roam_attempts_count,
708 policy_roam_successful_count: counters.policy_roam_successful_count,
709 policy_roam_disconnects_count: counters.policy_roam_disconnects_count,
710 tx_high_packet_drop_duration: counters.tx_high_packet_drop_duration.into_nanos(),
711 rx_high_packet_drop_duration: counters.rx_high_packet_drop_duration.into_nanos(),
712 tx_very_high_packet_drop_duration: counters.tx_very_high_packet_drop_duration.into_nanos(),
713 rx_very_high_packet_drop_duration: counters.rx_very_high_packet_drop_duration.into_nanos(),
714 no_rx_duration: counters.no_rx_duration.into_nanos(),
715 });
716 }
717 Ok(inspector)
718 }
719 .boxed()
720 })
721}
722
723fn inspect_record_connection_status(inspect_node: &InspectNode, telemetry_sender: TelemetrySender) {
724 inspect_node.record_lazy_child("connection_status", move|| {
725 let telemetry_sender = telemetry_sender.clone();
726 async move {
727 let inspector = Inspector::default();
728 let (sender, receiver) = oneshot::channel();
729 telemetry_sender.send(TelemetryEvent::QueryStatus { sender });
730 let info = match receiver.await {
731 Ok(result) => result.connection_state,
732 Err(e) => {
733 warn!("Unable to query data for Inspect connection status node: {}", e);
734 return Ok(inspector)
735 }
736 };
737
738 inspector.root().record_string("status_string", match &info {
739 ConnectionStateInfo::Idle => "idle".to_string(),
740 ConnectionStateInfo::Disconnected => "disconnected".to_string(),
741 ConnectionStateInfo::Connected { .. } => "connected".to_string(),
742 });
743 if let ConnectionStateInfo::Connected { ap_state, .. } = info {
744 inspect_insert!(inspector.root(), connected_network: {
745 rssi_dbm: ap_state.tracked.signal.rssi_dbm,
746 snr_db: ap_state.tracked.signal.snr_db,
747 bssid: ap_state.original().bssid.to_string(),
748 ssid: ap_state.original().ssid.to_string(),
749 protection: format!("{:?}", ap_state.original().protection()),
750 channel: format!("{}", ap_state.original().channel),
751 ht_cap?: ap_state.original().raw_ht_cap().map(|cap| InspectBytes(cap.bytes)),
752 vht_cap?: ap_state.original().raw_vht_cap().map(|cap| InspectBytes(cap.bytes)),
753 wsc?: ap_state.original().probe_resp_wsc().as_ref().map(|wsc| make_inspect_loggable!(
754 manufacturer: String::from_utf8_lossy(&wsc.manufacturer[..]).to_string(),
755 model_name: String::from_utf8_lossy(&wsc.model_name[..]).to_string(),
756 model_number: String::from_utf8_lossy(&wsc.model_number[..]).to_string(),
757 )),
758 is_wmm_assoc: ap_state.original().find_wmm_param().is_some(),
759 wmm_param?: ap_state.original().find_wmm_param().map(InspectBytes),
760 });
761 }
762 Ok(inspector)
763 }
764 .boxed()
765 });
766}
767
768fn inspect_record_external_data(
769 external_inspect_node: &ExternalInspectNode,
770 telemetry_sender: TelemetrySender,
771 defect_sender: mpsc::Sender<Defect>,
772) {
773 external_inspect_node.node.record_lazy_child("connection_status", move || {
774 let telemetry_sender = telemetry_sender.clone();
775 let mut defect_sender = defect_sender.clone();
776 async move {
777 let inspector = Inspector::default();
778 let (sender, receiver) = oneshot::channel();
779 telemetry_sender.send(TelemetryEvent::QueryStatus { sender });
780 let info = match receiver.await {
781 Ok(result) => result.connection_state,
782 Err(e) => {
783 warn!("Unable to query data for Inspect external node: {}", e);
784 return Ok(inspector);
785 }
786 };
787
788 if let ConnectionStateInfo::Connected { ap_state, telemetry_proxy, iface_id } = info {
789 inspect_insert!(inspector.root(), connected_network: {
790 rssi_dbm: ap_state.tracked.signal.rssi_dbm,
791 snr_db: ap_state.tracked.signal.snr_db,
792 wsc?: ap_state.original().probe_resp_wsc().as_ref().map(|wsc| make_inspect_loggable!(
793 manufacturer: String::from_utf8_lossy(&wsc.manufacturer[..]).to_string(),
794 model_name: String::from_utf8_lossy(&wsc.model_name[..]).to_string(),
795 model_number: String::from_utf8_lossy(&wsc.model_number[..]).to_string(),
796 )),
797 });
798
799 if let Some(proxy) = telemetry_proxy {
800 match proxy.get_histogram_stats()
801 .on_timeout(GET_IFACE_STATS_TIMEOUT, || {
802 warn!("Timed out waiting for histogram stats");
803
804 if let Err(e) = defect_sender
805 .try_send(Defect::Iface(IfaceFailure::Timeout {
806 iface_id,
807 source: TimeoutSource::GetHistogramStats,
808 })) {
809 warn!("Failed to report histogram stats defect: {:?}", e)
810 }
811
812 Ok(Err(zx::Status::TIMED_OUT.into_raw()))
813 })
814 .await {
815 Ok(Ok(stats)) => {
816 let mut histograms = HistogramsNode::new(
817 inspector.root().create_child("histograms"),
818 );
819 if let Some(snr_histograms) = &stats.snr_histograms {
820 histograms.log_per_antenna_snr_histograms(&snr_histograms[..]);
821 }
822 if let Some(rx_rate_histograms) = &stats.rx_rate_index_histograms {
823 histograms.log_per_antenna_rx_rate_histograms(
824 &rx_rate_histograms[..],
825 );
826 }
827 if let Some(noise_floor_histograms) = &stats.noise_floor_histograms {
828 histograms.log_per_antenna_noise_floor_histograms(
829 &noise_floor_histograms[..],
830 );
831 }
832 if let Some(rssi_histograms) = &stats.rssi_histograms {
833 histograms.log_per_antenna_rssi_histograms(
834 &rssi_histograms[..],
835 );
836 }
837
838 inspector.root().record(histograms);
839 }
840 error => {
841 info!("Error reading histogram stats: {:?}", error);
842 },
843 }
844 }
845 }
846 Ok(inspector)
847 }
848 .boxed()
849 });
850}
851
852#[derive(Debug)]
853struct HistogramsNode {
854 node: InspectNode,
855 antenna_nodes: HashMap<fidl_fuchsia_wlan_stats::AntennaId, InspectNode>,
856}
857
858impl InspectType for HistogramsNode {
859 fn into_recorded(self) -> fuchsia_inspect::RecordedInspectType {
860 fuchsia_inspect::RecordedInspectType::Boxed(Box::new(self))
861 }
862}
863
864macro_rules! fn_log_per_antenna_histograms {
865 ($name:ident, $field:ident, $histogram_ty:ty, $sample:ident => $sample_index_expr:expr) => {
866 paste::paste! {
867 pub fn [<log_per_antenna_ $name _histograms>](
868 &mut self,
869 histograms: &[$histogram_ty],
870 ) {
871 for histogram in histograms {
872 let antenna_id = match &histogram.antenna_id {
874 Some(id) => **id,
875 None => continue,
876 };
877 let antenna_node = self.create_or_get_antenna_node(antenna_id);
878
879 let samples = &histogram.$field;
880 let samples: Vec<_> = samples.iter().filter(|s| s.num_samples > 0).collect();
882 let array_size = samples.len() * 2;
883 let histogram_prop_name = concat!(stringify!($name), "_histogram");
884 let histogram_prop =
885 antenna_node.create_int_array(histogram_prop_name, array_size);
886
887 static ONCE: Once = Once::new();
888 const INSPECT_ARRAY_SIZE_LIMIT: usize = 254;
889 if array_size > INSPECT_ARRAY_SIZE_LIMIT {
890 ONCE.call_once(|| {
891 warn!("{} array size {} > {}. Array may not show up in Inspect",
892 histogram_prop_name, array_size, INSPECT_ARRAY_SIZE_LIMIT);
893 })
894 }
895
896 for (i, sample) in samples.iter().enumerate() {
897 let $sample = sample;
898 histogram_prop.set(i * 2, $sample_index_expr);
899 histogram_prop.set(i * 2 + 1, $sample.num_samples as i64);
900 }
901
902 let invalid_samples_name = concat!(stringify!($name), "_invalid_samples");
903 let invalid_samples =
904 antenna_node.create_uint(invalid_samples_name, histogram.invalid_samples);
905
906 antenna_node.record(histogram_prop);
907 antenna_node.record(invalid_samples);
908 }
909 }
910 }
911 };
912}
913
914impl HistogramsNode {
915 pub fn new(node: InspectNode) -> Self {
916 Self { node, antenna_nodes: HashMap::new() }
917 }
918
919 fn_log_per_antenna_histograms!(snr, snr_samples, fidl_fuchsia_wlan_stats::SnrHistogram,
921 sample => sample.bucket_index as i64);
922 fn_log_per_antenna_histograms!(rx_rate, rx_rate_index_samples,
924 fidl_fuchsia_wlan_stats::RxRateIndexHistogram,
925 sample => sample.bucket_index as i64);
926 fn_log_per_antenna_histograms!(noise_floor, noise_floor_samples,
928 fidl_fuchsia_wlan_stats::NoiseFloorHistogram,
929 sample => sample.bucket_index as i64 - 255);
930 fn_log_per_antenna_histograms!(rssi, rssi_samples, fidl_fuchsia_wlan_stats::RssiHistogram,
932 sample => sample.bucket_index as i64 - 255);
933
934 fn create_or_get_antenna_node(
935 &mut self,
936 antenna_id: fidl_fuchsia_wlan_stats::AntennaId,
937 ) -> &mut InspectNode {
938 let histograms_node = &self.node;
939 self.antenna_nodes.entry(antenna_id).or_insert_with(|| {
940 let freq = match antenna_id.freq {
941 fidl_fuchsia_wlan_stats::AntennaFreq::Antenna2G => "2Ghz",
942 fidl_fuchsia_wlan_stats::AntennaFreq::Antenna5G => "5Ghz",
943 };
944 let node =
945 histograms_node.create_child(format!("antenna{}_{}", antenna_id.index, freq));
946 node.record_uint("antenna_index", antenna_id.index as u64);
947 node.record_string("antenna_freq", freq);
948 node
949 })
950 }
951}
952
953macro_rules! log_cobalt {
956 ($cobalt_proxy:expr, $method_name:ident, $metric_id:expr, $value:expr, $event_codes:expr $(,)?) => {{
957 let status = $cobalt_proxy.$method_name($metric_id, $value, $event_codes).await;
958 match status {
959 Ok(Ok(())) => Ok(()),
960 Ok(Err(e)) => Err(format_err!("Failed logging metric: {}, error: {:?}", $metric_id, e)),
961 Err(e) => Err(format_err!("Failed logging metric: {}, error: {}", $metric_id, e)),
962 }
963 }};
964}
965
966macro_rules! log_cobalt_batch {
967 ($cobalt_proxy:expr, $events:expr, $context:expr $(,)?) => {{
968 if $events.is_empty() {
969 Ok(())
970 } else {
971 let status = $cobalt_proxy.log_metric_events($events).await;
972 match status {
973 Ok(Ok(())) => Ok(()),
974 Ok(Err(e)) => Err(format_err!(
975 "Failed logging batch metrics, context: {}, error: {:?}",
976 $context,
977 e
978 )),
979 Err(e) => Err(format_err!(
980 "Failed logging batch metrics, context: {}, error: {}",
981 $context,
982 e
983 )),
984 }
985 }
986 }};
987}
988
989const INSPECT_SCAN_EVENTS_LIMIT: usize = 7;
990const INSPECT_CONNECT_EVENTS_LIMIT: usize = 7;
991const INSPECT_DISCONNECT_EVENTS_LIMIT: usize = 7;
992const INSPECT_EXTERNAL_DISCONNECT_EVENTS_LIMIT: usize = 2;
993const INSPECT_ROAM_EVENTS_LIMIT: usize = 7;
994
995pub struct ExternalInspectNode {
998 node: InspectNode,
999 disconnect_events: Mutex<BoundedListNode>,
1000}
1001
1002impl ExternalInspectNode {
1003 pub fn new(node: InspectNode) -> Self {
1004 let disconnect_events = node.create_child("disconnect_events");
1005 Self {
1006 node,
1007 disconnect_events: Mutex::new(BoundedListNode::new(
1008 disconnect_events,
1009 INSPECT_EXTERNAL_DISCONNECT_EVENTS_LIMIT,
1010 )),
1011 }
1012 }
1013}
1014
1015const UNRESPONSIVE_FLAG_MIN_DURATION: zx::MonotonicDuration =
1017 zx::MonotonicDuration::from_seconds(60);
1018
1019pub struct Telemetry {
1020 monitor_svc_proxy: fidl_fuchsia_wlan_device_service::DeviceMonitorProxy,
1021 connection_state: ConnectionState,
1022 last_checked_connection_state: fasync::MonotonicInstant,
1023 stats_logger: StatsLogger,
1024
1025 inspect_node: InspectNode,
1027 get_iface_stats_fail_count: UintProperty,
1028 scan_events_node: Mutex<BoundedListNode>,
1029 connect_events_node: Mutex<BoundedListNode>,
1030 disconnect_events_node: Mutex<BoundedListNode>,
1031 roam_events_node: Mutex<BoundedListNode>,
1032 external_inspect_node: ExternalInspectNode,
1033
1034 last_enabled_client_connections: Option<fasync::MonotonicInstant>,
1037
1038 last_disabled_client_connections: Option<fasync::MonotonicInstant>,
1042 defect_sender: mpsc::Sender<Defect>,
1043}
1044
1045impl Telemetry {
1046 pub fn new(
1047 telemetry_sender: TelemetrySender,
1048 monitor_svc_proxy: fidl_fuchsia_wlan_device_service::DeviceMonitorProxy,
1049 cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
1050 inspect_node: InspectNode,
1051 external_inspect_node: InspectNode,
1052 defect_sender: mpsc::Sender<Defect>,
1053 ) -> Self {
1054 let stats_logger = StatsLogger::new(cobalt_proxy, &inspect_node);
1055 inspect_record_connection_status(&inspect_node, telemetry_sender.clone());
1056 let get_iface_stats_fail_count = inspect_node.create_uint("get_iface_stats_fail_count", 0);
1057 let scan_events = inspect_node.create_child("scan_events");
1058 let connect_events = inspect_node.create_child("connect_events");
1059 let disconnect_events = inspect_node.create_child("disconnect_events");
1060 let roam_events = inspect_node.create_child("roam_events");
1061 let external_inspect_node = ExternalInspectNode::new(external_inspect_node);
1062 inspect_record_external_data(
1063 &external_inspect_node,
1064 telemetry_sender,
1065 defect_sender.clone(),
1066 );
1067 Self {
1068 monitor_svc_proxy,
1069 connection_state: ConnectionState::Idle(IdleState { connect_start_time: None }),
1070 last_checked_connection_state: fasync::MonotonicInstant::now(),
1071 stats_logger,
1072 inspect_node,
1073 get_iface_stats_fail_count,
1074 scan_events_node: Mutex::new(BoundedListNode::new(
1075 scan_events,
1076 INSPECT_SCAN_EVENTS_LIMIT,
1077 )),
1078 connect_events_node: Mutex::new(BoundedListNode::new(
1079 connect_events,
1080 INSPECT_CONNECT_EVENTS_LIMIT,
1081 )),
1082 disconnect_events_node: Mutex::new(BoundedListNode::new(
1083 disconnect_events,
1084 INSPECT_DISCONNECT_EVENTS_LIMIT,
1085 )),
1086 roam_events_node: Mutex::new(BoundedListNode::new(
1087 roam_events,
1088 INSPECT_ROAM_EVENTS_LIMIT,
1089 )),
1090 external_inspect_node,
1091 last_enabled_client_connections: None,
1092 last_disabled_client_connections: None,
1093 defect_sender,
1094 }
1095 }
1096
1097 pub async fn handle_periodic_telemetry(&mut self) {
1098 let now = fasync::MonotonicInstant::now();
1099 let duration = now - self.last_checked_connection_state;
1100
1101 self.stats_logger.log_stat(StatOp::AddTotalDuration(duration)).await;
1102 self.stats_logger.log_queued_stats().await;
1103
1104 match &mut self.connection_state {
1105 ConnectionState::Idle(..) => (),
1106 ConnectionState::Connected(state) => {
1107 self.stats_logger.log_stat(StatOp::AddConnectedDuration(duration)).await;
1108 if let Some(proxy) = &state.telemetry_proxy {
1109 match proxy
1110 .get_iface_stats()
1111 .on_timeout(GET_IFACE_STATS_TIMEOUT, || {
1112 warn!("Timed out waiting for iface stats");
1113
1114 if let Err(e) =
1115 self.defect_sender.try_send(Defect::Iface(IfaceFailure::Timeout {
1116 iface_id: state.iface_id,
1117 source: TimeoutSource::GetIfaceStats,
1118 }))
1119 {
1120 warn!("Failed to report iface stats timeout: {:?}", e)
1121 }
1122
1123 Ok(Err(zx::Status::TIMED_OUT.into_raw()))
1124 })
1125 .await
1126 {
1127 Ok(Ok(stats)) => {
1128 *state.num_consecutive_get_counter_stats_failures.get_mut() = 0;
1129 if let (Some(prev_connection_stats), Some(current_connection_stats)) = (
1130 state.prev_connection_stats.as_ref(),
1131 stats.connection_stats.as_ref(),
1132 ) {
1133 diff_and_log_connection_stats(
1134 &mut self.stats_logger,
1135 prev_connection_stats,
1136 current_connection_stats,
1137 duration,
1138 )
1139 .await;
1140 }
1141 state.prev_connection_stats = stats.connection_stats;
1142 }
1143 error => {
1144 info!("Failed to get interface stats: {:?}", error);
1145 let _ = self.get_iface_stats_fail_count.add(1);
1146 *state.num_consecutive_get_counter_stats_failures.get_mut() += 1;
1147 #[expect(clippy::unwrap_used)]
1150 self.stats_logger
1151 .log_consecutive_counter_stats_failures(
1152 (*state.num_consecutive_get_counter_stats_failures)
1153 .try_into()
1154 .unwrap(),
1155 )
1156 .await;
1157 let _ = state.prev_connection_stats.take();
1158 }
1159 }
1160 }
1161
1162 let unresponsive_signal_ind =
1163 now - state.last_signal_report > UNRESPONSIVE_FLAG_MIN_DURATION;
1164 let mut is_driver_unresponsive = state.is_driver_unresponsive.get_mut();
1165 if unresponsive_signal_ind != *is_driver_unresponsive {
1166 *is_driver_unresponsive = unresponsive_signal_ind;
1167 if unresponsive_signal_ind {
1168 warn!("driver unresponsive due to missing signal report");
1169 }
1170 }
1171 }
1172 ConnectionState::Disconnected(state) => {
1173 self.stats_logger.log_stat(StatOp::AddDowntimeDuration(duration)).await;
1174 if let Some(prev) = state.latest_no_saved_neighbor_time.take() {
1175 let duration = now - prev;
1176 state.accounted_no_saved_neighbor_duration += duration;
1177 self.stats_logger
1178 .log_stat(StatOp::AddDowntimeNoSavedNeighborDuration(duration))
1179 .await;
1180 state.latest_no_saved_neighbor_time = Some(now);
1181 }
1182 }
1183 }
1184 self.last_checked_connection_state = now;
1185 }
1186
1187 pub async fn handle_telemetry_event(&mut self, event: TelemetryEvent) {
1188 let now = fasync::MonotonicInstant::now();
1189 match event {
1190 TelemetryEvent::QueryStatus { sender } => {
1191 let info = match &self.connection_state {
1192 ConnectionState::Idle(..) => ConnectionStateInfo::Idle,
1193 ConnectionState::Disconnected(..) => ConnectionStateInfo::Disconnected,
1194 ConnectionState::Connected(state) => ConnectionStateInfo::Connected {
1195 iface_id: state.iface_id,
1196 ap_state: state.ap_state.clone(),
1197 telemetry_proxy: state.telemetry_proxy.clone(),
1198 },
1199 };
1200 let _result = sender.send(QueryStatusResult { connection_state: info });
1201 }
1202 TelemetryEvent::StartEstablishConnection { reset_start_time } => {
1203 match &mut self.connection_state {
1204 ConnectionState::Idle(IdleState { connect_start_time }) => {
1205 if reset_start_time || connect_start_time.is_none() {
1206 let _prev = connect_start_time.replace(now);
1207 }
1208 }
1209 ConnectionState::Disconnected(state) => {
1210 if reset_start_time || state.connect_start_time.is_none() {
1211 let _prev = state.connect_start_time.replace(now);
1212 }
1213 }
1214 ConnectionState::Connected(state) => {
1215 if reset_start_time {
1218 let _prev = state.new_connect_start_time.replace(now);
1219 }
1220 }
1221 }
1222 }
1223 TelemetryEvent::ClearEstablishConnectionStartTime => match &mut self.connection_state {
1224 ConnectionState::Idle(state) => {
1225 let _start_time = state.connect_start_time.take();
1226 }
1227 ConnectionState::Disconnected(state) => {
1228 let _start_time = state.connect_start_time.take();
1229 }
1230 ConnectionState::Connected(state) => {
1231 let _start_time = state.new_connect_start_time.take();
1232 }
1233 },
1234 TelemetryEvent::ActiveScanRequested { num_ssids_requested } => {
1235 self.stats_logger
1236 .log_active_scan_requested_cobalt_metrics(num_ssids_requested)
1237 .await
1238 }
1239 TelemetryEvent::ActiveScanRequestedViaApi { num_ssids_requested } => {
1240 self.stats_logger
1241 .log_active_scan_requested_via_api_cobalt_metrics(num_ssids_requested)
1242 .await
1243 }
1244 TelemetryEvent::NetworkSelectionDecision {
1245 network_selection_type,
1246 num_candidates,
1247 selected_count,
1248 } => {
1249 self.stats_logger
1250 .log_network_selection_metrics(
1251 &mut self.connection_state,
1252 network_selection_type,
1253 num_candidates,
1254 selected_count,
1255 )
1256 .await;
1257 }
1258 TelemetryEvent::ConnectResult {
1259 iface_id,
1260 policy_connect_reason,
1261 result,
1262 multiple_bss_candidates,
1263 ap_state,
1264 network_is_likely_hidden,
1265 } => {
1266 let connect_start_time = match &self.connection_state {
1267 ConnectionState::Idle(state) => state.connect_start_time,
1268 ConnectionState::Disconnected(state) => state.connect_start_time,
1269 ConnectionState::Connected(..) => {
1270 warn!("Received ConnectResult event while still connected");
1271 None
1272 }
1273 };
1274 self.stats_logger
1275 .report_connect_result(
1276 policy_connect_reason,
1277 result.code,
1278 multiple_bss_candidates,
1279 &ap_state,
1280 connect_start_time,
1281 )
1282 .await;
1283 self.stats_logger.log_stat(StatOp::AddConnectAttemptsCount).await;
1284 if result.code == fidl_ieee80211::StatusCode::Success {
1285 self.log_connect_event_inspect(&ap_state, multiple_bss_candidates);
1286 self.stats_logger.log_stat(StatOp::AddConnectSuccessfulCount).await;
1287
1288 self.stats_logger
1289 .log_device_connected_cobalt_metrics(
1290 multiple_bss_candidates,
1291 &ap_state,
1292 network_is_likely_hidden,
1293 )
1294 .await;
1295 if let ConnectionState::Disconnected(state) = &self.connection_state {
1296 if state.latest_no_saved_neighbor_time.is_some() {
1297 warn!("'No saved neighbor' flag still set even though connected");
1298 }
1299 self.stats_logger.queue_stat_op(StatOp::AddDowntimeDuration(
1300 now - self.last_checked_connection_state,
1301 ));
1302 let total_downtime = now - state.disconnected_since;
1303 if total_downtime < state.accounted_no_saved_neighbor_duration {
1304 warn!(
1305 "Total downtime is less than no-saved-neighbor duration. \
1306 Total downtime: {:?}, No saved neighbor duration: {:?}",
1307 total_downtime, state.accounted_no_saved_neighbor_duration
1308 )
1309 }
1310 let adjusted_downtime = max(
1311 total_downtime - state.accounted_no_saved_neighbor_duration,
1312 zx::MonotonicDuration::from_seconds(0),
1313 );
1314
1315 if let Some(disconnect_info) = state.disconnect_info.as_ref() {
1316 self.stats_logger
1317 .log_downtime_cobalt_metrics(adjusted_downtime, disconnect_info)
1318 .await;
1319 self.stats_logger
1320 .log_reconnect_cobalt_metrics(
1321 total_downtime,
1322 disconnect_info.disconnect_source,
1323 )
1324 .await;
1325 }
1326 }
1327
1328 if let Some(recovery_reason) =
1330 self.stats_logger.recovery_record.connect_failure.take()
1331 {
1332 self.stats_logger
1333 .log_post_recovery_result(recovery_reason, RecoveryOutcome::Success)
1334 .await
1335 }
1336
1337 let (proxy, server) = fidl::endpoints::create_proxy();
1338 let telemetry_proxy = match self
1339 .monitor_svc_proxy
1340 .get_sme_telemetry(iface_id, server)
1341 .await
1342 {
1343 Ok(Ok(())) => Some(proxy),
1344 Ok(Err(e)) => {
1345 error!(
1346 "Request for SME telemetry for iface {} completed with error {}. No telemetry will be captured.",
1347 iface_id, e
1348 );
1349 None
1350 }
1351 Err(e) => {
1352 error!(
1353 "Failed to request SME telemetry for iface {} with error {}. No telemetry will be captured.",
1354 iface_id, e
1355 );
1356 None
1357 }
1358 };
1359 self.connection_state = ConnectionState::Connected(Box::new(ConnectedState {
1360 iface_id,
1361 new_connect_start_time: None,
1362 prev_connection_stats: None,
1363 multiple_bss_candidates,
1364 ap_state: Box::new(ap_state),
1365 network_is_likely_hidden,
1366
1367 last_signal_report: now,
1371 num_consecutive_get_counter_stats_failures: InspectableU64::new(
1374 0,
1375 &self.inspect_node,
1376 "num_consecutive_get_counter_stats_failures",
1377 ),
1378 is_driver_unresponsive: InspectableBool::new(
1379 false,
1380 &self.inspect_node,
1381 "is_driver_unresponsive",
1382 ),
1383
1384 telemetry_proxy,
1385 }));
1386 self.last_checked_connection_state = now;
1387 } else if !result.is_credential_rejected {
1388 self.stats_logger.log_connection_failure().await;
1391
1392 if let Some(recovery_reason) =
1394 self.stats_logger.recovery_record.connect_failure.take()
1395 {
1396 self.stats_logger
1397 .log_post_recovery_result(recovery_reason, RecoveryOutcome::Failure)
1398 .await
1399 }
1400 }
1401
1402 self.report_sme_timeout_resolved().await;
1404 }
1405 TelemetryEvent::PolicyInitiatedRoamResult {
1406 iface_id,
1407 result,
1408 updated_ap_state,
1409 original_ap_state,
1410 request,
1411 request_time,
1412 result_time,
1413 } => {
1414 if result.status_code == fidl_ieee80211::StatusCode::Success {
1415 match &self.connection_state {
1416 ConnectionState::Connected(state) => {
1417 self.connection_state =
1420 ConnectionState::Connected(Box::new(ConnectedState {
1421 iface_id,
1422 new_connect_start_time: None,
1423 prev_connection_stats: None,
1424 multiple_bss_candidates: state.multiple_bss_candidates,
1425 ap_state: Box::new(updated_ap_state.clone()),
1426 network_is_likely_hidden: state.network_is_likely_hidden,
1427
1428 last_signal_report: now,
1432 num_consecutive_get_counter_stats_failures: InspectableU64::new(
1435 0,
1436 &self.inspect_node,
1437 "num_consecutive_get_counter_stats_failures",
1438 ),
1439 is_driver_unresponsive: InspectableBool::new(
1440 false,
1441 &self.inspect_node,
1442 "is_driver_unresponsive",
1443 ),
1444
1445 telemetry_proxy: state.telemetry_proxy.clone(),
1446 }));
1447 self.last_checked_connection_state = now;
1448 }
1450 _ => {
1451 warn!(
1452 "Unexpectedly received a successful roam event while telemetry module ConnectionState is not Connected."
1453 );
1454 }
1455 }
1456 }
1457 self.log_roam_event_inspect(iface_id, &result, &request);
1459
1460 self.stats_logger
1462 .log_roam_result_metrics(
1463 result,
1464 updated_ap_state,
1465 original_ap_state,
1466 request,
1467 request_time,
1468 result_time,
1469 )
1470 .await;
1471 }
1472 TelemetryEvent::Disconnected { track_subsequent_downtime, info } => {
1473 let mut connect_start_time = None;
1474
1475 if let Some(info) = info.as_ref() {
1478 self.report_sme_timeout_resolved().await;
1482
1483 self.log_disconnect_event_inspect(info);
1484 self.stats_logger
1485 .log_stat(StatOp::AddDisconnectCount(info.disconnect_source))
1486 .await;
1487 self.stats_logger
1488 .log_pre_disconnect_score_deltas_by_signal(
1489 info.connected_duration,
1490 info.signals.clone(),
1491 )
1492 .await;
1493 self.stats_logger
1494 .log_pre_disconnect_rssi_deltas(
1495 info.connected_duration,
1496 info.signals.clone(),
1497 )
1498 .await;
1499
1500 if let ConnectionState::Connected(state) = &self.connection_state {
1503 self.stats_logger
1504 .log_disconnect_cobalt_metrics(info, state.multiple_bss_candidates)
1505 .await;
1506
1507 if info.connected_duration < METRICS_SHORT_CONNECT_DURATION {
1509 self.stats_logger
1510 .log_short_duration_connection_metrics(
1511 info.signals.clone(),
1512 info.disconnect_source,
1513 info.previous_connect_reason,
1514 )
1515 .await;
1516 }
1517 }
1518
1519 if info.is_sme_reconnecting {
1523 connect_start_time = Some(now);
1524 } else if let ConnectionState::Connected(state) = &self.connection_state {
1525 connect_start_time = state.new_connect_start_time
1526 }
1527 }
1528
1529 let duration = now - self.last_checked_connection_state;
1530 match &self.connection_state {
1531 ConnectionState::Connected(state) => {
1532 self.stats_logger.queue_stat_op(StatOp::AddConnectedDuration(duration));
1533 self.stats_logger
1536 .log_device_connected_cobalt_metrics(
1537 state.multiple_bss_candidates,
1538 &state.ap_state,
1539 state.network_is_likely_hidden,
1540 )
1541 .await;
1542 }
1543 _ => {
1544 warn!(
1545 "Received disconnect event while not connected. Metric may not be logged"
1546 );
1547 }
1548 }
1549
1550 self.connection_state = if track_subsequent_downtime {
1551 ConnectionState::Disconnected(Box::new(DisconnectedState {
1552 disconnected_since: now,
1553 disconnect_info: info,
1554 connect_start_time,
1555 latest_no_saved_neighbor_time: None,
1558 accounted_no_saved_neighbor_duration: zx::MonotonicDuration::from_seconds(
1559 0,
1560 ),
1561 }))
1562 } else {
1563 ConnectionState::Idle(IdleState { connect_start_time })
1564 };
1565 self.last_checked_connection_state = now;
1566 }
1567 TelemetryEvent::OnSignalReport { ind } => {
1568 if let ConnectionState::Connected(state) = &mut self.connection_state {
1569 state.ap_state.tracked.signal.rssi_dbm = ind.rssi_dbm;
1570 state.ap_state.tracked.signal.snr_db = ind.snr_db;
1571 state.last_signal_report = now;
1572 self.stats_logger.log_signal_report_metrics(ind.rssi_dbm).await;
1573 }
1574 }
1575 TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity } => {
1576 self.stats_logger.log_signal_velocity_metrics(rssi_velocity).await;
1577 }
1578 TelemetryEvent::OnChannelSwitched { info } => {
1579 if let ConnectionState::Connected(state) = &mut self.connection_state {
1580 let cbw = match Bandwidth::from_fidl(
1581 info.bandwidth,
1582 info.vht_secondary_80_channel.number,
1583 ) {
1584 Ok(cbw) => cbw,
1585 Err(e) => {
1586 error!("Invalid CBW in ChannelSwitchInfo: {}", e);
1590 state.ap_state.tracked.channel.bandwidth
1591 }
1592 };
1593 state.ap_state.tracked.channel = Channel::new(
1594 info.new_primary_channel.number,
1595 cbw,
1596 info.new_primary_channel.band,
1597 );
1598 self.stats_logger
1599 .log_device_connected_channel_cobalt_metrics(state.ap_state.tracked.channel)
1600 .await;
1601 }
1602 }
1603 TelemetryEvent::PolicyRoamScan { reasons } => {
1604 self.stats_logger.log_policy_roam_scan_metrics(reasons).await;
1605 }
1606 TelemetryEvent::PolicyRoamAttempt { request, connected_duration } => {
1607 self.stats_logger
1608 .log_policy_roam_attempt_metrics(request, connected_duration)
1609 .await;
1610 }
1611 TelemetryEvent::WouldRoamConnect => {
1612 self.stats_logger.log_would_roam_connect().await;
1613 }
1614 TelemetryEvent::SavedNetworkCount {
1615 saved_network_count,
1616 config_count_per_saved_network,
1617 } => {
1618 self.stats_logger
1619 .log_saved_network_counts(saved_network_count, config_count_per_saved_network)
1620 .await;
1621 }
1622 TelemetryEvent::NetworkSelectionScanInterval { time_since_last_scan } => {
1623 self.stats_logger.log_network_selection_scan_interval(time_since_last_scan).await;
1624 }
1625 TelemetryEvent::ConnectionSelectionScanResults {
1626 saved_network_count,
1627 bss_count_per_saved_network,
1628 saved_network_count_found_by_active_scan,
1629 } => {
1630 self.stats_logger
1631 .log_connection_selection_scan_results(
1632 saved_network_count,
1633 bss_count_per_saved_network,
1634 saved_network_count_found_by_active_scan,
1635 )
1636 .await;
1637 }
1638 TelemetryEvent::StartClientConnectionsRequest => {
1639 let now = fasync::MonotonicInstant::now();
1640 if self.last_enabled_client_connections.is_none() {
1641 self.last_enabled_client_connections = Some(now);
1642 }
1643 self.last_disabled_client_connections = None;
1644 }
1645 TelemetryEvent::StopClientConnectionsRequest => {
1646 let now = fasync::MonotonicInstant::now();
1647 if self.last_disabled_client_connections.is_none() {
1650 self.last_disabled_client_connections = Some(fasync::MonotonicInstant::now());
1651 }
1652 if let Some(enabled_time) = self.last_enabled_client_connections {
1653 let enabled_duration = now - enabled_time;
1654 self.stats_logger.log_stop_client_connections_request(enabled_duration).await
1655 }
1656 self.last_enabled_client_connections = None;
1657 }
1658 TelemetryEvent::StopAp { enabled_duration } => {
1659 self.stats_logger.log_stop_ap_cobalt_metrics(enabled_duration).await;
1660
1661 self.report_sme_timeout_resolved().await;
1663 }
1664 TelemetryEvent::IfaceCreationResult { result, .. } => {
1665 self.stats_logger.log_iface_creation_result(result.map(|_| ())).await;
1666 }
1667 TelemetryEvent::IfaceDestructionResult { result, .. } => {
1668 self.stats_logger.log_iface_destruction_result(result.map(|_| ())).await;
1669 }
1670 TelemetryEvent::StartApResult(result) => {
1671 self.stats_logger.log_ap_start_result(result).await;
1672
1673 self.report_sme_timeout_resolved().await;
1675 }
1676 TelemetryEvent::ScanRequestFulfillmentTime { duration, reason } => {
1677 self.stats_logger.log_scan_request_fulfillment_time(duration, reason).await;
1678 }
1679 TelemetryEvent::ScanQueueStatistics { fulfilled_requests, remaining_requests } => {
1680 self.stats_logger
1681 .log_scan_queue_statistics(fulfilled_requests, remaining_requests)
1682 .await;
1683 }
1684 TelemetryEvent::BssSelectionResult {
1685 reason,
1686 scored_candidates,
1687 selected_candidate,
1688 } => {
1689 self.stats_logger
1690 .log_bss_selection_metrics(reason, scored_candidates, selected_candidate)
1691 .await
1692 }
1693 TelemetryEvent::PostConnectionSignals { connect_time, signal_at_connect, signals } => {
1694 self.stats_logger
1695 .log_post_connection_score_deltas_by_signal(
1696 connect_time,
1697 signal_at_connect,
1698 signals.clone(),
1699 )
1700 .await;
1701 self.stats_logger
1702 .log_post_connection_rssi_deltas(connect_time, signal_at_connect, signals)
1703 .await;
1704 }
1705 TelemetryEvent::ScanEvent { inspect_data, scan_defects } => {
1706 self.log_scan_event_inspect(inspect_data);
1707 self.stats_logger.log_scan_issues(scan_defects).await;
1708
1709 self.report_sme_timeout_resolved().await;
1711 }
1712 TelemetryEvent::LongDurationSignals { signals } => {
1713 self.stats_logger
1714 .log_connection_score_average_by_signal(
1715 metrics::ConnectionScoreAverageMetricDimensionDuration::LongDuration as u32,
1716 signals.clone(),
1717 )
1718 .await;
1719 self.stats_logger
1720 .log_connection_rssi_average(
1721 metrics::ConnectionRssiAverageMetricDimensionDuration::LongDuration as u32,
1722 signals,
1723 )
1724 .await;
1725 }
1726 TelemetryEvent::RecoveryEvent { reason } => {
1727 self.stats_logger.log_recovery_occurrence(reason).await;
1728 }
1729 TelemetryEvent::SmeTimeout { .. } => {
1730 if let Some(recovery_reason) = self.stats_logger.recovery_record.timeout.take() {
1733 self.stats_logger
1734 .log_post_recovery_result(recovery_reason, RecoveryOutcome::Failure)
1735 .await
1736 }
1737 }
1738 }
1739 }
1740
1741 pub fn log_scan_event_inspect(&self, scan_event_info: ScanEventInspectData) {
1742 if !scan_event_info.unknown_protection_ies.is_empty() {
1743 inspect_log!(self.scan_events_node.lock(), {
1744 unknown_protection_ies: InspectList(&scan_event_info.unknown_protection_ies)
1745 });
1746 }
1747 }
1748
1749 pub fn log_connect_event_inspect(
1750 &self,
1751 ap_state: &client::types::ApState,
1752 multiple_bss_candidates: bool,
1753 ) {
1754 inspect_log!(self.connect_events_node.lock(), {
1755 multiple_bss_candidates: multiple_bss_candidates,
1756 network: {
1757 bssid: ap_state.original().bssid.to_string(),
1758 ssid: ap_state.original().ssid.to_string(),
1759 rssi_dbm: ap_state.tracked.signal.rssi_dbm,
1760 snr_db: ap_state.tracked.signal.snr_db,
1761 },
1762 });
1763 }
1764
1765 pub fn log_disconnect_event_inspect(&self, info: &DisconnectInfo) {
1766 inspect_log!(self.disconnect_events_node.lock(), {
1767 connected_duration: info.connected_duration.into_nanos(),
1768 disconnect_source: info.disconnect_source.inspect_string(),
1769 network: {
1770 rssi_dbm: info.ap_state.tracked.signal.rssi_dbm,
1771 snr_db: info.ap_state.tracked.signal.snr_db,
1772 bssid: info.ap_state.original().bssid.to_string(),
1773 ssid: info.ap_state.original().ssid.to_string(),
1774 protection: format!("{:?}", info.ap_state.original().protection()),
1775 channel: format!("{}", info.ap_state.tracked.channel),
1776 ht_cap?: info.ap_state.original().raw_ht_cap().map(|cap| InspectBytes(cap.bytes)),
1777 vht_cap?: info.ap_state.original().raw_vht_cap().map(|cap| InspectBytes(cap.bytes)),
1778 wsc?: info.ap_state.original().probe_resp_wsc().as_ref().map(|wsc| make_inspect_loggable!(
1779 manufacturer: String::from_utf8_lossy(&wsc.manufacturer[..]).to_string(),
1780 model_name: String::from_utf8_lossy(&wsc.model_name[..]).to_string(),
1781 model_number: String::from_utf8_lossy(&wsc.model_number[..]).to_string(),
1782 )),
1783 is_wmm_assoc: info.ap_state.original().find_wmm_param().is_some(),
1784 wmm_param?: info.ap_state.original().find_wmm_param().map(InspectBytes),
1785 }
1786 });
1787 inspect_log!(self.external_inspect_node.disconnect_events.lock(), {
1788 flattened_reason_code: info.disconnect_source.flattened_reason_code(),
1791 locally_initiated: info.disconnect_source.locally_initiated(),
1792 network: {
1793 channel: {
1794 primary: info.ap_state.tracked.channel.primary,
1795 },
1796 },
1797 });
1798 }
1799
1800 pub fn log_roam_event_inspect(
1801 &self,
1802 iface_id: u16,
1803 result: &fidl_sme::RoamResult,
1804 request: &PolicyRoamRequest,
1805 ) {
1806 inspect_log!(self.roam_events_node.lock(), {
1807 iface_id: iface_id,
1808 target: {
1809 ssid: request.candidate.network.ssid.to_string(),
1810 bssid: request.candidate.bss.bssid.to_string(),
1811 },
1812 reasons: InspectList(request.reasons.iter().map(|reason| format!("{reason:?}")).collect::<Vec<String>>().as_slice()),
1813 status: result.status_code.into_primitive(),
1814 original_association_maintained: result.original_association_maintained,
1815 });
1816 }
1817
1818 pub async fn log_daily_cobalt_metrics(&mut self) {
1819 self.stats_logger.log_daily_cobalt_metrics().await;
1820 if let ConnectionState::Connected(state) = &self.connection_state {
1821 self.stats_logger
1822 .log_device_connected_cobalt_metrics(
1823 state.multiple_bss_candidates,
1824 &state.ap_state,
1825 state.network_is_likely_hidden,
1826 )
1827 .await;
1828 }
1829 }
1830
1831 pub async fn signal_hr_passed(&mut self) {
1832 self.stats_logger.handle_hr_passed().await;
1833 }
1834
1835 pub async fn report_sme_timeout_resolved(&mut self) {
1837 if let Some(recovery_reason) = self.stats_logger.recovery_record.timeout.take() {
1838 self.stats_logger
1839 .log_post_recovery_result(recovery_reason, RecoveryOutcome::Success)
1840 .await
1841 }
1842 }
1843}
1844
1845fn float_to_ten_thousandth(value: f64) -> i64 {
1848 (value * 10000f64) as i64
1849}
1850
1851pub async fn connect_to_metrics_logger_factory()
1852-> Result<fidl_fuchsia_metrics::MetricEventLoggerFactoryProxy, Error> {
1853 let cobalt_svc = fuchsia_component::client::connect_to_protocol::<
1854 fidl_fuchsia_metrics::MetricEventLoggerFactoryMarker,
1855 >()
1856 .context("failed to connect to metrics service")?;
1857 Ok(cobalt_svc)
1858}
1859
1860pub async fn create_metrics_logger(
1863 factory_proxy: &fidl_fuchsia_metrics::MetricEventLoggerFactoryProxy,
1864) -> Result<fidl_fuchsia_metrics::MetricEventLoggerProxy, Error> {
1865 let (cobalt_proxy, cobalt_server) =
1866 fidl::endpoints::create_proxy::<fidl_fuchsia_metrics::MetricEventLoggerMarker>();
1867
1868 let project_spec = fidl_fuchsia_metrics::ProjectSpec {
1869 customer_id: None, project_id: Some(metrics::PROJECT_ID),
1871 ..Default::default()
1872 };
1873
1874 let status = factory_proxy
1875 .create_metric_event_logger(&project_spec, cobalt_server)
1876 .await
1877 .context("failed to create metrics event logger")?;
1878
1879 match status {
1880 Ok(_) => Ok(cobalt_proxy),
1881 Err(err) => Err(format_err!("failed to create metrics event logger: {:?}", err)),
1882 }
1883}
1884
1885const HIGH_PACKET_DROP_RATE_THRESHOLD: f64 = 0.02;
1886const VERY_HIGH_PACKET_DROP_RATE_THRESHOLD: f64 = 0.05;
1887
1888const DEVICE_LOW_CONNECTION_SUCCESS_RATE_THRESHOLD: f64 = 0.1;
1889
1890async fn diff_and_log_connection_stats(
1891 stats_logger: &mut StatsLogger,
1892 prev: &fidl_fuchsia_wlan_stats::ConnectionStats,
1893 current: &fidl_fuchsia_wlan_stats::ConnectionStats,
1894 duration: zx::MonotonicDuration,
1895) {
1896 match (current.rx_unicast_total, prev.rx_unicast_total) {
1900 (Some(current), Some(prev)) if current < prev => return,
1901 _ => (),
1902 }
1903 match (current.rx_unicast_drop, prev.rx_unicast_drop) {
1904 (Some(current), Some(prev)) if current < prev => return,
1905 _ => (),
1906 }
1907 match (current.tx_total, prev.tx_total) {
1908 (Some(current), Some(prev)) if current < prev => return,
1909 _ => (),
1910 }
1911 match (current.tx_drop, prev.tx_drop) {
1912 (Some(current), Some(prev)) if current < prev => return,
1913 _ => (),
1914 }
1915
1916 diff_and_log_rx_counters(stats_logger, prev, current, duration).await;
1917 diff_and_log_tx_counters(stats_logger, prev, current, duration).await;
1918}
1919
1920async fn diff_and_log_rx_counters(
1921 stats_logger: &mut StatsLogger,
1922 prev: &fidl_fuchsia_wlan_stats::ConnectionStats,
1923 current: &fidl_fuchsia_wlan_stats::ConnectionStats,
1924 duration: zx::MonotonicDuration,
1925) {
1926 let (current_rx_unicast_total, prev_rx_unicast_total) =
1927 match (current.rx_unicast_total, prev.rx_unicast_total) {
1928 (Some(current), Some(prev)) => (current, prev),
1929 _ => return,
1930 };
1931 let (current_rx_unicast_drop, prev_rx_unicast_drop) =
1932 match (current.rx_unicast_drop, prev.rx_unicast_drop) {
1933 (Some(current), Some(prev)) => (current, prev),
1934 _ => return,
1935 };
1936
1937 let rx_total: u64 = match current_rx_unicast_total.checked_sub(prev_rx_unicast_total) {
1938 Some(diff) => diff,
1939 _ => return,
1940 };
1941 let rx_drop = match current_rx_unicast_drop.checked_sub(prev_rx_unicast_drop) {
1942 Some(diff) => diff,
1943 _ => return,
1944 };
1945 let rx_drop_rate = if rx_total > 0 { rx_drop as f64 / rx_total as f64 } else { 0f64 };
1946
1947 if rx_drop_rate > HIGH_PACKET_DROP_RATE_THRESHOLD {
1948 stats_logger.log_stat(StatOp::AddRxHighPacketDropDuration(duration)).await;
1949 }
1950 if rx_drop_rate > VERY_HIGH_PACKET_DROP_RATE_THRESHOLD {
1951 stats_logger.log_stat(StatOp::AddRxVeryHighPacketDropDuration(duration)).await;
1952 }
1953 if rx_total == 0 {
1954 stats_logger.log_stat(StatOp::AddNoRxDuration(duration)).await;
1955 }
1956}
1957
1958async fn diff_and_log_tx_counters(
1959 stats_logger: &mut StatsLogger,
1960 prev: &fidl_fuchsia_wlan_stats::ConnectionStats,
1961 current: &fidl_fuchsia_wlan_stats::ConnectionStats,
1962 duration: zx::MonotonicDuration,
1963) {
1964 let (current_tx_total, prev_tx_total) = match (current.tx_total, prev.tx_total) {
1965 (Some(current), Some(prev)) => (current, prev),
1966 _ => return,
1967 };
1968 let (current_tx_drop, prev_tx_drop) = match (current.tx_drop, prev.tx_drop) {
1969 (Some(current), Some(prev)) => (current, prev),
1970 _ => return,
1971 };
1972
1973 let tx_total = match current_tx_total.checked_sub(prev_tx_total) {
1974 Some(diff) => diff,
1975 _ => return,
1976 };
1977 let tx_drop = match current_tx_drop.checked_sub(prev_tx_drop) {
1978 Some(diff) => diff,
1979 _ => return,
1980 };
1981 let tx_drop_rate = if tx_total > 0 { tx_drop as f64 / tx_total as f64 } else { 0f64 };
1982
1983 if tx_drop_rate > HIGH_PACKET_DROP_RATE_THRESHOLD {
1984 stats_logger.log_stat(StatOp::AddTxHighPacketDropDuration(duration)).await;
1985 }
1986 if tx_drop_rate > VERY_HIGH_PACKET_DROP_RATE_THRESHOLD {
1987 stats_logger.log_stat(StatOp::AddTxVeryHighPacketDropDuration(duration)).await;
1988 }
1989}
1990
1991struct StatsLogger {
1992 cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
1993 last_1d_stats: Arc<Mutex<WindowedStats<StatCounters>>>,
1994 last_7d_stats: Arc<Mutex<WindowedStats<StatCounters>>>,
1995 last_successful_recovery: UintProperty,
1996 successful_recoveries: UintProperty,
1997 last_1d_detailed_stats: DailyDetailedStats,
2003 stat_ops: Vec<StatOp>,
2004 hr_tick: u32,
2005 rssi_velocity_hist: HashMap<u32, fidl_fuchsia_metrics::HistogramBucket>,
2006 rssi_hist: HashMap<u32, fidl_fuchsia_metrics::HistogramBucket>,
2007 recovery_record: RecoveryRecord,
2008 throttled_error_logger: ThrottledErrorLogger,
2009
2010 _1d_counters_inspect_node: LazyNode,
2012 _7d_counters_inspect_node: LazyNode,
2013}
2014
2015impl StatsLogger {
2016 pub fn new(
2017 cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
2018 inspect_node: &InspectNode,
2019 ) -> Self {
2020 let last_1d_stats = Arc::new(Mutex::new(WindowedStats::new(24)));
2021 let last_7d_stats = Arc::new(Mutex::new(WindowedStats::new(7)));
2022 let last_successful_recovery = inspect_node.create_uint("last_successful_recovery", 0);
2023 let successful_recoveries = inspect_node.create_uint("successful_recoveries", 0);
2024 let _1d_counters_inspect_node =
2025 inspect_create_counters(inspect_node, "1d_counters", Arc::clone(&last_1d_stats));
2026 let _7d_counters_inspect_node =
2027 inspect_create_counters(inspect_node, "7d_counters", Arc::clone(&last_7d_stats));
2028
2029 Self {
2030 cobalt_proxy,
2031 last_1d_stats,
2032 last_7d_stats,
2033 last_successful_recovery,
2034 successful_recoveries,
2035 last_1d_detailed_stats: DailyDetailedStats::new(),
2036 stat_ops: vec![],
2037 hr_tick: 0,
2038 rssi_velocity_hist: HashMap::new(),
2039 rssi_hist: HashMap::new(),
2040 recovery_record: RecoveryRecord::new(),
2041 throttled_error_logger: ThrottledErrorLogger::new(
2042 MINUTES_BETWEEN_COBALT_SYSLOG_WARNINGS,
2043 ),
2044 _1d_counters_inspect_node,
2045 _7d_counters_inspect_node,
2046 }
2047 }
2048
2049 async fn log_stat(&mut self, stat_op: StatOp) {
2050 self.log_stat_counters(stat_op);
2051 }
2052
2053 fn log_stat_counters(&mut self, stat_op: StatOp) {
2054 let zero = StatCounters::default();
2055 let addition = match stat_op {
2056 StatOp::AddTotalDuration(duration) => StatCounters { total_duration: duration, ..zero },
2057 StatOp::AddConnectedDuration(duration) => {
2058 StatCounters { connected_duration: duration, ..zero }
2059 }
2060 StatOp::AddDowntimeDuration(duration) => {
2061 StatCounters { downtime_duration: duration, ..zero }
2062 }
2063 StatOp::AddDowntimeNoSavedNeighborDuration(duration) => {
2064 StatCounters { downtime_no_saved_neighbor_duration: duration, ..zero }
2065 }
2066 StatOp::AddConnectAttemptsCount => StatCounters { connect_attempts_count: 1, ..zero },
2067 StatOp::AddConnectSuccessfulCount => {
2068 StatCounters { connect_successful_count: 1, ..zero }
2069 }
2070 StatOp::AddDisconnectCount(disconnect_source) => {
2071 if disconnect_source.has_roaming_cause() {
2072 StatCounters { disconnect_count: 1, total_roam_disconnect_count: 1, ..zero }
2073 } else {
2074 StatCounters { disconnect_count: 1, total_non_roam_disconnect_count: 1, ..zero }
2075 }
2076 }
2077 StatOp::AddPolicyRoamAttemptsCount(reasons) => {
2078 let mut counters = StatCounters { policy_roam_attempts_count: 1, ..zero };
2079 for reason in reasons {
2080 let _ = counters.policy_roam_attempts_count_by_roam_reason.insert(reason, 1);
2081 }
2082 counters
2083 }
2084 StatOp::AddPolicyRoamSuccessfulCount(reasons) => {
2085 let mut counters = StatCounters { policy_roam_successful_count: 1, ..zero };
2086 for reason in reasons {
2087 let _ = counters.policy_roam_successful_count_by_roam_reason.insert(reason, 1);
2088 }
2089 counters
2090 }
2091 StatOp::AddPolicyRoamDisconnectsCount => {
2092 StatCounters { policy_roam_disconnects_count: 1, ..zero }
2093 }
2094 StatOp::AddTxHighPacketDropDuration(duration) => {
2095 StatCounters { tx_high_packet_drop_duration: duration, ..zero }
2096 }
2097 StatOp::AddRxHighPacketDropDuration(duration) => {
2098 StatCounters { rx_high_packet_drop_duration: duration, ..zero }
2099 }
2100 StatOp::AddTxVeryHighPacketDropDuration(duration) => {
2101 StatCounters { tx_very_high_packet_drop_duration: duration, ..zero }
2102 }
2103 StatOp::AddRxVeryHighPacketDropDuration(duration) => {
2104 StatCounters { rx_very_high_packet_drop_duration: duration, ..zero }
2105 }
2106 StatOp::AddNoRxDuration(duration) => StatCounters { no_rx_duration: duration, ..zero },
2107 };
2108
2109 if addition != StatCounters::default() {
2110 self.last_1d_stats.lock().saturating_add(&addition);
2111 self.last_7d_stats.lock().saturating_add(&addition);
2112 }
2113 }
2114
2115 fn queue_stat_op(&mut self, stat_op: StatOp) {
2119 self.stat_ops.push(stat_op);
2120 }
2121
2122 async fn log_queued_stats(&mut self) {
2123 while let Some(stat_op) = self.stat_ops.pop() {
2124 self.log_stat(stat_op).await;
2125 }
2126 }
2127
2128 async fn report_connect_result(
2129 &mut self,
2130 policy_connect_reason: Option<client::types::ConnectReason>,
2131 code: fidl_ieee80211::StatusCode,
2132 multiple_bss_candidates: bool,
2133 ap_state: &client::types::ApState,
2134 connect_start_time: Option<fasync::MonotonicInstant>,
2135 ) {
2136 self.log_establish_connection_cobalt_metrics(
2137 policy_connect_reason,
2138 code,
2139 multiple_bss_candidates,
2140 ap_state,
2141 connect_start_time,
2142 )
2143 .await;
2144
2145 *self.last_1d_detailed_stats.connect_attempts_status.entry(code).or_insert(0) += 1;
2146
2147 let is_multi_bss_dim = convert::convert_is_multi_bss(multiple_bss_candidates);
2148 self.last_1d_detailed_stats
2149 .connect_per_is_multi_bss
2150 .entry(is_multi_bss_dim)
2151 .or_default()
2152 .increment(code);
2153
2154 let security_type_dim = convert::convert_security_type(&ap_state.original().protection());
2155 self.last_1d_detailed_stats
2156 .connect_per_security_type
2157 .entry(security_type_dim)
2158 .or_default()
2159 .increment(code);
2160
2161 self.last_1d_detailed_stats
2162 .connect_per_primary_channel
2163 .entry(ap_state.tracked.channel.primary)
2164 .or_default()
2165 .increment(code);
2166
2167 let channel_band_dim = convert::convert_channel_band(ap_state.tracked.channel.band);
2168 self.last_1d_detailed_stats
2169 .connect_per_channel_band
2170 .entry(channel_band_dim)
2171 .or_default()
2172 .increment(code);
2173
2174 let rssi_bucket_dim = convert::convert_rssi_bucket(ap_state.tracked.signal.rssi_dbm);
2175 self.last_1d_detailed_stats
2176 .connect_per_rssi_bucket
2177 .entry(rssi_bucket_dim)
2178 .or_default()
2179 .increment(code);
2180
2181 let snr_bucket_dim = convert::convert_snr_bucket(ap_state.tracked.signal.snr_db);
2182 self.last_1d_detailed_stats
2183 .connect_per_snr_bucket
2184 .entry(snr_bucket_dim)
2185 .or_default()
2186 .increment(code);
2187 }
2188
2189 async fn log_daily_cobalt_metrics(&mut self) {
2190 self.log_daily_1d_cobalt_metrics().await;
2191 self.log_daily_7d_cobalt_metrics().await;
2192 self.log_daily_detailed_cobalt_metrics().await;
2193 }
2194
2195 async fn log_daily_1d_cobalt_metrics(&mut self) {
2196 let mut metric_events = vec![];
2197
2198 let c = self.last_1d_stats.lock().windowed_stat(None);
2199 let uptime_ratio = c.connected_duration.into_seconds() as f64
2200 / (c.connected_duration + c.adjusted_downtime()).into_seconds() as f64;
2201 if uptime_ratio.is_finite() {
2202 metric_events.push(MetricEvent {
2203 metric_id: metrics::CONNECTED_UPTIME_RATIO_METRIC_ID,
2204 event_codes: vec![],
2205 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(uptime_ratio)),
2206 });
2207 }
2208
2209 let connected_dur_in_day = c.connected_duration.into_seconds() as f64 / (24 * 3600) as f64;
2210 let dpdc_ratio = c.disconnect_count as f64 / connected_dur_in_day;
2211 if dpdc_ratio.is_finite() {
2212 metric_events.push(MetricEvent {
2213 metric_id: metrics::DISCONNECT_PER_DAY_CONNECTED_METRIC_ID,
2214 event_codes: vec![],
2215 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(dpdc_ratio)),
2216 });
2217 }
2218
2219 let roam_dpdc_ratio = c.policy_roam_disconnects_count as f64 / connected_dur_in_day;
2220 if roam_dpdc_ratio.is_finite() {
2221 metric_events.push(MetricEvent {
2222 metric_id: metrics::POLICY_ROAM_DISCONNECT_COUNT_PER_DAY_CONNECTED_METRIC_ID,
2223 event_codes: vec![],
2224 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(roam_dpdc_ratio)),
2225 });
2226 }
2227
2228 let non_roam_dpdc_ratio = c.total_non_roam_disconnect_count as f64 / connected_dur_in_day;
2229 if non_roam_dpdc_ratio.is_finite() {
2230 metric_events.push(MetricEvent {
2231 metric_id: metrics::NON_ROAM_DISCONNECT_PER_DAY_CONNECTED_METRIC_ID,
2232 event_codes: vec![],
2233 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
2234 non_roam_dpdc_ratio,
2235 )),
2236 });
2237 }
2238
2239 let high_rx_drop_time_ratio = c.rx_high_packet_drop_duration.into_seconds() as f64
2240 / c.connected_duration.into_seconds() as f64;
2241 if high_rx_drop_time_ratio.is_finite() {
2242 metric_events.push(MetricEvent {
2243 metric_id: metrics::TIME_RATIO_WITH_HIGH_RX_PACKET_DROP_METRIC_ID,
2244 event_codes: vec![],
2245 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
2246 high_rx_drop_time_ratio,
2247 )),
2248 });
2249 }
2250
2251 let high_tx_drop_time_ratio = c.tx_high_packet_drop_duration.into_seconds() as f64
2252 / c.connected_duration.into_seconds() as f64;
2253 if high_tx_drop_time_ratio.is_finite() {
2254 metric_events.push(MetricEvent {
2255 metric_id: metrics::TIME_RATIO_WITH_HIGH_TX_PACKET_DROP_METRIC_ID,
2256 event_codes: vec![],
2257 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
2258 high_tx_drop_time_ratio,
2259 )),
2260 });
2261 }
2262
2263 let very_high_rx_drop_time_ratio = c.rx_very_high_packet_drop_duration.into_seconds()
2264 as f64
2265 / c.connected_duration.into_seconds() as f64;
2266 if very_high_rx_drop_time_ratio.is_finite() {
2267 metric_events.push(MetricEvent {
2268 metric_id: metrics::TIME_RATIO_WITH_VERY_HIGH_RX_PACKET_DROP_METRIC_ID,
2269 event_codes: vec![],
2270 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
2271 very_high_rx_drop_time_ratio,
2272 )),
2273 });
2274 }
2275
2276 let very_high_tx_drop_time_ratio = c.tx_very_high_packet_drop_duration.into_seconds()
2277 as f64
2278 / c.connected_duration.into_seconds() as f64;
2279 if very_high_tx_drop_time_ratio.is_finite() {
2280 metric_events.push(MetricEvent {
2281 metric_id: metrics::TIME_RATIO_WITH_VERY_HIGH_TX_PACKET_DROP_METRIC_ID,
2282 event_codes: vec![],
2283 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
2284 very_high_tx_drop_time_ratio,
2285 )),
2286 });
2287 }
2288
2289 let no_rx_time_ratio =
2290 c.no_rx_duration.into_seconds() as f64 / c.connected_duration.into_seconds() as f64;
2291 if no_rx_time_ratio.is_finite() {
2292 metric_events.push(MetricEvent {
2293 metric_id: metrics::TIME_RATIO_WITH_NO_RX_METRIC_ID,
2294 event_codes: vec![],
2295 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
2296 no_rx_time_ratio,
2297 )),
2298 });
2299 }
2300
2301 let connection_success_rate = c.connection_success_rate();
2302 if connection_success_rate.is_finite() {
2303 metric_events.push(MetricEvent {
2304 metric_id: metrics::CONNECTION_SUCCESS_RATE_METRIC_ID,
2305 event_codes: vec![],
2306 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
2307 connection_success_rate,
2308 )),
2309 });
2310 }
2311
2312 let policy_roam_success_rate = c.policy_roam_success_rate();
2313 if policy_roam_success_rate.is_finite() {
2314 metric_events.push(MetricEvent {
2315 metric_id: metrics::POLICY_ROAM_SUCCESS_RATE_METRIC_ID,
2316 event_codes: vec![],
2317 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
2318 policy_roam_success_rate,
2319 )),
2320 });
2321 }
2322
2323 for reason in c.policy_roam_attempts_count_by_roam_reason.keys() {
2324 let success_rate = c.policy_roam_success_rate_by_roam_reason(reason);
2325 if success_rate.is_finite() {
2326 metric_events.push(MetricEvent {
2327 metric_id: metrics::POLICY_ROAM_SUCCESS_RATE_BY_ROAM_REASON_METRIC_ID,
2328 event_codes: vec![convert::convert_roam_reason_dimension(*reason) as u32],
2329 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
2330 success_rate,
2331 )),
2332 });
2333 }
2334 }
2335
2336 self.throttled_error_logger.throttle_error(log_cobalt_batch!(
2337 self.cobalt_proxy,
2338 &metric_events,
2339 "log_daily_1d_cobalt_metrics",
2340 ));
2341 }
2342
2343 async fn log_daily_7d_cobalt_metrics(&mut self) {
2344 let c = self.last_7d_stats.lock().windowed_stat(None);
2345 let connected_dur_in_day = c.connected_duration.into_seconds() as f64 / (24 * 3600) as f64;
2346 let dpdc_ratio = c.disconnect_count as f64 / connected_dur_in_day;
2347 #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
2348 if dpdc_ratio.is_finite() {
2349 let mut metric_events = vec![];
2350 metric_events.push(MetricEvent {
2351 metric_id: metrics::DISCONNECT_PER_DAY_CONNECTED_7D_METRIC_ID,
2352 event_codes: vec![],
2353 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(dpdc_ratio)),
2354 });
2355
2356 self.throttled_error_logger.throttle_error(log_cobalt_batch!(
2357 self.cobalt_proxy,
2358 &metric_events,
2359 "log_daily_7d_cobalt_metrics",
2360 ));
2361 }
2362 }
2363
2364 async fn log_daily_detailed_cobalt_metrics(&mut self) {
2365 let mut metric_events = vec![];
2366
2367 let c = self.last_1d_stats.lock().windowed_stat(None);
2368 if c.connection_success_rate().is_finite() {
2369 let device_low_connection_success =
2370 c.connection_success_rate() < DEVICE_LOW_CONNECTION_SUCCESS_RATE_THRESHOLD;
2371 for (status_code, count) in &self.last_1d_detailed_stats.connect_attempts_status {
2372 metric_events.push(MetricEvent {
2373 metric_id: if device_low_connection_success {
2374 metrics::CONNECT_ATTEMPT_ON_BAD_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID
2375 } else {
2376 metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID
2377 },
2378 event_codes: vec![(*status_code).into_primitive() as u32],
2379 payload: MetricEventPayload::Count(*count),
2380 });
2381 }
2382
2383 for (is_multi_bss_dim, counters) in
2384 &self.last_1d_detailed_stats.connect_per_is_multi_bss
2385 {
2386 let success_rate = counters.success as f64 / counters.total as f64;
2387 metric_events.push(MetricEvent {
2388 metric_id:
2389 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID,
2390 event_codes: vec![*is_multi_bss_dim as u32],
2391 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
2392 success_rate,
2393 )),
2394 });
2395 }
2396 }
2397
2398 self.throttled_error_logger.throttle_error(log_cobalt_batch!(
2399 self.cobalt_proxy,
2400 &metric_events,
2401 "log_daily_detailed_cobalt_metrics",
2402 ));
2403 }
2404
2405 async fn handle_hr_passed(&mut self) {
2406 self.log_hourly_fleetwise_quality_cobalt_metrics().await;
2407
2408 self.hr_tick = (self.hr_tick + 1) % 24;
2409 self.last_1d_stats.lock().slide_window();
2410 if self.hr_tick == 0 {
2411 self.last_7d_stats.lock().slide_window();
2412 self.last_1d_detailed_stats = DailyDetailedStats::new();
2413 }
2414
2415 self.log_hourly_rssi_histogram_metrics().await;
2416 }
2417
2418 async fn log_hourly_rssi_histogram_metrics(&mut self) {
2420 let rssi_buckets: Vec<_> = self.rssi_hist.values().copied().collect();
2421 self.throttled_error_logger.throttle_error(log_cobalt!(
2422 self.cobalt_proxy,
2423 log_integer_histogram,
2424 metrics::CONNECTION_RSSI_METRIC_ID,
2425 &rssi_buckets,
2426 &[],
2427 ));
2428 self.rssi_hist.clear();
2429
2430 let velocity_buckets: Vec<_> = self.rssi_velocity_hist.values().copied().collect();
2431 self.throttled_error_logger.throttle_error(log_cobalt!(
2432 self.cobalt_proxy,
2433 log_integer_histogram,
2434 metrics::RSSI_VELOCITY_METRIC_ID,
2435 &velocity_buckets,
2436 &[],
2437 ));
2438 self.rssi_velocity_hist.clear();
2439 }
2440
2441 async fn log_hourly_fleetwise_quality_cobalt_metrics(&mut self) {
2442 let mut metric_events = vec![];
2443
2444 let c = self.last_1d_stats.lock().windowed_stat(Some(1));
2446 let total_wlan_uptime = c.connected_duration + c.adjusted_downtime();
2447
2448 metric_events.push(MetricEvent {
2450 metric_id: metrics::TOTAL_WLAN_UPTIME_NEAR_SAVED_NETWORK_METRIC_ID,
2451 event_codes: vec![],
2452 payload: MetricEventPayload::IntegerValue(total_wlan_uptime.into_micros()),
2453 });
2454 metric_events.push(MetricEvent {
2455 metric_id: metrics::TOTAL_CONNECTED_UPTIME_METRIC_ID,
2456 event_codes: vec![],
2457 payload: MetricEventPayload::IntegerValue(c.connected_duration.into_micros()),
2458 });
2459 metric_events.push(MetricEvent {
2460 metric_id: metrics::TOTAL_TIME_WITH_HIGH_RX_PACKET_DROP_METRIC_ID,
2461 event_codes: vec![],
2462 payload: MetricEventPayload::IntegerValue(c.rx_high_packet_drop_duration.into_micros()),
2463 });
2464 metric_events.push(MetricEvent {
2465 metric_id: metrics::TOTAL_TIME_WITH_HIGH_TX_PACKET_DROP_METRIC_ID,
2466 event_codes: vec![],
2467 payload: MetricEventPayload::IntegerValue(c.tx_high_packet_drop_duration.into_micros()),
2468 });
2469 metric_events.push(MetricEvent {
2470 metric_id: metrics::TOTAL_TIME_WITH_VERY_HIGH_RX_PACKET_DROP_METRIC_ID,
2471 event_codes: vec![],
2472 payload: MetricEventPayload::IntegerValue(
2473 c.rx_very_high_packet_drop_duration.into_micros(),
2474 ),
2475 });
2476 metric_events.push(MetricEvent {
2477 metric_id: metrics::TOTAL_TIME_WITH_VERY_HIGH_TX_PACKET_DROP_METRIC_ID,
2478 event_codes: vec![],
2479 payload: MetricEventPayload::IntegerValue(
2480 c.tx_very_high_packet_drop_duration.into_micros(),
2481 ),
2482 });
2483 metric_events.push(MetricEvent {
2484 metric_id: metrics::TOTAL_TIME_WITH_NO_RX_METRIC_ID,
2485 event_codes: vec![],
2486 payload: MetricEventPayload::IntegerValue(c.no_rx_duration.into_micros()),
2487 });
2488
2489 self.throttled_error_logger.throttle_error(log_cobalt_batch!(
2490 self.cobalt_proxy,
2491 &metric_events,
2492 "log_hourly_fleetwise_quality_cobalt_metrics",
2493 ));
2494 }
2495
2496 async fn log_disconnect_cobalt_metrics(
2497 &mut self,
2498 disconnect_info: &DisconnectInfo,
2499 multiple_bss_candidates: bool,
2500 ) {
2501 let mut metric_events = vec![];
2502 let policy_disconnect_reason_dim = {
2503 use metrics::PolicyDisconnectionMigratedMetricDimensionReason::*;
2504 match &disconnect_info.disconnect_source {
2505 fidl_sme::DisconnectSource::User(reason) => match reason {
2506 fidl_sme::UserDisconnectReason::Unknown => Unknown,
2507 fidl_sme::UserDisconnectReason::FailedToConnect => FailedToConnect,
2508 fidl_sme::UserDisconnectReason::FidlConnectRequest => FidlConnectRequest,
2509 fidl_sme::UserDisconnectReason::FidlStopClientConnectionsRequest => {
2510 FidlStopClientConnectionsRequest
2511 }
2512 fidl_sme::UserDisconnectReason::ProactiveNetworkSwitch => {
2513 ProactiveNetworkSwitch
2514 }
2515 fidl_sme::UserDisconnectReason::DisconnectDetectedFromSme => {
2516 DisconnectDetectedFromSme
2517 }
2518 fidl_sme::UserDisconnectReason::RegulatoryRegionChange => {
2519 RegulatoryRegionChange
2520 }
2521 fidl_sme::UserDisconnectReason::Startup => Startup,
2522 fidl_sme::UserDisconnectReason::NetworkUnsaved => NetworkUnsaved,
2523 fidl_sme::UserDisconnectReason::NetworkConfigUpdated => NetworkConfigUpdated,
2524 fidl_sme::UserDisconnectReason::WlanstackUnitTesting
2525 | fidl_sme::UserDisconnectReason::WlanSmeUnitTesting
2526 | fidl_sme::UserDisconnectReason::WlanServiceUtilTesting
2527 | fidl_sme::UserDisconnectReason::WlanDevTool
2528 | fidl_sme::UserDisconnectReason::Recovery => Unknown,
2529 },
2530 fidl_sme::DisconnectSource::Ap(..) | fidl_sme::DisconnectSource::Mlme(..) => {
2531 DisconnectDetectedFromSme
2532 }
2533 }
2534 };
2535 metric_events.push(MetricEvent {
2536 metric_id: metrics::POLICY_DISCONNECTION_MIGRATED_METRIC_ID,
2537 event_codes: vec![policy_disconnect_reason_dim as u32],
2538 payload: MetricEventPayload::Count(1),
2539 });
2540
2541 let device_uptime_dim = {
2542 use metrics::DisconnectBreakdownByDeviceUptimeMetricDimensionDeviceUptime::*;
2543 match fasync::MonotonicInstant::now() - fasync::MonotonicInstant::from_nanos(0) {
2544 x if x < zx::MonotonicDuration::from_hours(1) => LessThan1Hour,
2545 x if x < zx::MonotonicDuration::from_hours(3) => LessThan3Hours,
2546 x if x < zx::MonotonicDuration::from_hours(12) => LessThan12Hours,
2547 x if x < zx::MonotonicDuration::from_hours(24) => LessThan1Day,
2548 x if x < zx::MonotonicDuration::from_hours(48) => LessThan2Days,
2549 _ => AtLeast2Days,
2550 }
2551 };
2552 metric_events.push(MetricEvent {
2553 metric_id: metrics::DISCONNECT_BREAKDOWN_BY_DEVICE_UPTIME_METRIC_ID,
2554 event_codes: vec![device_uptime_dim as u32],
2555 payload: MetricEventPayload::Count(1),
2556 });
2557
2558 let connected_duration_dim = {
2559 use metrics::DisconnectBreakdownByConnectedDurationMetricDimensionConnectedDuration::*;
2560 match disconnect_info.connected_duration {
2561 x if x < zx::MonotonicDuration::from_seconds(30) => LessThan30Seconds,
2562 x if x < zx::MonotonicDuration::from_minutes(5) => LessThan5Minutes,
2563 x if x < zx::MonotonicDuration::from_hours(1) => LessThan1Hour,
2564 x if x < zx::MonotonicDuration::from_hours(6) => LessThan6Hours,
2565 x if x < zx::MonotonicDuration::from_hours(24) => LessThan24Hours,
2566 _ => AtLeast24Hours,
2567 }
2568 };
2569 metric_events.push(MetricEvent {
2570 metric_id: metrics::DISCONNECT_BREAKDOWN_BY_CONNECTED_DURATION_METRIC_ID,
2571 event_codes: vec![connected_duration_dim as u32],
2572 payload: MetricEventPayload::Count(1),
2573 });
2574
2575 metric_events.push(MetricEvent {
2576 metric_id: metrics::DISCONNECT_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
2577 event_codes: vec![disconnect_info.ap_state.tracked.channel.primary as u32],
2578 payload: MetricEventPayload::Count(1),
2579 });
2580 let channel_band_dim =
2581 convert::convert_channel_band(disconnect_info.ap_state.tracked.channel.band);
2582 metric_events.push(MetricEvent {
2583 metric_id: metrics::DISCONNECT_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
2584 event_codes: vec![channel_band_dim as u32],
2585 payload: MetricEventPayload::Count(1),
2586 });
2587 let is_multi_bss_dim = convert::convert_is_multi_bss(multiple_bss_candidates);
2588 metric_events.push(MetricEvent {
2589 metric_id: metrics::DISCONNECT_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID,
2590 event_codes: vec![is_multi_bss_dim as u32],
2591 payload: MetricEventPayload::Count(1),
2592 });
2593 let security_type_dim =
2594 convert::convert_security_type(&disconnect_info.ap_state.original().protection());
2595 metric_events.push(MetricEvent {
2596 metric_id: metrics::DISCONNECT_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID,
2597 event_codes: vec![security_type_dim as u32],
2598 payload: MetricEventPayload::Count(1),
2599 });
2600
2601 let duration_minutes = disconnect_info.connected_duration.into_minutes();
2604 if !disconnect_info.disconnect_source.has_roaming_cause() {
2605 metric_events.push(MetricEvent {
2606 metric_id: metrics::CONNECTED_DURATION_BEFORE_NON_ROAM_DISCONNECT_METRIC_ID,
2607 event_codes: vec![],
2608 payload: MetricEventPayload::IntegerValue(duration_minutes),
2609 });
2610 metric_events.push(MetricEvent {
2612 metric_id: metrics::NON_ROAM_DISCONNECT_COUNTS_METRIC_ID,
2613 event_codes: vec![],
2614 payload: MetricEventPayload::Count(1),
2615 });
2616 metric_events.push(MetricEvent {
2618 metric_id: metrics::TOTAL_NON_ROAM_DISCONNECT_COUNT_METRIC_ID,
2619 event_codes: vec![],
2620 payload: MetricEventPayload::Count(1),
2621 })
2622 }
2623
2624 metric_events.push(MetricEvent {
2625 metric_id: metrics::CONNECTED_DURATION_BEFORE_DISCONNECT_METRIC_ID,
2626 event_codes: vec![],
2627 payload: MetricEventPayload::IntegerValue(duration_minutes),
2628 });
2629
2630 metric_events.push(MetricEvent {
2631 metric_id: metrics::NETWORK_DISCONNECT_COUNTS_METRIC_ID,
2632 event_codes: vec![],
2633 payload: MetricEventPayload::Count(1),
2634 });
2635
2636 self.throttled_error_logger.throttle_error(log_cobalt_batch!(
2637 self.cobalt_proxy,
2638 &metric_events,
2639 "log_disconnect_cobalt_metrics",
2640 ));
2641 }
2642
2643 async fn log_active_scan_requested_cobalt_metrics(&mut self, num_ssids_requested: usize) {
2644 use metrics::ActiveScanRequestedForNetworkSelectionMigratedMetricDimensionActiveScanSsidsRequested as ActiveScanSsidsRequested;
2645 let active_scan_ssids_requested_dim = match num_ssids_requested {
2646 0 => ActiveScanSsidsRequested::Zero,
2647 1 => ActiveScanSsidsRequested::One,
2648 2..=4 => ActiveScanSsidsRequested::TwoToFour,
2649 5..=10 => ActiveScanSsidsRequested::FiveToTen,
2650 11..=20 => ActiveScanSsidsRequested::ElevenToTwenty,
2651 21..=50 => ActiveScanSsidsRequested::TwentyOneToFifty,
2652 51..=100 => ActiveScanSsidsRequested::FiftyOneToOneHundred,
2653 101.. => ActiveScanSsidsRequested::OneHundredAndOneOrMore,
2654 };
2655 self.throttled_error_logger.throttle_error(log_cobalt!(
2656 self.cobalt_proxy,
2657 log_occurrence,
2658 metrics::ACTIVE_SCAN_REQUESTED_FOR_NETWORK_SELECTION_MIGRATED_METRIC_ID,
2659 1,
2660 &[active_scan_ssids_requested_dim as u32],
2661 ));
2662 }
2663
2664 async fn log_active_scan_requested_via_api_cobalt_metrics(
2665 &mut self,
2666 num_ssids_requested: usize,
2667 ) {
2668 use metrics::ActiveScanRequestedForPolicyApiMetricDimensionActiveScanSsidsRequested as ActiveScanSsidsRequested;
2669 let active_scan_ssids_requested_dim = match num_ssids_requested {
2670 0 => ActiveScanSsidsRequested::Zero,
2671 1 => ActiveScanSsidsRequested::One,
2672 2..=4 => ActiveScanSsidsRequested::TwoToFour,
2673 5..=10 => ActiveScanSsidsRequested::FiveToTen,
2674 11..=20 => ActiveScanSsidsRequested::ElevenToTwenty,
2675 21..=50 => ActiveScanSsidsRequested::TwentyOneToFifty,
2676 51..=100 => ActiveScanSsidsRequested::FiftyOneToOneHundred,
2677 101.. => ActiveScanSsidsRequested::OneHundredAndOneOrMore,
2678 };
2679 self.throttled_error_logger.throttle_error(log_cobalt!(
2680 self.cobalt_proxy,
2681 log_occurrence,
2682 metrics::ACTIVE_SCAN_REQUESTED_FOR_POLICY_API_METRIC_ID,
2683 1,
2684 &[active_scan_ssids_requested_dim as u32],
2685 ));
2686 }
2687
2688 async fn log_saved_network_counts(
2689 &mut self,
2690 saved_network_count: usize,
2691 config_count_per_saved_network: Vec<usize>,
2692 ) {
2693 let mut metric_events = vec![];
2694
2695 use metrics::SavedNetworksMigratedMetricDimensionSavedNetworks as SavedNetworksCount;
2697 let num_networks = match saved_network_count {
2698 0 => SavedNetworksCount::Zero,
2699 1 => SavedNetworksCount::One,
2700 2..=4 => SavedNetworksCount::TwoToFour,
2701 5..=40 => SavedNetworksCount::FiveToForty,
2702 41..=500 => SavedNetworksCount::FortyToFiveHundred,
2703 501.. => SavedNetworksCount::FiveHundredAndOneOrMore,
2704 };
2705 metric_events.push(MetricEvent {
2706 metric_id: metrics::SAVED_NETWORKS_MIGRATED_METRIC_ID,
2707 event_codes: vec![num_networks as u32],
2708 payload: MetricEventPayload::Count(1),
2709 });
2710
2711 use metrics::SavedConfigurationsForSavedNetworkMigratedMetricDimensionSavedConfigurations as ConfigCountDimension;
2713 for config_count in config_count_per_saved_network {
2714 let num_configs = match config_count {
2715 0 => ConfigCountDimension::Zero,
2716 1 => ConfigCountDimension::One,
2717 2..=4 => ConfigCountDimension::TwoToFour,
2718 5..=40 => ConfigCountDimension::FiveToForty,
2719 41..=500 => ConfigCountDimension::FortyToFiveHundred,
2720 501.. => ConfigCountDimension::FiveHundredAndOneOrMore,
2721 };
2722 metric_events.push(MetricEvent {
2723 metric_id: metrics::SAVED_CONFIGURATIONS_FOR_SAVED_NETWORK_MIGRATED_METRIC_ID,
2724 event_codes: vec![num_configs as u32],
2725 payload: MetricEventPayload::Count(1),
2726 });
2727 }
2728
2729 self.throttled_error_logger.throttle_error(log_cobalt_batch!(
2730 self.cobalt_proxy,
2731 &metric_events,
2732 "log_saved_network_counts",
2733 ));
2734 }
2735
2736 async fn log_network_selection_scan_interval(
2737 &mut self,
2738 time_since_last_scan: zx::MonotonicDuration,
2739 ) {
2740 self.throttled_error_logger.throttle_error(log_cobalt!(
2741 self.cobalt_proxy,
2742 log_integer,
2743 metrics::LAST_SCAN_AGE_WHEN_SCAN_REQUESTED_MIGRATED_METRIC_ID,
2744 time_since_last_scan.into_micros(),
2745 &[],
2746 ));
2747 }
2748
2749 async fn log_connection_selection_scan_results(
2750 &mut self,
2751 saved_network_count: usize,
2752 bss_count_per_saved_network: Vec<usize>,
2753 saved_network_count_found_by_active_scan: usize,
2754 ) {
2755 let mut metric_events = vec![];
2756
2757 use metrics::SavedNetworkInScanResultMigratedMetricDimensionBssCount as BssCount;
2758 for bss_count in bss_count_per_saved_network {
2759 let bss_count_metric = match bss_count {
2761 0 => BssCount::Zero, 1 => BssCount::One,
2763 2..=4 => BssCount::TwoToFour,
2764 5..=10 => BssCount::FiveToTen,
2765 11..=20 => BssCount::ElevenToTwenty,
2766 21.. => BssCount::TwentyOneOrMore,
2767 };
2768 metric_events.push(MetricEvent {
2769 metric_id: metrics::SAVED_NETWORK_IN_SCAN_RESULT_MIGRATED_METRIC_ID,
2770 event_codes: vec![bss_count_metric as u32],
2771 payload: MetricEventPayload::Count(1),
2772 });
2773 }
2774
2775 use metrics::ScanResultsReceivedMigratedMetricDimensionSavedNetworksCount as SavedNetworkCount;
2776 let saved_network_count_metric = match saved_network_count {
2777 0 => SavedNetworkCount::Zero,
2778 1 => SavedNetworkCount::One,
2779 2..=4 => SavedNetworkCount::TwoToFour,
2780 5..=20 => SavedNetworkCount::FiveToTwenty,
2781 21..=40 => SavedNetworkCount::TwentyOneToForty,
2782 41.. => SavedNetworkCount::FortyOneOrMore,
2783 };
2784 metric_events.push(MetricEvent {
2785 metric_id: metrics::SCAN_RESULTS_RECEIVED_MIGRATED_METRIC_ID,
2786 event_codes: vec![saved_network_count_metric as u32],
2787 payload: MetricEventPayload::Count(1),
2788 });
2789
2790 use metrics::SavedNetworkInScanResultWithActiveScanMigratedMetricDimensionActiveScanSsidsObserved as ActiveScanSsidsObserved;
2791 let actively_scanned_networks_metrics = match saved_network_count_found_by_active_scan {
2792 0 => ActiveScanSsidsObserved::Zero,
2793 1 => ActiveScanSsidsObserved::One,
2794 2..=4 => ActiveScanSsidsObserved::TwoToFour,
2795 5..=10 => ActiveScanSsidsObserved::FiveToTen,
2796 11..=20 => ActiveScanSsidsObserved::ElevenToTwenty,
2797 21..=50 => ActiveScanSsidsObserved::TwentyOneToFifty,
2798 51..=100 => ActiveScanSsidsObserved::FiftyOneToOneHundred,
2799 101.. => ActiveScanSsidsObserved::OneHundredAndOneOrMore,
2800 };
2801 metric_events.push(MetricEvent {
2802 metric_id: metrics::SAVED_NETWORK_IN_SCAN_RESULT_WITH_ACTIVE_SCAN_MIGRATED_METRIC_ID,
2803 event_codes: vec![actively_scanned_networks_metrics as u32],
2804 payload: MetricEventPayload::Count(1),
2805 });
2806
2807 self.throttled_error_logger.throttle_error(log_cobalt_batch!(
2808 self.cobalt_proxy,
2809 &metric_events,
2810 "log_connection_selection_scan_results",
2811 ));
2812 }
2813
2814 async fn log_establish_connection_cobalt_metrics(
2815 &mut self,
2816 policy_connect_reason: Option<client::types::ConnectReason>,
2817 code: fidl_ieee80211::StatusCode,
2818 multiple_bss_candidates: bool,
2819 ap_state: &client::types::ApState,
2820 connect_start_time: Option<fasync::MonotonicInstant>,
2821 ) {
2822 let metric_events = self.build_establish_connection_cobalt_metrics(
2823 policy_connect_reason,
2824 code,
2825 multiple_bss_candidates,
2826 ap_state,
2827 connect_start_time,
2828 );
2829 self.throttled_error_logger.throttle_error(log_cobalt_batch!(
2830 self.cobalt_proxy,
2831 &metric_events,
2832 "log_establish_connection_cobalt_metrics",
2833 ));
2834 }
2835
2836 fn build_establish_connection_cobalt_metrics(
2837 &mut self,
2838 policy_connect_reason: Option<client::types::ConnectReason>,
2839 code: fidl_ieee80211::StatusCode,
2840 multiple_bss_candidates: bool,
2841 ap_state: &client::types::ApState,
2842 connect_start_time: Option<fasync::MonotonicInstant>,
2843 ) -> Vec<MetricEvent> {
2844 let mut metric_events = vec![];
2845 if let Some(policy_connect_reason) = policy_connect_reason {
2846 metric_events.push(MetricEvent {
2847 metric_id: metrics::POLICY_CONNECTION_ATTEMPT_MIGRATED_METRIC_ID,
2848 event_codes: vec![policy_connect_reason as u32],
2849 payload: MetricEventPayload::Count(1),
2850 });
2851
2852 match policy_connect_reason {
2854 metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::FidlConnectRequest
2855 | metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::ProactiveNetworkSwitch
2856 | metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::IdleInterfaceAutoconnect
2857 | metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::NewSavedNetworkAutoconnect => {
2858 metric_events.push(MetricEvent {
2859 metric_id: metrics::POLICY_CONNECTION_ATTEMPTS_METRIC_ID,
2860 event_codes: vec![],
2861 payload: MetricEventPayload::Count(1),
2862 });
2863 }
2864 metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::RetryAfterDisconnectDetected
2865 | metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::RetryAfterFailedConnectAttempt
2866 | metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::RegulatoryChangeReconnect => (),
2867 }
2868 }
2869
2870 if code != fidl_ieee80211::StatusCode::Success {
2871 return metric_events;
2872 }
2873
2874 match connect_start_time {
2875 Some(start_time) => {
2876 let user_wait_time = fasync::MonotonicInstant::now() - start_time;
2877 let user_wait_time_dim = convert::convert_user_wait_time(user_wait_time);
2878 metric_events.push(MetricEvent {
2879 metric_id: metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID,
2880 event_codes: vec![user_wait_time_dim as u32],
2881 payload: MetricEventPayload::Count(1),
2882 });
2883 }
2884 None => warn!(
2885 "Metric for user wait time on connect is not logged because \
2886 the start time is not populated"
2887 ),
2888 }
2889
2890 let is_multi_bss_dim = convert::convert_is_multi_bss(multiple_bss_candidates);
2891 metric_events.push(MetricEvent {
2892 metric_id: metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID,
2893 event_codes: vec![is_multi_bss_dim as u32],
2894 payload: MetricEventPayload::Count(1),
2895 });
2896
2897 let security_type_dim = convert::convert_security_type(&ap_state.original().protection());
2898 metric_events.push(MetricEvent {
2899 metric_id: metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID,
2900 event_codes: vec![security_type_dim as u32],
2901 payload: MetricEventPayload::Count(1),
2902 });
2903
2904 metric_events.push(MetricEvent {
2905 metric_id: metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
2906 event_codes: vec![ap_state.tracked.channel.primary as u32],
2907 payload: MetricEventPayload::Count(1),
2908 });
2909
2910 let channel_band_dim = convert::convert_channel_band(ap_state.tracked.channel.band);
2911 metric_events.push(MetricEvent {
2912 metric_id: metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
2913 event_codes: vec![channel_band_dim as u32],
2914 payload: MetricEventPayload::Count(1),
2915 });
2916
2917 metric_events
2918 }
2919
2920 async fn log_downtime_cobalt_metrics(
2921 &mut self,
2922 downtime: zx::MonotonicDuration,
2923 disconnect_info: &DisconnectInfo,
2924 ) {
2925 let disconnect_source_dim =
2926 convert::convert_disconnect_source(&disconnect_info.disconnect_source);
2927 self.throttled_error_logger.throttle_error(log_cobalt!(
2928 self.cobalt_proxy,
2929 log_integer,
2930 metrics::DOWNTIME_BREAKDOWN_BY_DISCONNECT_REASON_METRIC_ID,
2931 downtime.into_micros(),
2932 &[
2933 disconnect_info.disconnect_source.cobalt_reason_code() as u32,
2934 disconnect_source_dim as u32
2935 ],
2936 ));
2937 }
2938
2939 async fn log_reconnect_cobalt_metrics(
2940 &mut self,
2941 reconnect_duration: zx::MonotonicDuration,
2942 disconnect_reason: fidl_sme::DisconnectSource,
2943 ) {
2944 let mut metric_events = vec![];
2945
2946 if !disconnect_reason.has_roaming_cause() {
2949 metric_events.push(MetricEvent {
2950 metric_id: metrics::NON_ROAM_RECONNECT_DURATION_METRIC_ID,
2951 event_codes: vec![],
2952 payload: MetricEventPayload::IntegerValue(reconnect_duration.into_micros()),
2953 });
2954 }
2955
2956 self.throttled_error_logger.throttle_error(log_cobalt_batch!(
2957 self.cobalt_proxy,
2958 &metric_events,
2959 "log_reconnect_cobalt_metrics",
2960 ));
2961 }
2962
2963 async fn log_device_connected_cobalt_metrics(
2966 &mut self,
2967 multiple_bss_candidates: bool,
2968 _ap_state: &client::types::ApState,
2969 network_is_likely_hidden: bool,
2970 ) {
2971 let mut metric_events = vec![];
2972
2973 let is_multi_bss_dim = convert::convert_is_multi_bss(multiple_bss_candidates);
2974 metric_events.push(MetricEvent {
2975 metric_id: metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID,
2976 event_codes: vec![is_multi_bss_dim as u32],
2977 payload: MetricEventPayload::Count(1),
2978 });
2979
2980 if network_is_likely_hidden {
2981 metric_events.push(MetricEvent {
2982 metric_id: metrics::CONNECT_TO_LIKELY_HIDDEN_NETWORK_METRIC_ID,
2983 event_codes: vec![],
2984 payload: MetricEventPayload::Count(1),
2985 });
2986 }
2987
2988 self.throttled_error_logger.throttle_error(log_cobalt_batch!(
2989 self.cobalt_proxy,
2990 &metric_events,
2991 "log_device_connected_cobalt_metrics",
2992 ));
2993 }
2994
2995 async fn log_device_connected_channel_cobalt_metrics(&mut self, channel: Channel) {
2996 let mut metric_events = vec![];
2997
2998 append_device_connected_channel_cobalt_metrics(&mut metric_events, channel);
2999
3000 self.throttled_error_logger.throttle_error(log_cobalt_batch!(
3001 self.cobalt_proxy,
3002 &metric_events,
3003 "log_device_connected_channel_cobalt_metrics",
3004 ));
3005 }
3006
3007 async fn log_policy_roam_scan_metrics(&mut self, reasons: Vec<RoamReason>) {
3008 self.throttled_error_logger.throttle_error(log_cobalt!(
3009 self.cobalt_proxy,
3010 log_occurrence,
3011 metrics::POLICY_ROAM_SCAN_COUNT_METRIC_ID,
3012 1,
3013 &[],
3014 ));
3015 for reason in reasons {
3016 self.throttled_error_logger.throttle_error(log_cobalt!(
3017 self.cobalt_proxy,
3018 log_occurrence,
3019 metrics::POLICY_ROAM_SCAN_COUNT_BY_ROAM_REASON_METRIC_ID,
3020 1,
3021 &[convert::convert_roam_reason_dimension(reason) as u32],
3022 ));
3023 }
3024 }
3025
3026 async fn log_policy_roam_attempt_metrics(
3027 &mut self,
3028 request: PolicyRoamRequest,
3029 connected_duration: zx::MonotonicDuration,
3030 ) {
3031 self.throttled_error_logger.throttle_error(log_cobalt!(
3032 self.cobalt_proxy,
3033 log_occurrence,
3034 metrics::POLICY_ROAM_ATTEMPT_COUNT_METRIC_ID,
3035 1,
3036 &[],
3037 ));
3038 for reason in &request.reasons {
3039 self.throttled_error_logger.throttle_error(log_cobalt!(
3040 self.cobalt_proxy,
3041 log_occurrence,
3042 metrics::POLICY_ROAM_ATTEMPT_COUNT_BY_ROAM_REASON_METRIC_ID,
3043 1,
3044 &[convert::convert_roam_reason_dimension(*reason) as u32],
3045 ));
3046 self.throttled_error_logger.throttle_error(log_cobalt!(
3047 self.cobalt_proxy,
3048 log_integer,
3049 metrics::POLICY_ROAM_CONNECTED_DURATION_BEFORE_ROAM_ATTEMPT_METRIC_ID,
3050 connected_duration.into_minutes(),
3051 &[convert::convert_roam_reason_dimension(*reason) as u32],
3052 ));
3053 }
3054 self.log_stat(StatOp::AddPolicyRoamAttemptsCount(request.reasons)).await;
3055 }
3056
3057 async fn log_roam_result_metrics(
3058 &mut self,
3059 result: fidl_sme::RoamResult,
3060 updated_ap_state: client::types::ApState,
3061 original_ap_state: Box<client::types::ApState>,
3062 request: Box<PolicyRoamRequest>,
3063 request_time: fasync::MonotonicInstant,
3064 result_time: fasync::MonotonicInstant,
3065 ) {
3066 let was_roam_successful = if result.status_code == fidl_ieee80211::StatusCode::Success {
3069 metrics::PolicyRoamAttemptCountDetailedMetricDimensionWasRoamSuccessful::Yes as u32
3070 } else {
3071 metrics::PolicyRoamAttemptCountDetailedMetricDimensionWasRoamSuccessful::No as u32
3072 };
3073 let ghz_band_transition = convert::get_ghz_band_transition(
3074 &original_ap_state.tracked.channel,
3075 &request.candidate.bss.channel,
3076 ) as u32;
3077 for reason in &request.reasons {
3078 self.throttled_error_logger.throttle_error(log_cobalt!(
3081 self.cobalt_proxy,
3082 log_occurrence,
3083 metrics::POLICY_ROAM_ATTEMPT_COUNT_DETAILED_METRIC_ID,
3084 1,
3085 &[
3086 convert::convert_roam_reason_dimension(*reason) as u32,
3087 was_roam_successful,
3088 ghz_band_transition,
3089 0, ],
3091 ));
3092 self.throttled_error_logger.throttle_error(log_cobalt!(
3093 self.cobalt_proxy,
3094 log_occurrence,
3095 metrics::POLICY_ROAM_ATTEMPT_COUNT_DETAILED_2_METRIC_ID,
3096 1,
3097 &[
3098 convert::convert_roam_reason_dimension(*reason) as u32,
3099 was_roam_successful,
3100 ghz_band_transition,
3101 ],
3102 ));
3103 }
3104
3105 if result.original_association_maintained {
3107 return;
3108 }
3109
3110 self.throttled_error_logger.throttle_error(log_cobalt!(
3114 self.cobalt_proxy,
3115 log_occurrence,
3116 metrics::POLICY_ROAM_DISCONNECT_COUNT_METRIC_ID,
3117 1,
3118 &[],
3119 ));
3120 self.log_stat(StatOp::AddPolicyRoamDisconnectsCount).await;
3122 for reason in &request.reasons {
3124 self.throttled_error_logger.throttle_error(log_cobalt!(
3125 self.cobalt_proxy,
3126 log_occurrence,
3127 metrics::POLICY_ROAM_DISCONNECT_COUNT_BY_ROAM_REASON_METRIC_ID,
3128 1,
3129 &[convert::convert_roam_reason_dimension(*reason) as u32],
3130 ));
3131 }
3132 self.throttled_error_logger.throttle_error(log_cobalt!(
3134 self.cobalt_proxy,
3135 log_occurrence,
3136 metrics::TOTAL_ROAM_DISCONNECT_COUNT_METRIC_ID,
3137 1,
3138 &[],
3139 ));
3140
3141 if result.status_code == fidl_ieee80211::StatusCode::Success {
3142 self.log_stat(StatOp::AddPolicyRoamSuccessfulCount(request.reasons.clone())).await;
3143 self.throttled_error_logger.throttle_error(log_cobalt!(
3144 self.cobalt_proxy,
3145 log_integer,
3146 metrics::POLICY_ROAM_RECONNECT_DURATION_METRIC_ID,
3147 fasync::MonotonicDuration::from(result_time - request_time).into_micros(),
3148 &[],
3149 ));
3150
3151 let rssi_delta = (updated_ap_state.tracked.signal.rssi_dbm)
3153 .saturating_sub(original_ap_state.tracked.signal.rssi_dbm);
3154 for reason in &request.reasons {
3155 self.throttled_error_logger.throttle_error(log_cobalt!(
3156 self.cobalt_proxy,
3157 log_integer,
3158 metrics::POLICY_ROAM_TRANSITION_RSSI_DELTA_BY_ROAM_REASON_METRIC_ID,
3159 convert::calculate_rssi_delta_bucket(rssi_delta),
3160 &[convert::convert_roam_reason_dimension(*reason) as u32],
3161 ))
3162 }
3163 }
3164 }
3165
3166 async fn log_would_roam_connect(&mut self) {
3169 self.throttled_error_logger.throttle_error(log_cobalt!(
3170 self.cobalt_proxy,
3171 log_occurrence,
3172 metrics::POLICY_ROAM_ATTEMPT_COUNT_METRIC_ID,
3173 1,
3174 &[],
3175 ));
3176 }
3177
3178 async fn log_stop_client_connections_request(
3179 &mut self,
3180 enabled_duration: zx::MonotonicDuration,
3181 ) {
3182 self.throttled_error_logger.throttle_error(log_cobalt!(
3183 self.cobalt_proxy,
3184 log_integer,
3185 metrics::CLIENT_CONNECTIONS_ENABLED_DURATION_MIGRATED_METRIC_ID,
3186 enabled_duration.into_micros(),
3187 &[],
3188 ));
3189 }
3190
3191 async fn log_stop_ap_cobalt_metrics(&mut self, enabled_duration: zx::MonotonicDuration) {
3192 self.throttled_error_logger.throttle_error(log_cobalt!(
3193 self.cobalt_proxy,
3194 log_integer,
3195 metrics::ACCESS_POINT_ENABLED_DURATION_MIGRATED_METRIC_ID,
3196 enabled_duration.into_micros(),
3197 &[],
3198 ));
3199 }
3200
3201 async fn log_signal_report_metrics(&mut self, rssi: i8) {
3202 let index = min(130, rssi as i16 + 129) as u32;
3210 let entry = self
3211 .rssi_hist
3212 .entry(index)
3213 .or_insert(fidl_fuchsia_metrics::HistogramBucket { index, count: 0 });
3214 entry.count += 1;
3215 }
3216
3217 async fn log_signal_velocity_metrics(&mut self, rssi_velocity: f64) {
3218 const RSSI_VELOCITY_MIN_IDX: f64 = 0.0;
3222 const RSSI_VELOCITY_MAX_IDX: f64 = 22.0;
3223 const RSSI_VELOCITY_HIST_OFFSET: f64 = 11.0;
3224 let index = (rssi_velocity + RSSI_VELOCITY_HIST_OFFSET)
3225 .clamp(RSSI_VELOCITY_MIN_IDX, RSSI_VELOCITY_MAX_IDX) as u32;
3226 let entry = self
3227 .rssi_velocity_hist
3228 .entry(index)
3229 .or_insert(fidl_fuchsia_metrics::HistogramBucket { index, count: 0 });
3230 entry.count += 1;
3231 }
3232
3233 async fn log_iface_creation_result(&mut self, result: Result<(), ()>) {
3234 if let Some(reason) = self.recovery_record.create_iface_failure.take() {
3235 match result {
3236 Ok(()) => self.log_post_recovery_result(reason, RecoveryOutcome::Success).await,
3237 Err(()) => self.log_post_recovery_result(reason, RecoveryOutcome::Failure).await,
3238 }
3239 }
3240 }
3241
3242 async fn log_iface_destruction_result(&mut self, result: Result<(), ()>) {
3243 if let Some(reason) = self.recovery_record.destroy_iface_failure.take() {
3244 match result {
3245 Ok(()) => self.log_post_recovery_result(reason, RecoveryOutcome::Success).await,
3246 Err(()) => self.log_post_recovery_result(reason, RecoveryOutcome::Failure).await,
3247 }
3248 }
3249 }
3250
3251 async fn log_scan_issues(&mut self, issues: Vec<ScanIssue>) {
3252 if let Some(reason) = self.recovery_record.scan_failure.take() {
3255 let outcome = match issues.contains(&ScanIssue::ScanFailure) {
3256 true => RecoveryOutcome::Failure,
3257 false => RecoveryOutcome::Success,
3258 };
3259 self.log_post_recovery_result(reason, outcome).await;
3260 }
3261 if let Some(reason) = self.recovery_record.scan_cancellation.take() {
3262 let outcome = match issues.contains(&ScanIssue::AbortedScan) {
3263 true => RecoveryOutcome::Failure,
3264 false => RecoveryOutcome::Success,
3265 };
3266 self.log_post_recovery_result(reason, outcome).await;
3267 }
3268 if let Some(reason) = self.recovery_record.scan_results_empty.take() {
3269 let outcome = match issues.contains(&ScanIssue::EmptyScanResults) {
3270 true => RecoveryOutcome::Failure,
3271 false => RecoveryOutcome::Success,
3272 };
3273 self.log_post_recovery_result(reason, outcome).await;
3274 }
3275
3276 for issue in issues {
3278 self.throttled_error_logger.throttle_error(log_cobalt!(
3279 self.cobalt_proxy,
3280 log_occurrence,
3281 issue.as_metric_id(),
3282 1,
3283 &[]
3284 ))
3285 }
3286 }
3287
3288 async fn log_connection_failure(&mut self) {
3289 self.throttled_error_logger.throttle_error(log_cobalt!(
3290 self.cobalt_proxy,
3291 log_occurrence,
3292 metrics::CONNECTION_FAILURES_METRIC_ID,
3293 1,
3294 &[]
3295 ))
3296 }
3297
3298 async fn log_ap_start_result(&mut self, result: Result<(), ()>) {
3299 if result.is_err() {
3300 self.throttled_error_logger.throttle_error(log_cobalt!(
3301 self.cobalt_proxy,
3302 log_occurrence,
3303 metrics::AP_START_FAILURE_METRIC_ID,
3304 1,
3305 &[]
3306 ))
3307 }
3308
3309 if let Some(reason) = self.recovery_record.start_ap_failure.take() {
3310 match result {
3311 Ok(()) => self.log_post_recovery_result(reason, RecoveryOutcome::Success).await,
3312 Err(()) => self.log_post_recovery_result(reason, RecoveryOutcome::Failure).await,
3313 }
3314 }
3315 }
3316
3317 async fn log_scan_request_fulfillment_time(
3318 &mut self,
3319 duration: zx::MonotonicDuration,
3320 reason: client::scan::ScanReason,
3321 ) {
3322 let fulfillment_time_dim = {
3323 use metrics::ConnectivityWlanMetricDimensionScanFulfillmentTime::*;
3324 match duration.into_millis() {
3325 ..=0_000 => Unknown,
3326 1..=1_000 => LessThanOneSecond,
3327 1_001..=2_000 => LessThanTwoSeconds,
3328 2_001..=3_000 => LessThanThreeSeconds,
3329 3_001..=5_000 => LessThanFiveSeconds,
3330 5_001..=8_000 => LessThanEightSeconds,
3331 8_001..=13_000 => LessThanThirteenSeconds,
3332 13_001..=21_000 => LessThanTwentyOneSeconds,
3333 21_001..=34_000 => LessThanThirtyFourSeconds,
3334 34_001..=55_000 => LessThanFiftyFiveSeconds,
3335 55_001.. => MoreThanFiftyFiveSeconds,
3336 }
3337 };
3338 let reason_dim = {
3339 use client::scan::ScanReason;
3340 use metrics::ConnectivityWlanMetricDimensionScanReason::*;
3341 match reason {
3342 ScanReason::ClientRequest => ClientRequest,
3343 ScanReason::NetworkSelection => NetworkSelection,
3344 ScanReason::BssSelection => BssSelection,
3345 ScanReason::BssSelectionAugmentation => BssSelectionAugmentation,
3346 ScanReason::RoamSearch => ProactiveRoaming,
3347 }
3348 };
3349 self.throttled_error_logger.throttle_error(log_cobalt!(
3350 self.cobalt_proxy,
3351 log_occurrence,
3352 metrics::SUCCESSFUL_SCAN_REQUEST_FULFILLMENT_TIME_METRIC_ID,
3353 1,
3354 &[fulfillment_time_dim as u32, reason_dim as u32],
3355 ))
3356 }
3357
3358 async fn log_scan_queue_statistics(
3359 &mut self,
3360 fulfilled_requests: usize,
3361 remaining_requests: usize,
3362 ) {
3363 let fulfilled_requests_dim = {
3364 use metrics::ConnectivityWlanMetricDimensionScanRequestsFulfilled::*;
3365 match fulfilled_requests {
3366 0 => Zero,
3367 1 => One,
3368 2 => Two,
3369 3 => Three,
3370 4 => Four,
3371 5..=9 => FiveToNine,
3372 10.. => TenOrMore,
3373 }
3374 };
3375 let remaining_requests_dim = {
3376 use metrics::ConnectivityWlanMetricDimensionScanRequestsRemaining::*;
3377 match remaining_requests {
3378 0 => Zero,
3379 1 => One,
3380 2 => Two,
3381 3 => Three,
3382 4 => Four,
3383 5..=9 => FiveToNine,
3384 10..=14 => TenToFourteen,
3385 15.. => FifteenOrMore,
3386 }
3387 };
3388 self.throttled_error_logger.throttle_error(log_cobalt!(
3389 self.cobalt_proxy,
3390 log_occurrence,
3391 metrics::SCAN_QUEUE_STATISTICS_AFTER_COMPLETED_SCAN_METRIC_ID,
3392 1,
3393 &[fulfilled_requests_dim as u32, remaining_requests_dim as u32],
3394 ))
3395 }
3396
3397 async fn log_consecutive_counter_stats_failures(&mut self, count: i64) {
3398 self.throttled_error_logger.throttle_error(log_cobalt!(
3399 self.cobalt_proxy,
3400 log_integer,
3401 metrics::CONSECUTIVE_COUNTER_STATS_FAILURES_METRIC_ID,
3404 count,
3405 &[]
3406 ))
3407 }
3408
3409 async fn log_average_delta_metric_by_signal(
3418 &mut self,
3419 metric_id: u32,
3420 signals: Vec<client::types::TimestampedSignal>,
3421 baseline_signal: client::types::Signal,
3422 time_dimension: u32,
3423 ) {
3424 if signals.is_empty() {
3425 warn!("Signals list for time dimension {:?} is empty.", time_dimension);
3426 return;
3427 }
3428 let mut ewma_signal = EwmaSignalData::new(
3430 baseline_signal.rssi_dbm,
3431 baseline_signal.snr_db,
3432 EWMA_SMOOTHING_FACTOR_FOR_METRICS,
3433 );
3434 let mut velocity = RssiVelocity::new(baseline_signal.rssi_dbm);
3435 let baseline_score =
3436 client::connection_selection::scoring_functions::score_current_connection_signal_data(
3437 ewma_signal,
3438 0.0,
3439 );
3440 let score_dimension = {
3441 use metrics::AverageScoreDeltaAfterConnectionByInitialScoreMetricDimensionInitialScore::*;
3444 match baseline_score {
3445 u8::MIN..=20 => _0To20,
3446 21..=40 => _21To40,
3447 41..=60 => _41To60,
3448 61..=80 => _61To80,
3449 81..=u8::MAX => _81To100,
3450 }
3451 };
3452 let mut sum_score = baseline_score as u32;
3453
3454 for timed_signal in &signals {
3458 ewma_signal.update_with_new_measurement(
3459 timed_signal.signal.rssi_dbm,
3460 timed_signal.signal.snr_db,
3461 );
3462 velocity.update(ewma_signal.ewma_rssi.get());
3463 let score = client::connection_selection::scoring_functions::score_current_connection_signal_data(ewma_signal, velocity.get());
3464 sum_score = sum_score.saturating_add(score as u32);
3465 }
3466
3467 let avg_score = sum_score / (signals.len() + 1) as u32;
3469
3470 let delta = (avg_score as i64).saturating_sub(baseline_score as i64);
3471 self.throttled_error_logger.throttle_error(log_cobalt!(
3472 &self.cobalt_proxy,
3473 log_integer,
3474 metric_id,
3475 delta,
3476 &[score_dimension as u32, time_dimension],
3477 ));
3478 }
3479
3480 async fn log_average_rssi_delta_metric(
3487 &mut self,
3488 metric_id: u32,
3489 signals: Vec<client::types::TimestampedSignal>,
3490 baseline_signal: client::types::Signal,
3491 time_dimension: u32,
3492 ) {
3493 if signals.is_empty() {
3494 warn!("Signals list for time dimension {:?} is empty.", time_dimension);
3495 return;
3496 }
3497
3498 let rssi_dimension = {
3499 use metrics::AverageRssiDeltaAfterConnectionByInitialRssiMetricDimensionRssiBucket::*;
3500 match baseline_signal.rssi_dbm {
3501 i8::MIN..=-90 => From128To90,
3502 -89..=-86 => From89To86,
3503 -85..=-83 => From85To83,
3504 -82..=-80 => From82To80,
3505 -79..=-77 => From79To77,
3506 -76..=-74 => From76To74,
3507 -73..=-71 => From73To71,
3508 -70..=-66 => From70To66,
3509 -65..=-61 => From65To61,
3510 -60..=-51 => From60To51,
3511 -50..=-35 => From50To35,
3512 -34..=-28 => From34To28,
3513 -27..=-1 => From27To1,
3514 0..=i8::MAX => _0,
3515 }
3516 };
3517 let mut sum_rssi = baseline_signal.rssi_dbm as i64;
3519 for s in &signals {
3520 sum_rssi = sum_rssi.saturating_add(s.signal.rssi_dbm as i64);
3521 }
3522 let average_rssi = sum_rssi / (signals.len() + 1) as i64;
3523
3524 let delta = (average_rssi).saturating_sub(baseline_signal.rssi_dbm as i64);
3525 self.throttled_error_logger.throttle_error(log_cobalt!(
3526 &self.cobalt_proxy,
3527 log_integer,
3528 metric_id,
3529 delta,
3530 &[rssi_dimension as u32, time_dimension],
3531 ));
3532 }
3533
3534 async fn log_post_connection_score_deltas_by_signal(
3535 &mut self,
3536 connect_time: fasync::MonotonicInstant,
3537 signal_at_connect: client::types::Signal,
3538 signals: HistoricalList<client::types::TimestampedSignal>,
3539 ) {
3540 use metrics::AverageScoreDeltaAfterConnectionByInitialScoreMetricDimensionTimeSinceConnect as DurationDimension;
3544
3545 self.log_average_delta_metric_by_signal(
3546 metrics::AVERAGE_SCORE_DELTA_AFTER_CONNECTION_BY_INITIAL_SCORE_METRIC_ID,
3547 signals
3548 .get_between(connect_time, connect_time + zx::MonotonicDuration::from_millis(1100)),
3549 signal_at_connect,
3550 DurationDimension::OneSecond as u32,
3551 )
3552 .await;
3553
3554 self.log_average_delta_metric_by_signal(
3555 metrics::AVERAGE_SCORE_DELTA_AFTER_CONNECTION_BY_INITIAL_SCORE_METRIC_ID,
3556 signals
3557 .get_between(connect_time, connect_time + zx::MonotonicDuration::from_millis(5100)),
3558 signal_at_connect,
3559 DurationDimension::FiveSeconds as u32,
3560 )
3561 .await;
3562
3563 self.log_average_delta_metric_by_signal(
3564 metrics::AVERAGE_SCORE_DELTA_AFTER_CONNECTION_BY_INITIAL_SCORE_METRIC_ID,
3565 signals.get_between(
3566 connect_time,
3567 connect_time + zx::MonotonicDuration::from_millis(10100),
3568 ),
3569 signal_at_connect,
3570 DurationDimension::TenSeconds as u32,
3571 )
3572 .await;
3573
3574 self.log_average_delta_metric_by_signal(
3575 metrics::AVERAGE_SCORE_DELTA_AFTER_CONNECTION_BY_INITIAL_SCORE_METRIC_ID,
3576 signals.get_between(
3577 connect_time,
3578 connect_time + zx::MonotonicDuration::from_millis(30100),
3579 ),
3580 signal_at_connect,
3581 DurationDimension::ThirtySeconds as u32,
3582 )
3583 .await;
3584 }
3585
3586 async fn log_pre_disconnect_score_deltas_by_signal(
3587 &mut self,
3588 connect_duration: zx::MonotonicDuration,
3589 mut signals: HistoricalList<client::types::TimestampedSignal>,
3590 ) {
3591 use metrics::AverageScoreDeltaBeforeDisconnectByFinalScoreMetricDimensionTimeUntilDisconnect as DurationDimension;
3595 if connect_duration >= AVERAGE_SCORE_DELTA_MINIMUM_DURATION {
3596 if let Some(client::types::TimestampedSignal {
3598 signal: final_signal,
3599 time: final_signal_time,
3600 }) = signals.0.pop_back()
3601 {
3602 self.log_average_delta_metric_by_signal(
3603 metrics::AVERAGE_SCORE_DELTA_BEFORE_DISCONNECT_BY_FINAL_SCORE_METRIC_ID,
3604 signals
3605 .get_recent(final_signal_time - zx::MonotonicDuration::from_millis(1100)),
3606 final_signal,
3607 DurationDimension::OneSecond as u32,
3608 )
3609 .await;
3610 self.log_average_delta_metric_by_signal(
3611 metrics::AVERAGE_SCORE_DELTA_BEFORE_DISCONNECT_BY_FINAL_SCORE_METRIC_ID,
3612 signals
3613 .get_recent(final_signal_time - zx::MonotonicDuration::from_millis(5100)),
3614 final_signal,
3615 DurationDimension::FiveSeconds as u32,
3616 )
3617 .await;
3618 self.log_average_delta_metric_by_signal(
3619 metrics::AVERAGE_SCORE_DELTA_BEFORE_DISCONNECT_BY_FINAL_SCORE_METRIC_ID,
3620 signals
3621 .get_recent(final_signal_time - zx::MonotonicDuration::from_millis(10100)),
3622 final_signal,
3623 DurationDimension::TenSeconds as u32,
3624 )
3625 .await;
3626 self.log_average_delta_metric_by_signal(
3627 metrics::AVERAGE_SCORE_DELTA_BEFORE_DISCONNECT_BY_FINAL_SCORE_METRIC_ID,
3628 signals
3629 .get_recent(final_signal_time - zx::MonotonicDuration::from_millis(30100)),
3630 final_signal,
3631 DurationDimension::ThirtySeconds as u32,
3632 )
3633 .await;
3634 } else {
3635 warn!("Past signals list is unexpectedly empty");
3636 }
3637 }
3638 }
3639
3640 async fn log_post_connection_rssi_deltas(
3641 &mut self,
3642 connect_time: fasync::MonotonicInstant,
3643 signal_at_connect: client::types::Signal,
3644 signals: HistoricalList<client::types::TimestampedSignal>,
3645 ) {
3646 use metrics::AverageRssiDeltaAfterConnectionByInitialRssiMetricDimensionTimeSinceConnect as DurationDimension;
3650
3651 self.log_average_rssi_delta_metric(
3652 metrics::AVERAGE_RSSI_DELTA_AFTER_CONNECTION_BY_INITIAL_RSSI_METRIC_ID,
3653 signals
3654 .get_between(connect_time, connect_time + zx::MonotonicDuration::from_millis(1100)),
3655 signal_at_connect,
3656 DurationDimension::OneSecond as u32,
3657 )
3658 .await;
3659
3660 self.log_average_rssi_delta_metric(
3661 metrics::AVERAGE_RSSI_DELTA_AFTER_CONNECTION_BY_INITIAL_RSSI_METRIC_ID,
3662 signals
3663 .get_between(connect_time, connect_time + zx::MonotonicDuration::from_millis(5100)),
3664 signal_at_connect,
3665 DurationDimension::FiveSeconds as u32,
3666 )
3667 .await;
3668
3669 self.log_average_rssi_delta_metric(
3670 metrics::AVERAGE_RSSI_DELTA_AFTER_CONNECTION_BY_INITIAL_RSSI_METRIC_ID,
3671 signals.get_between(
3672 connect_time,
3673 connect_time + zx::MonotonicDuration::from_millis(10100),
3674 ),
3675 signal_at_connect,
3676 DurationDimension::TenSeconds as u32,
3677 )
3678 .await;
3679
3680 self.log_average_rssi_delta_metric(
3681 metrics::AVERAGE_RSSI_DELTA_AFTER_CONNECTION_BY_INITIAL_RSSI_METRIC_ID,
3682 signals.get_between(
3683 connect_time,
3684 connect_time + zx::MonotonicDuration::from_millis(30100),
3685 ),
3686 signal_at_connect,
3687 DurationDimension::ThirtySeconds as u32,
3688 )
3689 .await;
3690 }
3691
3692 async fn log_pre_disconnect_rssi_deltas(
3693 &mut self,
3694 connect_duration: zx::MonotonicDuration,
3695 mut signals: HistoricalList<client::types::TimestampedSignal>,
3696 ) {
3697 use metrics::AverageRssiDeltaAfterConnectionByInitialRssiMetricDimensionTimeSinceConnect as DurationDimension;
3701
3702 if connect_duration >= AVERAGE_SCORE_DELTA_MINIMUM_DURATION {
3703 if let Some(client::types::TimestampedSignal {
3705 signal: final_signal,
3706 time: final_signal_time,
3707 }) = signals.0.pop_back()
3708 {
3709 self.log_average_rssi_delta_metric(
3710 metrics::AVERAGE_RSSI_DELTA_BEFORE_DISCONNECT_BY_FINAL_RSSI_METRIC_ID,
3711 signals.get_between(
3712 final_signal_time - zx::MonotonicDuration::from_millis(1100),
3713 final_signal_time,
3714 ),
3715 final_signal,
3716 DurationDimension::OneSecond as u32,
3717 )
3718 .await;
3719
3720 self.log_average_rssi_delta_metric(
3721 metrics::AVERAGE_RSSI_DELTA_BEFORE_DISCONNECT_BY_FINAL_RSSI_METRIC_ID,
3722 signals.get_between(
3723 final_signal_time - zx::MonotonicDuration::from_millis(5100),
3724 final_signal_time,
3725 ),
3726 final_signal,
3727 DurationDimension::FiveSeconds as u32,
3728 )
3729 .await;
3730
3731 self.log_average_rssi_delta_metric(
3732 metrics::AVERAGE_RSSI_DELTA_BEFORE_DISCONNECT_BY_FINAL_RSSI_METRIC_ID,
3733 signals.get_between(
3734 final_signal_time - zx::MonotonicDuration::from_millis(10100),
3735 final_signal_time,
3736 ),
3737 final_signal,
3738 DurationDimension::TenSeconds as u32,
3739 )
3740 .await;
3741
3742 self.log_average_rssi_delta_metric(
3743 metrics::AVERAGE_RSSI_DELTA_BEFORE_DISCONNECT_BY_FINAL_RSSI_METRIC_ID,
3744 signals.get_between(
3745 final_signal_time - zx::MonotonicDuration::from_millis(30100),
3746 final_signal_time,
3747 ),
3748 final_signal,
3749 DurationDimension::ThirtySeconds as u32,
3750 )
3751 .await;
3752 }
3753 }
3754 }
3755
3756 async fn log_short_duration_connection_metrics(
3757 &mut self,
3758 signals: HistoricalList<client::types::TimestampedSignal>,
3759 disconnect_source: fidl_sme::DisconnectSource,
3760 previous_connect_reason: client::types::ConnectReason,
3761 ) {
3762 self.log_connection_score_average_by_signal(
3763 metrics::ConnectionScoreAverageMetricDimensionDuration::ShortDuration as u32,
3764 signals.get_before(fasync::MonotonicInstant::now()),
3765 )
3766 .await;
3767 self.log_connection_rssi_average(
3768 metrics::ConnectionRssiAverageMetricDimensionDuration::ShortDuration as u32,
3769 signals.get_before(fasync::MonotonicInstant::now()),
3770 )
3771 .await;
3772 match disconnect_source {
3775 fidl_sme::DisconnectSource::User(
3776 fidl_sme::UserDisconnectReason::FidlConnectRequest,
3777 )
3778 | fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::NetworkUnsaved) => {
3779 let metric_events = vec![
3780 MetricEvent {
3781 metric_id: metrics::POLICY_FIDL_CONNECTION_ATTEMPTS_DURING_SHORT_CONNECTION_METRIC_ID,
3782 event_codes: vec![],
3783 payload: MetricEventPayload::Count(1),
3784 },
3785 MetricEvent {
3786 metric_id: metrics::POLICY_FIDL_CONNECTION_ATTEMPTS_DURING_SHORT_CONNECTION_DETAILED_METRIC_ID,
3787 event_codes: vec![previous_connect_reason as u32],
3788 payload: MetricEventPayload::Count(1),
3789 }
3790 ];
3791
3792 self.throttled_error_logger.throttle_error(log_cobalt_batch!(
3793 self.cobalt_proxy,
3794 &metric_events,
3795 "log_short_duration_connection_metrics",
3796 ));
3797 }
3798 _ => {}
3799 }
3800 }
3801
3802 async fn log_network_selection_metrics(
3803 &mut self,
3804 connection_state: &mut ConnectionState,
3805 network_selection_type: NetworkSelectionType,
3806 num_candidates: Result<usize, ()>,
3807 selected_count: usize,
3808 ) {
3809 let now = fasync::MonotonicInstant::now();
3810 let mut metric_events = vec![];
3811 metric_events.push(MetricEvent {
3812 metric_id: metrics::NETWORK_SELECTION_COUNT_METRIC_ID,
3813 event_codes: vec![],
3814 payload: MetricEventPayload::Count(1),
3815 });
3816
3817 match num_candidates {
3818 Ok(n) if n > 0 => {
3819 if let ConnectionState::Disconnected(state) = connection_state
3823 && let Some(prev) = state.latest_no_saved_neighbor_time.take()
3824 {
3825 let duration = now - prev;
3826 state.accounted_no_saved_neighbor_duration += duration;
3827 self.queue_stat_op(StatOp::AddDowntimeNoSavedNeighborDuration(duration));
3828 }
3829
3830 if network_selection_type == NetworkSelectionType::Undirected {
3831 metric_events.push(MetricEvent {
3833 metric_id: metrics::NUM_NETWORKS_SELECTED_METRIC_ID,
3834 event_codes: vec![],
3835 payload: MetricEventPayload::IntegerValue(selected_count as i64),
3836 });
3837 }
3838 }
3839 Ok(0) if network_selection_type == NetworkSelectionType::Undirected => {
3840 if let ConnectionState::Disconnected(state) = connection_state
3844 && state.latest_no_saved_neighbor_time.is_none()
3845 {
3846 state.latest_no_saved_neighbor_time = Some(now);
3847 }
3848 }
3849 _ => (),
3850 }
3851
3852 self.throttled_error_logger.throttle_error(log_cobalt_batch!(
3853 self.cobalt_proxy,
3854 &metric_events,
3855 "log_network_selection_metrics",
3856 ));
3857 }
3858
3859 #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
3860 async fn log_bss_selection_metrics(
3861 &mut self,
3862 reason: client::types::ConnectReason,
3863 mut scored_candidates: Vec<(client::types::ScannedCandidate, i16)>,
3864 selected_candidate: Option<(client::types::ScannedCandidate, i16)>,
3865 ) {
3866 let mut metric_events = vec![];
3867
3868 metric_events.push(MetricEvent {
3870 metric_id: metrics::BSS_SELECTION_COUNT_METRIC_ID,
3871 event_codes: vec![],
3872 payload: MetricEventPayload::Count(1),
3873 });
3874
3875 metric_events.push(MetricEvent {
3877 metric_id: metrics::BSS_SELECTION_COUNT_DETAILED_METRIC_ID,
3878 event_codes: vec![reason as u32],
3879 payload: MetricEventPayload::Count(1),
3880 });
3881
3882 metric_events.push(MetricEvent {
3884 metric_id: metrics::NUM_BSS_CONSIDERED_IN_SELECTION_METRIC_ID,
3885 event_codes: vec![],
3886 payload: MetricEventPayload::IntegerValue(scored_candidates.len() as i64),
3887 });
3888 metric_events.push(MetricEvent {
3890 metric_id: metrics::NUM_BSS_CONSIDERED_IN_SELECTION_DETAILED_METRIC_ID,
3891 event_codes: vec![reason as u32],
3892 payload: MetricEventPayload::IntegerValue(scored_candidates.len() as i64),
3893 });
3894
3895 if !scored_candidates.is_empty() {
3896 let (mut best_score_2g, mut best_score_5g) = (None, None);
3897 let mut unique_networks = HashSet::new();
3898
3899 for (candidate, score) in &scored_candidates {
3900 metric_events.push(MetricEvent {
3902 metric_id: metrics::BSS_CANDIDATE_SCORE_METRIC_ID,
3903 event_codes: vec![],
3904 payload: MetricEventPayload::IntegerValue(*score as i64),
3905 });
3906
3907 let _ = unique_networks.insert(&candidate.network);
3908
3909 if candidate.bss.channel.band == fidl_ieee80211::WlanBand::TwoGhz {
3910 best_score_2g = best_score_2g.or(Some(*score)).map(|s| max(s, *score));
3911 } else {
3912 best_score_5g = best_score_5g.or(Some(*score)).map(|s| max(s, *score));
3913 }
3914 }
3915
3916 metric_events.push(MetricEvent {
3920 metric_id: metrics::NUM_NETWORKS_REPRESENTED_IN_BSS_SELECTION_METRIC_ID,
3921 event_codes: vec![reason as u32],
3922 payload: MetricEventPayload::IntegerValue(unique_networks.len() as i64),
3923 });
3924
3925 if let Some((_, score)) = selected_candidate {
3926 metric_events.push(MetricEvent {
3928 metric_id: metrics::SELECTED_BSS_SCORE_METRIC_ID,
3929 event_codes: vec![],
3930 payload: MetricEventPayload::IntegerValue(score as i64),
3931 });
3932
3933 scored_candidates.sort_by_key(|(_, score)| Reverse(*score));
3937 #[expect(clippy::get_first)]
3938 if let (Some(first_candidate), Some(second_candidate)) =
3939 (scored_candidates.get(0), scored_candidates.get(1))
3940 && score == first_candidate.1
3941 {
3942 let delta = first_candidate.1 - second_candidate.1;
3943 metric_events.push(MetricEvent {
3944 metric_id: metrics::RUNNER_UP_CANDIDATE_SCORE_DELTA_METRIC_ID,
3945 event_codes: vec![],
3946 payload: MetricEventPayload::IntegerValue(delta as i64),
3947 });
3948 }
3949 }
3950
3951 let ghz_event_code =
3952 if let (Some(score_2g), Some(score_5g)) = (best_score_2g, best_score_5g) {
3953 metric_events.push(MetricEvent {
3955 metric_id: metrics::BEST_CANDIDATES_GHZ_SCORE_DELTA_METRIC_ID,
3956 event_codes: vec![],
3957 payload: MetricEventPayload::IntegerValue((score_5g - score_2g) as i64),
3958 });
3959 metrics::ConnectivityWlanMetricDimensionBands::MultiBand
3960 } else if best_score_2g.is_some() {
3961 metrics::ConnectivityWlanMetricDimensionBands::Band2Dot4Ghz
3962 } else {
3963 metrics::ConnectivityWlanMetricDimensionBands::Band5Ghz
3964 };
3965
3966 metric_events.push(MetricEvent {
3967 metric_id: metrics::GHZ_BANDS_AVAILABLE_IN_BSS_SELECTION_METRIC_ID,
3968 event_codes: vec![ghz_event_code as u32],
3969 payload: MetricEventPayload::Count(1),
3970 });
3971 }
3972
3973 self.throttled_error_logger.throttle_error(log_cobalt_batch!(
3974 self.cobalt_proxy,
3975 &metric_events,
3976 "log_bss_selection_cobalt_metrics",
3977 ));
3978 }
3979
3980 async fn log_connection_score_average_by_signal(
3981 &mut self,
3982 duration_dim: u32,
3983 signals: Vec<client::types::TimestampedSignal>,
3984 ) {
3985 let Some(first_signal) = signals.first() else {
3986 warn!("Connection signals list is unexpectedly empty.");
3987 return;
3988 };
3989 let mut sum_scores = 0;
3990 let mut ewma_signal = EwmaSignalData::new(
3991 first_signal.signal.rssi_dbm,
3992 first_signal.signal.snr_db,
3993 EWMA_SMOOTHING_FACTOR_FOR_METRICS,
3994 );
3995 let mut velocity = RssiVelocity::new(first_signal.signal.rssi_dbm);
3996 for timed_signal in &signals {
3997 ewma_signal.update_with_new_measurement(
3998 timed_signal.signal.rssi_dbm,
3999 timed_signal.signal.snr_db,
4000 );
4001 velocity.update(ewma_signal.ewma_rssi.get());
4002 let score = client::connection_selection::scoring_functions::score_current_connection_signal_data(ewma_signal, velocity.get());
4003 sum_scores = sum_scores.saturating_add(&(score as u32));
4004 }
4005 let avg = sum_scores / (signals.len()) as u32;
4006 self.throttled_error_logger.throttle_error(log_cobalt!(
4007 self.cobalt_proxy,
4008 log_integer,
4009 metrics::CONNECTION_SCORE_AVERAGE_METRIC_ID,
4010 avg as i64,
4011 &[duration_dim],
4012 ));
4013 }
4014
4015 async fn log_connection_rssi_average(
4016 &mut self,
4017 duration_dim: u32,
4018 signals: Vec<client::types::TimestampedSignal>,
4019 ) {
4020 if signals.is_empty() {
4021 warn!("Connection signals list is unexpectedly empty.");
4022 return;
4023 }
4024 let mut sum_rssi: i64 = 0;
4025 for s in &signals {
4026 sum_rssi = sum_rssi.saturating_add(s.signal.rssi_dbm as i64);
4027 }
4028 let average_rssi = sum_rssi / (signals.len()) as i64;
4029 self.throttled_error_logger.throttle_error(log_cobalt!(
4030 self.cobalt_proxy,
4031 log_integer,
4032 metrics::CONNECTION_RSSI_AVERAGE_METRIC_ID,
4033 average_rssi,
4034 &[duration_dim]
4035 ));
4036 }
4037
4038 async fn log_recovery_occurrence(&mut self, reason: RecoveryReason) {
4039 self.recovery_record.record_recovery_attempt(reason);
4040
4041 let dimension = match reason {
4042 RecoveryReason::CreateIfaceFailure(_) => {
4043 metrics::RecoveryOccurrenceMetricDimensionReason::InterfaceCreationFailure
4044 }
4045 RecoveryReason::DestroyIfaceFailure(_) => {
4046 metrics::RecoveryOccurrenceMetricDimensionReason::InterfaceDestructionFailure
4047 }
4048 RecoveryReason::Timeout(_) => metrics::RecoveryOccurrenceMetricDimensionReason::Timeout,
4049 RecoveryReason::ConnectFailure(_) => {
4050 metrics::RecoveryOccurrenceMetricDimensionReason::ClientConnectionFailure
4051 }
4052 RecoveryReason::StartApFailure(_) => {
4053 metrics::RecoveryOccurrenceMetricDimensionReason::ApStartFailure
4054 }
4055 RecoveryReason::ScanFailure(_) => {
4056 metrics::RecoveryOccurrenceMetricDimensionReason::ScanFailure
4057 }
4058 RecoveryReason::ScanCancellation(_) => {
4059 metrics::RecoveryOccurrenceMetricDimensionReason::ScanCancellation
4060 }
4061 RecoveryReason::ScanResultsEmpty(_) => {
4062 metrics::RecoveryOccurrenceMetricDimensionReason::ScanResultsEmpty
4063 }
4064 };
4065
4066 self.throttled_error_logger.throttle_error(log_cobalt!(
4067 self.cobalt_proxy,
4068 log_occurrence,
4069 metrics::RECOVERY_OCCURRENCE_METRIC_ID,
4070 1,
4071 &[dimension.as_event_code()],
4072 ))
4073 }
4074
4075 async fn log_post_recovery_result(&mut self, reason: RecoveryReason, outcome: RecoveryOutcome) {
4076 async fn log_post_recovery_metric(
4077 throttled_error_logger: &mut ThrottledErrorLogger,
4078 proxy: &mut fidl_fuchsia_metrics::MetricEventLoggerProxy,
4079 metric_id: u32,
4080 event_codes: &[u32],
4081 ) {
4082 throttled_error_logger.throttle_error(log_cobalt!(
4083 proxy,
4084 log_occurrence,
4085 metric_id,
4086 1,
4087 event_codes,
4088 ))
4089 }
4090
4091 if outcome == RecoveryOutcome::Success {
4092 self.last_successful_recovery.set(fasync::MonotonicInstant::now().into_nanos() as u64);
4093 let _ = self.successful_recoveries.add(1);
4094 }
4095
4096 match reason {
4097 RecoveryReason::CreateIfaceFailure(_) => {
4098 log_post_recovery_metric(
4099 &mut self.throttled_error_logger,
4100 &mut self.cobalt_proxy,
4101 metrics::INTERFACE_CREATION_RECOVERY_OUTCOME_METRIC_ID,
4102 &[outcome.as_event_code()],
4103 )
4104 .await;
4105 }
4106 RecoveryReason::DestroyIfaceFailure(_) => {
4107 log_post_recovery_metric(
4108 &mut self.throttled_error_logger,
4109 &mut self.cobalt_proxy,
4110 metrics::INTERFACE_DESTRUCTION_RECOVERY_OUTCOME_METRIC_ID,
4111 &[outcome.as_event_code()],
4112 )
4113 .await;
4114 }
4115 RecoveryReason::Timeout(mechanism) => {
4116 log_post_recovery_metric(
4117 &mut self.throttled_error_logger,
4118 &mut self.cobalt_proxy,
4119 metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
4120 &[outcome.as_event_code(), mechanism.as_event_code()],
4121 )
4122 .await;
4123 }
4124 RecoveryReason::ConnectFailure(mechanism) => {
4125 log_post_recovery_metric(
4126 &mut self.throttled_error_logger,
4127 &mut self.cobalt_proxy,
4128 metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
4129 &[outcome.as_event_code(), mechanism.as_event_code()],
4130 )
4131 .await;
4132 }
4133 RecoveryReason::StartApFailure(mechanism) => {
4134 log_post_recovery_metric(
4135 &mut self.throttled_error_logger,
4136 &mut self.cobalt_proxy,
4137 metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
4138 &[outcome.as_event_code(), mechanism.as_event_code()],
4139 )
4140 .await;
4141 }
4142 RecoveryReason::ScanFailure(mechanism) => {
4143 log_post_recovery_metric(
4144 &mut self.throttled_error_logger,
4145 &mut self.cobalt_proxy,
4146 metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
4147 &[outcome.as_event_code(), mechanism.as_event_code()],
4148 )
4149 .await;
4150 }
4151 RecoveryReason::ScanCancellation(mechanism) => {
4152 log_post_recovery_metric(
4153 &mut self.throttled_error_logger,
4154 &mut self.cobalt_proxy,
4155 metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
4156 &[outcome.as_event_code(), mechanism.as_event_code()],
4157 )
4158 .await;
4159 }
4160 RecoveryReason::ScanResultsEmpty(mechanism) => {
4161 log_post_recovery_metric(
4162 &mut self.throttled_error_logger,
4163 &mut self.cobalt_proxy,
4164 metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
4165 &[outcome.as_event_code(), mechanism.as_event_code()],
4166 )
4167 .await;
4168 }
4169 }
4170 }
4171}
4172
4173fn append_device_connected_channel_cobalt_metrics(
4174 metric_events: &mut Vec<MetricEvent>,
4175 channel: Channel,
4176) {
4177 metric_events.push(MetricEvent {
4178 metric_id: metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
4179 event_codes: vec![channel.primary as u32],
4180 payload: MetricEventPayload::Count(1),
4181 });
4182
4183 let channel_band_dim = convert::convert_channel_band(channel.band);
4184 metric_events.push(MetricEvent {
4185 metric_id: metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
4186 event_codes: vec![channel_band_dim as u32],
4187 payload: MetricEventPayload::Count(1),
4188 });
4189}
4190
4191#[allow(clippy::enum_variant_names, reason = "mass allow for https://fxbug.dev/381896734")]
4192enum StatOp {
4193 AddTotalDuration(zx::MonotonicDuration),
4194 AddConnectedDuration(zx::MonotonicDuration),
4195 AddDowntimeDuration(zx::MonotonicDuration),
4196 AddDowntimeNoSavedNeighborDuration(zx::MonotonicDuration),
4198 AddConnectAttemptsCount,
4199 AddConnectSuccessfulCount,
4200 AddDisconnectCount(fidl_sme::DisconnectSource),
4201 AddPolicyRoamAttemptsCount(Vec<RoamReason>),
4202 AddPolicyRoamSuccessfulCount(Vec<RoamReason>),
4203 AddPolicyRoamDisconnectsCount,
4204 AddTxHighPacketDropDuration(zx::MonotonicDuration),
4205 AddRxHighPacketDropDuration(zx::MonotonicDuration),
4206 AddTxVeryHighPacketDropDuration(zx::MonotonicDuration),
4207 AddRxVeryHighPacketDropDuration(zx::MonotonicDuration),
4208 AddNoRxDuration(zx::MonotonicDuration),
4209}
4210
4211#[derive(Clone, PartialEq, Default)]
4212struct StatCounters {
4213 total_duration: zx::MonotonicDuration,
4214 connected_duration: zx::MonotonicDuration,
4215 downtime_duration: zx::MonotonicDuration,
4216 downtime_no_saved_neighbor_duration: zx::MonotonicDuration,
4217 connect_attempts_count: u64,
4218 connect_successful_count: u64,
4219 disconnect_count: u64,
4220 total_non_roam_disconnect_count: u64,
4221 total_roam_disconnect_count: u64,
4222 policy_roam_attempts_count: u64,
4223 policy_roam_successful_count: u64,
4224 policy_roam_disconnects_count: u64,
4225 policy_roam_attempts_count_by_roam_reason: HashMap<RoamReason, u64>,
4226 policy_roam_successful_count_by_roam_reason: HashMap<RoamReason, u64>,
4227 tx_high_packet_drop_duration: zx::MonotonicDuration,
4228 rx_high_packet_drop_duration: zx::MonotonicDuration,
4229 tx_very_high_packet_drop_duration: zx::MonotonicDuration,
4230 rx_very_high_packet_drop_duration: zx::MonotonicDuration,
4231 no_rx_duration: zx::MonotonicDuration,
4232}
4233
4234impl StatCounters {
4235 fn adjusted_downtime(&self) -> zx::MonotonicDuration {
4236 max(
4237 zx::MonotonicDuration::from_seconds(0),
4238 self.downtime_duration - self.downtime_no_saved_neighbor_duration,
4239 )
4240 }
4241
4242 fn connection_success_rate(&self) -> f64 {
4243 self.connect_successful_count as f64 / self.connect_attempts_count as f64
4244 }
4245
4246 fn policy_roam_success_rate(&self) -> f64 {
4247 self.policy_roam_successful_count as f64 / self.policy_roam_attempts_count as f64
4248 }
4249
4250 fn policy_roam_success_rate_by_roam_reason(&self, reason: &RoamReason) -> f64 {
4251 self.policy_roam_successful_count_by_roam_reason.get(reason).copied().unwrap_or(0) as f64
4252 / self.policy_roam_attempts_count_by_roam_reason.get(reason).copied().unwrap_or(0)
4253 as f64
4254 }
4255}
4256
4257impl Add for StatCounters {
4259 type Output = Self;
4260
4261 fn add(self, other: Self) -> Self {
4262 let mut policy_roam_attempts_count_by_roam_reason =
4264 other.policy_roam_attempts_count_by_roam_reason.clone();
4265 for (reason, count) in self.policy_roam_attempts_count_by_roam_reason {
4266 *policy_roam_attempts_count_by_roam_reason.entry(reason).or_insert(0) += count
4267 }
4268 let mut policy_roam_successful_count_by_roam_reason =
4269 other.policy_roam_successful_count_by_roam_reason.clone();
4270 for (reason, count) in self.policy_roam_successful_count_by_roam_reason {
4271 *policy_roam_successful_count_by_roam_reason.entry(reason).or_insert(0) += count
4272 }
4273
4274 Self {
4275 total_duration: self.total_duration + other.total_duration,
4276 connected_duration: self.connected_duration + other.connected_duration,
4277 downtime_duration: self.downtime_duration + other.downtime_duration,
4278 downtime_no_saved_neighbor_duration: self.downtime_no_saved_neighbor_duration
4279 + other.downtime_no_saved_neighbor_duration,
4280 connect_attempts_count: self.connect_attempts_count + other.connect_attempts_count,
4281 connect_successful_count: self.connect_successful_count
4282 + other.connect_successful_count,
4283 disconnect_count: self.disconnect_count + other.disconnect_count,
4284 total_non_roam_disconnect_count: self.total_non_roam_disconnect_count
4285 + other.total_non_roam_disconnect_count,
4286 total_roam_disconnect_count: self.total_roam_disconnect_count
4287 + other.total_roam_disconnect_count,
4288 policy_roam_attempts_count: self.policy_roam_attempts_count
4289 + other.policy_roam_attempts_count,
4290 policy_roam_successful_count: self.policy_roam_successful_count
4291 + other.policy_roam_successful_count,
4292 policy_roam_disconnects_count: self.policy_roam_disconnects_count
4293 + other.policy_roam_disconnects_count,
4294 policy_roam_attempts_count_by_roam_reason,
4295 policy_roam_successful_count_by_roam_reason,
4296 tx_high_packet_drop_duration: self.tx_high_packet_drop_duration
4297 + other.tx_high_packet_drop_duration,
4298 rx_high_packet_drop_duration: self.rx_high_packet_drop_duration
4299 + other.rx_high_packet_drop_duration,
4300 tx_very_high_packet_drop_duration: self.tx_very_high_packet_drop_duration
4301 + other.tx_very_high_packet_drop_duration,
4302 rx_very_high_packet_drop_duration: self.rx_very_high_packet_drop_duration
4303 + other.rx_very_high_packet_drop_duration,
4304 no_rx_duration: self.no_rx_duration + other.no_rx_duration,
4305 }
4306 }
4307}
4308
4309impl SaturatingAdd for StatCounters {
4310 fn saturating_add(&self, v: &Self) -> Self {
4311 let mut policy_roam_attempts_count_by_roam_reason =
4313 v.policy_roam_attempts_count_by_roam_reason.clone();
4314 for (reason, count) in &self.policy_roam_attempts_count_by_roam_reason {
4315 let _ = policy_roam_attempts_count_by_roam_reason
4316 .entry(*reason)
4317 .and_modify(|e| *e = e.saturating_add(*count))
4318 .or_insert(*count);
4319 }
4320 let mut policy_roam_successful_count_by_roam_reason =
4321 v.policy_roam_successful_count_by_roam_reason.clone();
4322 for (reason, count) in &self.policy_roam_successful_count_by_roam_reason {
4323 let _ = policy_roam_successful_count_by_roam_reason
4324 .entry(*reason)
4325 .and_modify(|e| *e = e.saturating_add(*count))
4326 .or_insert(*count);
4327 }
4328
4329 Self {
4330 total_duration: zx::MonotonicDuration::from_nanos(
4331 self.total_duration.into_nanos().saturating_add(v.total_duration.into_nanos()),
4332 ),
4333 connected_duration: zx::MonotonicDuration::from_nanos(
4334 self.connected_duration
4335 .into_nanos()
4336 .saturating_add(v.connected_duration.into_nanos()),
4337 ),
4338 downtime_duration: zx::MonotonicDuration::from_nanos(
4339 self.downtime_duration
4340 .into_nanos()
4341 .saturating_add(v.downtime_duration.into_nanos()),
4342 ),
4343 downtime_no_saved_neighbor_duration: zx::MonotonicDuration::from_nanos(
4344 self.downtime_no_saved_neighbor_duration
4345 .into_nanos()
4346 .saturating_add(v.downtime_no_saved_neighbor_duration.into_nanos()),
4347 ),
4348 connect_attempts_count: self
4349 .connect_attempts_count
4350 .saturating_add(v.connect_attempts_count),
4351 connect_successful_count: self
4352 .connect_successful_count
4353 .saturating_add(v.connect_successful_count),
4354 disconnect_count: self.disconnect_count.saturating_add(v.disconnect_count),
4355 total_non_roam_disconnect_count: self
4356 .total_non_roam_disconnect_count
4357 .saturating_add(v.total_non_roam_disconnect_count),
4358 total_roam_disconnect_count: self
4359 .total_roam_disconnect_count
4360 .saturating_add(v.total_roam_disconnect_count),
4361 policy_roam_attempts_count: self
4362 .policy_roam_attempts_count
4363 .saturating_add(v.policy_roam_attempts_count),
4364 policy_roam_successful_count: self
4365 .policy_roam_successful_count
4366 .saturating_add(v.policy_roam_successful_count),
4367 policy_roam_disconnects_count: self
4368 .policy_roam_disconnects_count
4369 .saturating_add(v.policy_roam_disconnects_count),
4370 policy_roam_attempts_count_by_roam_reason,
4371 policy_roam_successful_count_by_roam_reason,
4372 tx_high_packet_drop_duration: zx::MonotonicDuration::from_nanos(
4373 self.tx_high_packet_drop_duration
4374 .into_nanos()
4375 .saturating_add(v.tx_high_packet_drop_duration.into_nanos()),
4376 ),
4377 rx_high_packet_drop_duration: zx::MonotonicDuration::from_nanos(
4378 self.rx_high_packet_drop_duration
4379 .into_nanos()
4380 .saturating_add(v.rx_high_packet_drop_duration.into_nanos()),
4381 ),
4382 tx_very_high_packet_drop_duration: zx::MonotonicDuration::from_nanos(
4383 self.tx_very_high_packet_drop_duration
4384 .into_nanos()
4385 .saturating_add(v.tx_very_high_packet_drop_duration.into_nanos()),
4386 ),
4387 rx_very_high_packet_drop_duration: zx::MonotonicDuration::from_nanos(
4388 self.rx_very_high_packet_drop_duration
4389 .into_nanos()
4390 .saturating_add(v.rx_very_high_packet_drop_duration.into_nanos()),
4391 ),
4392 no_rx_duration: zx::MonotonicDuration::from_nanos(
4393 self.no_rx_duration.into_nanos().saturating_add(v.no_rx_duration.into_nanos()),
4394 ),
4395 }
4396 }
4397}
4398
4399#[derive(Debug)]
4400struct DailyDetailedStats {
4401 connect_attempts_status: HashMap<fidl_ieee80211::StatusCode, u64>,
4402 connect_per_is_multi_bss: HashMap<
4403 metrics::SuccessfulConnectBreakdownByIsMultiBssMetricDimensionIsMultiBss,
4404 ConnectAttemptsCounter,
4405 >,
4406 connect_per_security_type: HashMap<
4407 metrics::SuccessfulConnectBreakdownBySecurityTypeMetricDimensionSecurityType,
4408 ConnectAttemptsCounter,
4409 >,
4410 connect_per_primary_channel: HashMap<u8, ConnectAttemptsCounter>,
4411 connect_per_channel_band: HashMap<
4412 metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand,
4413 ConnectAttemptsCounter,
4414 >,
4415 connect_per_rssi_bucket:
4416 HashMap<metrics::ConnectivityWlanMetricDimensionRssiBucket, ConnectAttemptsCounter>,
4417 connect_per_snr_bucket:
4418 HashMap<metrics::ConnectivityWlanMetricDimensionSnrBucket, ConnectAttemptsCounter>,
4419}
4420
4421impl DailyDetailedStats {
4422 pub fn new() -> Self {
4423 Self {
4424 connect_attempts_status: HashMap::new(),
4425 connect_per_is_multi_bss: HashMap::new(),
4426 connect_per_security_type: HashMap::new(),
4427 connect_per_primary_channel: HashMap::new(),
4428 connect_per_channel_band: HashMap::new(),
4429 connect_per_rssi_bucket: HashMap::new(),
4430 connect_per_snr_bucket: HashMap::new(),
4431 }
4432 }
4433}
4434
4435#[derive(Debug, Default, Copy, Clone, PartialEq)]
4436struct ConnectAttemptsCounter {
4437 success: u64,
4438 total: u64,
4439}
4440
4441impl ConnectAttemptsCounter {
4442 fn increment(&mut self, code: fidl_ieee80211::StatusCode) {
4443 self.total += 1;
4444 if code == fidl_ieee80211::StatusCode::Success {
4445 self.success += 1;
4446 }
4447 }
4448}
4449
4450#[cfg(test)]
4451mod tests {
4452 use super::*;
4453 use crate::util::testing::{
4454 generate_disconnect_info, generate_policy_roam_request, generate_random_ap_state,
4455 generate_random_bss, generate_random_channel, generate_random_scanned_candidate,
4456 };
4457 use assert_matches::assert_matches;
4458 use diagnostics_assertions::{
4459 AnyBoolProperty, AnyNumericProperty, AnyStringProperty, NonZeroUintProperty,
4460 };
4461 use fidl::endpoints::create_proxy_and_stream;
4462 use fidl_fuchsia_metrics::{MetricEvent, MetricEventLoggerRequest, MetricEventPayload};
4463 use fidl_fuchsia_wlan_ieee80211::WlanBand::{FiveGhz, TwoGhz};
4464 use fuchsia_inspect::reader;
4465 use futures::TryStreamExt;
4466 use futures::stream::FusedStream;
4467 use futures::task::Poll;
4468 use ieee80211_testutils::{BSSID_REGEX, SSID_REGEX};
4469 use rand::Rng;
4470 use regex::Regex;
4471 use std::collections::VecDeque;
4472 use std::pin::{Pin, pin};
4473 use test_case::test_case;
4474 use test_util::assert_gt;
4475 use wlan_common::bss::BssDescription;
4476 use wlan_common::ie::IeType;
4477 use wlan_common::test_utils::fake_stas::IesOverrides;
4478 use wlan_common::{random_bss_description, random_fidl_bss_description};
4479
4480 const IFACE_ID: u16 = 1;
4481
4482 macro_rules! assert_data_tree_with_respond_blocking_req {
4485 ($test_helper:expr, $test_fut:expr, $($rest:tt)+) => {{
4486 use {
4487 fuchsia_inspect::reader, diagnostics_assertions::assert_data_tree,
4488 };
4489
4490 let inspector = $test_helper.inspector.clone();
4491 let read_fut = reader::read(&inspector);
4492 let mut read_fut = pin!(read_fut);
4493 loop {
4494 match $test_helper.exec.run_until_stalled(&mut read_fut) {
4495 Poll::Pending => {
4496 $test_helper.drain_cobalt_events(&mut $test_fut);
4499 $test_helper.telemetry_svc_streams.retain(|s| !s.is_terminated());
4501 for telemetry_svc_stream in &mut $test_helper.telemetry_svc_streams {
4502 respond_iface_histogram_stats_req(
4503 &mut $test_helper.exec,
4504 telemetry_svc_stream,
4505 );
4506 }
4507
4508 }
4509 Poll::Ready(result) => {
4510 let hierarchy = result.expect("failed to get hierarchy");
4511 assert_data_tree!(@executor $test_helper.exec, hierarchy, $($rest)+);
4512 break
4513 }
4514 }
4515 }
4516 }}
4517 }
4518
4519 #[fuchsia::test]
4520 fn test_detect_driver_unresponsive_signal_ind() {
4521 let (mut test_helper, mut test_fut) = setup_test();
4522 test_helper.send_connected_event(random_bss_description!(Wpa2));
4523
4524 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4525 stats: contains {
4526 is_driver_unresponsive: false,
4527 }
4528 });
4529
4530 test_helper.advance_by(
4531 UNRESPONSIVE_FLAG_MIN_DURATION - TELEMETRY_QUERY_INTERVAL,
4532 test_fut.as_mut(),
4533 );
4534 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4535 stats: contains {
4536 is_driver_unresponsive: false,
4537 }
4538 });
4539
4540 let ind = fidl_internal::SignalReportIndication { rssi_dbm: -40, snr_db: 30 };
4542 test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind });
4543
4544 test_helper.advance_by(UNRESPONSIVE_FLAG_MIN_DURATION, test_fut.as_mut());
4545 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4546 stats: contains {
4547 is_driver_unresponsive: false,
4548 }
4549 });
4550
4551 test_helper.advance_by(TELEMETRY_QUERY_INTERVAL, test_fut.as_mut());
4553 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4554 stats: contains {
4555 is_driver_unresponsive: true,
4556 }
4557 });
4558 }
4559
4560 #[fuchsia::test]
4561 fn test_histogram_stats_timeout() {
4562 let mut exec = fasync::TestExecutor::new();
4563
4564 let inspector = Inspector::default();
4565 let external_node = inspector.root().create_child("external");
4566 let external_inspect_node = ExternalInspectNode::new(external_node);
4567
4568 let (telemetry_sender, mut telemetry_receiver) =
4569 mpsc::channel::<TelemetryEvent>(TELEMETRY_EVENT_BUFFER_SIZE);
4570 let (defect_sender, mut defect_receiver) = mpsc::channel(100);
4571
4572 inspect_record_external_data(
4575 &external_inspect_node,
4576 TelemetrySender::new(telemetry_sender),
4577 defect_sender,
4578 );
4579
4580 let fut = reader::read(&inspector);
4582 let mut fut = pin!(fut);
4583 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4584
4585 let (telemetry_proxy, _telemetry_server) =
4589 fidl::endpoints::create_proxy::<fidl_sme::TelemetryMarker>();
4590 assert_matches!(
4591 telemetry_receiver.try_next(),
4592 Ok(Some(TelemetryEvent::QueryStatus {sender})) => {
4593 sender.send(QueryStatusResult {
4594 connection_state: ConnectionStateInfo::Connected {
4595 iface_id: 0,
4596 ap_state: Box::new(random_bss_description!(Wpa2).into()),
4597 telemetry_proxy: Some(telemetry_proxy)
4598 }
4599 }).expect("failed to send query status result")
4600 }
4601 );
4602 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4603
4604 assert!(exec.wake_next_timer().is_some());
4606 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(_));
4607
4608 assert_matches!(
4610 defect_receiver.try_next(),
4611 Ok(Some(Defect::Iface(IfaceFailure::Timeout {
4612 iface_id: 0,
4613 source: TimeoutSource::GetHistogramStats,
4614 })))
4615 );
4616 }
4617
4618 #[fuchsia::test]
4619 fn test_telemetry_timeout() {
4620 let mut exec = fasync::TestExecutor::new();
4621
4622 let (sender, _receiver) = mpsc::channel::<TelemetryEvent>(TELEMETRY_EVENT_BUFFER_SIZE);
4624 let (monitor_svc_proxy, _monitor_svc_stream) =
4625 create_proxy_and_stream::<fidl_fuchsia_wlan_device_service::DeviceMonitorMarker>();
4626 let (cobalt_proxy, _cobalt_stream) =
4627 create_proxy_and_stream::<fidl_fuchsia_metrics::MetricEventLoggerMarker>();
4628 let inspector = Inspector::default();
4629 let inspect_node = inspector.root().create_child("stats");
4630 let external_inspect_node = inspector.root().create_child("external");
4631 let (defect_sender, mut defect_receiver) = mpsc::channel(100);
4632
4633 let mut telemetry = Telemetry::new(
4634 TelemetrySender::new(sender),
4635 monitor_svc_proxy,
4636 cobalt_proxy.clone(),
4637 inspect_node,
4638 external_inspect_node,
4639 defect_sender,
4640 );
4641
4642 let (telemetry_proxy, _telemetry_server) =
4644 fidl::endpoints::create_proxy::<fidl_sme::TelemetryMarker>();
4645 telemetry.connection_state = ConnectionState::Connected(Box::new(ConnectedState {
4646 iface_id: 0,
4647 ap_state: Box::new(random_bss_description!(Wpa2).into()),
4648 telemetry_proxy: Some(telemetry_proxy),
4649
4650 new_connect_start_time: None,
4652 prev_connection_stats: None,
4653 multiple_bss_candidates: false,
4654 network_is_likely_hidden: false,
4655 last_signal_report: fasync::MonotonicInstant::now(),
4656 num_consecutive_get_counter_stats_failures: InspectableU64::new(
4657 0,
4658 &telemetry.inspect_node,
4659 "num_consecutive_get_counter_stats_failures",
4660 ),
4661 is_driver_unresponsive: InspectableBool::new(
4662 false,
4663 &telemetry.inspect_node,
4664 "is_driver_unresponsive",
4665 ),
4666 }));
4667
4668 let fut = telemetry.handle_periodic_telemetry();
4670 let mut fut = pin!(fut);
4671 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4672
4673 assert!(exec.wake_next_timer().is_some());
4675 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4676
4677 assert_matches!(
4679 defect_receiver.try_next(),
4680 Ok(Some(Defect::Iface(IfaceFailure::Timeout {
4681 iface_id: 0,
4682 source: TimeoutSource::GetIfaceStats,
4683 })))
4684 );
4685 }
4686
4687 #[fuchsia::test]
4688 fn test_logging_num_consecutive_get_iface_stats_failures() {
4689 let (mut test_helper, mut test_fut) = setup_test();
4690 test_helper.set_iface_stats_resp(Box::new(|| Err(zx::sys::ZX_ERR_TIMED_OUT)));
4691 test_helper.send_connected_event(random_bss_description!(Wpa2));
4692
4693 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4694 stats: contains {
4695 num_consecutive_get_counter_stats_failures: 0u64,
4696 }
4697 });
4698
4699 test_helper.advance_by(TELEMETRY_QUERY_INTERVAL * 20i64, test_fut.as_mut());
4700 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4701 stats: contains {
4702 num_consecutive_get_counter_stats_failures: 20u64,
4703 }
4704 });
4705
4706 test_helper.drain_cobalt_events(&mut test_fut);
4708 let logged_metrics =
4709 test_helper.get_logged_metrics(metrics::CONSECUTIVE_COUNTER_STATS_FAILURES_METRIC_ID);
4710 assert_eq!(logged_metrics.len(), 20);
4711
4712 assert_eq!(
4713 logged_metrics[19].payload,
4714 fidl_fuchsia_metrics::MetricEventPayload::IntegerValue(20)
4715 );
4716 }
4717
4718 #[fuchsia::test]
4719 fn test_log_connect_event_correct_shape() {
4720 let (mut test_helper, mut test_fut) = setup_test();
4721 test_helper.send_connected_event(random_bss_description!(Wpa2));
4722
4723 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
4724
4725 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4726 stats: contains {
4727 connect_events: {
4728 "0": {
4729 "@time": AnyNumericProperty,
4730 multiple_bss_candidates: AnyBoolProperty,
4731 network: {
4732 bssid: &*BSSID_REGEX,
4733 ssid: &*SSID_REGEX,
4734 rssi_dbm: AnyNumericProperty,
4735 snr_db: AnyNumericProperty,
4736 }
4737 }
4738 }
4739 }
4740 });
4741 }
4742
4743 #[fuchsia::test]
4744 fn test_log_connection_status_correct_shape() {
4745 let (mut test_helper, mut test_fut) = setup_test();
4746 test_helper.send_connected_event(random_bss_description!(Wpa2));
4747
4748 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
4749
4750 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4751 stats: contains {
4752 connection_status: contains {
4753 status_string: AnyStringProperty,
4754 connected_network: contains {
4755 rssi_dbm: AnyNumericProperty,
4756 snr_db: AnyNumericProperty,
4757 bssid: &*BSSID_REGEX,
4758 ssid: &*SSID_REGEX,
4759 protection: AnyStringProperty,
4760 channel: AnyStringProperty,
4761 is_wmm_assoc: AnyBoolProperty,
4762 }
4763 }
4764 }
4765 });
4766 }
4767
4768 #[allow(clippy::regex_creation_in_loops, reason = "mass allow for https://fxbug.dev/381896734")]
4769 #[fuchsia::test]
4770 fn test_log_disconnect_event_correct_shape() {
4771 let (mut test_helper, mut test_fut) = setup_test();
4772
4773 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
4774 track_subsequent_downtime: false,
4775 info: Some(fake_disconnect_info()),
4776 });
4777 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
4778
4779 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4780 external: contains {
4781 stats: contains {
4782 disconnect_events: {
4783 "0": {
4784 "@time": AnyNumericProperty,
4785 flattened_reason_code: AnyNumericProperty,
4786 locally_initiated: AnyBoolProperty,
4787 network: {
4788 channel: {
4789 primary: AnyNumericProperty,
4790 }
4791 }
4792 }
4793 }
4794 }
4795 },
4796 stats: contains {
4797 disconnect_events: {
4798 "0": {
4799 "@time": AnyNumericProperty,
4800 connected_duration: AnyNumericProperty,
4801 disconnect_source: Regex::new("^source: [^,]+, reason: [^,]+(?:, mlme_event_name: [^,]+)?$").unwrap(),
4802 network: contains {
4803 rssi_dbm: AnyNumericProperty,
4804 snr_db: AnyNumericProperty,
4805 bssid: &*BSSID_REGEX,
4806 ssid: &*SSID_REGEX,
4807 protection: AnyStringProperty,
4808 channel: AnyStringProperty,
4809 is_wmm_assoc: AnyBoolProperty,
4810 }
4811 }
4812 }
4813 }
4814 });
4815 }
4816
4817 #[fuchsia::test]
4818 fn test_log_disconnect_on_recovery() {
4819 let mut exec = fasync::TestExecutor::new();
4820
4821 let (sender, _receiver) = mpsc::channel::<TelemetryEvent>(TELEMETRY_EVENT_BUFFER_SIZE);
4823 let (monitor_svc_proxy, _monitor_svc_stream) =
4824 create_proxy_and_stream::<fidl_fuchsia_wlan_device_service::DeviceMonitorMarker>();
4825 let (cobalt_1dot1_proxy, mut cobalt_1dot1_stream) =
4826 create_proxy_and_stream::<fidl_fuchsia_metrics::MetricEventLoggerMarker>();
4827 let inspector = Inspector::default();
4828 let inspect_node = inspector.root().create_child("stats");
4829 let external_inspect_node = inspector.root().create_child("external");
4830 let (defect_sender, _defect_receiver) = mpsc::channel(100);
4831
4832 let mut telemetry = Telemetry::new(
4834 TelemetrySender::new(sender),
4835 monitor_svc_proxy,
4836 cobalt_1dot1_proxy.clone(),
4837 inspect_node,
4838 external_inspect_node,
4839 defect_sender,
4840 );
4841
4842 telemetry.connection_state = ConnectionState::Connected(Box::new(ConnectedState {
4843 iface_id: 0,
4844 new_connect_start_time: None,
4845 prev_connection_stats: None,
4846 multiple_bss_candidates: false,
4847 ap_state: Box::new(generate_random_ap_state()),
4848 network_is_likely_hidden: false,
4849 last_signal_report: fasync::MonotonicInstant::now(),
4850 num_consecutive_get_counter_stats_failures: InspectableU64::new(
4851 0,
4852 &telemetry.inspect_node,
4853 "num_consecutive_get_counter_stats_failures",
4854 ),
4855 is_driver_unresponsive: InspectableBool::new(
4856 false,
4857 &telemetry.inspect_node,
4858 "is_driver_unresponsive",
4859 ),
4860 telemetry_proxy: None,
4861 }));
4862
4863 {
4864 let fut = telemetry.handle_telemetry_event(TelemetryEvent::Disconnected {
4866 track_subsequent_downtime: false,
4867 info: None,
4868 });
4869 let mut fut = pin!(fut);
4870
4871 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4872
4873 assert_matches!(
4875 exec.run_until_stalled(&mut cobalt_1dot1_stream.next()),
4876 Poll::Ready(Some(Ok(fidl_fuchsia_metrics::MetricEventLoggerRequest::LogMetricEvents {
4877 events: _,
4878 responder
4879 }))) => {
4880 responder.send(Ok(())).expect("failed to send response");
4881 }
4882 );
4883
4884 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
4886 }
4887
4888 assert_matches!(
4890 telemetry.connection_state,
4891 ConnectionState::Idle(IdleState { connect_start_time: None })
4892 );
4893 }
4894
4895 #[fuchsia::test]
4896 fn test_stat_cycles() {
4897 let (mut test_helper, mut test_fut) = setup_test();
4898 test_helper.send_connected_event(random_bss_description!(Wpa2));
4899 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
4900
4901 test_helper.advance_by(
4902 zx::MonotonicDuration::from_hours(24) - TELEMETRY_QUERY_INTERVAL,
4903 test_fut.as_mut(),
4904 );
4905 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4906 stats: contains {
4907 "1d_counters": contains {
4908 total_duration: (zx::MonotonicDuration::from_hours(24) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
4909 connected_duration: (zx::MonotonicDuration::from_hours(24) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
4910 },
4911 "7d_counters": contains {
4912 total_duration: (zx::MonotonicDuration::from_hours(24) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
4913 connected_duration: (zx::MonotonicDuration::from_hours(24) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
4914 },
4915 }
4916 });
4917
4918 test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
4919 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4920 stats: contains {
4921 "1d_counters": contains {
4922 total_duration: zx::MonotonicDuration::from_hours(23).into_nanos(),
4925 connected_duration: zx::MonotonicDuration::from_hours(23).into_nanos(),
4926 },
4927 "7d_counters": contains {
4928 total_duration: zx::MonotonicDuration::from_hours(24).into_nanos(),
4929 connected_duration: zx::MonotonicDuration::from_hours(24).into_nanos(),
4930 },
4931 }
4932 });
4933
4934 test_helper.advance_by(zx::MonotonicDuration::from_hours(2), test_fut.as_mut());
4935 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4936 stats: contains {
4937 "1d_counters": contains {
4938 total_duration: zx::MonotonicDuration::from_hours(23).into_nanos(),
4939 connected_duration: zx::MonotonicDuration::from_hours(23).into_nanos(),
4940 },
4941 "7d_counters": contains {
4942 total_duration: zx::MonotonicDuration::from_hours(26).into_nanos(),
4943 connected_duration: zx::MonotonicDuration::from_hours(26).into_nanos(),
4944 },
4945 }
4946 });
4947
4948 let info = fake_disconnect_info();
4950 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
4951 track_subsequent_downtime: false,
4952 info: Some(info),
4953 });
4954 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
4955
4956 test_helper.advance_by(zx::MonotonicDuration::from_hours(8), test_fut.as_mut());
4957 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4958 stats: contains {
4959 "1d_counters": contains {
4960 total_duration: zx::MonotonicDuration::from_hours(23).into_nanos(),
4961 connected_duration: zx::MonotonicDuration::from_hours(15).into_nanos(),
4963 },
4964 "7d_counters": contains {
4965 total_duration: zx::MonotonicDuration::from_hours(34).into_nanos(),
4966 connected_duration: zx::MonotonicDuration::from_hours(26).into_nanos(),
4967 },
4968 }
4969 });
4970
4971 test_helper.advance_by(zx::MonotonicDuration::from_hours(14), test_fut.as_mut());
4973 test_helper.advance_by(
4974 zx::MonotonicDuration::from_hours(5 * 24) - TELEMETRY_QUERY_INTERVAL,
4975 test_fut.as_mut(),
4976 );
4977 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4978 stats: contains {
4979 "1d_counters": contains {
4980 total_duration: (zx::MonotonicDuration::from_hours(24) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
4981 connected_duration: 0i64,
4982 },
4983 "7d_counters": contains {
4984 total_duration: (zx::MonotonicDuration::from_hours(7 * 24) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
4985 connected_duration: zx::MonotonicDuration::from_hours(26).into_nanos(),
4986 },
4987 }
4988 });
4989
4990 test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
4992 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4993 stats: contains {
4994 "1d_counters": contains {
4995 total_duration: zx::MonotonicDuration::from_hours(23).into_nanos(),
4996 connected_duration: 0i64,
4997 },
4998 "7d_counters": contains {
4999 total_duration: zx::MonotonicDuration::from_hours(6 * 24).into_nanos(),
5000 connected_duration: zx::MonotonicDuration::from_hours(2).into_nanos(),
5001 },
5002 }
5003 });
5004 }
5005
5006 #[fuchsia::test]
5007 fn test_daily_detailed_stat_cycles() {
5008 let (mut test_helper, mut test_fut) = setup_test();
5009 for _ in 0..10 {
5010 test_helper.send_connected_event(random_bss_description!(Wpa2));
5011 }
5012 test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
5013
5014 let status_codes = test_helper.get_logged_metrics(
5016 metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
5017 );
5018 assert_eq!(status_codes.len(), 1);
5019 assert_eq!(
5020 status_codes[0].event_codes,
5021 vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32]
5022 );
5023 assert_eq!(status_codes[0].payload, MetricEventPayload::Count(10));
5024
5025 test_helper.cobalt_events.clear();
5026
5027 test_helper.send_connected_event(random_bss_description!(Wpa2));
5028 test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
5029
5030 let status_codes = test_helper.get_logged_metrics(
5032 metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
5033 );
5034 assert_eq!(status_codes.len(), 1);
5035 assert_eq!(
5036 status_codes[0].event_codes,
5037 vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32]
5038 );
5039 assert_eq!(status_codes[0].payload, MetricEventPayload::Count(1));
5040 }
5041
5042 #[fuchsia::test]
5043 fn test_total_duration_counters() {
5044 let (mut test_helper, mut test_fut) = setup_test();
5045
5046 test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
5047 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5048 stats: contains {
5049 "1d_counters": contains {
5050 total_duration: zx::MonotonicDuration::from_minutes(30).into_nanos(),
5051 },
5052 "7d_counters": contains {
5053 total_duration: zx::MonotonicDuration::from_minutes(30).into_nanos(),
5054 },
5055 }
5056 });
5057
5058 test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
5059 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5060 stats: contains {
5061 "1d_counters": contains {
5062 total_duration: zx::MonotonicDuration::from_hours(1).into_nanos(),
5063 },
5064 "7d_counters": contains {
5065 total_duration: zx::MonotonicDuration::from_hours(1).into_nanos(),
5066 },
5067 }
5068 });
5069 }
5070
5071 #[fuchsia::test]
5072 fn test_counters_when_idle() {
5073 let (mut test_helper, mut test_fut) = setup_test();
5074
5075 test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
5076 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5077 stats: contains {
5078 "1d_counters": contains {
5079 connected_duration: 0i64,
5080 downtime_duration: 0i64,
5081 downtime_no_saved_neighbor_duration: 0i64,
5082 },
5083 "7d_counters": contains {
5084 connected_duration: 0i64,
5085 downtime_duration: 0i64,
5086 downtime_no_saved_neighbor_duration: 0i64,
5087 },
5088 }
5089 });
5090
5091 test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
5092 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5093 stats: contains {
5094 "1d_counters": contains {
5095 connected_duration: 0i64,
5096 downtime_duration: 0i64,
5097 downtime_no_saved_neighbor_duration: 0i64,
5098 },
5099 "7d_counters": contains {
5100 connected_duration: 0i64,
5101 downtime_duration: 0i64,
5102 downtime_no_saved_neighbor_duration: 0i64,
5103 },
5104 }
5105 });
5106 }
5107
5108 #[fuchsia::test]
5109 fn test_connected_counters_increase_when_connected() {
5110 let (mut test_helper, mut test_fut) = setup_test();
5111 test_helper.send_connected_event(random_bss_description!(Wpa2));
5112 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5113
5114 test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
5115 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5116 stats: contains {
5117 "1d_counters": contains {
5118 connected_duration: zx::MonotonicDuration::from_minutes(30).into_nanos(),
5119 downtime_duration: 0i64,
5120 downtime_no_saved_neighbor_duration: 0i64,
5121 },
5122 "7d_counters": contains {
5123 connected_duration: zx::MonotonicDuration::from_minutes(30).into_nanos(),
5124 downtime_duration: 0i64,
5125 downtime_no_saved_neighbor_duration: 0i64,
5126 },
5127 }
5128 });
5129
5130 test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
5131 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5132 stats: contains {
5133 "1d_counters": contains {
5134 connected_duration: zx::MonotonicDuration::from_hours(1).into_nanos(),
5135 downtime_duration: 0i64,
5136 downtime_no_saved_neighbor_duration: 0i64,
5137 },
5138 "7d_counters": contains {
5139 connected_duration: zx::MonotonicDuration::from_hours(1).into_nanos(),
5140 downtime_duration: 0i64,
5141 downtime_no_saved_neighbor_duration: 0i64,
5142 },
5143 }
5144 });
5145 }
5146
5147 #[fuchsia::test]
5148 fn test_downtime_counter() {
5149 let (mut test_helper, mut test_fut) = setup_test();
5150
5151 let info = fake_disconnect_info();
5153 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5154 track_subsequent_downtime: false,
5155 info: Some(info),
5156 });
5157 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5158
5159 test_helper.advance_by(zx::MonotonicDuration::from_minutes(10), test_fut.as_mut());
5160
5161 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5162 stats: contains {
5163 "1d_counters": contains {
5164 connected_duration: 0i64,
5165 downtime_duration: 0i64,
5166 downtime_no_saved_neighbor_duration: 0i64,
5167 },
5168 "7d_counters": contains {
5169 connected_duration: 0i64,
5170 downtime_duration: 0i64,
5171 downtime_no_saved_neighbor_duration: 0i64,
5172 },
5173 }
5174 });
5175
5176 let info = fake_disconnect_info();
5178 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5179 track_subsequent_downtime: true,
5180 info: Some(info),
5181 });
5182 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5183
5184 test_helper.advance_by(zx::MonotonicDuration::from_minutes(15), test_fut.as_mut());
5185
5186 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5187 stats: contains {
5188 "1d_counters": contains {
5189 connected_duration: 0i64,
5190 downtime_duration: zx::MonotonicDuration::from_minutes(15).into_nanos(),
5191 downtime_no_saved_neighbor_duration: 0i64,
5192 },
5193 "7d_counters": contains {
5194 connected_duration: 0i64,
5195 downtime_duration: zx::MonotonicDuration::from_minutes(15).into_nanos(),
5196 downtime_no_saved_neighbor_duration: 0i64,
5197 },
5198 }
5199 });
5200 }
5201
5202 #[fuchsia::test]
5203 fn test_counters_connect_then_disconnect() {
5204 let (mut test_helper, mut test_fut) = setup_test();
5205 test_helper.send_connected_event(random_bss_description!(Wpa2));
5206 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5207
5208 test_helper.advance_by(zx::MonotonicDuration::from_seconds(5), test_fut.as_mut());
5209
5210 let info = fake_disconnect_info();
5212 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5213 track_subsequent_downtime: true,
5214 info: Some(info),
5215 });
5216 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5217
5218 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5220 stats: contains {
5221 "1d_counters": contains {
5222 connected_duration: 0i64,
5223 downtime_duration: 0i64,
5224 downtime_no_saved_neighbor_duration: 0i64,
5225 },
5226 "7d_counters": contains {
5227 connected_duration: 0i64,
5228 downtime_duration: 0i64,
5229 downtime_no_saved_neighbor_duration: 0i64,
5230 },
5231 }
5232 });
5233
5234 let downtime_start = fasync::MonotonicInstant::now();
5236 test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5237 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5238 stats: contains {
5239 "1d_counters": contains {
5240 connected_duration: zx::MonotonicDuration::from_seconds(5).into_nanos(),
5241 downtime_duration: (fasync::MonotonicInstant::now() - downtime_start).into_nanos(),
5242 downtime_no_saved_neighbor_duration: 0i64,
5243 },
5244 "7d_counters": contains {
5245 connected_duration: zx::MonotonicDuration::from_seconds(5).into_nanos(),
5246 downtime_duration: (fasync::MonotonicInstant::now() - downtime_start).into_nanos(),
5247 downtime_no_saved_neighbor_duration: 0i64,
5248 },
5249 }
5250 });
5251 }
5252
5253 #[fuchsia::test]
5254 fn test_downtime_no_saved_neighbor_duration_counter() {
5255 let (mut test_helper, mut test_fut) = setup_test();
5256 test_helper.send_connected_event(random_bss_description!(Wpa2));
5257 test_helper.drain_cobalt_events(&mut test_fut);
5258
5259 let info = fake_disconnect_info();
5261 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5262 track_subsequent_downtime: true,
5263 info: Some(info),
5264 });
5265 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5266
5267 test_helper.advance_by(zx::MonotonicDuration::from_seconds(5), test_fut.as_mut());
5268 test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
5270 network_selection_type: NetworkSelectionType::Undirected,
5271 num_candidates: Ok(0),
5272 selected_count: 0,
5273 });
5274 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5275
5276 test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5277 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5278 stats: contains {
5279 "1d_counters": contains {
5280 connected_duration: 0i64,
5281 downtime_duration: TELEMETRY_QUERY_INTERVAL.into_nanos(),
5282 downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL - zx::MonotonicDuration::from_seconds(5)).into_nanos(),
5283 },
5284 "7d_counters": contains {
5285 connected_duration: 0i64,
5286 downtime_duration: TELEMETRY_QUERY_INTERVAL.into_nanos(),
5287 downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL - zx::MonotonicDuration::from_seconds(5)).into_nanos(),
5288 },
5289 }
5290 });
5291
5292 test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5293 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5294 stats: contains {
5295 "1d_counters": contains {
5296 connected_duration: 0i64,
5297 downtime_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5298 downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL*2 - zx::MonotonicDuration::from_seconds(5)).into_nanos(),
5299 },
5300 "7d_counters": contains {
5301 connected_duration: 0i64,
5302 downtime_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5303 downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL*2 - zx::MonotonicDuration::from_seconds(5)).into_nanos(),
5304 },
5305 }
5306 });
5307
5308 test_helper.advance_by(zx::MonotonicDuration::from_seconds(5), test_fut.as_mut());
5309 test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
5311 network_selection_type: NetworkSelectionType::Undirected,
5312 num_candidates: Ok(1),
5313 selected_count: 0,
5314 });
5315 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5316
5317 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5319 stats: contains {
5320 "1d_counters": contains {
5321 connected_duration: 0i64,
5322 downtime_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5323 downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL*2 - zx::MonotonicDuration::from_seconds(5)).into_nanos(),
5324 },
5325 "7d_counters": contains {
5326 connected_duration: 0i64,
5327 downtime_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5328 downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL*2 - zx::MonotonicDuration::from_seconds(5)).into_nanos(),
5329 },
5330 }
5331 });
5332
5333 test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5335 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5336 stats: contains {
5337 "1d_counters": contains {
5338 connected_duration: 0i64,
5339 downtime_duration: (TELEMETRY_QUERY_INTERVAL * 3).into_nanos(),
5340 downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5341 },
5342 "7d_counters": contains {
5343 connected_duration: 0i64,
5344 downtime_duration: (TELEMETRY_QUERY_INTERVAL * 3).into_nanos(),
5345 downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5346 },
5347 }
5348 });
5349
5350 let info = fake_disconnect_info();
5352 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5353 track_subsequent_downtime: false,
5354 info: Some(info),
5355 });
5356
5357 test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
5359 network_selection_type: NetworkSelectionType::Undirected,
5360 num_candidates: Ok(0),
5361 selected_count: 0,
5362 });
5363 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5364 test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5365
5366 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5368 stats: contains {
5369 "1d_counters": contains {
5370 connected_duration: 0i64,
5371 downtime_duration: (TELEMETRY_QUERY_INTERVAL * 3).into_nanos(),
5372 downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5373 },
5374 "7d_counters": contains {
5375 connected_duration: 0i64,
5376 downtime_duration: (TELEMETRY_QUERY_INTERVAL * 3).into_nanos(),
5377 downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5378 },
5379 }
5380 });
5381 }
5382
5383 #[fuchsia::test]
5384 fn test_log_connect_attempt_counters() {
5385 let (mut test_helper, mut test_fut) = setup_test();
5386
5387 for i in 0..10 {
5389 let event = TelemetryEvent::ConnectResult {
5390 iface_id: IFACE_ID,
5391 policy_connect_reason: Some(
5392 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
5393 ),
5394 result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
5395 multiple_bss_candidates: true,
5396 ap_state: random_bss_description!(Wpa1).into(),
5397 network_is_likely_hidden: false,
5398 };
5399 test_helper.telemetry_sender.send(event);
5400
5401 test_helper.drain_cobalt_events(&mut test_fut);
5403 let logged_metrics =
5404 test_helper.get_logged_metrics(metrics::CONNECTION_FAILURES_METRIC_ID);
5405 assert_eq!(logged_metrics.len(), i + 1);
5406 }
5407 test_helper.send_connected_event(random_bss_description!(Wpa2));
5408 test_helper.drain_cobalt_events(&mut test_fut);
5409
5410 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5411 stats: contains {
5412 "1d_counters": contains {
5413 connect_attempts_count: 11u64,
5414 connect_successful_count: 1u64,
5415 },
5416 "7d_counters": contains {
5417 connect_attempts_count: 11u64,
5418 connect_successful_count: 1u64,
5419 },
5420 }
5421 });
5422 }
5423
5424 #[fuchsia::test]
5425 fn test_disconnect_count_counter() {
5426 let (mut test_helper, mut test_fut) = setup_test();
5427 test_helper.send_connected_event(random_bss_description!(Wpa2));
5428 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5429
5430 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5431 stats: contains {
5432 "1d_counters": contains {
5433 disconnect_count: 0u64,
5434 policy_roam_disconnects_count: 0u64,
5435 },
5436 "7d_counters": contains {
5437 disconnect_count: 0u64,
5438 policy_roam_disconnects_count: 0u64,
5439 },
5440 }
5441 });
5442
5443 let info = DisconnectInfo {
5444 disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
5445 reason_code: fidl_ieee80211::ReasonCode::StaLeaving,
5446 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DisassociateIndication,
5447 }),
5448 ..fake_disconnect_info()
5449 };
5450 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5451 track_subsequent_downtime: true,
5452 info: Some(info),
5453 });
5454 test_helper.drain_cobalt_events(&mut test_fut);
5455
5456 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5457 stats: contains {
5458 "1d_counters": contains {
5459 disconnect_count: 1u64,
5460 policy_roam_disconnects_count: 0u64,
5461 total_non_roam_disconnect_count: 1u64,
5462 total_roam_disconnect_count: 0u64,
5463 },
5464 "7d_counters": contains {
5465 disconnect_count: 1u64,
5466 policy_roam_disconnects_count: 0u64,
5467 total_non_roam_disconnect_count: 1u64,
5468 total_roam_disconnect_count: 0u64,
5469 },
5470 }
5471 });
5472
5473 let info = DisconnectInfo {
5474 disconnect_source: fidl_sme::DisconnectSource::User(
5475 fidl_sme::UserDisconnectReason::Startup,
5476 ),
5477 ..fake_disconnect_info()
5478 };
5479 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5480 track_subsequent_downtime: false,
5481 info: Some(info),
5482 });
5483 test_helper.drain_cobalt_events(&mut test_fut);
5484
5485 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5486 stats: contains {
5487 "1d_counters": contains {
5488 disconnect_count: 2u64,
5489 policy_roam_disconnects_count: 0u64,
5490 total_non_roam_disconnect_count: 2u64,
5491 total_roam_disconnect_count: 0u64,
5492 },
5493 "7d_counters": contains {
5494 disconnect_count: 2u64,
5495 policy_roam_disconnects_count: 0u64,
5496 total_non_roam_disconnect_count: 2u64,
5497 total_roam_disconnect_count: 0u64,
5498 },
5499 }
5500 });
5501
5502 let info = DisconnectInfo {
5504 disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
5505 reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
5506 mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
5507 }),
5508 ..fake_disconnect_info()
5509 };
5510 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5511 track_subsequent_downtime: false,
5512 info: Some(info),
5513 });
5514 test_helper.drain_cobalt_events(&mut test_fut);
5515
5516 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5517 stats: contains {
5518 "1d_counters": contains {
5519 disconnect_count: 3u64,
5520 policy_roam_disconnects_count: 0u64,
5521 total_non_roam_disconnect_count: 2u64,
5522 total_roam_disconnect_count: 1u64,
5523 },
5524 "7d_counters": contains {
5525 disconnect_count: 3u64,
5526 policy_roam_disconnects_count: 0u64,
5527 total_non_roam_disconnect_count: 2u64,
5528 total_roam_disconnect_count: 1u64,
5529 },
5530 }
5531 });
5532 }
5533
5534 #[fuchsia::test]
5535 fn test_policy_roam_disconnects_count_counter() {
5536 let (mut test_helper, mut test_fut) = setup_test();
5537 test_helper.send_connected_event(random_bss_description!(Wpa2));
5538 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5539 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5540 stats: contains {
5541 "1d_counters": contains {
5542 disconnect_count: 0u64,
5543 policy_roam_disconnects_count: 0u64,
5544 total_roam_disconnect_count: 0u64,
5545 total_non_roam_disconnect_count: 0u64,
5546 },
5547 "7d_counters": contains {
5548 disconnect_count: 0u64,
5549 policy_roam_disconnects_count: 0u64,
5550 total_roam_disconnect_count: 0u64,
5551 total_non_roam_disconnect_count: 0u64,
5552 },
5553 }
5554 });
5555
5556 let mut roam_result = fidl_sme::RoamResult {
5558 bssid: [1, 1, 1, 1, 1, 1],
5559 status_code: fidl_ieee80211::StatusCode::Success,
5560 original_association_maintained: false,
5561 bss_description: Some(Box::new(random_fidl_bss_description!())),
5562 disconnect_info: None,
5563 is_credential_rejected: false,
5564 };
5565 test_helper.telemetry_sender.send(TelemetryEvent::PolicyInitiatedRoamResult {
5566 iface_id: 1,
5567 result: roam_result.clone(),
5568 updated_ap_state: generate_random_ap_state(),
5569 original_ap_state: Box::new(generate_random_ap_state()),
5570 request: Box::new(generate_policy_roam_request([1, 1, 1, 1, 1, 1].into())),
5571 request_time: fasync::MonotonicInstant::now(),
5572 result_time: fasync::MonotonicInstant::now(),
5573 });
5574 test_helper.drain_cobalt_events(&mut test_fut);
5575 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5576 stats: contains {
5577 "1d_counters": contains {
5578 disconnect_count: 0u64,
5579 policy_roam_disconnects_count: 1u64,
5580 total_roam_disconnect_count: 0u64,
5583 total_non_roam_disconnect_count: 0u64,
5584 },
5585 "7d_counters": contains {
5586 disconnect_count: 0u64,
5587 policy_roam_disconnects_count: 1u64,
5588 total_roam_disconnect_count: 0u64,
5591 total_non_roam_disconnect_count: 0u64,
5592 },
5593 }
5594 });
5595
5596 roam_result.status_code = fidl_ieee80211::StatusCode::RefusedReasonUnspecified;
5598 roam_result.disconnect_info = Some(Box::new(generate_disconnect_info(false)));
5599 test_helper.telemetry_sender.send(TelemetryEvent::PolicyInitiatedRoamResult {
5600 iface_id: 1,
5601 result: roam_result.clone(),
5602 updated_ap_state: generate_random_ap_state(),
5603 original_ap_state: Box::new(generate_random_ap_state()),
5604 request: Box::new(generate_policy_roam_request([1, 1, 1, 1, 1, 1].into())),
5605 request_time: fasync::MonotonicInstant::now(),
5606 result_time: fasync::MonotonicInstant::now(),
5607 });
5608 test_helper.drain_cobalt_events(&mut test_fut);
5609 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5610 stats: contains {
5611 "1d_counters": contains {
5612 disconnect_count: 0u64,
5613 policy_roam_disconnects_count: 2u64,
5614 total_roam_disconnect_count: 0u64,
5617 total_non_roam_disconnect_count: 0u64,
5618 },
5619 "7d_counters": contains {
5620 disconnect_count: 0u64,
5621 policy_roam_disconnects_count: 2u64,
5622 total_roam_disconnect_count: 0u64,
5625 total_non_roam_disconnect_count: 0u64,
5626 },
5627 }
5628 });
5629
5630 roam_result.original_association_maintained = true;
5632 test_helper.telemetry_sender.send(TelemetryEvent::PolicyInitiatedRoamResult {
5633 iface_id: 1,
5634 result: roam_result,
5635 updated_ap_state: generate_random_ap_state(),
5636 original_ap_state: Box::new(generate_random_ap_state()),
5637 request: Box::new(generate_policy_roam_request([1, 1, 1, 1, 1, 1].into())),
5638 request_time: fasync::MonotonicInstant::now(),
5639 result_time: fasync::MonotonicInstant::now(),
5640 });
5641 test_helper.drain_cobalt_events(&mut test_fut);
5642 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5643 stats: contains {
5644 "1d_counters": contains {
5645 disconnect_count: 0u64,
5646 policy_roam_disconnects_count: 2u64,
5647 total_roam_disconnect_count: 0u64,
5648 total_non_roam_disconnect_count: 0u64,
5649 },
5650 "7d_counters": contains {
5651 disconnect_count: 0u64,
5652 policy_roam_disconnects_count: 2u64,
5653 total_roam_disconnect_count: 0u64,
5654 total_non_roam_disconnect_count: 0u64,
5655 },
5656 }
5657 });
5658 }
5659
5660 #[fuchsia::test]
5661 fn test_rx_tx_counters_no_issue() {
5662 let (mut test_helper, mut test_fut) = setup_test();
5663 test_helper.send_connected_event(random_bss_description!(Wpa2));
5664 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5665
5666 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
5667 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5668 stats: contains {
5669 get_iface_stats_fail_count: 0u64,
5670 "1d_counters": contains {
5671 tx_high_packet_drop_duration: 0i64,
5672 rx_high_packet_drop_duration: 0i64,
5673 tx_very_high_packet_drop_duration: 0i64,
5674 rx_very_high_packet_drop_duration: 0i64,
5675 no_rx_duration: 0i64,
5676 },
5677 "7d_counters": contains {
5678 tx_high_packet_drop_duration: 0i64,
5679 rx_high_packet_drop_duration: 0i64,
5680 tx_very_high_packet_drop_duration: 0i64,
5681 rx_very_high_packet_drop_duration: 0i64,
5682 no_rx_duration: 0i64,
5683 },
5684 }
5685 });
5686 }
5687
5688 #[fuchsia::test]
5689 fn test_tx_high_packet_drop_duration_counters() {
5690 let (mut test_helper, mut test_fut) = setup_test();
5691 test_helper.set_iface_stats_resp(Box::new(|| {
5692 let seed = fasync::MonotonicInstant::now().into_nanos() as u64;
5693 Ok(fidl_fuchsia_wlan_stats::IfaceStats {
5694 connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
5695 tx_total: Some(10 * seed),
5696 tx_drop: Some(3 * seed),
5697 ..fake_connection_stats(seed)
5698 }),
5699 ..Default::default()
5700 })
5701 }));
5702
5703 test_helper.send_connected_event(random_bss_description!(Wpa2));
5704 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5705
5706 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
5707 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5708 stats: contains {
5709 get_iface_stats_fail_count: 0u64,
5710 "1d_counters": contains {
5711 tx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5714 rx_high_packet_drop_duration: 0i64,
5715 tx_very_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5716 rx_very_high_packet_drop_duration: 0i64,
5717 no_rx_duration: 0i64,
5718 },
5719 "7d_counters": contains {
5720 tx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5721 rx_high_packet_drop_duration: 0i64,
5722 tx_very_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5723 rx_very_high_packet_drop_duration: 0i64,
5724 no_rx_duration: 0i64,
5725 },
5726 }
5727 });
5728 }
5729
5730 #[fuchsia::test]
5731 fn test_rx_high_packet_drop_duration_counters() {
5732 let (mut test_helper, mut test_fut) = setup_test();
5733 test_helper.set_iface_stats_resp(Box::new(|| {
5734 let seed = fasync::MonotonicInstant::now().into_nanos() as u64;
5735 Ok(fidl_fuchsia_wlan_stats::IfaceStats {
5736 connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
5737 rx_unicast_total: Some(10 * seed),
5738 rx_unicast_drop: Some(3 * seed),
5739 ..fake_connection_stats(seed)
5740 }),
5741 ..Default::default()
5742 })
5743 }));
5744
5745 test_helper.send_connected_event(random_bss_description!(Wpa2));
5746 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5747
5748 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
5749 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5750 stats: contains {
5751 get_iface_stats_fail_count: 0u64,
5752 "1d_counters": contains {
5753 rx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5756 tx_high_packet_drop_duration: 0i64,
5757 rx_very_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5758 tx_very_high_packet_drop_duration: 0i64,
5759 no_rx_duration: 0i64,
5760 },
5761 "7d_counters": contains {
5762 rx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5763 tx_high_packet_drop_duration: 0i64,
5764 rx_very_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5765 tx_very_high_packet_drop_duration: 0i64,
5766 no_rx_duration: 0i64,
5767 },
5768 }
5769 });
5770 }
5771
5772 #[fuchsia::test]
5773 fn test_rx_tx_high_but_not_very_high_packet_drop_duration_counters() {
5774 let (mut test_helper, mut test_fut) = setup_test();
5775 test_helper.set_iface_stats_resp(Box::new(|| {
5776 let seed = fasync::MonotonicInstant::now().into_nanos() as u64;
5777 Ok(fidl_fuchsia_wlan_stats::IfaceStats {
5778 connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
5779 rx_unicast_total: Some(100 * seed),
5781 rx_unicast_drop: Some(3 * seed),
5782 tx_total: Some(100 * seed),
5783 tx_drop: Some(3 * seed),
5784 ..fake_connection_stats(seed)
5785 }),
5786 ..Default::default()
5787 })
5788 }));
5789
5790 test_helper.send_connected_event(random_bss_description!(Wpa2));
5791 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5792
5793 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
5794 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5795 stats: contains {
5796 get_iface_stats_fail_count: 0u64,
5797 "1d_counters": contains {
5798 rx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5801 tx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5802 rx_very_high_packet_drop_duration: 0i64,
5804 tx_very_high_packet_drop_duration: 0i64,
5805 no_rx_duration: 0i64,
5806 },
5807 "7d_counters": contains {
5808 rx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5809 tx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5810 rx_very_high_packet_drop_duration: 0i64,
5811 tx_very_high_packet_drop_duration: 0i64,
5812 no_rx_duration: 0i64,
5813 },
5814 }
5815 });
5816 }
5817
5818 #[fuchsia::test]
5819 fn test_rx_tx_reset() {
5820 let (mut test_helper, mut test_fut) = setup_test();
5821 test_helper.set_iface_stats_resp(Box::new(|| {
5822 let seed = (fasync::MonotonicInstant::now() - fasync::MonotonicInstant::from_nanos(0))
5823 .into_seconds() as u64;
5824 Ok(fidl_fuchsia_wlan_stats::IfaceStats {
5825 connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
5826 rx_unicast_total: Some(999999 - seed),
5827 rx_unicast_drop: Some(999999 - seed),
5828 tx_total: Some(999999 - seed),
5829 tx_drop: Some(999999 - seed),
5830 ..fake_connection_stats(seed)
5831 }),
5832 ..Default::default()
5833 })
5834 }));
5835
5836 test_helper.send_connected_event(random_bss_description!(Wpa2));
5837 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5838
5839 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
5841 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5843 stats: contains {
5844 get_iface_stats_fail_count: 0u64,
5845 "1d_counters": contains {
5846 rx_high_packet_drop_duration: 0i64,
5849 tx_high_packet_drop_duration: 0i64,
5850 rx_very_high_packet_drop_duration: 0i64,
5852 tx_very_high_packet_drop_duration: 0i64,
5853 no_rx_duration: 0i64,
5854 },
5855 "7d_counters": contains {
5856 rx_high_packet_drop_duration: 0i64,
5857 tx_high_packet_drop_duration: 0i64,
5858 rx_very_high_packet_drop_duration: 0i64,
5859 tx_very_high_packet_drop_duration: 0i64,
5860 no_rx_duration: 0i64,
5861 },
5862 }
5863 });
5864 }
5865
5866 #[fuchsia::test]
5867 fn test_no_rx_duration_counters() {
5868 let (mut test_helper, mut test_fut) = setup_test();
5869 test_helper.set_iface_stats_resp(Box::new(|| {
5870 let seed = fasync::MonotonicInstant::now().into_nanos() as u64;
5871 Ok(fidl_fuchsia_wlan_stats::IfaceStats {
5872 connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
5873 rx_unicast_total: Some(10),
5874 ..fake_connection_stats(seed)
5875 }),
5876 ..Default::default()
5877 })
5878 }));
5879
5880 test_helper.send_connected_event(random_bss_description!(Wpa2));
5881 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5882
5883 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
5884 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5885 stats: contains {
5886 get_iface_stats_fail_count: 0u64,
5887 "1d_counters": contains {
5888 no_rx_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5891 rx_high_packet_drop_duration: 0i64,
5892 tx_high_packet_drop_duration: 0i64,
5893 rx_very_high_packet_drop_duration: 0i64,
5894 tx_very_high_packet_drop_duration: 0i64,
5895 },
5896 "7d_counters": contains {
5897 no_rx_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5898 rx_high_packet_drop_duration: 0i64,
5899 tx_high_packet_drop_duration: 0i64,
5900 rx_very_high_packet_drop_duration: 0i64,
5901 tx_very_high_packet_drop_duration: 0i64,
5902 },
5903 }
5904 });
5905 }
5906
5907 #[fuchsia::test]
5908 fn test_get_iface_stats_fail() {
5909 let (mut test_helper, mut test_fut) = setup_test();
5910 test_helper.set_iface_stats_resp(Box::new(|| Err(zx::sys::ZX_ERR_NOT_SUPPORTED)));
5911
5912 test_helper.send_connected_event(random_bss_description!(Wpa2));
5913 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5914
5915 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
5916 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5917 stats: contains {
5918 get_iface_stats_fail_count: NonZeroUintProperty,
5919 "1d_counters": contains {
5920 no_rx_duration: 0i64,
5921 rx_high_packet_drop_duration: 0i64,
5922 tx_high_packet_drop_duration: 0i64,
5923 rx_very_high_packet_drop_duration: 0i64,
5924 tx_very_high_packet_drop_duration: 0i64,
5925 },
5926 "7d_counters": contains {
5927 no_rx_duration: 0i64,
5928 rx_high_packet_drop_duration: 0i64,
5929 tx_high_packet_drop_duration: 0i64,
5930 rx_very_high_packet_drop_duration: 0i64,
5931 tx_very_high_packet_drop_duration: 0i64,
5932 },
5933 }
5934 });
5935 }
5936
5937 #[fuchsia::test]
5938 fn test_log_signal_histograms_inspect() {
5939 let (mut test_helper, mut test_fut) = setup_test();
5940 test_helper.send_connected_event(random_bss_description!(Wpa2));
5941 test_helper.drain_cobalt_events(&mut test_fut);
5942
5943 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5945 external: contains {
5946 stats: contains {
5947 connection_status: contains {
5948 histograms: {
5949 antenna0_2Ghz: {
5950 antenna_index: 0u64,
5951 antenna_freq: "2Ghz",
5952 snr_histogram: vec![30i64, 999],
5953 snr_invalid_samples: 11u64,
5954 noise_floor_histogram: vec![-55i64, 999],
5955 noise_floor_invalid_samples: 44u64,
5956 rssi_histogram: vec![-25i64, 999],
5957 rssi_invalid_samples: 55u64,
5958 },
5959 antenna1_5Ghz: {
5960 antenna_index: 1u64,
5961 antenna_freq: "5Ghz",
5962 rx_rate_histogram: vec![100i64, 1500],
5963 rx_rate_invalid_samples: 33u64,
5964 },
5965 }
5966 }
5967 }
5968 }
5969 });
5970 }
5971
5972 #[fuchsia::test]
5973 fn test_log_daily_uptime_ratio_cobalt_metric() {
5974 let (mut test_helper, mut test_fut) = setup_test();
5975 test_helper.send_connected_event(random_bss_description!(Wpa2));
5976 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5977
5978 test_helper.advance_by(zx::MonotonicDuration::from_hours(12), test_fut.as_mut());
5979
5980 let info = fake_disconnect_info();
5981 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5982 track_subsequent_downtime: true,
5983 info: Some(info),
5984 });
5985 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5986
5987 test_helper.advance_by(zx::MonotonicDuration::from_hours(6), test_fut.as_mut());
5988
5989 test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
5991 network_selection_type: NetworkSelectionType::Undirected,
5992 num_candidates: Ok(0),
5993 selected_count: 0,
5994 });
5995
5996 test_helper.advance_by(zx::MonotonicDuration::from_hours(6), test_fut.as_mut());
5997
5998 let uptime_ratios =
5999 test_helper.get_logged_metrics(metrics::CONNECTED_UPTIME_RATIO_METRIC_ID);
6000 assert_eq!(uptime_ratios.len(), 1);
6001 assert_eq!(uptime_ratios[0].payload, MetricEventPayload::IntegerValue(6666));
6003 }
6004
6005 fn connect_and_disconnect_with_source(
6008 test_helper: &mut TestHelper,
6009 mut test_fut: Pin<&mut impl Future<Output = ()>>,
6010 disconnect_source: fidl_sme::DisconnectSource,
6011 ) {
6012 test_helper.send_connected_event(random_bss_description!(Wpa2));
6013 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6014
6015 test_helper.advance_by(zx::MonotonicDuration::from_hours(6), test_fut.as_mut());
6016
6017 let info = DisconnectInfo { disconnect_source, ..fake_disconnect_info() };
6018 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6019 track_subsequent_downtime: true,
6020 info: Some(info),
6021 });
6022 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6023 test_helper.drain_cobalt_events(&mut test_fut);
6024 }
6025
6026 #[fuchsia::test]
6027 fn test_log_daily_disconnect_per_day_connected_cobalt_metric() {
6028 let (mut test_helper, mut test_fut) = setup_test();
6029
6030 let mlme_non_roam_source = fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
6033 reason_code: fidl_ieee80211::ReasonCode::LeavingNetworkDeauth,
6034 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
6035 });
6036 connect_and_disconnect_with_source(
6037 &mut test_helper,
6038 test_fut.as_mut(),
6039 mlme_non_roam_source,
6040 );
6041
6042 let mlme_roam_source = fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
6043 reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
6044 mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamConfirmation,
6045 });
6046 connect_and_disconnect_with_source(&mut test_helper, test_fut.as_mut(), mlme_roam_source);
6047
6048 test_helper.advance_by(zx::MonotonicDuration::from_hours(12), test_fut.as_mut());
6049
6050 let dpdc_ratios =
6051 test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
6052 assert_eq!(dpdc_ratios.len(), 1);
6053 assert_eq!(dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(40_000));
6055
6056 let non_roam_dpdc_ratios = test_helper
6058 .get_logged_metrics(metrics::NON_ROAM_DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
6059 assert_eq!(non_roam_dpdc_ratios.len(), 1);
6060 assert_eq!(non_roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(20_000));
6061
6062 let roam_dpdc_ratios = test_helper
6065 .get_logged_metrics(metrics::POLICY_ROAM_DISCONNECT_COUNT_PER_DAY_CONNECTED_METRIC_ID);
6066 assert_eq!(roam_dpdc_ratios.len(), 1);
6067 assert_eq!(roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6068
6069 let dpdc_ratios_7d =
6070 test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_7D_METRIC_ID);
6071 assert_eq!(dpdc_ratios_7d.len(), 1);
6072 assert_eq!(dpdc_ratios_7d[0].payload, MetricEventPayload::IntegerValue(40_000));
6073
6074 test_helper.cobalt_events.clear();
6076
6077 test_helper.send_connected_event(random_bss_description!(Wpa2));
6079 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6080
6081 test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
6082
6083 let dpdc_ratios =
6085 test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
6086 assert_eq!(dpdc_ratios.len(), 1);
6087 assert_eq!(dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6088
6089 let non_roam_dpdc_ratios = test_helper
6090 .get_logged_metrics(metrics::NON_ROAM_DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
6091 assert_eq!(non_roam_dpdc_ratios.len(), 1);
6092 assert_eq!(non_roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6093
6094 let roam_dpdc_ratios = test_helper
6095 .get_logged_metrics(metrics::POLICY_ROAM_DISCONNECT_COUNT_PER_DAY_CONNECTED_METRIC_ID);
6096 assert_eq!(roam_dpdc_ratios.len(), 1);
6097 assert_eq!(roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6098
6099 let dpdc_ratios_7d =
6100 test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_7D_METRIC_ID);
6101 assert_eq!(dpdc_ratios_7d.len(), 1);
6102 assert_eq!(dpdc_ratios_7d[0].payload, MetricEventPayload::IntegerValue(13_333));
6105 }
6106
6107 #[fuchsia::test]
6108 fn test_log_daily_policy_roam_disconnect_per_day_connected_cobalt_metric() {
6109 let (mut test_helper, mut test_fut) = setup_test();
6110 test_helper.send_connected_event(random_bss_description!(Wpa2));
6111 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6112
6113 let bss_desc = random_fidl_bss_description!();
6115 let roam_result = fidl_sme::RoamResult {
6116 bssid: [1, 1, 1, 1, 1, 1],
6117 status_code: fidl_ieee80211::StatusCode::Success,
6118 original_association_maintained: false,
6119 bss_description: Some(Box::new(bss_desc.clone())),
6120 disconnect_info: None,
6121 is_credential_rejected: false,
6122 };
6123 test_helper.telemetry_sender.send(TelemetryEvent::PolicyInitiatedRoamResult {
6124 iface_id: 1,
6125 result: roam_result,
6126 updated_ap_state: generate_random_ap_state(),
6127 original_ap_state: Box::new(generate_random_ap_state()),
6128 request: Box::new(generate_policy_roam_request([1, 1, 1, 1, 1, 1].into())),
6129 request_time: fasync::MonotonicInstant::now(),
6130 result_time: fasync::MonotonicInstant::now(),
6131 });
6132 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6133 test_helper.advance_by(zx::MonotonicDuration::from_hours(12), test_fut.as_mut());
6134
6135 let bss_desc = random_fidl_bss_description!();
6137 let roam_result = fidl_sme::RoamResult {
6138 bssid: [2, 2, 2, 2, 2, 2],
6139 status_code: fidl_ieee80211::StatusCode::Success,
6140 original_association_maintained: false,
6141 bss_description: Some(Box::new(bss_desc.clone())),
6142 disconnect_info: None,
6143 is_credential_rejected: false,
6144 };
6145 test_helper.telemetry_sender.send(TelemetryEvent::PolicyInitiatedRoamResult {
6146 iface_id: 1,
6147 result: roam_result,
6148 updated_ap_state: generate_random_ap_state(),
6149 original_ap_state: Box::new(generate_random_ap_state()),
6150 request: Box::new(generate_policy_roam_request([2, 2, 2, 2, 2, 2].into())),
6151 request_time: fasync::MonotonicInstant::now(),
6152 result_time: fasync::MonotonicInstant::now(),
6153 });
6154 let info = DisconnectInfo {
6156 disconnect_source: fidl_sme::DisconnectSource::User(
6157 fidl_sme::UserDisconnectReason::Unknown,
6158 ),
6159 ..fake_disconnect_info()
6160 };
6161 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6162 track_subsequent_downtime: true,
6163 info: Some(info),
6164 });
6165 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6166 test_helper.advance_by(zx::MonotonicDuration::from_hours(12), test_fut.as_mut());
6167
6168 let dpdc_ratios =
6169 test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
6170 assert_eq!(dpdc_ratios.len(), 1);
6171 assert_eq!(dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(20_000));
6174
6175 let roam_dpdc_ratios = test_helper
6177 .get_logged_metrics(metrics::POLICY_ROAM_DISCONNECT_COUNT_PER_DAY_CONNECTED_METRIC_ID);
6178 assert_eq!(roam_dpdc_ratios.len(), 1);
6179 assert_eq!(roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(40_000));
6180 }
6181
6182 #[fuchsia::test]
6183 fn test_log_daily_disconnect_per_day_connected_cobalt_metric_device_high_disconnect() {
6184 let (mut test_helper, mut test_fut) = setup_test();
6185 test_helper.send_connected_event(random_bss_description!(Wpa2));
6186 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6187
6188 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6189 let info = fake_disconnect_info();
6190 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6191 track_subsequent_downtime: true,
6192 info: Some(info),
6193 });
6194 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6195
6196 test_helper.advance_by(zx::MonotonicDuration::from_hours(23), test_fut.as_mut());
6197 }
6198
6199 #[fuchsia::test]
6200 fn test_log_daily_rx_tx_ratio_cobalt_metrics() {
6201 let (mut test_helper, mut test_fut) = setup_test();
6202 test_helper.set_iface_stats_resp(Box::new(|| {
6203 let seed = fasync::MonotonicInstant::now().into_nanos() as u64 / 1_000_000_000;
6204 Ok(fidl_fuchsia_wlan_stats::IfaceStats {
6205 connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
6206 tx_total: Some(10 * seed),
6207 tx_drop: Some(
6211 3 * min(
6212 seed,
6213 (zx::MonotonicDuration::from_hours(3) + TELEMETRY_QUERY_INTERVAL)
6214 .into_seconds() as u64,
6215 ),
6216 ),
6217 rx_unicast_total: Some(
6219 10 * min(seed, zx::MonotonicDuration::from_hours(23).into_seconds() as u64),
6220 ),
6221 rx_unicast_drop: Some(
6223 3 * min(
6224 seed,
6225 (zx::MonotonicDuration::from_hours(4) + TELEMETRY_QUERY_INTERVAL)
6226 .into_seconds() as u64,
6227 ),
6228 ),
6229 ..fake_connection_stats(seed)
6230 }),
6231 ..Default::default()
6232 })
6233 }));
6234
6235 test_helper.send_connected_event(random_bss_description!(Wpa2));
6236 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6237
6238 test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
6239
6240 let high_rx_drop_time_ratios =
6241 test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_HIGH_RX_PACKET_DROP_METRIC_ID);
6242 assert_eq!(high_rx_drop_time_ratios.len(), 1);
6244 assert_eq!(high_rx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(1666));
6245
6246 let high_tx_drop_time_ratios =
6247 test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_HIGH_TX_PACKET_DROP_METRIC_ID);
6248 assert_eq!(high_tx_drop_time_ratios.len(), 1);
6250 assert_eq!(high_tx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(1250));
6251
6252 let very_high_rx_drop_time_ratios = test_helper
6253 .get_logged_metrics(metrics::TIME_RATIO_WITH_VERY_HIGH_RX_PACKET_DROP_METRIC_ID);
6254 assert_eq!(very_high_rx_drop_time_ratios.len(), 1);
6255 assert_eq!(
6256 very_high_rx_drop_time_ratios[0].payload,
6257 MetricEventPayload::IntegerValue(1666)
6258 );
6259
6260 let very_high_tx_drop_time_ratios = test_helper
6261 .get_logged_metrics(metrics::TIME_RATIO_WITH_VERY_HIGH_TX_PACKET_DROP_METRIC_ID);
6262 assert_eq!(very_high_tx_drop_time_ratios.len(), 1);
6263 assert_eq!(
6264 very_high_tx_drop_time_ratios[0].payload,
6265 MetricEventPayload::IntegerValue(1250)
6266 );
6267
6268 let no_rx_time_ratios =
6270 test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_NO_RX_METRIC_ID);
6271 assert_eq!(no_rx_time_ratios.len(), 1);
6272 assert_eq!(no_rx_time_ratios[0].payload, MetricEventPayload::IntegerValue(416));
6273 }
6274
6275 #[fuchsia::test]
6276 fn test_log_daily_rx_tx_ratio_cobalt_metrics_zero() {
6277 let (mut test_helper, mut test_fut) = setup_test();
6280
6281 test_helper.send_connected_event(random_bss_description!(Wpa2));
6282 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6283
6284 test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
6285
6286 let high_rx_drop_time_ratios =
6287 test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_HIGH_RX_PACKET_DROP_METRIC_ID);
6288 assert_eq!(high_rx_drop_time_ratios.len(), 1);
6289 assert_eq!(high_rx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6290
6291 let high_tx_drop_time_ratios =
6292 test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_HIGH_TX_PACKET_DROP_METRIC_ID);
6293 assert_eq!(high_tx_drop_time_ratios.len(), 1);
6294 assert_eq!(high_tx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6295
6296 let very_high_rx_drop_time_ratios = test_helper
6297 .get_logged_metrics(metrics::TIME_RATIO_WITH_VERY_HIGH_RX_PACKET_DROP_METRIC_ID);
6298 assert_eq!(very_high_rx_drop_time_ratios.len(), 1);
6299 assert_eq!(very_high_rx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6300
6301 let very_high_tx_drop_time_ratios = test_helper
6302 .get_logged_metrics(metrics::TIME_RATIO_WITH_VERY_HIGH_TX_PACKET_DROP_METRIC_ID);
6303 assert_eq!(very_high_tx_drop_time_ratios.len(), 1);
6304 assert_eq!(very_high_tx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6305
6306 let no_rx_time_ratios =
6307 test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_NO_RX_METRIC_ID);
6308 assert_eq!(no_rx_time_ratios.len(), 1);
6309 assert_eq!(no_rx_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6310 }
6311
6312 #[fuchsia::test]
6313 fn test_log_daily_establish_connection_metrics() {
6314 let (mut test_helper, mut test_fut) = setup_test();
6315
6316 for _ in 0..10 {
6318 let event = TelemetryEvent::ConnectResult {
6319 iface_id: IFACE_ID,
6320 policy_connect_reason: Some(
6321 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
6322 ),
6323 result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
6324 multiple_bss_candidates: true,
6325 ap_state: random_bss_description!(Wpa1).into(),
6326 network_is_likely_hidden: true,
6327 };
6328 test_helper.telemetry_sender.send(event);
6329 }
6330 test_helper.send_connected_event(random_bss_description!(Wpa2));
6331
6332 test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
6333
6334 let connection_success_rate =
6335 test_helper.get_logged_metrics(metrics::CONNECTION_SUCCESS_RATE_METRIC_ID);
6336 assert_eq!(connection_success_rate.len(), 1);
6337 assert_eq!(connection_success_rate[0].payload, MetricEventPayload::IntegerValue(909));
6339 }
6340
6341 #[fuchsia::test]
6342 fn test_log_hourly_fleetwide_uptime_cobalt_metrics() {
6343 let (mut test_helper, mut test_fut) = setup_test();
6344
6345 test_helper.send_connected_event(random_bss_description!(Wpa2));
6346 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6347
6348 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6349
6350 let total_wlan_uptime_durs =
6351 test_helper.get_logged_metrics(metrics::TOTAL_WLAN_UPTIME_NEAR_SAVED_NETWORK_METRIC_ID);
6352 assert_eq!(total_wlan_uptime_durs.len(), 1);
6353 assert_eq!(
6354 total_wlan_uptime_durs[0].payload,
6355 MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_hours(1).into_micros())
6356 );
6357
6358 let connected_durs =
6359 test_helper.get_logged_metrics(metrics::TOTAL_CONNECTED_UPTIME_METRIC_ID);
6360 assert_eq!(connected_durs.len(), 1);
6361 assert_eq!(
6362 connected_durs[0].payload,
6363 MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_hours(1).into_micros())
6364 );
6365
6366 test_helper.cobalt_events.clear();
6368
6369 test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
6370
6371 let info = fake_disconnect_info();
6372 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6373 track_subsequent_downtime: true,
6374 info: Some(info),
6375 });
6376 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6377
6378 test_helper.advance_by(zx::MonotonicDuration::from_minutes(15), test_fut.as_mut());
6379
6380 test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
6382 network_selection_type: NetworkSelectionType::Undirected,
6383 num_candidates: Ok(0),
6384 selected_count: 0,
6385 });
6386 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6387
6388 test_helper.advance_by(zx::MonotonicDuration::from_minutes(15), test_fut.as_mut());
6389
6390 let total_wlan_uptime_durs =
6391 test_helper.get_logged_metrics(metrics::TOTAL_WLAN_UPTIME_NEAR_SAVED_NETWORK_METRIC_ID);
6392 assert_eq!(total_wlan_uptime_durs.len(), 1);
6393 assert_eq!(
6395 total_wlan_uptime_durs[0].payload,
6396 MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(45).into_micros())
6397 );
6398
6399 let connected_durs =
6400 test_helper.get_logged_metrics(metrics::TOTAL_CONNECTED_UPTIME_METRIC_ID);
6401 assert_eq!(connected_durs.len(), 1);
6402 assert_eq!(
6403 connected_durs[0].payload,
6404 MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(30).into_micros())
6405 );
6406 }
6407
6408 #[fuchsia::test]
6409 fn test_log_hourly_fleetwide_rx_tx_cobalt_metrics() {
6410 let (mut test_helper, mut test_fut) = setup_test();
6411 test_helper.set_iface_stats_resp(Box::new(|| {
6412 let seed = fasync::MonotonicInstant::now().into_nanos() as u64 / 1_000_000_000;
6413 Ok(fidl_fuchsia_wlan_stats::IfaceStats {
6414 connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
6415 tx_total: Some(10 * seed),
6416 tx_drop: Some(
6420 3 * min(
6421 seed,
6422 (zx::MonotonicDuration::from_minutes(10) + TELEMETRY_QUERY_INTERVAL)
6423 .into_seconds() as u64,
6424 ),
6425 ),
6426 rx_unicast_total: Some(
6428 10 * min(
6429 seed,
6430 zx::MonotonicDuration::from_minutes(45).into_seconds() as u64,
6431 ),
6432 ),
6433 rx_unicast_drop: Some(
6435 3 * min(
6436 seed,
6437 (zx::MonotonicDuration::from_minutes(20) + TELEMETRY_QUERY_INTERVAL)
6438 .into_seconds() as u64,
6439 ),
6440 ),
6441 ..fake_connection_stats(seed)
6442 }),
6443 ..Default::default()
6444 })
6445 }));
6446
6447 test_helper.send_connected_event(random_bss_description!(Wpa2));
6448 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6449
6450 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6451
6452 let rx_high_drop_durs =
6453 test_helper.get_logged_metrics(metrics::TOTAL_TIME_WITH_HIGH_RX_PACKET_DROP_METRIC_ID);
6454 assert_eq!(rx_high_drop_durs.len(), 1);
6455 assert_eq!(
6456 rx_high_drop_durs[0].payload,
6457 MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(20).into_micros())
6458 );
6459
6460 let tx_high_drop_durs =
6461 test_helper.get_logged_metrics(metrics::TOTAL_TIME_WITH_HIGH_TX_PACKET_DROP_METRIC_ID);
6462 assert_eq!(tx_high_drop_durs.len(), 1);
6463 assert_eq!(
6464 tx_high_drop_durs[0].payload,
6465 MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(10).into_micros())
6466 );
6467
6468 let rx_very_high_drop_durs = test_helper
6469 .get_logged_metrics(metrics::TOTAL_TIME_WITH_VERY_HIGH_RX_PACKET_DROP_METRIC_ID);
6470 assert_eq!(rx_very_high_drop_durs.len(), 1);
6471 assert_eq!(
6472 rx_very_high_drop_durs[0].payload,
6473 MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(20).into_micros())
6474 );
6475
6476 let tx_very_high_drop_durs = test_helper
6477 .get_logged_metrics(metrics::TOTAL_TIME_WITH_VERY_HIGH_TX_PACKET_DROP_METRIC_ID);
6478 assert_eq!(tx_very_high_drop_durs.len(), 1);
6479 assert_eq!(
6480 tx_very_high_drop_durs[0].payload,
6481 MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(10).into_micros())
6482 );
6483
6484 let no_rx_durs = test_helper.get_logged_metrics(metrics::TOTAL_TIME_WITH_NO_RX_METRIC_ID);
6485 assert_eq!(no_rx_durs.len(), 1);
6486 assert_eq!(
6487 no_rx_durs[0].payload,
6488 MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(15).into_micros())
6489 );
6490 }
6491
6492 #[fuchsia::test]
6493 fn test_log_rssi_hourly() {
6494 let (mut test_helper, mut test_fut) = setup_test();
6495
6496 test_helper.send_connected_event(random_bss_description!(Wpa2));
6498
6499 let ind_1 = fidl_internal::SignalReportIndication { rssi_dbm: -50, snr_db: 30 };
6501 let ind_2 = fidl_internal::SignalReportIndication { rssi_dbm: -61, snr_db: 40 };
6502 test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_1 });
6503 test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_1 });
6504 test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_2 });
6505
6506 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6508 test_helper.drain_cobalt_events(&mut test_fut);
6509
6510 let metrics = test_helper.get_logged_metrics(metrics::CONNECTION_RSSI_METRIC_ID);
6511 assert_eq!(metrics.len(), 1);
6512 assert_matches!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => {
6513 assert_eq!(buckets.len(), 2);
6514 assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket{index: 79, count: 2}));
6515 assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket{index: 68, count: 1}));
6516 });
6517 test_helper.clear_cobalt_events();
6518
6519 let ind_3 = fidl_internal::SignalReportIndication { rssi_dbm: -75, snr_db: 30 };
6521 test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_3 });
6522 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6523
6524 test_helper.drain_cobalt_events(&mut test_fut);
6527
6528 let metrics = test_helper.get_logged_metrics(metrics::CONNECTION_RSSI_METRIC_ID);
6529 assert_eq!(metrics.len(), 1);
6530 let buckets =
6531 assert_matches!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => buckets);
6532 assert_eq!(buckets.len(), 1);
6533 assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 54, count: 1 }));
6534 }
6535
6536 #[fuchsia::test]
6537 fn test_log_rssi_velocity_hourly() {
6538 let (mut test_helper, mut test_fut) = setup_test();
6539
6540 test_helper.send_connected_event(random_bss_description!(Wpa2));
6542
6543 let rssi_velocity_1 = -2.0;
6545 let rssi_velocity_2 = 2.0;
6546 test_helper
6547 .telemetry_sender
6548 .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: rssi_velocity_1 });
6549 test_helper
6550 .telemetry_sender
6551 .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: rssi_velocity_2 });
6552 test_helper
6553 .telemetry_sender
6554 .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: rssi_velocity_2 });
6555
6556 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6558 test_helper.drain_cobalt_events(&mut test_fut);
6559
6560 let metrics = test_helper.get_logged_metrics(metrics::RSSI_VELOCITY_METRIC_ID);
6561 assert_eq!(metrics.len(), 1);
6562 assert_matches!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => {
6563 assert_eq!(buckets.len(), 2);
6565 assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket{index: 9, count: 1}));
6566 assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket{index: 13, count: 2}));
6567 });
6568 test_helper.clear_cobalt_events();
6569
6570 let rssi_velocity_3 = 3.0;
6572 test_helper
6573 .telemetry_sender
6574 .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: rssi_velocity_3 });
6575 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6576
6577 test_helper.drain_cobalt_events(&mut test_fut);
6580
6581 let metrics = test_helper.get_logged_metrics(metrics::RSSI_VELOCITY_METRIC_ID);
6582 assert_eq!(metrics.len(), 1);
6583 assert_eq!(
6584 metrics[0].payload,
6585 MetricEventPayload::Histogram(vec![fidl_fuchsia_metrics::HistogramBucket {
6586 index: 14,
6587 count: 1
6588 }])
6589 );
6590 }
6591
6592 #[fuchsia::test]
6593 fn test_log_rssi_histogram_bounds() {
6594 let (mut test_helper, mut test_fut) = setup_test();
6595
6596 test_helper.send_connected_event(random_bss_description!(Wpa2));
6598
6599 let ind_min = fidl_internal::SignalReportIndication { rssi_dbm: -128, snr_db: 30 };
6600 let ind_max = fidl_internal::SignalReportIndication { rssi_dbm: 0, snr_db: 30 };
6602 let ind_overflow_1 = fidl_internal::SignalReportIndication { rssi_dbm: 1, snr_db: 30 };
6603 let ind_overflow_2 = fidl_internal::SignalReportIndication { rssi_dbm: 127, snr_db: 30 };
6604 test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_min });
6606 test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_min });
6607 test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_min });
6608 test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_max });
6609 test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_overflow_1 });
6610 test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_overflow_2 });
6611 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6612
6613 test_helper.drain_cobalt_events(&mut test_fut);
6615 let metrics = test_helper.get_logged_metrics(metrics::CONNECTION_RSSI_METRIC_ID);
6617 assert_eq!(metrics.len(), 1);
6618 let buckets =
6619 assert_matches!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => buckets);
6620 assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 1, count: 3 }));
6621 assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 129, count: 1 }));
6622 assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 130, count: 2 }));
6623 }
6624
6625 #[fuchsia::test]
6626 fn test_log_rssi_velocity_histogram_bounds() {
6627 let (mut test_helper, mut test_fut) = setup_test();
6628
6629 test_helper.send_connected_event(random_bss_description!(Wpa2));
6631
6632 test_helper
6634 .telemetry_sender
6635 .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: -11.0 });
6636 test_helper
6637 .telemetry_sender
6638 .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: -15.0 });
6639 test_helper
6640 .telemetry_sender
6641 .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: 11.0 });
6642 test_helper
6643 .telemetry_sender
6644 .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: 20.0 });
6645 test_helper
6646 .telemetry_sender
6647 .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: -10.0 });
6648 test_helper
6649 .telemetry_sender
6650 .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: 10.0 });
6651 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6652
6653 test_helper.drain_cobalt_events(&mut test_fut);
6655
6656 let metrics = test_helper.get_logged_metrics(metrics::RSSI_VELOCITY_METRIC_ID);
6658 assert_eq!(metrics.len(), 1);
6659 let buckets =
6660 assert_matches!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => buckets);
6661 assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 1, count: 1 }));
6663 assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 21, count: 1 }));
6664 assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 0, count: 2 }));
6665 assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 22, count: 2 }));
6666 }
6667
6668 #[fuchsia::test]
6669 fn test_log_short_duration_connection_metrics() {
6670 let (mut test_helper, mut test_fut) = setup_test();
6671 let now = fasync::MonotonicInstant::now();
6672 test_helper.send_connected_event(random_bss_description!(Wpa2));
6673 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6674
6675 let channel = generate_random_channel();
6676 let ap_state = random_bss_description!(Wpa2, channel: channel).into();
6677 let mut signals = HistoricalList::new(5);
6678 signals.add(client::types::TimestampedSignal {
6679 signal: client::types::Signal { rssi_dbm: -30, snr_db: 60 },
6680 time: now,
6681 });
6682 signals.add(client::types::TimestampedSignal {
6683 signal: client::types::Signal { rssi_dbm: -30, snr_db: 60 },
6684 time: now,
6685 });
6686 let info = DisconnectInfo {
6688 connected_duration: METRICS_SHORT_CONNECT_DURATION
6689 - zx::MonotonicDuration::from_seconds(1),
6690 disconnect_source: fidl_sme::DisconnectSource::User(
6691 fidl_sme::UserDisconnectReason::FidlConnectRequest,
6692 ),
6693 ap_state,
6694 signals,
6695 ..fake_disconnect_info()
6696 };
6697 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6698 track_subsequent_downtime: true,
6699 info: Some(info.clone()),
6700 });
6701
6702 test_helper.send_connected_event(random_bss_description!(Wpa2));
6703 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6704
6705 let info = DisconnectInfo {
6707 disconnect_source: fidl_sme::DisconnectSource::User(
6708 fidl_sme::UserDisconnectReason::NetworkUnsaved,
6709 ),
6710 ..info
6711 };
6712 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6713 track_subsequent_downtime: true,
6714 info: Some(info.clone()),
6715 });
6716
6717 test_helper.send_connected_event(random_bss_description!(Wpa2));
6718 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6719
6720 let info = DisconnectInfo {
6722 connected_duration: METRICS_SHORT_CONNECT_DURATION
6723 + zx::MonotonicDuration::from_seconds(1),
6724 ..info
6725 };
6726 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6727 track_subsequent_downtime: true,
6728 info: Some(info.clone()),
6729 });
6730
6731 test_helper.drain_cobalt_events(&mut test_fut);
6732
6733 let logged_metrics = test_helper.get_logged_metrics(
6734 metrics::POLICY_FIDL_CONNECTION_ATTEMPTS_DURING_SHORT_CONNECTION_METRIC_ID,
6735 );
6736 assert_eq!(logged_metrics.len(), 2);
6737
6738 let logged_metrics = test_helper.get_logged_metrics(
6739 metrics::POLICY_FIDL_CONNECTION_ATTEMPTS_DURING_SHORT_CONNECTION_DETAILED_METRIC_ID,
6740 );
6741 assert_eq!(logged_metrics.len(), 2);
6742 assert_eq!(logged_metrics[0].event_codes, vec![info.previous_connect_reason as u32]);
6743
6744 let logged_metrics =
6745 test_helper.get_logged_metrics(metrics::CONNECTION_SCORE_AVERAGE_METRIC_ID);
6746 assert_eq!(logged_metrics.len(), 2);
6747 assert_eq!(
6748 logged_metrics[0].event_codes,
6749 vec![metrics::ConnectionScoreAverageMetricDimensionDuration::ShortDuration as u32]
6750 );
6751 assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(100));
6752 }
6753
6754 #[fuchsia::test]
6755 fn test_log_disconnect_cobalt_metrics() {
6756 let (mut test_helper, mut test_fut) = setup_test();
6757 test_helper.advance_by(zx::MonotonicDuration::from_hours(3), test_fut.as_mut());
6758 test_helper.send_connected_event(random_bss_description!(Wpa2));
6759 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6760
6761 test_helper.advance_by(zx::MonotonicDuration::from_hours(5), test_fut.as_mut());
6762
6763 let primary_channel = 8;
6764 let channel = Channel::new(primary_channel, Bandwidth::Cbw20, TwoGhz);
6765 let ap_state: client::types::ApState =
6766 random_bss_description!(Wpa2, channel: channel).into();
6767 let info = DisconnectInfo {
6768 connected_duration: zx::MonotonicDuration::from_hours(5),
6769 disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
6770 reason_code: fidl_ieee80211::ReasonCode::LeavingNetworkDeauth,
6771 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
6772 }),
6773 ap_state: ap_state.clone(),
6774 ..fake_disconnect_info()
6775 };
6776 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6777 track_subsequent_downtime: true,
6778 info: Some(info),
6779 });
6780 test_helper.drain_cobalt_events(&mut test_fut);
6781
6782 let policy_disconnection_reasons =
6783 test_helper.get_logged_metrics(metrics::POLICY_DISCONNECTION_MIGRATED_METRIC_ID);
6784 assert_eq!(policy_disconnection_reasons.len(), 1);
6785 assert_eq!(policy_disconnection_reasons[0].payload, MetricEventPayload::Count(1));
6786 assert_eq!(
6787 policy_disconnection_reasons[0].event_codes,
6788 vec![client::types::DisconnectReason::DisconnectDetectedFromSme as u32]
6789 );
6790
6791 let breakdowns_by_device_uptime = test_helper
6792 .get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_DEVICE_UPTIME_METRIC_ID);
6793 assert_eq!(breakdowns_by_device_uptime.len(), 1);
6794 assert_eq!(breakdowns_by_device_uptime[0].event_codes, vec![
6795 metrics::DisconnectBreakdownByDeviceUptimeMetricDimensionDeviceUptime::LessThan12Hours as u32,
6796 ]);
6797 assert_eq!(breakdowns_by_device_uptime[0].payload, MetricEventPayload::Count(1));
6798
6799 let breakdowns_by_connected_duration = test_helper
6800 .get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_CONNECTED_DURATION_METRIC_ID);
6801 assert_eq!(breakdowns_by_connected_duration.len(), 1);
6802 assert_eq!(breakdowns_by_connected_duration[0].event_codes, vec![
6803 metrics::DisconnectBreakdownByConnectedDurationMetricDimensionConnectedDuration::LessThan6Hours as u32,
6804 ]);
6805 assert_eq!(breakdowns_by_connected_duration[0].payload, MetricEventPayload::Count(1));
6806
6807 let breakdowns_by_reason =
6808 test_helper.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID);
6809 assert_eq!(breakdowns_by_reason.len(), 1);
6810 assert_eq!(
6811 breakdowns_by_reason[0].event_codes,
6812 vec![3u32, metrics::ConnectivityWlanMetricDimensionDisconnectSource::Mlme as u32,]
6813 );
6814 assert_eq!(breakdowns_by_reason[0].payload, MetricEventPayload::Count(1));
6815
6816 let breakdowns_by_channel = test_helper
6817 .get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID);
6818 assert_eq!(breakdowns_by_channel.len(), 1);
6819 assert_eq!(breakdowns_by_channel[0].event_codes, vec![channel.primary as u32]);
6820 assert_eq!(breakdowns_by_channel[0].payload, MetricEventPayload::Count(1));
6821
6822 let breakdowns_by_channel_band =
6823 test_helper.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID);
6824 assert_eq!(breakdowns_by_channel_band.len(), 1);
6825 assert_eq!(
6826 breakdowns_by_channel_band[0].event_codes,
6827 vec![
6828 metrics::DisconnectBreakdownByChannelBandMetricDimensionChannelBand::Band2Dot4Ghz
6829 as u32
6830 ]
6831 );
6832 assert_eq!(breakdowns_by_channel_band[0].payload, MetricEventPayload::Count(1));
6833
6834 let breakdowns_by_is_multi_bss =
6835 test_helper.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID);
6836 assert_eq!(breakdowns_by_is_multi_bss.len(), 1);
6837 assert_eq!(
6838 breakdowns_by_is_multi_bss[0].event_codes,
6839 vec![metrics::DisconnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::Yes as u32]
6840 );
6841 assert_eq!(breakdowns_by_is_multi_bss[0].payload, MetricEventPayload::Count(1));
6842
6843 let breakdowns_by_security_type = test_helper
6844 .get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID);
6845 assert_eq!(breakdowns_by_security_type.len(), 1);
6846 assert_eq!(
6847 breakdowns_by_security_type[0].event_codes,
6848 vec![
6849 metrics::DisconnectBreakdownBySecurityTypeMetricDimensionSecurityType::Wpa2Personal
6850 as u32
6851 ]
6852 );
6853 assert_eq!(breakdowns_by_security_type[0].payload, MetricEventPayload::Count(1));
6854
6855 let connected_duration_before_disconnect =
6858 test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_DISCONNECT_METRIC_ID);
6859 assert_eq!(connected_duration_before_disconnect.len(), 1);
6860 assert_eq!(
6861 connected_duration_before_disconnect[0].payload,
6862 MetricEventPayload::IntegerValue(300)
6863 );
6864 let connected_duration_before_non_roam_disconnect = test_helper
6865 .get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_NON_ROAM_DISCONNECT_METRIC_ID);
6866 assert_eq!(connected_duration_before_non_roam_disconnect.len(), 1);
6867 assert_eq!(
6868 connected_duration_before_non_roam_disconnect[0].payload,
6869 MetricEventPayload::IntegerValue(300)
6870 );
6871 let connected_duration_before_roam_attempt = test_helper.get_logged_metrics(
6872 metrics::POLICY_ROAM_CONNECTED_DURATION_BEFORE_ROAM_ATTEMPT_METRIC_ID,
6873 );
6874 assert_eq!(connected_duration_before_roam_attempt.len(), 0);
6875
6876 let network_disconnect_counts =
6879 test_helper.get_logged_metrics(metrics::NETWORK_DISCONNECT_COUNTS_METRIC_ID);
6880 assert_eq!(network_disconnect_counts.len(), 1);
6881 assert_eq!(network_disconnect_counts[0].payload, MetricEventPayload::Count(1));
6882
6883 let non_roam_disconnect_counts =
6884 test_helper.get_logged_metrics(metrics::NON_ROAM_DISCONNECT_COUNTS_METRIC_ID);
6885 assert_eq!(non_roam_disconnect_counts.len(), 1);
6886 assert_eq!(non_roam_disconnect_counts[0].payload, MetricEventPayload::Count(1));
6887
6888 let roam_disconnect_counts =
6889 test_helper.get_logged_metrics(metrics::POLICY_ROAM_DISCONNECT_COUNT_METRIC_ID);
6890 assert!(roam_disconnect_counts.is_empty());
6891
6892 test_helper.clear_cobalt_events();
6894
6895 test_helper.advance_by(zx::MonotonicDuration::from_minutes(1), test_fut.as_mut());
6897 test_helper.send_connected_event(random_bss_description!(Wpa2));
6898 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6899
6900 test_helper.advance_by(zx::MonotonicDuration::from_hours(6), test_fut.as_mut());
6901
6902 let info = DisconnectInfo {
6904 connected_duration: zx::MonotonicDuration::from_hours(6),
6905 disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
6906 reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
6907 mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
6908 }),
6909 ap_state,
6910 ..fake_disconnect_info()
6911 };
6912 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6913 track_subsequent_downtime: true,
6914 info: Some(info),
6915 });
6916 test_helper.drain_cobalt_events(&mut test_fut);
6917
6918 let connected_duration_before_disconnect =
6921 test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_DISCONNECT_METRIC_ID);
6922 assert_eq!(connected_duration_before_disconnect.len(), 1);
6923 assert_eq!(
6924 connected_duration_before_disconnect[0].payload,
6925 MetricEventPayload::IntegerValue(360)
6926 );
6927
6928 let connected_duration_before_non_roam_disconnect = test_helper
6929 .get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_NON_ROAM_DISCONNECT_METRIC_ID);
6930 assert!(connected_duration_before_non_roam_disconnect.is_empty());
6931
6932 let connected_duration_before_roam_attempt = test_helper.get_logged_metrics(
6936 metrics::POLICY_ROAM_CONNECTED_DURATION_BEFORE_ROAM_ATTEMPT_METRIC_ID,
6937 );
6938 assert!(connected_duration_before_roam_attempt.is_empty());
6939
6940 let network_disconnect_counts =
6943 test_helper.get_logged_metrics(metrics::NETWORK_DISCONNECT_COUNTS_METRIC_ID);
6944 assert_eq!(network_disconnect_counts.len(), 1);
6945 assert_eq!(network_disconnect_counts[0].payload, MetricEventPayload::Count(1));
6946
6947 let non_roam_disconnect_counts =
6948 test_helper.get_logged_metrics(metrics::NON_ROAM_DISCONNECT_COUNTS_METRIC_ID);
6949 assert!(non_roam_disconnect_counts.is_empty());
6950
6951 let roam_disconnect_counts =
6953 test_helper.get_logged_metrics(metrics::POLICY_ROAM_DISCONNECT_COUNT_METRIC_ID);
6954 assert!(roam_disconnect_counts.is_empty());
6955 }
6956
6957 #[fuchsia::test]
6958 fn test_log_user_disconnect_cobalt_metrics() {
6959 let (mut test_helper, mut test_fut) = setup_test();
6960 test_helper.advance_by(zx::MonotonicDuration::from_hours(3), test_fut.as_mut());
6961 test_helper.send_connected_event(random_bss_description!(Wpa2));
6962 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6963
6964 const DUR_MIN: i64 = 250;
6965 test_helper.advance_by(zx::MonotonicDuration::from_minutes(DUR_MIN), test_fut.as_mut());
6966
6967 let info = DisconnectInfo {
6969 connected_duration: zx::MonotonicDuration::from_minutes(DUR_MIN),
6970 disconnect_source: fidl_sme::DisconnectSource::User(
6971 fidl_sme::UserDisconnectReason::FidlConnectRequest,
6972 ),
6973 ..fake_disconnect_info()
6974 };
6975 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6976 track_subsequent_downtime: true,
6977 info: Some(info),
6978 });
6979 test_helper.drain_cobalt_events(&mut test_fut);
6980
6981 let roam_connected_duration = test_helper.get_logged_metrics(
6983 metrics::POLICY_ROAM_CONNECTED_DURATION_BEFORE_ROAM_ATTEMPT_METRIC_ID,
6984 );
6985 assert_eq!(roam_connected_duration.len(), 0);
6986
6987 let non_roam_connected_duration = test_helper
6989 .get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_NON_ROAM_DISCONNECT_METRIC_ID);
6990 assert_eq!(non_roam_connected_duration.len(), 1);
6991
6992 let roam_disconnect_counts =
6993 test_helper.get_logged_metrics(metrics::POLICY_ROAM_DISCONNECT_COUNT_METRIC_ID);
6994 assert!(roam_disconnect_counts.is_empty());
6995
6996 let non_roam_disconnect_counts =
6997 test_helper.get_logged_metrics(metrics::NON_ROAM_DISCONNECT_COUNTS_METRIC_ID);
6998 assert!(!non_roam_disconnect_counts.is_empty());
6999
7000 let total_connected_duration =
7002 test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_DISCONNECT_METRIC_ID);
7003 assert_eq!(total_connected_duration.len(), 1);
7004 assert_eq!(total_connected_duration[0].payload, MetricEventPayload::IntegerValue(DUR_MIN));
7005
7006 let total_disconnect_counts =
7007 test_helper.get_logged_metrics(metrics::NETWORK_DISCONNECT_COUNTS_METRIC_ID);
7008 assert_eq!(total_disconnect_counts.len(), 1);
7009 assert_eq!(total_disconnect_counts[0].payload, MetricEventPayload::Count(1));
7010 }
7011
7012 #[fuchsia::test]
7013 fn test_log_saved_networks_count() {
7014 let (mut test_helper, mut test_fut) = setup_test();
7015
7016 let event = TelemetryEvent::SavedNetworkCount {
7017 saved_network_count: 4,
7018 config_count_per_saved_network: vec![1, 1],
7019 };
7020 test_helper.telemetry_sender.send(event);
7021 test_helper.drain_cobalt_events(&mut test_fut);
7022
7023 let saved_networks_count =
7024 test_helper.get_logged_metrics(metrics::SAVED_NETWORKS_MIGRATED_METRIC_ID);
7025 assert_eq!(saved_networks_count.len(), 1);
7026 assert_eq!(
7027 saved_networks_count[0].event_codes,
7028 vec![metrics::SavedNetworksMigratedMetricDimensionSavedNetworks::TwoToFour as u32]
7029 );
7030
7031 let config_count = test_helper
7032 .get_logged_metrics(metrics::SAVED_CONFIGURATIONS_FOR_SAVED_NETWORK_MIGRATED_METRIC_ID);
7033 assert_eq!(config_count.len(), 2);
7034 assert_eq!(
7035 config_count[0].event_codes,
7036 vec![metrics::SavedConfigurationsForSavedNetworkMigratedMetricDimensionSavedConfigurations::One as u32]
7037 );
7038 assert_eq!(
7039 config_count[1].event_codes,
7040 vec![metrics::SavedConfigurationsForSavedNetworkMigratedMetricDimensionSavedConfigurations::One as u32]
7041 );
7042 }
7043
7044 #[fuchsia::test]
7045 fn test_log_network_selection_scan_interval() {
7046 let (mut test_helper, mut test_fut) = setup_test();
7047
7048 let duration = zx::MonotonicDuration::from_seconds(rand::random_range(0..100));
7049
7050 let event = TelemetryEvent::NetworkSelectionScanInterval { time_since_last_scan: duration };
7051 test_helper.telemetry_sender.send(event);
7052 test_helper.drain_cobalt_events(&mut test_fut);
7053
7054 let last_scan_age = test_helper
7055 .get_logged_metrics(metrics::LAST_SCAN_AGE_WHEN_SCAN_REQUESTED_MIGRATED_METRIC_ID);
7056 assert_eq!(last_scan_age.len(), 1);
7057 assert_eq!(
7058 last_scan_age[0].payload,
7059 fidl_fuchsia_metrics::MetricEventPayload::IntegerValue(duration.into_micros())
7060 );
7061 }
7062
7063 #[fuchsia::test]
7064 fn test_log_connection_selection_scan_results() {
7065 let (mut test_helper, mut test_fut) = setup_test();
7066
7067 let event = TelemetryEvent::ConnectionSelectionScanResults {
7068 saved_network_count: 4,
7069 saved_network_count_found_by_active_scan: 1,
7070 bss_count_per_saved_network: vec![10, 10],
7071 };
7072 test_helper.telemetry_sender.send(event);
7073 test_helper.drain_cobalt_events(&mut test_fut);
7074
7075 let saved_networks_count =
7076 test_helper.get_logged_metrics(metrics::SCAN_RESULTS_RECEIVED_MIGRATED_METRIC_ID);
7077 assert_eq!(saved_networks_count.len(), 1);
7078 assert_eq!(
7079 saved_networks_count[0].event_codes,
7080 vec![
7081 metrics::ScanResultsReceivedMigratedMetricDimensionSavedNetworksCount::TwoToFour
7082 as u32
7083 ]
7084 );
7085
7086 let active_scanned_network = test_helper.get_logged_metrics(
7087 metrics::SAVED_NETWORK_IN_SCAN_RESULT_WITH_ACTIVE_SCAN_MIGRATED_METRIC_ID,
7088 );
7089 assert_eq!(active_scanned_network.len(), 1);
7090 assert_eq!(
7091 active_scanned_network[0].event_codes,
7092 vec![metrics::SavedNetworkInScanResultWithActiveScanMigratedMetricDimensionActiveScanSsidsObserved::One as u32]
7093 );
7094
7095 let bss_count = test_helper
7096 .get_logged_metrics(metrics::SAVED_NETWORK_IN_SCAN_RESULT_MIGRATED_METRIC_ID);
7097 assert_eq!(bss_count.len(), 2);
7098 assert_eq!(
7099 bss_count[0].event_codes,
7100 vec![
7101 metrics::SavedNetworkInScanResultMigratedMetricDimensionBssCount::FiveToTen as u32
7102 ]
7103 );
7104 assert_eq!(
7105 bss_count[1].event_codes,
7106 vec![
7107 metrics::SavedNetworkInScanResultMigratedMetricDimensionBssCount::FiveToTen as u32
7108 ]
7109 );
7110 }
7111
7112 #[fuchsia::test]
7113 fn test_log_establish_connection_cobalt_metrics() {
7114 let (mut test_helper, mut test_fut) = setup_test();
7115
7116 let primary_channel = 8;
7117 let channel = Channel::new(primary_channel, Bandwidth::Cbw20, TwoGhz);
7118 let ap_state = random_bss_description!(Wpa2,
7119 bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
7120 channel: channel,
7121 rssi_dbm: -50,
7122 snr_db: 25,
7123 )
7124 .into();
7125 let event = TelemetryEvent::ConnectResult {
7126 iface_id: IFACE_ID,
7127 policy_connect_reason: Some(client::types::ConnectReason::FidlConnectRequest),
7128 result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
7129 multiple_bss_candidates: true,
7130 ap_state,
7131 network_is_likely_hidden: true,
7132 };
7133 test_helper.telemetry_sender.send(event);
7134 test_helper.drain_cobalt_events(&mut test_fut);
7135
7136 let policy_connect_reasons =
7137 test_helper.get_logged_metrics(metrics::POLICY_CONNECTION_ATTEMPT_MIGRATED_METRIC_ID);
7138 assert_eq!(policy_connect_reasons.len(), 1);
7139 assert_eq!(
7140 policy_connect_reasons[0].event_codes,
7141 vec![client::types::ConnectReason::FidlConnectRequest as u32]
7142 );
7143 assert_eq!(policy_connect_reasons[0].payload, MetricEventPayload::Count(1));
7144
7145 let breakdowns_by_user_wait_time = test_helper
7146 .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
7147 assert_eq!(breakdowns_by_user_wait_time.len(), 0);
7150
7151 let breakdowns_by_is_multi_bss = test_helper
7152 .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID);
7153 assert_eq!(breakdowns_by_is_multi_bss.len(), 1);
7154 assert_eq!(
7155 breakdowns_by_is_multi_bss[0].event_codes,
7156 vec![
7157 metrics::SuccessfulConnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::Yes
7158 as u32
7159 ]
7160 );
7161 assert_eq!(breakdowns_by_is_multi_bss[0].payload, MetricEventPayload::Count(1));
7162
7163 let breakdowns_by_security_type = test_helper
7164 .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID);
7165 assert_eq!(breakdowns_by_security_type.len(), 1);
7166 assert_eq!(
7167 breakdowns_by_security_type[0].event_codes,
7168 vec![
7169 metrics::SuccessfulConnectBreakdownBySecurityTypeMetricDimensionSecurityType::Wpa2Personal
7170 as u32
7171 ]
7172 );
7173 assert_eq!(breakdowns_by_security_type[0].payload, MetricEventPayload::Count(1));
7174
7175 let breakdowns_by_channel = test_helper
7176 .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID);
7177 assert_eq!(breakdowns_by_channel.len(), 1);
7178 assert_eq!(breakdowns_by_channel[0].event_codes, vec![primary_channel as u32]);
7179 assert_eq!(breakdowns_by_channel[0].payload, MetricEventPayload::Count(1));
7180
7181 let breakdowns_by_channel_band = test_helper
7182 .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID);
7183 assert_eq!(breakdowns_by_channel_band.len(), 1);
7184 assert_eq!(breakdowns_by_channel_band[0].event_codes, vec![
7185 metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band2Dot4Ghz as u32
7186 ]);
7187 assert_eq!(breakdowns_by_channel_band[0].payload, MetricEventPayload::Count(1));
7188
7189 let fidl_connect_count =
7190 test_helper.get_logged_metrics(metrics::POLICY_CONNECTION_ATTEMPTS_METRIC_ID);
7191 assert_eq!(fidl_connect_count.len(), 1);
7192 assert_eq!(fidl_connect_count[0].payload, MetricEventPayload::Count(1));
7193
7194 let network_is_likely_hidden =
7195 test_helper.get_logged_metrics(metrics::CONNECT_TO_LIKELY_HIDDEN_NETWORK_METRIC_ID);
7196 assert_eq!(network_is_likely_hidden.len(), 1);
7197 assert_eq!(network_is_likely_hidden[0].payload, MetricEventPayload::Count(1));
7198 }
7199
7200 #[fuchsia::test]
7201 fn test_log_establish_connection_status_code_cobalt_metrics_normal_device() {
7202 let (mut test_helper, mut test_fut) = setup_test();
7203 for _ in 0..3 {
7204 let event = TelemetryEvent::ConnectResult {
7205 iface_id: IFACE_ID,
7206 policy_connect_reason: Some(
7207 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
7208 ),
7209 result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
7210 multiple_bss_candidates: true,
7211 ap_state: random_bss_description!(Wpa1).into(),
7212 network_is_likely_hidden: true,
7213 };
7214 test_helper.telemetry_sender.send(event);
7215 }
7216 test_helper.send_connected_event(random_bss_description!(Wpa2));
7217 test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
7218
7219 let status_codes = test_helper.get_logged_metrics(
7220 metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
7221 );
7222 assert_eq!(status_codes.len(), 2);
7223 assert_eq_cobalt_events(
7224 status_codes,
7225 vec![
7226 MetricEvent {
7227 metric_id:
7228 metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
7229 event_codes: vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32],
7230 payload: MetricEventPayload::Count(1),
7231 },
7232 MetricEvent {
7233 metric_id:
7234 metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
7235 event_codes: vec![
7236 fidl_ieee80211::StatusCode::RefusedReasonUnspecified.into_primitive()
7237 as u32,
7238 ],
7239 payload: MetricEventPayload::Count(3),
7240 },
7241 ],
7242 );
7243 }
7244
7245 #[fuchsia::test]
7246 fn test_log_establish_connection_status_code_cobalt_metrics_bad_device() {
7247 let (mut test_helper, mut test_fut) = setup_test();
7248 for _ in 0..10 {
7249 let event = TelemetryEvent::ConnectResult {
7250 iface_id: IFACE_ID,
7251 policy_connect_reason: Some(
7252 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
7253 ),
7254 result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
7255 multiple_bss_candidates: true,
7256 ap_state: random_bss_description!(Wpa1).into(),
7257 network_is_likely_hidden: true,
7258 };
7259 test_helper.telemetry_sender.send(event);
7260 }
7261 test_helper.send_connected_event(random_bss_description!(Wpa2));
7262 test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
7263
7264 let status_codes = test_helper.get_logged_metrics(
7265 metrics::CONNECT_ATTEMPT_ON_BAD_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
7266 );
7267 assert_eq!(status_codes.len(), 2);
7268 assert_eq_cobalt_events(
7269 status_codes,
7270 vec![
7271 MetricEvent {
7272 metric_id:
7273 metrics::CONNECT_ATTEMPT_ON_BAD_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
7274 event_codes: vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32],
7275 payload: MetricEventPayload::Count(1),
7276 },
7277 MetricEvent {
7278 metric_id:
7279 metrics::CONNECT_ATTEMPT_ON_BAD_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
7280 event_codes: vec![
7281 fidl_ieee80211::StatusCode::RefusedReasonUnspecified.into_primitive()
7282 as u32,
7283 ],
7284 payload: MetricEventPayload::Count(10),
7285 },
7286 ],
7287 );
7288 }
7289
7290 #[fuchsia::test]
7291 fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_no_reset() {
7292 let (mut test_helper, mut test_fut) = setup_test();
7293
7294 test_helper
7295 .telemetry_sender
7296 .send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
7297 test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
7298 test_helper
7299 .telemetry_sender
7300 .send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
7301 test_helper.advance_by(zx::MonotonicDuration::from_seconds(4), test_fut.as_mut());
7302 test_helper.send_connected_event(random_bss_description!(Wpa2));
7303 test_helper.drain_cobalt_events(&mut test_fut);
7304
7305 let breakdowns_by_user_wait_time = test_helper
7306 .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
7307 assert_eq!(breakdowns_by_user_wait_time.len(), 1);
7308 assert_eq!(
7309 breakdowns_by_user_wait_time[0].event_codes,
7310 vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan8Seconds as u32]
7313 );
7314 }
7315
7316 #[fuchsia::test]
7317 fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_with_reset() {
7318 let (mut test_helper, mut test_fut) = setup_test();
7319
7320 test_helper
7321 .telemetry_sender
7322 .send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
7323 test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
7324 test_helper
7325 .telemetry_sender
7326 .send(TelemetryEvent::StartEstablishConnection { reset_start_time: true });
7327 test_helper.advance_by(zx::MonotonicDuration::from_seconds(4), test_fut.as_mut());
7328 test_helper.send_connected_event(random_bss_description!(Wpa2));
7329 test_helper.drain_cobalt_events(&mut test_fut);
7330
7331 let breakdowns_by_user_wait_time = test_helper
7332 .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
7333 assert_eq!(breakdowns_by_user_wait_time.len(), 1);
7334 assert_eq!(
7335 breakdowns_by_user_wait_time[0].event_codes,
7336 vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan5Seconds as u32]
7338 );
7339 }
7340
7341 #[fuchsia::test]
7342 fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_with_clear() {
7343 let (mut test_helper, mut test_fut) = setup_test();
7344
7345 test_helper
7346 .telemetry_sender
7347 .send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
7348 test_helper.advance_by(zx::MonotonicDuration::from_seconds(10), test_fut.as_mut());
7349 test_helper.telemetry_sender.send(TelemetryEvent::ClearEstablishConnectionStartTime);
7350
7351 test_helper.advance_by(zx::MonotonicDuration::from_seconds(30), test_fut.as_mut());
7352
7353 test_helper
7354 .telemetry_sender
7355 .send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
7356 test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
7357 test_helper.send_connected_event(random_bss_description!(Wpa2));
7358 test_helper.drain_cobalt_events(&mut test_fut);
7359
7360 let breakdowns_by_user_wait_time = test_helper
7361 .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
7362 assert_eq!(breakdowns_by_user_wait_time.len(), 1);
7363 assert_eq!(
7364 breakdowns_by_user_wait_time[0].event_codes,
7365 vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan3Seconds as u32]
7367 );
7368 }
7369
7370 #[test_case(
7371 (true, random_bss_description!(Wpa2)),
7372 (false, random_bss_description!(Wpa2)),
7373 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID,
7374 metrics::SuccessfulConnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::Yes as u32,
7375 metrics::SuccessfulConnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::No as u32;
7376 "breakdown_by_is_multi_bss"
7377 )]
7378 #[test_case(
7379 (false, random_bss_description!(Wpa1)),
7380 (false, random_bss_description!(Wpa2)),
7381 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID,
7382 metrics::SuccessfulConnectBreakdownBySecurityTypeMetricDimensionSecurityType::Wpa1 as u32,
7383 metrics::SuccessfulConnectBreakdownBySecurityTypeMetricDimensionSecurityType::Wpa2Personal as u32;
7384 "breakdown_by_security_type"
7385 )]
7386 #[test_case(
7387 (false, random_bss_description!(Wpa2, channel: Channel::new(6, Bandwidth::Cbw20, TwoGhz))),
7388 (false, random_bss_description!(Wpa2, channel: Channel::new(157, Bandwidth::Cbw40, FiveGhz))),
7389 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
7390 6,
7391 157;
7392 "breakdown_by_primary_channel"
7393 )]
7394 #[test_case(
7395 (false, random_bss_description!(Wpa2, channel: Channel::new(6, Bandwidth::Cbw20, TwoGhz))),
7396 (false, random_bss_description!(Wpa2, channel: Channel::new(157, Bandwidth::Cbw40, FiveGhz))),
7397 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
7398 metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band2Dot4Ghz as u32,
7399 metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band5Ghz as u32;
7400 "breakdown_by_channel_band"
7401 )]
7402 #[test_case(
7403 (false, random_bss_description!(Wpa2, rssi_dbm: -79)),
7404 (false, random_bss_description!(Wpa2, rssi_dbm: -40)),
7405 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_RSSI_BUCKET_METRIC_ID,
7406 metrics::ConnectivityWlanMetricDimensionRssiBucket::From79To77 as u32,
7407 metrics::ConnectivityWlanMetricDimensionRssiBucket::From50To35 as u32;
7408 "breakdown_by_rssi_bucket"
7409 )]
7410 #[test_case(
7411 (false, random_bss_description!(Wpa2, snr_db: 11)),
7412 (false, random_bss_description!(Wpa2, snr_db: 35)),
7413 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SNR_BUCKET_METRIC_ID,
7414 metrics::ConnectivityWlanMetricDimensionSnrBucket::From11To15 as u32,
7415 metrics::ConnectivityWlanMetricDimensionSnrBucket::From26To40 as u32;
7416 "breakdown_by_snr_bucket"
7417 )]
7418 #[fuchsia::test(add_test_attr = false)]
7419 fn test_log_daily_connect_success_rate_breakdown_cobalt_metrics(
7420 first_connect_result_params: (bool, BssDescription),
7421 second_connect_result_params: (bool, BssDescription),
7422 metric_id: u32,
7423 event_code_1: u32,
7424 event_code_2: u32,
7425 ) {
7426 let (mut test_helper, mut test_fut) = setup_test();
7427
7428 for i in 0..3 {
7429 let code = if i == 0 {
7430 fidl_ieee80211::StatusCode::Success
7431 } else {
7432 fidl_ieee80211::StatusCode::RefusedReasonUnspecified
7433 };
7434 let event = TelemetryEvent::ConnectResult {
7435 iface_id: IFACE_ID,
7436 policy_connect_reason: Some(
7437 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
7438 ),
7439 result: fake_connect_result(code),
7440 multiple_bss_candidates: first_connect_result_params.0,
7441 ap_state: first_connect_result_params.1.clone().into(),
7442 network_is_likely_hidden: true,
7443 };
7444 test_helper.telemetry_sender.send(event);
7445 }
7446 for i in 0..2 {
7447 let code = if i == 0 {
7448 fidl_ieee80211::StatusCode::Success
7449 } else {
7450 fidl_ieee80211::StatusCode::RefusedReasonUnspecified
7451 };
7452 let event = TelemetryEvent::ConnectResult {
7453 iface_id: IFACE_ID,
7454 policy_connect_reason: Some(
7455 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
7456 ),
7457 result: fake_connect_result(code),
7458 multiple_bss_candidates: second_connect_result_params.0,
7459 ap_state: second_connect_result_params.1.clone().into(),
7460 network_is_likely_hidden: true,
7461 };
7462 test_helper.telemetry_sender.send(event);
7463 }
7464
7465 test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
7466
7467 let metrics = test_helper.get_logged_metrics(metric_id);
7468 assert_eq!(metrics.len(), 2);
7469 assert_eq_cobalt_events(
7470 metrics,
7471 vec![
7472 MetricEvent {
7473 metric_id,
7474 event_codes: vec![event_code_1],
7475 payload: MetricEventPayload::IntegerValue(3333), },
7477 MetricEvent {
7478 metric_id,
7479 event_codes: vec![event_code_2],
7480 payload: MetricEventPayload::IntegerValue(5000), },
7482 ],
7483 );
7484 }
7485
7486 #[fuchsia::test]
7487 fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_while_connected() {
7488 let (mut test_helper, mut test_fut) = setup_test();
7489 test_helper.send_connected_event(random_bss_description!(Wpa2));
7490 test_helper.drain_cobalt_events(&mut test_fut);
7491 test_helper.cobalt_events.clear();
7492
7493 test_helper
7494 .telemetry_sender
7495 .send(TelemetryEvent::StartEstablishConnection { reset_start_time: true });
7496 test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
7497 let info = fake_disconnect_info();
7498 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7499 track_subsequent_downtime: false,
7500 info: Some(info),
7501 });
7502 test_helper.advance_by(zx::MonotonicDuration::from_seconds(4), test_fut.as_mut());
7503 test_helper.send_connected_event(random_bss_description!(Wpa2));
7504 test_helper.drain_cobalt_events(&mut test_fut);
7505
7506 let breakdowns_by_user_wait_time = test_helper
7507 .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
7508 assert_eq!(breakdowns_by_user_wait_time.len(), 1);
7509 assert_eq!(
7510 breakdowns_by_user_wait_time[0].event_codes,
7511 vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan8Seconds as u32]
7514 );
7515 }
7516
7517 #[fuchsia::test]
7518 fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_with_clear_while_connected()
7519 {
7520 let (mut test_helper, mut test_fut) = setup_test();
7521 test_helper.send_connected_event(random_bss_description!(Wpa2));
7522 test_helper.drain_cobalt_events(&mut test_fut);
7523 test_helper.cobalt_events.clear();
7524
7525 test_helper
7526 .telemetry_sender
7527 .send(TelemetryEvent::StartEstablishConnection { reset_start_time: true });
7528 test_helper.telemetry_sender.send(TelemetryEvent::ClearEstablishConnectionStartTime);
7529 let info = fake_disconnect_info();
7530 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7531 track_subsequent_downtime: false,
7532 info: Some(info),
7533 });
7534 test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
7535 test_helper
7536 .telemetry_sender
7537 .send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
7538 test_helper.advance_by(zx::MonotonicDuration::from_seconds(4), test_fut.as_mut());
7539 test_helper.send_connected_event(random_bss_description!(Wpa2));
7540 test_helper.drain_cobalt_events(&mut test_fut);
7541
7542 let breakdowns_by_user_wait_time = test_helper
7543 .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
7544 assert_eq!(breakdowns_by_user_wait_time.len(), 1);
7545 assert_eq!(
7546 breakdowns_by_user_wait_time[0].event_codes,
7547 vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan5Seconds as u32]
7549 );
7550 }
7551
7552 #[fuchsia::test]
7553 fn test_log_establish_connection_cobalt_metrics_user_wait_time_logged_for_sme_reconnecting() {
7554 let (mut test_helper, mut test_fut) = setup_test();
7555 test_helper.send_connected_event(random_bss_description!(Wpa2));
7556 test_helper.drain_cobalt_events(&mut test_fut);
7557 test_helper.cobalt_events.clear();
7558
7559 let info = DisconnectInfo { is_sme_reconnecting: true, ..fake_disconnect_info() };
7560 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7561 track_subsequent_downtime: false,
7562 info: Some(info),
7563 });
7564 test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
7565 test_helper.send_connected_event(random_bss_description!(Wpa2));
7566 test_helper.drain_cobalt_events(&mut test_fut);
7567
7568 let breakdowns_by_user_wait_time = test_helper
7569 .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
7570 assert_eq!(breakdowns_by_user_wait_time.len(), 1);
7571 assert_eq!(
7572 breakdowns_by_user_wait_time[0].event_codes,
7573 vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan3Seconds as u32]
7574 );
7575 }
7576
7577 #[fuchsia::test]
7578 fn test_log_downtime_cobalt_metrics() {
7579 let (mut test_helper, mut test_fut) = setup_test();
7580 test_helper.send_connected_event(random_bss_description!(Wpa2));
7581 test_helper.drain_cobalt_events(&mut test_fut);
7582
7583 let info = DisconnectInfo {
7584 disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
7585 reason_code: fidl_ieee80211::ReasonCode::LeavingNetworkDeauth,
7586 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
7587 }),
7588 ..fake_disconnect_info()
7589 };
7590 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7591 track_subsequent_downtime: true,
7592 info: Some(info),
7593 });
7594 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
7595
7596 test_helper.advance_by(zx::MonotonicDuration::from_minutes(42), test_fut.as_mut());
7597 test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
7599 network_selection_type: NetworkSelectionType::Undirected,
7600 num_candidates: Ok(0),
7601 selected_count: 0,
7602 });
7603 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
7604
7605 test_helper.advance_by(zx::MonotonicDuration::from_minutes(5), test_fut.as_mut());
7606 test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
7608 network_selection_type: NetworkSelectionType::Undirected,
7609 num_candidates: Ok(5),
7610 selected_count: 1,
7611 });
7612 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
7613
7614 test_helper.advance_by(zx::MonotonicDuration::from_minutes(7), test_fut.as_mut());
7615 test_helper.send_connected_event(random_bss_description!(Wpa2));
7617 test_helper.drain_cobalt_events(&mut test_fut);
7618
7619 let breakdowns_by_reason = test_helper
7620 .get_logged_metrics(metrics::DOWNTIME_BREAKDOWN_BY_DISCONNECT_REASON_METRIC_ID);
7621 assert_eq!(breakdowns_by_reason.len(), 1);
7622 assert_eq!(
7623 breakdowns_by_reason[0].event_codes,
7624 vec![3u32, metrics::ConnectivityWlanMetricDimensionDisconnectSource::Mlme as u32,]
7625 );
7626 assert_eq!(
7627 breakdowns_by_reason[0].payload,
7628 MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(49).into_micros())
7629 );
7630 }
7631
7632 #[fuchsia::test]
7633 fn test_log_reconnect_cobalt_metrics() {
7634 let (mut test_helper, mut test_fut) = setup_test();
7635 test_helper.send_connected_event(random_bss_description!(Wpa2));
7636 test_helper.drain_cobalt_events(&mut test_fut);
7637
7638 let info = DisconnectInfo {
7640 disconnect_source: fidl_sme::DisconnectSource::User(
7641 fidl_sme::UserDisconnectReason::ProactiveNetworkSwitch,
7642 ),
7643 ..fake_disconnect_info()
7644 };
7645 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7646 track_subsequent_downtime: true,
7647 info: Some(info),
7648 });
7649 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
7650
7651 test_helper.advance_by(zx::MonotonicDuration::from_seconds(3), test_fut.as_mut());
7652 test_helper.send_connected_event(random_bss_description!(Wpa2));
7654 test_helper.drain_cobalt_events(&mut test_fut);
7655
7656 let metrics =
7658 test_helper.get_logged_metrics(metrics::NON_ROAM_RECONNECT_DURATION_METRIC_ID);
7659 assert_eq!(metrics.len(), 1);
7660 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(3_000_000));
7661 assert!(
7662 test_helper
7663 .get_logged_metrics(metrics::POLICY_ROAM_RECONNECT_DURATION_METRIC_ID)
7664 .is_empty()
7665 );
7666
7667 test_helper.clear_cobalt_events();
7669 let info = DisconnectInfo {
7670 disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
7671 reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
7672 mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
7673 }),
7674 ..fake_disconnect_info()
7675 };
7676 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7677 track_subsequent_downtime: true,
7678 info: Some(info),
7679 });
7680 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
7681 test_helper.advance_by(zx::MonotonicDuration::from_seconds(1), test_fut.as_mut());
7682 test_helper.send_connected_event(random_bss_description!(Wpa2));
7684 test_helper.drain_cobalt_events(&mut test_fut);
7685
7686 assert!(
7689 test_helper
7690 .get_logged_metrics(metrics::NON_ROAM_RECONNECT_DURATION_METRIC_ID)
7691 .is_empty()
7692 );
7693 assert!(
7697 test_helper
7698 .get_logged_metrics(metrics::POLICY_ROAM_RECONNECT_DURATION_METRIC_ID)
7699 .is_empty()
7700 );
7701 }
7702
7703 #[fuchsia::test]
7704 fn test_log_device_connected_cobalt_metrics() {
7705 let (mut test_helper, mut test_fut) = setup_test();
7706
7707 let wmm_info = vec![0x80]; #[rustfmt::skip]
7709 let rm_enabled_capabilities = vec![
7710 0x03, 0x00, 0x00, 0x00, 0x00,
7712 ];
7713 #[rustfmt::skip]
7714 let ext_capabilities = vec![
7715 0x04, 0x00,
7716 0x08, 0x00, 0x00, 0x00, 0x00, 0x40
7718 ];
7719 let bss_description = random_bss_description!(Wpa2,
7720 channel: Channel::new(157, Bandwidth::Cbw40, FiveGhz),
7721 ies_overrides: IesOverrides::new()
7722 .remove(IeType::WMM_PARAM)
7723 .set(IeType::WMM_INFO, wmm_info)
7724 .set(IeType::RM_ENABLED_CAPABILITIES, rm_enabled_capabilities)
7725 .set(IeType::MOBILITY_DOMAIN, vec![0x00; 3])
7726 .set(IeType::EXT_CAPABILITIES, ext_capabilities),
7727 bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
7728 );
7729 test_helper.send_connected_event(bss_description);
7730 test_helper.drain_cobalt_events(&mut test_fut);
7731
7732 let num_devices_connected =
7733 test_helper.get_logged_metrics(metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID);
7734 assert_eq!(num_devices_connected.len(), 1);
7735 assert_eq!(num_devices_connected[0].payload, MetricEventPayload::Count(1));
7736
7737 let connected_security_type =
7738 test_helper.get_logged_metrics(metrics::CONNECTED_NETWORK_SECURITY_TYPE_METRIC_ID);
7739 assert_eq!(connected_security_type.len(), 1);
7740 assert_eq!(
7741 connected_security_type[0].event_codes,
7742 vec![
7743 metrics::ConnectedNetworkSecurityTypeMetricDimensionSecurityType::Wpa2Personal
7744 as u32
7745 ]
7746 );
7747 assert_eq!(connected_security_type[0].payload, MetricEventPayload::Count(1));
7748
7749 let connected_apsd = test_helper
7750 .get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_APSD_METRIC_ID);
7751 assert_eq!(connected_apsd.len(), 1);
7752 assert_eq!(connected_apsd[0].payload, MetricEventPayload::Count(1));
7753
7754 let connected_link_measurement = test_helper.get_logged_metrics(
7755 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_LINK_MEASUREMENT_METRIC_ID,
7756 );
7757 assert_eq!(connected_link_measurement.len(), 1);
7758 assert_eq!(connected_link_measurement[0].payload, MetricEventPayload::Count(1));
7759
7760 let connected_neighbor_report = test_helper.get_logged_metrics(
7761 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_NEIGHBOR_REPORT_METRIC_ID,
7762 );
7763 assert_eq!(connected_neighbor_report.len(), 1);
7764 assert_eq!(connected_neighbor_report[0].payload, MetricEventPayload::Count(1));
7765
7766 let connected_ft = test_helper
7767 .get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_FT_METRIC_ID);
7768 assert_eq!(connected_ft.len(), 1);
7769 assert_eq!(connected_ft[0].payload, MetricEventPayload::Count(1));
7770
7771 let connected_bss_transition_mgmt = test_helper.get_logged_metrics(
7772 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_BSS_TRANSITION_MANAGEMENT_METRIC_ID,
7773 );
7774 assert_eq!(connected_bss_transition_mgmt.len(), 1);
7775 assert_eq!(connected_bss_transition_mgmt[0].payload, MetricEventPayload::Count(1));
7776
7777 let breakdown_by_is_multi_bss = test_helper.get_logged_metrics(
7778 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID,
7779 );
7780 assert_eq!(breakdown_by_is_multi_bss.len(), 1);
7781 assert_eq!(
7782 breakdown_by_is_multi_bss[0].event_codes,
7783 vec![
7784 metrics::SuccessfulConnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::Yes
7785 as u32
7786 ]
7787 );
7788 assert_eq!(breakdown_by_is_multi_bss[0].payload, MetricEventPayload::Count(1));
7789
7790 let breakdown_by_primary_channel = test_helper.get_logged_metrics(
7791 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
7792 );
7793 assert_eq!(breakdown_by_primary_channel.len(), 1);
7794 assert_eq!(breakdown_by_primary_channel[0].event_codes, vec![157]);
7795 assert_eq!(breakdown_by_primary_channel[0].payload, MetricEventPayload::Count(1));
7796
7797 let breakdown_by_channel_band = test_helper.get_logged_metrics(
7798 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
7799 );
7800 assert_eq!(breakdown_by_channel_band.len(), 1);
7801 assert_eq!(
7802 breakdown_by_channel_band[0].event_codes,
7803 vec![
7804 metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band5Ghz
7805 as u32
7806 ]
7807 );
7808 assert_eq!(breakdown_by_channel_band[0].payload, MetricEventPayload::Count(1));
7809
7810 let ap_oui_connected =
7811 test_helper.get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID);
7812 assert_eq!(ap_oui_connected.len(), 1);
7813 assert_eq!(
7814 ap_oui_connected[0].payload,
7815 MetricEventPayload::StringValue("00F620".to_string())
7816 );
7817
7818 let network_is_likely_hidden =
7819 test_helper.get_logged_metrics(metrics::CONNECT_TO_LIKELY_HIDDEN_NETWORK_METRIC_ID);
7820 assert_eq!(network_is_likely_hidden.len(), 1);
7821 assert_eq!(network_is_likely_hidden[0].payload, MetricEventPayload::Count(1));
7822 }
7823
7824 #[fuchsia::test]
7825 fn test_log_device_connected_cobalt_metrics_ap_features_not_supported() {
7826 let (mut test_helper, mut test_fut) = setup_test();
7827
7828 let bss_description = random_bss_description!(Wpa2,
7829 ies_overrides: IesOverrides::new()
7830 .remove(IeType::WMM_PARAM)
7831 .remove(IeType::WMM_INFO)
7832 .remove(IeType::RM_ENABLED_CAPABILITIES)
7833 .remove(IeType::MOBILITY_DOMAIN)
7834 .remove(IeType::EXT_CAPABILITIES)
7835 );
7836 test_helper.send_connected_event(bss_description);
7837 test_helper.drain_cobalt_events(&mut test_fut);
7838
7839 let connected_apsd = test_helper
7840 .get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_APSD_METRIC_ID);
7841 assert_eq!(connected_apsd.len(), 0);
7842
7843 let connected_link_measurement = test_helper.get_logged_metrics(
7844 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_LINK_MEASUREMENT_METRIC_ID,
7845 );
7846 assert_eq!(connected_link_measurement.len(), 0);
7847
7848 let connected_neighbor_report = test_helper.get_logged_metrics(
7849 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_NEIGHBOR_REPORT_METRIC_ID,
7850 );
7851 assert_eq!(connected_neighbor_report.len(), 0);
7852
7853 let connected_ft = test_helper
7854 .get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_FT_METRIC_ID);
7855 assert_eq!(connected_ft.len(), 0);
7856
7857 let connected_bss_transition_mgmt = test_helper.get_logged_metrics(
7858 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_BSS_TRANSITION_MANAGEMENT_METRIC_ID,
7859 );
7860 assert_eq!(connected_bss_transition_mgmt.len(), 0);
7861 }
7862
7863 #[test_case(metrics::CONNECT_TO_LIKELY_HIDDEN_NETWORK_METRIC_ID, None; "connect_to_likely_hidden_network")]
7864 #[test_case(metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID, None; "breakdown_by_is_multi_bss")]
7865 #[fuchsia::test(add_test_attr = false)]
7866 fn test_log_device_connected_cobalt_metrics_on_disconnect_and_periodically(
7867 metric_id: u32,
7868 payload: Option<Vec<MetricEvent>>,
7869 ) {
7870 let (mut test_helper, mut test_fut) = setup_test();
7871
7872 let bss_description = random_bss_description!(Wpa2,
7873 bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
7874 );
7875 test_helper.send_connected_event(bss_description);
7876 test_helper.drain_cobalt_events(&mut test_fut);
7877 test_helper.cobalt_events.clear();
7878
7879 test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
7880
7881 let metrics = test_helper.get_logged_metrics(metric_id);
7884 assert!(!metrics.is_empty());
7885
7886 if let Some(payload) = payload {
7887 assert_eq_cobalt_events(metrics, payload)
7888 }
7889
7890 test_helper.cobalt_events.clear();
7891
7892 let info = fake_disconnect_info();
7893 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7894 track_subsequent_downtime: false,
7895 info: Some(info),
7896 });
7897 test_helper.drain_cobalt_events(&mut test_fut);
7898
7899 let metrics = test_helper.get_logged_metrics(metric_id);
7901 assert_eq!(metrics.len(), 1);
7902 }
7903
7904 #[fuchsia::test]
7905 fn test_log_device_connected_cobalt_metrics_on_channel_switched() {
7906 let (mut test_helper, mut test_fut) = setup_test();
7907 let bss_description = random_bss_description!(Wpa2,
7908 channel: Channel::new(4, Bandwidth::Cbw20, TwoGhz),
7909 );
7910 test_helper.send_connected_event(bss_description);
7911 test_helper.drain_cobalt_events(&mut test_fut);
7912
7913 let breakdown_by_primary_channel = test_helper.get_logged_metrics(
7914 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
7915 );
7916 assert_eq!(breakdown_by_primary_channel.len(), 1);
7917 assert_eq!(breakdown_by_primary_channel[0].event_codes, vec![4]);
7918 assert_eq!(breakdown_by_primary_channel[0].payload, MetricEventPayload::Count(1));
7919
7920 let breakdown_by_channel_band = test_helper.get_logged_metrics(
7921 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
7922 );
7923 assert_eq!(breakdown_by_channel_band.len(), 1);
7924 assert_eq!(
7925 breakdown_by_channel_band[0].event_codes,
7926 vec![
7927 metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band2Dot4Ghz
7928 as u32
7929 ]
7930 );
7931 assert_eq!(breakdown_by_channel_band[0].payload, MetricEventPayload::Count(1));
7932
7933 test_helper.cobalt_events.clear();
7935
7936 test_helper.telemetry_sender.send(TelemetryEvent::OnChannelSwitched {
7937 info: fidl_internal::ChannelSwitchInfo {
7938 new_primary_channel: fidl_ieee80211::ChannelNumber {
7939 band: fidl_ieee80211::WlanBand::FiveGhz,
7940 number: 157,
7941 },
7942 bandwidth: fidl_ieee80211::ChannelBandwidth::Cbw20,
7943 vht_secondary_80_channel: fidl_ieee80211::ChannelNumber {
7944 band: fidl_ieee80211::WlanBand::FiveGhz,
7945 number: 0,
7946 },
7947 },
7948 });
7949 test_helper.drain_cobalt_events(&mut test_fut);
7950
7951 let breakdown_by_primary_channel = test_helper.get_logged_metrics(
7954 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
7955 );
7956 assert_eq!(breakdown_by_primary_channel.len(), 1);
7957 assert_eq!(breakdown_by_primary_channel[0].event_codes, vec![157]);
7958 assert_eq!(breakdown_by_primary_channel[0].payload, MetricEventPayload::Count(1));
7959
7960 let breakdown_by_channel_band = test_helper.get_logged_metrics(
7961 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
7962 );
7963 assert_eq!(breakdown_by_channel_band.len(), 1);
7964 assert_eq!(
7965 breakdown_by_channel_band[0].event_codes,
7966 vec![
7967 metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band5Ghz
7968 as u32
7969 ]
7970 );
7971 assert_eq!(breakdown_by_channel_band[0].payload, MetricEventPayload::Count(1));
7972 }
7973
7974 #[fuchsia::test]
7975 fn test_active_scan_requested_metric() {
7976 let (mut test_helper, mut test_fut) = setup_test();
7977
7978 test_helper
7979 .telemetry_sender
7980 .send(TelemetryEvent::ActiveScanRequested { num_ssids_requested: 4 });
7981
7982 test_helper.drain_cobalt_events(&mut test_fut);
7983 let metrics = test_helper.get_logged_metrics(
7984 metrics::ACTIVE_SCAN_REQUESTED_FOR_NETWORK_SELECTION_MIGRATED_METRIC_ID,
7985 );
7986 assert_eq!(metrics.len(), 1);
7987 assert_eq!(metrics[0].event_codes, vec![metrics::ActiveScanRequestedForNetworkSelectionMigratedMetricDimensionActiveScanSsidsRequested::TwoToFour as u32]);
7988 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
7989 }
7990
7991 #[fuchsia::test]
7992 fn test_log_device_performed_roaming_scan() {
7993 let (mut test_helper, mut test_fut) = setup_test();
7994
7995 test_helper.telemetry_sender.send(TelemetryEvent::PolicyRoamScan {
7997 reasons: vec![RoamReason::RssiBelowThreshold, RoamReason::SnrBelowThreshold],
7998 });
7999 test_helper.drain_cobalt_events(&mut test_fut);
8000
8001 let metrics = test_helper.get_logged_metrics(metrics::POLICY_ROAM_SCAN_COUNT_METRIC_ID);
8003 assert_eq!(metrics.len(), 1);
8004 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8005
8006 let metrics = test_helper
8008 .get_logged_metrics(metrics::POLICY_ROAM_SCAN_COUNT_BY_ROAM_REASON_METRIC_ID);
8009 assert_eq!(metrics.len(), 2);
8010 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8011 assert_eq!(
8012 metrics[0].event_codes,
8013 vec![convert::convert_roam_reason_dimension(RoamReason::RssiBelowThreshold) as u32]
8014 );
8015 assert_eq!(metrics[1].payload, MetricEventPayload::Count(1));
8016 assert_eq!(
8017 metrics[1].event_codes,
8018 vec![convert::convert_roam_reason_dimension(RoamReason::SnrBelowThreshold) as u32]
8019 );
8020 }
8021
8022 #[fuchsia::test]
8023 fn test_log_policy_roam_attempt() {
8024 let (mut test_helper, mut test_fut) = setup_test();
8025
8026 let candidate = generate_random_scanned_candidate();
8028 test_helper.telemetry_sender.send(TelemetryEvent::PolicyRoamAttempt {
8029 request: PolicyRoamRequest {
8030 candidate,
8031 reasons: vec![RoamReason::RssiBelowThreshold, RoamReason::SnrBelowThreshold],
8032 },
8033 connected_duration: zx::Duration::from_hours(1),
8034 });
8035 test_helper.drain_cobalt_events(&mut test_fut);
8036
8037 let metrics = test_helper.get_logged_metrics(metrics::POLICY_ROAM_ATTEMPT_COUNT_METRIC_ID);
8038 assert_eq!(metrics.len(), 1);
8039 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8040
8041 let metrics = test_helper
8043 .get_logged_metrics(metrics::POLICY_ROAM_ATTEMPT_COUNT_BY_ROAM_REASON_METRIC_ID);
8044 assert_eq!(metrics.len(), 2);
8045 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8046 assert_eq!(
8047 metrics[0].event_codes,
8048 vec![convert::convert_roam_reason_dimension(RoamReason::RssiBelowThreshold) as u32]
8049 );
8050 assert_eq!(metrics[1].payload, MetricEventPayload::Count(1));
8051 assert_eq!(
8052 metrics[1].event_codes,
8053 vec![convert::convert_roam_reason_dimension(RoamReason::SnrBelowThreshold) as u32]
8054 );
8055
8056 let metrics = test_helper.get_logged_metrics(
8058 metrics::POLICY_ROAM_CONNECTED_DURATION_BEFORE_ROAM_ATTEMPT_METRIC_ID,
8059 );
8060 assert_eq!(metrics.len(), 2);
8061 assert_eq!(metrics.len(), 2);
8062 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(60));
8063 assert_eq!(
8064 metrics[0].event_codes,
8065 vec![convert::convert_roam_reason_dimension(RoamReason::RssiBelowThreshold) as u32]
8066 );
8067 assert_eq!(metrics[1].payload, MetricEventPayload::IntegerValue(60));
8068 assert_eq!(
8069 metrics[1].event_codes,
8070 vec![convert::convert_roam_reason_dimension(RoamReason::SnrBelowThreshold) as u32]
8071 );
8072 }
8073
8074 fn log_policy_roam_attempt_and_result(
8076 test_helper: &mut TestHelper,
8077 is_success: bool,
8078 reasons: Vec<RoamReason>,
8079 ) {
8080 let status_code = if is_success {
8081 fidl_ieee80211::StatusCode::Success
8082 } else {
8083 fidl_ieee80211::StatusCode::RefusedReasonUnspecified
8084 };
8085
8086 let request = PolicyRoamRequest { candidate: generate_random_scanned_candidate(), reasons };
8088 let event = TelemetryEvent::PolicyRoamAttempt {
8089 request: request.clone(),
8090 connected_duration: zx::MonotonicDuration::from_hours(1),
8091 };
8092 test_helper.telemetry_sender.send(event);
8093
8094 let result = fidl_sme::RoamResult {
8096 bssid: [1, 1, 1, 1, 1, 1],
8097 status_code,
8098 original_association_maintained: false,
8099 bss_description: Some(Box::new(random_fidl_bss_description!())),
8100 disconnect_info: None,
8101 is_credential_rejected: false,
8102 };
8103
8104 let event = TelemetryEvent::PolicyInitiatedRoamResult {
8105 iface_id: IFACE_ID,
8106 result,
8107 updated_ap_state: random_bss_description!().into(),
8108 original_ap_state: Box::new(random_bss_description!().into()),
8109 request: Box::new(request.clone()),
8110 request_time: fasync::MonotonicInstant::now(),
8111 result_time: fasync::MonotonicInstant::now(),
8112 };
8113 test_helper.telemetry_sender.send(event);
8114 }
8115
8116 #[fuchsia::test]
8117 fn test_log_policy_roam_success_rate_cobalt_metrics() {
8118 let (mut test_helper, mut test_fut) = setup_test();
8119 test_helper.send_connected_event(random_bss_description!(Wpa1));
8120
8121 log_policy_roam_attempt_and_result(
8123 &mut test_helper,
8124 true,
8125 vec![RoamReason::RssiBelowThreshold],
8126 );
8127 log_policy_roam_attempt_and_result(
8128 &mut test_helper,
8129 true,
8130 vec![RoamReason::RssiBelowThreshold],
8131 );
8132
8133 log_policy_roam_attempt_and_result(
8135 &mut test_helper,
8136 false,
8137 vec![RoamReason::RssiBelowThreshold],
8138 );
8139
8140 test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
8141
8142 let metrics = test_helper.get_logged_metrics(metrics::POLICY_ROAM_SUCCESS_RATE_METRIC_ID);
8143 assert_eq!(metrics.len(), 1);
8144 assert_eq_cobalt_events(
8145 metrics,
8146 vec![MetricEvent {
8147 metric_id: metrics::POLICY_ROAM_SUCCESS_RATE_METRIC_ID,
8148 event_codes: vec![],
8149 payload: MetricEventPayload::IntegerValue(6666), }],
8151 );
8152 }
8153
8154 #[fuchsia::test]
8155 fn test_log_policy_roam_success_rate_by_roam_reason_cobalt_metrics() {
8156 let (mut test_helper, mut test_fut) = setup_test();
8157 test_helper.send_connected_event(random_bss_description!(Wpa1));
8158
8159 log_policy_roam_attempt_and_result(
8161 &mut test_helper,
8162 true,
8163 vec![RoamReason::RssiBelowThreshold],
8164 );
8165 log_policy_roam_attempt_and_result(
8166 &mut test_helper,
8167 true,
8168 vec![RoamReason::RssiBelowThreshold, RoamReason::SnrBelowThreshold],
8169 );
8170
8171 log_policy_roam_attempt_and_result(
8173 &mut test_helper,
8174 false,
8175 vec![RoamReason::RssiBelowThreshold],
8176 );
8177
8178 test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
8179
8180 let metrics = test_helper
8181 .get_logged_metrics(metrics::POLICY_ROAM_SUCCESS_RATE_BY_ROAM_REASON_METRIC_ID);
8182 assert_eq!(metrics.len(), 2);
8183 assert_eq_cobalt_events(
8184 metrics,
8185 vec![
8186 MetricEvent {
8187 metric_id: metrics::POLICY_ROAM_SUCCESS_RATE_BY_ROAM_REASON_METRIC_ID,
8188 event_codes: vec![convert::convert_roam_reason_dimension(
8189 RoamReason::RssiBelowThreshold,
8190 ) as u32],
8191 payload: MetricEventPayload::IntegerValue(6666), },
8193 MetricEvent {
8194 metric_id: metrics::POLICY_ROAM_SUCCESS_RATE_BY_ROAM_REASON_METRIC_ID,
8195 event_codes: vec![convert::convert_roam_reason_dimension(
8196 RoamReason::SnrBelowThreshold,
8197 ) as u32],
8198 payload: MetricEventPayload::IntegerValue(10000), },
8200 ],
8201 );
8202 }
8203
8204 #[fuchsia::test]
8205 fn test_connection_enabled_duration_metric() {
8206 let (mut test_helper, mut test_fut) = setup_test();
8207
8208 test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
8209 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8210 test_helper.advance_by(zx::MonotonicDuration::from_seconds(10), test_fut.as_mut());
8211 test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
8212
8213 test_helper.drain_cobalt_events(&mut test_fut);
8214 let metrics = test_helper
8215 .get_logged_metrics(metrics::CLIENT_CONNECTIONS_ENABLED_DURATION_MIGRATED_METRIC_ID);
8216 assert_eq!(metrics.len(), 1);
8217 assert_eq!(
8218 metrics[0].payload,
8219 MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_seconds(10).into_micros())
8220 );
8221 }
8222
8223 #[fuchsia::test]
8224 fn test_stop_ap_metric() {
8225 let (mut test_helper, mut test_fut) = setup_test();
8226
8227 test_helper.telemetry_sender.send(TelemetryEvent::StopAp {
8228 enabled_duration: zx::MonotonicDuration::from_seconds(50),
8229 });
8230
8231 test_helper.drain_cobalt_events(&mut test_fut);
8232 let metrics = test_helper
8233 .get_logged_metrics(metrics::ACCESS_POINT_ENABLED_DURATION_MIGRATED_METRIC_ID);
8234 assert_eq!(metrics.len(), 1);
8235 assert_eq!(
8236 metrics[0].payload,
8237 MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_seconds(50).into_micros())
8238 );
8239 }
8240
8241 #[derive(PartialEq)]
8242 enum CreateMetricsLoggerFailureMode {
8243 None,
8244 FactoryRequest,
8245 ApiFailure,
8246 }
8247
8248 #[test_case(CreateMetricsLoggerFailureMode::None)]
8249 #[test_case(CreateMetricsLoggerFailureMode::FactoryRequest)]
8250 #[test_case(CreateMetricsLoggerFailureMode::ApiFailure)]
8251 #[fuchsia::test]
8252 fn test_create_metrics_logger(failure_mode: CreateMetricsLoggerFailureMode) {
8253 let mut exec = fasync::TestExecutor::new();
8254 let (factory_proxy, mut factory_stream) = fidl::endpoints::create_proxy_and_stream::<
8255 fidl_fuchsia_metrics::MetricEventLoggerFactoryMarker,
8256 >();
8257
8258 let fut = create_metrics_logger(&factory_proxy);
8259 let mut fut = pin!(fut);
8260
8261 if failure_mode == CreateMetricsLoggerFailureMode::FactoryRequest {
8263 drop(factory_stream);
8264 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(Err(_)));
8265 return;
8266 }
8267
8268 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
8271
8272 let request = exec.run_until_stalled(&mut factory_stream.next());
8273 assert_matches!(
8274 request,
8275 Poll::Ready(Some(Ok(fidl_fuchsia_metrics::MetricEventLoggerFactoryRequest::CreateMetricEventLogger {
8276 project_spec: fidl_fuchsia_metrics::ProjectSpec {
8277 customer_id: None,
8278 project_id: Some(metrics::PROJECT_ID),
8279 ..
8280 },
8281 responder,
8282 ..
8283 }))) => {
8284 match failure_mode {
8285 CreateMetricsLoggerFailureMode::FactoryRequest => panic!("The factory request failure should have been handled already."),
8286 CreateMetricsLoggerFailureMode::None => responder.send(Ok(())).expect("failed to send response"),
8287 CreateMetricsLoggerFailureMode::ApiFailure => responder.send(Err(fidl_fuchsia_metrics::Error::InvalidArguments)).expect("failed to send response"),
8288 }
8289 }
8290 );
8291
8292 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(result) => {
8295 match failure_mode {
8296 CreateMetricsLoggerFailureMode::FactoryRequest => panic!("The factory request failure should have been handled already."),
8297 CreateMetricsLoggerFailureMode::None => assert_matches!(result, Ok(_)),
8298 CreateMetricsLoggerFailureMode::ApiFailure => assert_matches!(result, Err(_))
8299 }
8300 });
8301 }
8302
8303 #[test_case(ScanIssue::ScanFailure, metrics::CLIENT_SCAN_FAILURE_METRIC_ID)]
8304 #[test_case(ScanIssue::AbortedScan, metrics::ABORTED_SCAN_METRIC_ID)]
8305 #[test_case(ScanIssue::EmptyScanResults, metrics::EMPTY_SCAN_RESULTS_METRIC_ID)]
8306 #[fuchsia::test(add_test_attr = false)]
8307 fn test_scan_defect_metrics(scan_issue: ScanIssue, expected_metric_id: u32) {
8308 let (mut test_helper, mut test_fut) = setup_test();
8309
8310 let event = TelemetryEvent::ScanEvent {
8311 inspect_data: ScanEventInspectData::new(),
8312 scan_defects: vec![scan_issue],
8313 };
8314
8315 test_helper.telemetry_sender.send(event);
8317
8318 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8320
8321 test_helper.drain_cobalt_events(&mut test_fut);
8323 let logged_metrics = test_helper.get_logged_metrics(expected_metric_id);
8324 assert_eq!(logged_metrics.len(), 1);
8325 }
8326
8327 #[fuchsia::test]
8328 fn test_log_ap_start_failure() {
8329 let (mut test_helper, mut test_fut) = setup_test();
8330
8331 test_helper.telemetry_sender.send(TelemetryEvent::StartApResult(Err(())));
8333
8334 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8336
8337 test_helper.drain_cobalt_events(&mut test_fut);
8339 let logged_metrics = test_helper.get_logged_metrics(metrics::AP_START_FAILURE_METRIC_ID);
8340 assert_eq!(logged_metrics.len(), 1);
8341 }
8342
8343 #[test_case(
8344 RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset),
8345 metrics::RecoveryOccurrenceMetricDimensionReason::InterfaceCreationFailure ;
8346 "log recovery event for iface creation failure"
8347 )]
8348 #[test_case(
8349 RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset),
8350 metrics::RecoveryOccurrenceMetricDimensionReason::InterfaceDestructionFailure ;
8351 "log recovery event for iface destruction failure"
8352 )]
8353 #[test_case(
8354 RecoveryReason::ConnectFailure(ClientRecoveryMechanism::Disconnect),
8355 metrics::RecoveryOccurrenceMetricDimensionReason::ClientConnectionFailure ;
8356 "log recovery event for connect failure"
8357 )]
8358 #[test_case(
8359 RecoveryReason::StartApFailure(ApRecoveryMechanism::StopAp),
8360 metrics::RecoveryOccurrenceMetricDimensionReason::ApStartFailure ;
8361 "log recovery event for start AP failure"
8362 )]
8363 #[test_case(
8364 RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect),
8365 metrics::RecoveryOccurrenceMetricDimensionReason::ScanFailure ;
8366 "log recovery event for scan failure"
8367 )]
8368 #[test_case(
8369 RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect),
8370 metrics::RecoveryOccurrenceMetricDimensionReason::ScanCancellation ;
8371 "log recovery event for scan cancellation"
8372 )]
8373 #[test_case(
8374 RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect),
8375 metrics::RecoveryOccurrenceMetricDimensionReason::ScanResultsEmpty ;
8376 "log recovery event for empty scan results"
8377 )]
8378 #[fuchsia::test(add_test_attr = false)]
8379 fn test_log_recovery_occurrence(
8380 reason: RecoveryReason,
8381 expected_dimension: metrics::RecoveryOccurrenceMetricDimensionReason,
8382 ) {
8383 let (mut test_helper, mut test_fut) = setup_test();
8384
8385 test_helper.telemetry_sender.send(TelemetryEvent::RecoveryEvent { reason });
8387
8388 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8390
8391 assert_matches!(
8393 test_helper.exec.run_until_stalled(&mut test_helper.cobalt_stream.next()),
8394 Poll::Ready(Some(Ok(fidl_fuchsia_metrics::MetricEventLoggerRequest::LogOccurrence {
8395 metric_id, event_codes, responder, ..
8396 }))) => {
8397 assert_eq!(metric_id, metrics::RECOVERY_OCCURRENCE_METRIC_ID);
8398 assert_eq!(event_codes, vec![expected_dimension.as_event_code()]);
8399
8400 assert!(responder.send(Ok(())).is_ok());
8401 });
8402 }
8403
8404 #[test_case(
8405 RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset),
8406 RecoveryOutcome::Success,
8407 metrics::INTERFACE_CREATION_RECOVERY_OUTCOME_METRIC_ID,
8408 vec![RecoveryOutcome::Success as u32] ;
8409 "create iface fixed by resetting PHY"
8410 )]
8411 #[test_case(
8412 RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset),
8413 RecoveryOutcome::Failure,
8414 metrics::INTERFACE_CREATION_RECOVERY_OUTCOME_METRIC_ID,
8415 vec![RecoveryOutcome::Failure as u32] ;
8416 "create iface not fixed by resetting PHY"
8417 )]
8418 #[test_case(
8419 RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset),
8420 RecoveryOutcome::Success,
8421 metrics::INTERFACE_DESTRUCTION_RECOVERY_OUTCOME_METRIC_ID,
8422 vec![RecoveryOutcome::Success as u32] ;
8423 "destroy iface fixed by resetting PHY"
8424 )]
8425 #[test_case(
8426 RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset),
8427 RecoveryOutcome::Failure,
8428 metrics::INTERFACE_DESTRUCTION_RECOVERY_OUTCOME_METRIC_ID,
8429 vec![RecoveryOutcome::Failure as u32] ;
8430 "destroy iface not fixed by resetting PHY"
8431 )]
8432 #[test_case(
8433 RecoveryReason::ConnectFailure(ClientRecoveryMechanism::Disconnect),
8434 RecoveryOutcome::Success,
8435 metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8436 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
8437 "connect works after disconnecting"
8438 )]
8439 #[test_case(
8440 RecoveryReason::ConnectFailure(ClientRecoveryMechanism::DestroyIface),
8441 RecoveryOutcome::Success,
8442 metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8443 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
8444 "connect works after destroying iface"
8445 )]
8446 #[test_case(
8447 RecoveryReason::ConnectFailure(ClientRecoveryMechanism::PhyReset),
8448 RecoveryOutcome::Success,
8449 metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8450 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
8451 "connect works after resetting PHY"
8452 )]
8453 #[test_case(
8454 RecoveryReason::ConnectFailure(ClientRecoveryMechanism::Disconnect),
8455 RecoveryOutcome::Failure,
8456 metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8457 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
8458 "connect still fails after disconnecting"
8459 )]
8460 #[test_case(
8461 RecoveryReason::ConnectFailure(ClientRecoveryMechanism::DestroyIface),
8462 RecoveryOutcome::Failure,
8463 metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8464 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
8465 "connect still fails after destroying iface"
8466 )]
8467 #[test_case(
8468 RecoveryReason::ConnectFailure(ClientRecoveryMechanism::PhyReset),
8469 RecoveryOutcome::Failure,
8470 metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8471 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
8472 "connect still fails after resetting PHY"
8473 )]
8474 #[test_case(
8475 RecoveryReason::StartApFailure(ApRecoveryMechanism::StopAp),
8476 RecoveryOutcome::Success,
8477 metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
8478 vec![RecoveryOutcome::Success as u32, ApRecoveryMechanism::StopAp as u32] ;
8479 "start AP works after stopping AP"
8480 )]
8481 #[test_case(
8482 RecoveryReason::StartApFailure(ApRecoveryMechanism::DestroyIface),
8483 RecoveryOutcome::Success,
8484 metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
8485 vec![RecoveryOutcome::Success as u32, ApRecoveryMechanism::DestroyIface as u32] ;
8486 "start AP works after destroying iface"
8487 )]
8488 #[test_case(
8489 RecoveryReason::StartApFailure(ApRecoveryMechanism::ResetPhy),
8490 RecoveryOutcome::Success,
8491 metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
8492 vec![RecoveryOutcome::Success as u32, ApRecoveryMechanism::ResetPhy as u32] ;
8493 "start AP works after resetting PHY"
8494 )]
8495 #[test_case(
8496 RecoveryReason::StartApFailure(ApRecoveryMechanism::StopAp),
8497 RecoveryOutcome::Failure,
8498 metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
8499 vec![RecoveryOutcome::Failure as u32, ApRecoveryMechanism::StopAp as u32] ;
8500 "start AP still fails after stopping AP"
8501 )]
8502 #[test_case(
8503 RecoveryReason::StartApFailure(ApRecoveryMechanism::DestroyIface),
8504 RecoveryOutcome::Failure,
8505 metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
8506 vec![RecoveryOutcome::Failure as u32, ApRecoveryMechanism::DestroyIface as u32] ;
8507 "start AP still fails after destroying iface"
8508 )]
8509 #[test_case(
8510 RecoveryReason::StartApFailure(ApRecoveryMechanism::ResetPhy),
8511 RecoveryOutcome::Failure,
8512 metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
8513 vec![RecoveryOutcome::Failure as u32, ApRecoveryMechanism::ResetPhy as u32] ;
8514 "start AP still fails after resetting PHY"
8515 )]
8516 #[test_case(
8517 RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect),
8518 RecoveryOutcome::Success,
8519 metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8520 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
8521 "scan works after disconnecting"
8522 )]
8523 #[test_case(
8524 RecoveryReason::ScanFailure(ClientRecoveryMechanism::DestroyIface),
8525 RecoveryOutcome::Success,
8526 metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8527 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
8528 "scan works after destroying iface"
8529 )]
8530 #[test_case(
8531 RecoveryReason::ScanFailure(ClientRecoveryMechanism::PhyReset),
8532 RecoveryOutcome::Success,
8533 metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8534 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
8535 "scan works after resetting PHY"
8536 )]
8537 #[test_case(
8538 RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect),
8539 RecoveryOutcome::Failure,
8540 metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8541 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
8542 "scan still fails after disconnecting"
8543 )]
8544 #[test_case(
8545 RecoveryReason::ScanFailure(ClientRecoveryMechanism::DestroyIface),
8546 RecoveryOutcome::Failure,
8547 metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8548 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
8549 "scan still fails after destroying iface"
8550 )]
8551 #[test_case(
8552 RecoveryReason::ScanFailure(ClientRecoveryMechanism::PhyReset),
8553 RecoveryOutcome::Failure,
8554 metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8555 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
8556 "scan still fails after resetting PHY"
8557 )]
8558 #[test_case(
8559 RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect),
8560 RecoveryOutcome::Success,
8561 metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
8562 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
8563 "scan is no longer cancelled after disconnecting"
8564 )]
8565 #[test_case(
8566 RecoveryReason::ScanCancellation(ClientRecoveryMechanism::DestroyIface),
8567 RecoveryOutcome::Success,
8568 metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
8569 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
8570 "scan is no longer cancelled after destroying iface"
8571 )]
8572 #[test_case(
8573 RecoveryReason::ScanCancellation(ClientRecoveryMechanism::PhyReset),
8574 RecoveryOutcome::Success,
8575 metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
8576 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
8577 "scan is no longer cancelled after resetting PHY"
8578 )]
8579 #[test_case(
8580 RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect),
8581 RecoveryOutcome::Failure,
8582 metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
8583 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
8584 "scan is still cancelled after disconnect"
8585 )]
8586 #[test_case(
8587 RecoveryReason::ScanCancellation(ClientRecoveryMechanism::DestroyIface),
8588 RecoveryOutcome::Failure,
8589 metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
8590 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
8591 "scan is still cancelled after destroying iface"
8592 )]
8593 #[test_case(
8594 RecoveryReason::ScanCancellation(ClientRecoveryMechanism::PhyReset),
8595 RecoveryOutcome::Failure,
8596 metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
8597 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
8598 "scan is still cancelled after resetting PHY"
8599 )]
8600 #[test_case(
8601 RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect),
8602 RecoveryOutcome::Success,
8603 metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
8604 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
8605 "scan results not empty after disconnect"
8606 )]
8607 #[test_case(
8608 RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::DestroyIface),
8609 RecoveryOutcome::Success,
8610 metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
8611 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
8612 "scan results not empty after destroy iface"
8613 )]
8614 #[test_case(
8615 RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::PhyReset),
8616 RecoveryOutcome::Success,
8617 metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
8618 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
8619 "scan results not empty after PHY reset"
8620 )]
8621 #[test_case(
8622 RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect),
8623 RecoveryOutcome::Failure,
8624 metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
8625 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
8626 "scan results still empty after disconnect"
8627 )]
8628 #[test_case(
8629 RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::DestroyIface),
8630 RecoveryOutcome::Failure,
8631 metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
8632 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
8633 "scan results still empty after destroy iface"
8634 )]
8635 #[test_case(
8636 RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::PhyReset),
8637 RecoveryOutcome::Failure,
8638 metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
8639 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
8640 "scan results still empty after PHY reset"
8641 )]
8642 #[fuchsia::test(add_test_attr = false)]
8643 fn test_log_post_recovery_result(
8644 reason: RecoveryReason,
8645 outcome: RecoveryOutcome,
8646 expected_metric_id: u32,
8647 expected_event_codes: Vec<u32>,
8648 ) {
8649 let mut exec = fasync::TestExecutor::new();
8650
8651 let (cobalt_proxy, mut cobalt_stream) =
8653 create_proxy_and_stream::<fidl_fuchsia_metrics::MetricEventLoggerMarker>();
8654
8655 let inspector = Inspector::default();
8656 let inspect_node = inspector.root().create_child("stats");
8657
8658 let mut stats_logger = StatsLogger::new(cobalt_proxy, &inspect_node);
8659
8660 let fut = stats_logger.log_post_recovery_result(reason, outcome);
8662 let mut fut = pin!(fut);
8663 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
8664
8665 assert_matches!(
8667 exec.run_until_stalled(&mut cobalt_stream.next()),
8668 Poll::Ready(Some(Ok(fidl_fuchsia_metrics::MetricEventLoggerRequest::LogOccurrence {
8669 metric_id, event_codes, responder, ..
8670 }))) => {
8671 assert_eq!(metric_id, expected_metric_id);
8672 assert_eq!(event_codes, expected_event_codes);
8673
8674 assert!(responder.send(Ok(())).is_ok());
8675 });
8676
8677 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
8679 }
8680
8681 #[fuchsia::test]
8682 fn test_post_recovery_connect_success() {
8683 let (mut test_helper, mut test_fut) = setup_test();
8684
8685 let reason = RecoveryReason::ConnectFailure(ClientRecoveryMechanism::PhyReset);
8687 let event = TelemetryEvent::RecoveryEvent { reason };
8688 test_helper.telemetry_sender.send(event);
8689
8690 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8692
8693 test_helper.drain_cobalt_events(&mut test_fut);
8695 let logged_metrics = test_helper.get_logged_metrics(metrics::RECOVERY_OCCURRENCE_METRIC_ID);
8696 assert_eq!(logged_metrics.len(), 1);
8697
8698 assert_eq!(
8700 logged_metrics[0].event_codes,
8701 vec![
8702 metrics::RecoveryOccurrenceMetricDimensionReason::ClientConnectionFailure
8703 .as_event_code()
8704 ]
8705 );
8706
8707 test_helper.telemetry_sender.send(TelemetryEvent::ConnectResult {
8709 iface_id: IFACE_ID,
8710 policy_connect_reason: Some(
8711 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
8712 ),
8713 result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
8714 multiple_bss_candidates: true,
8715 ap_state: random_bss_description!(Wpa1).into(),
8716 network_is_likely_hidden: false,
8717 });
8718
8719 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8721
8722 test_helper.drain_cobalt_events(&mut test_fut);
8724 let logged_metrics =
8725 test_helper.get_logged_metrics(metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID);
8726 assert_eq!(
8727 logged_metrics[0].event_codes,
8728 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32]
8729 );
8730
8731 test_helper.telemetry_sender.send(TelemetryEvent::ConnectResult {
8733 iface_id: IFACE_ID,
8734 policy_connect_reason: Some(
8735 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
8736 ),
8737 result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
8738 multiple_bss_candidates: true,
8739 ap_state: random_bss_description!(Wpa1).into(),
8740 network_is_likely_hidden: false,
8741 });
8742
8743 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8744
8745 test_helper.cobalt_events = Vec::new();
8746 test_helper.drain_cobalt_events(&mut test_fut);
8747 let logged_metrics =
8748 test_helper.get_logged_metrics(metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID);
8749 assert!(logged_metrics.is_empty());
8750 }
8751
8752 #[fuchsia::test]
8753 fn test_post_recovery_connect_failure() {
8754 let (mut test_helper, mut test_fut) = setup_test();
8755
8756 let reason = RecoveryReason::ConnectFailure(ClientRecoveryMechanism::PhyReset);
8758 let event = TelemetryEvent::RecoveryEvent { reason };
8759 test_helper.telemetry_sender.send(event);
8760
8761 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8763
8764 test_helper.drain_cobalt_events(&mut test_fut);
8766 let logged_metrics = test_helper.get_logged_metrics(metrics::RECOVERY_OCCURRENCE_METRIC_ID);
8767 assert_eq!(logged_metrics.len(), 1);
8768
8769 assert_eq!(
8771 logged_metrics[0].event_codes,
8772 vec![
8773 metrics::RecoveryOccurrenceMetricDimensionReason::ClientConnectionFailure
8774 .as_event_code()
8775 ]
8776 );
8777
8778 test_helper.telemetry_sender.send(TelemetryEvent::ConnectResult {
8780 iface_id: IFACE_ID,
8781 policy_connect_reason: Some(
8782 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
8783 ),
8784 result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
8785 multiple_bss_candidates: true,
8786 ap_state: random_bss_description!(Wpa1).into(),
8787 network_is_likely_hidden: false,
8788 });
8789
8790 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8792
8793 test_helper.drain_cobalt_events(&mut test_fut);
8795 let logged_metrics =
8796 test_helper.get_logged_metrics(metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID);
8797 assert_eq!(
8798 logged_metrics[0].event_codes,
8799 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32]
8800 );
8801
8802 test_helper.telemetry_sender.send(TelemetryEvent::ConnectResult {
8804 iface_id: IFACE_ID,
8805 policy_connect_reason: Some(
8806 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
8807 ),
8808 result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
8809 multiple_bss_candidates: true,
8810 ap_state: random_bss_description!(Wpa1).into(),
8811 network_is_likely_hidden: false,
8812 });
8813
8814 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8815
8816 test_helper.cobalt_events = Vec::new();
8817 test_helper.drain_cobalt_events(&mut test_fut);
8818 let logged_metrics =
8819 test_helper.get_logged_metrics(metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID);
8820 assert!(logged_metrics.is_empty());
8821 }
8822
8823 fn test_generic_post_recovery_event(
8824 recovery_event: TelemetryEvent,
8825 post_recovery_event: TelemetryEvent,
8826 duplicate_check_event: TelemetryEvent,
8827 expected_metric_id: u32,
8828 dimensions: Vec<u32>,
8829 ) {
8830 let (mut test_helper, mut test_fut) = setup_test();
8831 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(1));
8832
8833 test_helper.telemetry_sender.send(recovery_event);
8835 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8836
8837 test_helper.telemetry_sender.send(post_recovery_event);
8839 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8840
8841 test_helper.drain_cobalt_events(&mut test_fut);
8843 let logged_metrics = test_helper.get_logged_metrics(expected_metric_id);
8844
8845 assert_eq!(logged_metrics.len(), 1);
8846 assert_eq!(logged_metrics[0].event_codes, dimensions);
8847
8848 test_helper.cobalt_events = Vec::new();
8850 test_helper.telemetry_sender.send(duplicate_check_event);
8851 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8852 let logged_metrics = test_helper.get_logged_metrics(expected_metric_id);
8853 assert!(logged_metrics.is_empty());
8854
8855 if dimensions[0] == RecoveryOutcome::Success.as_event_code() {
8858 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
8859 stats: contains {
8860 last_successful_recovery: 1_u64,
8861 successful_recoveries: 1_u64
8862 }
8863 });
8864 } else {
8865 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
8866 stats: contains {
8867 last_successful_recovery: 0_u64,
8868 successful_recoveries: 0_u64
8869 }
8870 });
8871 }
8872 }
8873
8874 #[test_case(
8875 TelemetryEvent::RecoveryEvent {
8876 reason: RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect)
8877 },
8878 TelemetryEvent::ScanEvent {
8879 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8880 scan_defects: vec![]
8881 },
8882 TelemetryEvent::ScanEvent {
8883 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8884 scan_defects: vec![]
8885 },
8886 metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8887 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
8888 "Scan succeeds after recovery with no other defects"
8889 )]
8890 #[test_case(
8891 TelemetryEvent::RecoveryEvent {
8892 reason: RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect)
8893 },
8894 TelemetryEvent::ScanEvent {
8895 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8896 scan_defects: vec![ScanIssue::ScanFailure]
8897 },
8898 TelemetryEvent::ScanEvent {
8899 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8900 scan_defects: vec![ScanIssue::ScanFailure]
8901 },
8902 metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8903 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
8904 "Scan still fails following recovery"
8905 )]
8906 #[test_case(
8907 TelemetryEvent::RecoveryEvent {
8908 reason: RecoveryReason::ScanFailure(ClientRecoveryMechanism::DestroyIface)
8909 },
8910 TelemetryEvent::ScanEvent {
8911 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8912 scan_defects: vec![ScanIssue::AbortedScan]
8913 },
8914 TelemetryEvent::ScanEvent {
8915 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8916 scan_defects: vec![ScanIssue::AbortedScan]
8917 },
8918 metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8919 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
8920 "Scan succeeds after recovery but the scan was cancelled"
8921 )]
8922 #[test_case(
8923 TelemetryEvent::RecoveryEvent {
8924 reason: RecoveryReason::ScanFailure(ClientRecoveryMechanism::PhyReset)
8925 },
8926 TelemetryEvent::ScanEvent {
8927 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8928 scan_defects: vec![ScanIssue::EmptyScanResults]
8929 },
8930 TelemetryEvent::ScanEvent {
8931 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8932 scan_defects: vec![ScanIssue::EmptyScanResults]
8933 },
8934 metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
8935 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
8936 "Scan succeeds after recovery but the results are empty"
8937 )]
8938 #[test_case(
8939 TelemetryEvent::RecoveryEvent {
8940 reason: RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect)
8941 },
8942 TelemetryEvent::ScanEvent {
8943 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8944 scan_defects: vec![]
8945 },
8946 TelemetryEvent::ScanEvent {
8947 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8948 scan_defects: vec![]
8949 },
8950 metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
8951 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
8952 "Scan no longer cancelled after recovery"
8953 )]
8954 #[test_case(
8955 TelemetryEvent::RecoveryEvent {
8956 reason: RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect)
8957 },
8958 TelemetryEvent::ScanEvent {
8959 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8960 scan_defects: vec![ScanIssue::ScanFailure]
8961 },
8962 TelemetryEvent::ScanEvent {
8963 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8964 scan_defects: vec![ScanIssue::ScanFailure]
8965 },
8966 metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
8967 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
8968 "Scan not cancelled after recovery but fails instead"
8969 )]
8970 #[test_case(
8971 TelemetryEvent::RecoveryEvent {
8972 reason: RecoveryReason::ScanCancellation(ClientRecoveryMechanism::DestroyIface)
8973 },
8974 TelemetryEvent::ScanEvent {
8975 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8976 scan_defects: vec![ScanIssue::AbortedScan]
8977 },
8978 TelemetryEvent::ScanEvent {
8979 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8980 scan_defects: vec![ScanIssue::AbortedScan]
8981 },
8982 metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
8983 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
8984 "Scan still cancelled after recovery"
8985 )]
8986 #[test_case(
8987 TelemetryEvent::RecoveryEvent {
8988 reason: RecoveryReason::ScanCancellation(ClientRecoveryMechanism::PhyReset)
8989 },
8990 TelemetryEvent::ScanEvent {
8991 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8992 scan_defects: vec![ScanIssue::EmptyScanResults]
8993 },
8994 TelemetryEvent::ScanEvent {
8995 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
8996 scan_defects: vec![ScanIssue::EmptyScanResults]
8997 },
8998 metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
8999 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9000 "Scan not cancelled after recovery but results are empty"
9001 )]
9002 #[test_case(
9003 TelemetryEvent::RecoveryEvent {
9004 reason: RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect)
9005 },
9006 TelemetryEvent::ScanEvent {
9007 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9008 scan_defects: vec![]
9009 },
9010 TelemetryEvent::ScanEvent {
9011 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9012 scan_defects: vec![]
9013 },
9014 metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9015 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9016 "Scan results not empty after recovery and no other errors"
9017 )]
9018 #[test_case(
9019 TelemetryEvent::RecoveryEvent {
9020 reason: RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect)
9021 },
9022 TelemetryEvent::ScanEvent {
9023 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9024 scan_defects: vec![ScanIssue::ScanFailure]
9025 },
9026 TelemetryEvent::ScanEvent {
9027 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9028 scan_defects: vec![ScanIssue::ScanFailure]
9029 },
9030 metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9031 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9032 "Scan results no longer empty after recovery, but scan fails"
9033 )]
9034 #[test_case(
9035 TelemetryEvent::RecoveryEvent {
9036 reason: RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::DestroyIface)
9037 },
9038 TelemetryEvent::ScanEvent {
9039 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9040 scan_defects: vec![ScanIssue::AbortedScan]
9041 },
9042 TelemetryEvent::ScanEvent {
9043 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9044 scan_defects: vec![ScanIssue::AbortedScan]
9045 },
9046 metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9047 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9048 "Scan results not empty after recovery but scan is cancelled"
9049 )]
9050 #[test_case(
9051 TelemetryEvent::RecoveryEvent {
9052 reason: RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::PhyReset)
9053 },
9054 TelemetryEvent::ScanEvent {
9055 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9056 scan_defects: vec![ScanIssue::EmptyScanResults]
9057 },
9058 TelemetryEvent::ScanEvent {
9059 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9060 scan_defects: vec![ScanIssue::EmptyScanResults]
9061 },
9062 metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9063 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9064 "Scan results still empty after recovery"
9065 )]
9066 #[fuchsia::test(add_test_attr = false)]
9067 fn test_post_recovery_scan_metrics(
9068 recovery_event: TelemetryEvent,
9069 post_recovery_event: TelemetryEvent,
9070 duplicate_check_event: TelemetryEvent,
9071 expected_metric_id: u32,
9072 dimensions: Vec<u32>,
9073 ) {
9074 test_generic_post_recovery_event(
9075 recovery_event,
9076 post_recovery_event,
9077 duplicate_check_event,
9078 expected_metric_id,
9079 dimensions,
9080 );
9081 }
9082
9083 #[test_case(
9084 TelemetryEvent::RecoveryEvent {
9085 reason: RecoveryReason::StartApFailure(ApRecoveryMechanism::ResetPhy)
9086 },
9087 TelemetryEvent::StartApResult(Err(())),
9088 TelemetryEvent::StartApResult(Err(())),
9089 metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
9090 vec![RecoveryOutcome::Failure as u32, ApRecoveryMechanism::ResetPhy as u32] ;
9091 "start AP still does not work after recovery"
9092 )]
9093 #[test_case(
9094 TelemetryEvent::RecoveryEvent {
9095 reason: RecoveryReason::StartApFailure(ApRecoveryMechanism::ResetPhy)
9096 },
9097 TelemetryEvent::StartApResult(Ok(())),
9098 TelemetryEvent::StartApResult(Ok(())),
9099 metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
9100 vec![RecoveryOutcome::Success as u32, ApRecoveryMechanism::ResetPhy as u32] ;
9101 "start AP works after recovery"
9102 )]
9103 #[fuchsia::test(add_test_attr = false)]
9104 fn test_post_recovery_start_ap(
9105 recovery_event: TelemetryEvent,
9106 post_recovery_event: TelemetryEvent,
9107 duplicate_check_event: TelemetryEvent,
9108 expected_metric_id: u32,
9109 dimensions: Vec<u32>,
9110 ) {
9111 test_generic_post_recovery_event(
9112 recovery_event,
9113 post_recovery_event,
9114 duplicate_check_event,
9115 expected_metric_id,
9116 dimensions,
9117 );
9118 }
9119
9120 #[test_case(
9121 TelemetryEvent::RecoveryEvent {
9122 reason: RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset)
9123 },
9124 TelemetryEvent::IfaceCreationResult { role: fidl_common::WlanMacRole::Client, result: Err(()) },
9125 TelemetryEvent::IfaceCreationResult { role: fidl_common::WlanMacRole::Client, result: Err(()) },
9126 metrics::INTERFACE_CREATION_RECOVERY_OUTCOME_METRIC_ID,
9127 vec![RecoveryOutcome::Failure as u32] ;
9128 "create iface still does not work after recovery"
9129 )]
9130 #[test_case(
9131 TelemetryEvent::RecoveryEvent {
9132 reason: RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset)
9133 },
9134 TelemetryEvent::IfaceCreationResult { role: fidl_common::WlanMacRole::Client, result: Ok(IFACE_ID) },
9135 TelemetryEvent::IfaceCreationResult { role: fidl_common::WlanMacRole::Client, result: Ok(IFACE_ID) },
9136 metrics::INTERFACE_CREATION_RECOVERY_OUTCOME_METRIC_ID,
9137 vec![RecoveryOutcome::Success as u32] ;
9138 "create iface works after recovery"
9139 )]
9140 #[fuchsia::test(add_test_attr = false)]
9141 fn test_post_recovery_create_iface(
9142 recovery_event: TelemetryEvent,
9143 post_recovery_event: TelemetryEvent,
9144 duplicate_check_event: TelemetryEvent,
9145 expected_metric_id: u32,
9146 dimensions: Vec<u32>,
9147 ) {
9148 test_generic_post_recovery_event(
9149 recovery_event,
9150 post_recovery_event,
9151 duplicate_check_event,
9152 expected_metric_id,
9153 dimensions,
9154 );
9155 }
9156
9157 #[test_case(
9158 TelemetryEvent::RecoveryEvent {
9159 reason: RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset)
9160 },
9161 TelemetryEvent::IfaceDestructionResult { role: fidl_common::WlanMacRole::Client, result: Err(()) },
9162 TelemetryEvent::IfaceDestructionResult { role: fidl_common::WlanMacRole::Client, result: Err(()) },
9163 metrics::INTERFACE_DESTRUCTION_RECOVERY_OUTCOME_METRIC_ID,
9164 vec![RecoveryOutcome::Failure as u32] ;
9165 "destroy iface does not work after recovery"
9166 )]
9167 #[test_case(
9168 TelemetryEvent::RecoveryEvent {
9169 reason: RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset)
9170 },
9171 TelemetryEvent::IfaceDestructionResult { role: fidl_common::WlanMacRole::Client, result: Ok(IFACE_ID) },
9172 TelemetryEvent::IfaceDestructionResult { role: fidl_common::WlanMacRole::Client, result: Ok(IFACE_ID) },
9173 metrics::INTERFACE_DESTRUCTION_RECOVERY_OUTCOME_METRIC_ID,
9174 vec![RecoveryOutcome::Success as u32] ;
9175 "destroy iface works after recovery"
9176 )]
9177 #[fuchsia::test(add_test_attr = false)]
9178 fn test_post_recovery_destroy_iface(
9179 recovery_event: TelemetryEvent,
9180 post_recovery_event: TelemetryEvent,
9181 duplicate_check_event: TelemetryEvent,
9182 expected_metric_id: u32,
9183 dimensions: Vec<u32>,
9184 ) {
9185 test_generic_post_recovery_event(
9186 recovery_event,
9187 post_recovery_event,
9188 duplicate_check_event,
9189 expected_metric_id,
9190 dimensions,
9191 );
9192 }
9193
9194 #[test_case(
9195 TelemetryEvent::RecoveryEvent {
9196 reason: RecoveryReason::Timeout(TimeoutRecoveryMechanism::PhyReset)
9197 },
9198 TelemetryEvent::ConnectResult {
9199 iface_id: IFACE_ID,
9200 policy_connect_reason: Some(
9201 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
9202 ),
9203 result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
9204 multiple_bss_candidates: true,
9205 ap_state: random_bss_description!(Wpa2).into(),
9206 network_is_likely_hidden: true,
9207 },
9208 TelemetryEvent::ConnectResult {
9209 iface_id: IFACE_ID,
9210 policy_connect_reason: Some(
9211 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
9212 ),
9213 result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
9214 multiple_bss_candidates: true,
9215 ap_state: random_bss_description!(Wpa2).into(),
9216 network_is_likely_hidden: true,
9217 },
9218 metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
9219 vec![RecoveryOutcome::Success as u32, TimeoutRecoveryMechanism::PhyReset as u32] ;
9220 "Connect works after recovery"
9221 )]
9222 #[test_case(
9223 TelemetryEvent::RecoveryEvent {
9224 reason: RecoveryReason::Timeout(TimeoutRecoveryMechanism::PhyReset)
9225 },
9226 TelemetryEvent::Disconnected {
9227 track_subsequent_downtime: false,
9228 info: Some(fake_disconnect_info()),
9229 },
9230 TelemetryEvent::Disconnected {
9231 track_subsequent_downtime: false,
9232 info: Some(fake_disconnect_info()),
9233 },
9234 metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
9235 vec![RecoveryOutcome::Success as u32, TimeoutRecoveryMechanism::PhyReset as u32] ;
9236 "Disconnect works after recovery"
9237 )]
9238 #[test_case(
9239 TelemetryEvent::RecoveryEvent {
9240 reason: RecoveryReason::Timeout(TimeoutRecoveryMechanism::PhyReset)
9241 },
9242 TelemetryEvent::StopAp { enabled_duration: zx::MonotonicDuration::from_seconds(0) },
9243 TelemetryEvent::StopAp { enabled_duration: zx::MonotonicDuration::from_seconds(0) },
9244 metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
9245 vec![RecoveryOutcome::Success as u32, TimeoutRecoveryMechanism::PhyReset as u32] ;
9246 "Stop AP works after recovery"
9247 )]
9248 #[test_case(
9249 TelemetryEvent::RecoveryEvent {
9250 reason: RecoveryReason::Timeout(TimeoutRecoveryMechanism::PhyReset)
9251 },
9252 TelemetryEvent::StartApResult(Ok(())),
9253 TelemetryEvent::StartApResult(Ok(())),
9254 metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
9255 vec![RecoveryOutcome::Success as u32, TimeoutRecoveryMechanism::PhyReset as u32] ;
9256 "Start AP works after recovery"
9257 )]
9258 #[test_case(
9259 TelemetryEvent::RecoveryEvent {
9260 reason: RecoveryReason::Timeout(TimeoutRecoveryMechanism::DestroyIface)
9261 },
9262 TelemetryEvent::ScanEvent {
9263 inspect_data: ScanEventInspectData::default(),
9264 scan_defects: vec![]
9265 },
9266 TelemetryEvent::ScanEvent {
9267 inspect_data: ScanEventInspectData::default(),
9268 scan_defects: vec![]
9269 },
9270 metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
9271 vec![RecoveryOutcome::Success as u32, TimeoutRecoveryMechanism::DestroyIface as u32] ;
9272 "Scan works after timeout recovery"
9273 )]
9274 #[test_case(
9275 TelemetryEvent::RecoveryEvent {
9276 reason: RecoveryReason::Timeout(TimeoutRecoveryMechanism::PhyReset)
9277 },
9278 TelemetryEvent::SmeTimeout { source: TimeoutSource::Scan },
9279 TelemetryEvent::SmeTimeout { source: TimeoutSource::Scan },
9280 metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
9281 vec![RecoveryOutcome::Failure as u32, TimeoutRecoveryMechanism::PhyReset as u32] ;
9282 "SME timeout after recovery"
9283 )]
9284 #[fuchsia::test(add_test_attr = false)]
9285 fn test_post_recovery_timeout(
9286 recovery_event: TelemetryEvent,
9287 post_recovery_event: TelemetryEvent,
9288 duplicate_check_event: TelemetryEvent,
9289 expected_metric_id: u32,
9290 dimensions: Vec<u32>,
9291 ) {
9292 test_generic_post_recovery_event(
9293 recovery_event,
9294 post_recovery_event,
9295 duplicate_check_event,
9296 expected_metric_id,
9297 dimensions,
9298 );
9299 }
9300
9301 #[fuchsia::test]
9302 fn test_log_scan_request_fulfillment_time() {
9303 let (mut test_helper, mut test_fut) = setup_test();
9304
9305 let duration = zx::MonotonicDuration::from_seconds(15);
9307 test_helper.telemetry_sender.send(TelemetryEvent::ScanRequestFulfillmentTime {
9308 duration,
9309 reason: client::scan::ScanReason::ClientRequest,
9310 });
9311
9312 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9314
9315 test_helper.drain_cobalt_events(&mut test_fut);
9317 let logged_metrics = test_helper
9318 .get_logged_metrics(metrics::SUCCESSFUL_SCAN_REQUEST_FULFILLMENT_TIME_METRIC_ID);
9319 assert_eq!(logged_metrics.len(), 1);
9320 assert_eq!(
9321 logged_metrics[0].event_codes,
9322 vec![
9323 metrics::ConnectivityWlanMetricDimensionScanFulfillmentTime::LessThanTwentyOneSeconds as u32,
9324 metrics::ConnectivityWlanMetricDimensionScanReason::ClientRequest as u32
9325 ]
9326 );
9327 }
9328
9329 #[fuchsia::test]
9330 fn test_log_scan_queue_statistics() {
9331 let (mut test_helper, mut test_fut) = setup_test();
9332
9333 test_helper.telemetry_sender.send(TelemetryEvent::ScanQueueStatistics {
9335 fulfilled_requests: 4,
9336 remaining_requests: 12,
9337 });
9338
9339 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9341
9342 test_helper.drain_cobalt_events(&mut test_fut);
9344 let logged_metrics = test_helper
9345 .get_logged_metrics(metrics::SCAN_QUEUE_STATISTICS_AFTER_COMPLETED_SCAN_METRIC_ID);
9346 assert_eq!(logged_metrics.len(), 1);
9347 assert_eq!(
9348 logged_metrics[0].event_codes,
9349 vec![
9350 metrics::ConnectivityWlanMetricDimensionScanRequestsFulfilled::Four as u32,
9351 metrics::ConnectivityWlanMetricDimensionScanRequestsRemaining::TenToFourteen as u32
9352 ]
9353 );
9354 }
9355
9356 #[fuchsia::test]
9357 fn test_log_post_connection_score_deltas_by_signal_and_post_connection_rssi_deltas() {
9358 let (mut test_helper, mut test_fut) = setup_test();
9359 let connect_time = fasync::MonotonicInstant::from_nanos(31_000_000_000);
9360
9361 let signals_deque: VecDeque<client::types::TimestampedSignal> = VecDeque::from_iter([
9362 client::types::TimestampedSignal {
9363 signal: client::types::Signal { rssi_dbm: -70, snr_db: 10 },
9364 time: connect_time + zx::MonotonicDuration::from_millis(500),
9365 },
9366 client::types::TimestampedSignal {
9367 signal: client::types::Signal { rssi_dbm: -50, snr_db: 30 },
9368 time: connect_time + zx::MonotonicDuration::from_seconds(4),
9369 },
9370 client::types::TimestampedSignal {
9371 signal: client::types::Signal { rssi_dbm: -30, snr_db: 60 },
9372 time: connect_time + zx::MonotonicDuration::from_seconds(9),
9373 },
9374 client::types::TimestampedSignal {
9375 signal: client::types::Signal { rssi_dbm: -10, snr_db: 80 },
9376 time: connect_time + zx::MonotonicDuration::from_seconds(20),
9377 },
9378 ]);
9379 let signals = HistoricalList(signals_deque);
9380 let signal_at_connect = client::types::Signal { rssi_dbm: -90, snr_db: 0 };
9381
9382 test_helper.telemetry_sender.send(TelemetryEvent::PostConnectionSignals {
9383 connect_time,
9384 signal_at_connect,
9385 signals,
9386 });
9387
9388 test_helper.drain_cobalt_events(&mut test_fut);
9390 let logged_metrics = test_helper.get_logged_metrics(
9391 metrics::AVERAGE_SCORE_DELTA_AFTER_CONNECTION_BY_INITIAL_SCORE_METRIC_ID,
9392 );
9393
9394 use metrics::AverageScoreDeltaAfterConnectionByInitialScoreMetricDimensionTimeSinceConnect as DurationDimension;
9395
9396 assert_eq!(logged_metrics.len(), 4);
9398
9399 let mut prev_score = 0;
9400 assert_eq!(logged_metrics[0].event_codes[1], DurationDimension::OneSecond as u32);
9402 assert_matches!(&logged_metrics[0].payload, MetricEventPayload::IntegerValue(delta) => {
9403 assert_gt!(*delta, prev_score);
9404 prev_score = *delta;
9405 });
9406
9407 assert_eq!(logged_metrics[1].event_codes[1], DurationDimension::FiveSeconds as u32);
9409 assert_matches!(&logged_metrics[1].payload, MetricEventPayload::IntegerValue(delta) => {
9410 assert_gt!(*delta, prev_score);
9411 prev_score = *delta;
9412 });
9413 assert_eq!(logged_metrics[2].event_codes[1], DurationDimension::TenSeconds as u32);
9415 assert_matches!(&logged_metrics[2].payload, MetricEventPayload::IntegerValue(delta) => {
9416 assert_gt!(*delta, prev_score);
9417 prev_score = *delta;
9418 });
9419 assert_eq!(logged_metrics[3].event_codes[1], DurationDimension::ThirtySeconds as u32);
9421 assert_matches!(&logged_metrics[3].payload, MetricEventPayload::IntegerValue(delta) => {
9422 assert_gt!(*delta, prev_score);
9423 });
9424
9425 test_helper.drain_cobalt_events(&mut test_fut);
9427 let logged_metrics = test_helper.get_logged_metrics(
9428 metrics::AVERAGE_RSSI_DELTA_AFTER_CONNECTION_BY_INITIAL_RSSI_METRIC_ID,
9429 );
9430 assert_eq!(logged_metrics.len(), 4);
9432
9433 assert_eq!(logged_metrics[0].event_codes[1], DurationDimension::OneSecond as u32);
9435 assert_matches!(&logged_metrics[0].payload, MetricEventPayload::IntegerValue(delta) => {
9436 assert_eq!(*delta, 10);
9437 });
9438
9439 assert_eq!(logged_metrics[1].event_codes[1], DurationDimension::FiveSeconds as u32);
9441 assert_matches!(&logged_metrics[1].payload, MetricEventPayload::IntegerValue(delta) => {
9442 assert_eq!(*delta, 20);
9443 });
9444 assert_eq!(logged_metrics[2].event_codes[1], DurationDimension::TenSeconds as u32);
9446 assert_matches!(&logged_metrics[2].payload, MetricEventPayload::IntegerValue(delta) => {
9447 assert_eq!(*delta, 30);
9448 });
9449 assert_eq!(logged_metrics[3].event_codes[1], DurationDimension::ThirtySeconds as u32);
9451 assert_matches!(&logged_metrics[3].payload, MetricEventPayload::IntegerValue(delta) => {
9452 assert_eq!(*delta, 40);
9453 });
9454 }
9455
9456 #[fuchsia::test]
9457 fn test_log_pre_disconnect_score_deltas_by_signal_and_pre_disconnect_rssi_deltas() {
9458 let (mut test_helper, mut test_fut) = setup_test();
9459 let final_score_time = fasync::MonotonicInstant::from_nanos(31_000_000_000);
9461
9462 let signals_deque: VecDeque<client::types::TimestampedSignal> = VecDeque::from_iter([
9463 client::types::TimestampedSignal {
9464 signal: client::types::Signal { rssi_dbm: -10, snr_db: 80 },
9465 time: final_score_time - zx::MonotonicDuration::from_seconds(20),
9466 },
9467 client::types::TimestampedSignal {
9468 signal: client::types::Signal { rssi_dbm: -30, snr_db: 60 },
9469 time: final_score_time - zx::MonotonicDuration::from_seconds(9),
9470 },
9471 client::types::TimestampedSignal {
9472 signal: client::types::Signal { rssi_dbm: -50, snr_db: 30 },
9473 time: final_score_time - zx::MonotonicDuration::from_seconds(4),
9474 },
9475 client::types::TimestampedSignal {
9476 signal: client::types::Signal { rssi_dbm: -70, snr_db: 10 },
9477 time: final_score_time - zx::MonotonicDuration::from_millis(500),
9478 },
9479 client::types::TimestampedSignal {
9480 signal: client::types::Signal { rssi_dbm: -90, snr_db: 0 },
9481 time: final_score_time,
9482 },
9483 ]);
9484 let signals = HistoricalList(signals_deque);
9485
9486 let disconnect_info = DisconnectInfo {
9487 connected_duration: AVERAGE_SCORE_DELTA_MINIMUM_DURATION,
9488 signals,
9489 ..fake_disconnect_info()
9490 };
9491 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
9492 track_subsequent_downtime: false,
9493 info: Some(disconnect_info),
9494 });
9495
9496 test_helper.drain_cobalt_events(&mut test_fut);
9498 let logged_metrics = test_helper.get_logged_metrics(
9499 metrics::AVERAGE_SCORE_DELTA_BEFORE_DISCONNECT_BY_FINAL_SCORE_METRIC_ID,
9500 );
9501
9502 use metrics::AverageScoreDeltaBeforeDisconnectByFinalScoreMetricDimensionTimeUntilDisconnect as DurationDimension;
9503
9504 assert_eq!(logged_metrics.len(), 4);
9506
9507 let mut prev_score = 0;
9508 assert_eq!(logged_metrics[0].event_codes[1], DurationDimension::OneSecond as u32);
9510 assert_matches!(&logged_metrics[0].payload, MetricEventPayload::IntegerValue(delta) => {
9511 assert_gt!(*delta, prev_score);
9512 prev_score = *delta;
9513 });
9514
9515 assert_eq!(logged_metrics[1].event_codes[1], DurationDimension::FiveSeconds as u32);
9517 assert_matches!(&logged_metrics[1].payload, MetricEventPayload::IntegerValue(delta) => {
9518 assert_gt!(*delta, prev_score);
9519 prev_score = *delta;
9520 });
9521 assert_eq!(logged_metrics[2].event_codes[1], DurationDimension::TenSeconds as u32);
9523 assert_matches!(&logged_metrics[2].payload, MetricEventPayload::IntegerValue(delta) => {
9524 assert_gt!(*delta, prev_score);
9525 prev_score = *delta;
9526 });
9527 assert_eq!(logged_metrics[3].event_codes[1], DurationDimension::ThirtySeconds as u32);
9529 assert_matches!(&logged_metrics[3].payload, MetricEventPayload::IntegerValue(delta) => {
9530 assert_gt!(*delta, prev_score);
9531 });
9532
9533 test_helper.drain_cobalt_events(&mut test_fut);
9535 let logged_metrics = test_helper.get_logged_metrics(
9536 metrics::AVERAGE_RSSI_DELTA_BEFORE_DISCONNECT_BY_FINAL_RSSI_METRIC_ID,
9537 );
9538 assert_eq!(logged_metrics.len(), 4);
9540
9541 assert_eq!(logged_metrics[0].event_codes[1], DurationDimension::OneSecond as u32);
9543 assert_matches!(&logged_metrics[0].payload, MetricEventPayload::IntegerValue(delta) => {
9544 assert_eq!(*delta, 10);
9545 });
9546
9547 assert_eq!(logged_metrics[1].event_codes[1], DurationDimension::FiveSeconds as u32);
9549 assert_matches!(&logged_metrics[1].payload, MetricEventPayload::IntegerValue(delta) => {
9550 assert_eq!(*delta, 20);
9551 });
9552 assert_eq!(logged_metrics[2].event_codes[1], DurationDimension::TenSeconds as u32);
9554 assert_matches!(&logged_metrics[2].payload, MetricEventPayload::IntegerValue(delta) => {
9555 assert_eq!(*delta, 30);
9556 });
9557 assert_eq!(logged_metrics[3].event_codes[1], DurationDimension::ThirtySeconds as u32);
9559 assert_matches!(&logged_metrics[3].payload, MetricEventPayload::IntegerValue(delta) => {
9560 assert_eq!(*delta, 40);
9561 });
9562
9563 let disconnect_info = DisconnectInfo {
9565 connected_duration: AVERAGE_SCORE_DELTA_MINIMUM_DURATION
9566 - zx::MonotonicDuration::from_seconds(1),
9567 ..fake_disconnect_info()
9568 };
9569 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
9570 track_subsequent_downtime: false,
9571 info: Some(disconnect_info),
9572 });
9573 test_helper.drain_cobalt_events(&mut test_fut);
9574
9575 let logged_metrics = test_helper.get_logged_metrics(
9577 metrics::AVERAGE_SCORE_DELTA_BEFORE_DISCONNECT_BY_FINAL_SCORE_METRIC_ID,
9578 );
9579 assert_eq!(logged_metrics.len(), 4);
9580 let logged_metrics = test_helper.get_logged_metrics(
9581 metrics::AVERAGE_RSSI_DELTA_BEFORE_DISCONNECT_BY_FINAL_RSSI_METRIC_ID,
9582 );
9583 assert_eq!(logged_metrics.len(), 4);
9584 }
9585
9586 #[fuchsia::test]
9587 fn test_log_network_selection_metrics() {
9588 let (mut test_helper, mut test_fut) = setup_test();
9589
9590 test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
9592 network_selection_type: NetworkSelectionType::Undirected,
9593 num_candidates: Ok(3),
9594 selected_count: 2,
9595 });
9596
9597 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9599 test_helper.drain_cobalt_events(&mut test_fut);
9600
9601 let logged_metrics =
9603 test_helper.get_logged_metrics(metrics::NETWORK_SELECTION_COUNT_METRIC_ID);
9604 assert_eq!(logged_metrics.len(), 1);
9605 assert_eq!(logged_metrics[0].payload, MetricEventPayload::Count(1));
9606
9607 let logged_metrics =
9609 test_helper.get_logged_metrics(metrics::NUM_NETWORKS_SELECTED_METRIC_ID);
9610 assert_eq!(logged_metrics.len(), 1);
9611 assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(2));
9612
9613 test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
9615 network_selection_type: NetworkSelectionType::Undirected,
9616 num_candidates: Ok(0),
9617 selected_count: 0,
9618 });
9619
9620 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9622 test_helper.drain_cobalt_events(&mut test_fut);
9623
9624 let logged_metrics =
9626 test_helper.get_logged_metrics(metrics::NETWORK_SELECTION_COUNT_METRIC_ID);
9627 assert_eq!(logged_metrics.len(), 2);
9628
9629 let logged_metrics =
9632 test_helper.get_logged_metrics(metrics::NUM_NETWORKS_SELECTED_METRIC_ID);
9633 assert_eq!(logged_metrics.len(), 1);
9634 }
9635
9636 #[fuchsia::test]
9637 fn test_log_bss_selection_metrics() {
9638 let (mut test_helper, mut test_fut) = setup_test();
9639
9640 let selected_candidate_2g = client::types::ScannedCandidate {
9641 bss: client::types::Bss {
9642 channel: Channel::new(1, wlan_common::channel::Bandwidth::Cbw20, TwoGhz),
9643 ..generate_random_bss()
9644 },
9645 ..generate_random_scanned_candidate()
9646 };
9647 let candidate_2g = client::types::ScannedCandidate {
9648 bss: client::types::Bss {
9649 channel: Channel::new(1, wlan_common::channel::Bandwidth::Cbw20, TwoGhz),
9650 ..generate_random_bss()
9651 },
9652 ..generate_random_scanned_candidate()
9653 };
9654 let candidate_5g = client::types::ScannedCandidate {
9655 bss: client::types::Bss {
9656 channel: Channel::new(36, wlan_common::channel::Bandwidth::Cbw40, FiveGhz),
9657 ..generate_random_bss()
9658 },
9659 ..generate_random_scanned_candidate()
9660 };
9661 let scored_candidates =
9662 vec![(selected_candidate_2g.clone(), 70), (candidate_2g, 60), (candidate_5g, 50)];
9663
9664 test_helper.telemetry_sender.send(TelemetryEvent::BssSelectionResult {
9665 reason: client::types::ConnectReason::FidlConnectRequest,
9666 scored_candidates: scored_candidates.clone(),
9667 selected_candidate: Some((selected_candidate_2g, 70)),
9668 });
9669
9670 test_helper.drain_cobalt_events(&mut test_fut);
9671
9672 let fidl_connect_event_code = vec![
9673 metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::FidlConnectRequest
9674 as u32,
9675 ];
9676 let logged_metrics = test_helper.get_logged_metrics(metrics::BSS_SELECTION_COUNT_METRIC_ID);
9678 assert_eq!(logged_metrics.len(), 1);
9679 assert_eq!(logged_metrics[0].event_codes, Vec::<u32>::new());
9680 assert_eq!(logged_metrics[0].payload, MetricEventPayload::Count(1));
9681
9682 let logged_metrics =
9683 test_helper.get_logged_metrics(metrics::BSS_SELECTION_COUNT_DETAILED_METRIC_ID);
9684 assert_eq!(logged_metrics.len(), 1);
9685 assert_eq!(logged_metrics[0].event_codes, fidl_connect_event_code);
9686 assert_eq!(logged_metrics[0].payload, MetricEventPayload::Count(1));
9687
9688 let logged_metrics =
9690 test_helper.get_logged_metrics(metrics::NUM_BSS_CONSIDERED_IN_SELECTION_METRIC_ID);
9691 assert_eq!(logged_metrics.len(), 1);
9692 assert_eq!(logged_metrics[0].event_codes, Vec::<u32>::new());
9693 assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(3));
9694
9695 let logged_metrics = test_helper
9696 .get_logged_metrics(metrics::NUM_BSS_CONSIDERED_IN_SELECTION_DETAILED_METRIC_ID);
9697 assert_eq!(logged_metrics.len(), 1);
9698 assert_eq!(logged_metrics[0].event_codes, fidl_connect_event_code);
9699 assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(3));
9700
9701 let logged_metrics = test_helper.get_logged_metrics(metrics::BSS_CANDIDATE_SCORE_METRIC_ID);
9703 assert_eq!(logged_metrics.len(), 3);
9704 for i in 0..3 {
9705 assert_eq!(
9706 logged_metrics[i].payload,
9707 MetricEventPayload::IntegerValue(scored_candidates[i].1 as i64)
9708 )
9709 }
9710
9711 let logged_metrics = test_helper
9713 .get_logged_metrics(metrics::NUM_NETWORKS_REPRESENTED_IN_BSS_SELECTION_METRIC_ID);
9714 assert_eq!(logged_metrics.len(), 1);
9715 assert_eq!(logged_metrics[0].event_codes, fidl_connect_event_code);
9716 assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(3));
9717
9718 let logged_metrics = test_helper.get_logged_metrics(metrics::SELECTED_BSS_SCORE_METRIC_ID);
9720 assert_eq!(logged_metrics.len(), 1);
9721 assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(70));
9722
9723 let logged_metrics =
9725 test_helper.get_logged_metrics(metrics::RUNNER_UP_CANDIDATE_SCORE_DELTA_METRIC_ID);
9726 assert_eq!(logged_metrics.len(), 1);
9727 assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(10));
9728
9729 let logged_metrics =
9731 test_helper.get_logged_metrics(metrics::BEST_CANDIDATES_GHZ_SCORE_DELTA_METRIC_ID);
9732 assert_eq!(logged_metrics.len(), 1);
9733 assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(-20));
9734
9735 let logged_metrics =
9737 test_helper.get_logged_metrics(metrics::GHZ_BANDS_AVAILABLE_IN_BSS_SELECTION_METRIC_ID);
9738 assert_eq!(logged_metrics.len(), 1);
9739 assert_eq!(
9740 logged_metrics[0].event_codes,
9741 vec![metrics::GhzBandsAvailableInBssSelectionMetricDimensionBands::MultiBand as u32]
9742 );
9743 assert_eq!(logged_metrics[0].payload, MetricEventPayload::Count(1));
9744 }
9745
9746 #[fuchsia::test]
9747 fn test_log_bss_selection_metrics_none_selected() {
9748 let (mut test_helper, mut test_fut) = setup_test();
9749
9750 test_helper.telemetry_sender.send(TelemetryEvent::BssSelectionResult {
9751 reason: client::types::ConnectReason::FidlConnectRequest,
9752 scored_candidates: vec![],
9753 selected_candidate: None,
9754 });
9755
9756 test_helper.drain_cobalt_events(&mut test_fut);
9757
9758 assert!(!test_helper.get_logged_metrics(metrics::BSS_SELECTION_COUNT_METRIC_ID).is_empty());
9760 assert!(
9761 !test_helper
9762 .get_logged_metrics(metrics::BSS_SELECTION_COUNT_DETAILED_METRIC_ID)
9763 .is_empty()
9764 );
9765 assert!(
9766 !test_helper
9767 .get_logged_metrics(metrics::NUM_BSS_CONSIDERED_IN_SELECTION_METRIC_ID)
9768 .is_empty()
9769 );
9770 assert!(
9771 !test_helper
9772 .get_logged_metrics(metrics::NUM_BSS_CONSIDERED_IN_SELECTION_DETAILED_METRIC_ID)
9773 .is_empty()
9774 );
9775 assert!(test_helper.get_logged_metrics(metrics::BSS_CANDIDATE_SCORE_METRIC_ID).is_empty());
9776 assert!(
9777 test_helper
9778 .get_logged_metrics(metrics::NUM_NETWORKS_REPRESENTED_IN_BSS_SELECTION_METRIC_ID)
9779 .is_empty()
9780 );
9781 assert!(
9782 test_helper
9783 .get_logged_metrics(metrics::RUNNER_UP_CANDIDATE_SCORE_DELTA_METRIC_ID)
9784 .is_empty()
9785 );
9786 assert!(
9787 test_helper
9788 .get_logged_metrics(metrics::NUM_NETWORKS_REPRESENTED_IN_BSS_SELECTION_METRIC_ID)
9789 .is_empty()
9790 );
9791 assert!(
9792 test_helper
9793 .get_logged_metrics(metrics::BEST_CANDIDATES_GHZ_SCORE_DELTA_METRIC_ID)
9794 .is_empty()
9795 );
9796 assert!(
9797 test_helper
9798 .get_logged_metrics(metrics::GHZ_BANDS_AVAILABLE_IN_BSS_SELECTION_METRIC_ID)
9799 .is_empty()
9800 );
9801 }
9802
9803 #[fuchsia::test]
9804 fn test_log_bss_selection_metrics_runner_up_delta_not_recorded() {
9805 let (mut test_helper, mut test_fut) = setup_test();
9806
9807 let scored_candidates = vec![
9808 (generate_random_scanned_candidate(), 90),
9809 (generate_random_scanned_candidate(), 60),
9810 (generate_random_scanned_candidate(), 50),
9811 ];
9812
9813 test_helper.telemetry_sender.send(TelemetryEvent::BssSelectionResult {
9814 reason: client::types::ConnectReason::FidlConnectRequest,
9815 scored_candidates,
9816 selected_candidate: Some((generate_random_scanned_candidate(), 60)),
9818 });
9819
9820 test_helper.drain_cobalt_events(&mut test_fut);
9821
9822 assert!(
9824 test_helper
9825 .get_logged_metrics(metrics::RUNNER_UP_CANDIDATE_SCORE_DELTA_METRIC_ID)
9826 .is_empty()
9827 );
9828 }
9829
9830 #[fuchsia::test]
9831 fn test_log_connection_score_average_long_duration() {
9832 let (mut test_helper, mut test_fut) = setup_test();
9833 let now = fasync::MonotonicInstant::now();
9834 let signals = vec![
9835 client::types::TimestampedSignal {
9836 signal: client::types::Signal { rssi_dbm: -60, snr_db: 30 },
9837 time: now,
9838 },
9839 client::types::TimestampedSignal {
9840 signal: client::types::Signal { rssi_dbm: -60, snr_db: 30 },
9841 time: now,
9842 },
9843 client::types::TimestampedSignal {
9844 signal: client::types::Signal { rssi_dbm: -80, snr_db: 10 },
9845 time: now,
9846 },
9847 client::types::TimestampedSignal {
9848 signal: client::types::Signal { rssi_dbm: -80, snr_db: 10 },
9849 time: now,
9850 },
9851 ];
9852
9853 test_helper.telemetry_sender.send(TelemetryEvent::LongDurationSignals { signals });
9854 test_helper.drain_cobalt_events(&mut test_fut);
9855
9856 let logged_metrics =
9857 test_helper.get_logged_metrics(metrics::CONNECTION_SCORE_AVERAGE_METRIC_ID);
9858 assert_eq!(logged_metrics.len(), 1);
9859 assert_eq!(
9860 logged_metrics[0].event_codes,
9861 vec![metrics::ConnectionScoreAverageMetricDimensionDuration::LongDuration as u32]
9862 );
9863 assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(55));
9864
9865 test_helper.telemetry_sender.send(TelemetryEvent::LongDurationSignals { signals: vec![] });
9867 test_helper.drain_cobalt_events(&mut test_fut);
9868 assert_eq!(
9869 test_helper.get_logged_metrics(metrics::CONNECTION_SCORE_AVERAGE_METRIC_ID).len(),
9870 1
9871 );
9872 }
9873
9874 #[fuchsia::test]
9875 fn test_log_connection_rssi_average_long_duration() {
9876 let (mut test_helper, mut test_fut) = setup_test();
9877 let now = fasync::MonotonicInstant::now();
9878 let signals = vec![
9879 client::types::TimestampedSignal {
9880 signal: client::types::Signal { rssi_dbm: -60, snr_db: 30 },
9881 time: now,
9882 },
9883 client::types::TimestampedSignal {
9884 signal: client::types::Signal { rssi_dbm: -60, snr_db: 30 },
9885 time: now,
9886 },
9887 client::types::TimestampedSignal {
9888 signal: client::types::Signal { rssi_dbm: -80, snr_db: 10 },
9889 time: now,
9890 },
9891 client::types::TimestampedSignal {
9892 signal: client::types::Signal { rssi_dbm: -80, snr_db: 10 },
9893 time: now,
9894 },
9895 ];
9896
9897 test_helper.telemetry_sender.send(TelemetryEvent::LongDurationSignals { signals });
9898 test_helper.drain_cobalt_events(&mut test_fut);
9899
9900 let logged_metrics =
9901 test_helper.get_logged_metrics(metrics::CONNECTION_RSSI_AVERAGE_METRIC_ID);
9902 assert_eq!(logged_metrics.len(), 1);
9903 assert_eq!(
9904 logged_metrics[0].event_codes,
9905 vec![metrics::ConnectionScoreAverageMetricDimensionDuration::LongDuration as u32]
9906 );
9907 assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(-70));
9908
9909 test_helper.telemetry_sender.send(TelemetryEvent::LongDurationSignals { signals: vec![] });
9911 test_helper.drain_cobalt_events(&mut test_fut);
9912 assert_eq!(
9913 test_helper.get_logged_metrics(metrics::CONNECTION_RSSI_AVERAGE_METRIC_ID).len(),
9914 1
9915 );
9916 }
9917
9918 struct TestHelper {
9919 telemetry_sender: TelemetrySender,
9920 inspector: Inspector,
9921 monitor_svc_stream: fidl_fuchsia_wlan_device_service::DeviceMonitorRequestStream,
9922 telemetry_svc_streams: Vec<fidl_fuchsia_wlan_sme::TelemetryRequestStream>,
9923 cobalt_stream: fidl_fuchsia_metrics::MetricEventLoggerRequestStream,
9924 iface_stats_resp:
9925 Option<Box<dyn Fn() -> fidl_fuchsia_wlan_sme::TelemetryGetIfaceStatsResult>>,
9926 cobalt_events: Vec<MetricEvent>,
9929 _defect_receiver: mpsc::Receiver<Defect>,
9930
9931 exec: fasync::TestExecutor,
9933 }
9934
9935 impl TestHelper {
9936 fn advance_test_fut<T>(
9940 &mut self,
9941 test_fut: &mut (impl Future<Output = T> + Unpin),
9942 ) -> Poll<T> {
9943 let mut result = self.exec.run_until_stalled(test_fut);
9944 while let Poll::Ready(Some(Ok(req))) =
9945 self.exec.run_until_stalled(&mut self.monitor_svc_stream.next())
9946 {
9947 match req {
9948 fidl_fuchsia_wlan_device_service::DeviceMonitorRequest::GetSmeTelemetry {
9949 iface_id,
9950 telemetry_server,
9951 responder,
9952 } => {
9953 assert_eq!(iface_id, IFACE_ID);
9954 let telemetry_stream = telemetry_server.into_stream();
9955 responder.send(Ok(())).expect("Failed to respond to telemetry request");
9956 self.telemetry_svc_streams.push(telemetry_stream);
9957 result = self.exec.run_until_stalled(test_fut);
9958 }
9959 _ => panic!("Unexpected device monitor request: {req:?}"),
9960 }
9961 }
9962 result
9963 }
9964
9965 fn advance_by(
9970 &mut self,
9971 duration: zx::MonotonicDuration,
9972 mut test_fut: Pin<&mut impl Future<Output = ()>>,
9973 ) {
9974 let target_time = self.exec.now() + duration;
9975
9976 assert_eq!(self.advance_test_fut(&mut test_fut), Poll::Pending);
9984 self.telemetry_svc_streams.retain(|s| !s.is_terminated());
9985 for telemetry_svc_stream in &mut self.telemetry_svc_streams {
9986 respond_iface_counter_stats_req(
9987 &mut self.exec,
9988 telemetry_svc_stream,
9989 &self.iface_stats_resp,
9990 );
9991 }
9992 self.drain_cobalt_events(&mut test_fut);
9993 assert_eq!(self.advance_test_fut(&mut test_fut), Poll::Pending);
9994
9995 while self.exec.now() < target_time {
9996 let next_timer = fasync::TestExecutor::next_timer();
9997 let next_wake = match next_timer {
9998 Some(time) if time <= target_time => time,
9999 _ => target_time,
10000 };
10001
10002 if next_wake > self.exec.now() {
10003 self.exec.set_fake_time(next_wake);
10004 }
10005
10006 let _ = self.exec.wake_expired_timers();
10007 assert_eq!(self.advance_test_fut(&mut test_fut), Poll::Pending);
10008
10009 self.telemetry_svc_streams.retain(|s| !s.is_terminated());
10010 for telemetry_svc_stream in &mut self.telemetry_svc_streams {
10011 respond_iface_counter_stats_req(
10012 &mut self.exec,
10013 telemetry_svc_stream,
10014 &self.iface_stats_resp,
10015 );
10016 }
10017
10018 self.drain_cobalt_events(&mut test_fut);
10021
10022 assert_eq!(self.advance_test_fut(&mut test_fut), Poll::Pending);
10023 }
10024 }
10025
10026 fn set_iface_stats_resp(
10027 &mut self,
10028 iface_stats_resp: Box<dyn Fn() -> fidl_fuchsia_wlan_sme::TelemetryGetIfaceStatsResult>,
10029 ) {
10030 let _ = self.iface_stats_resp.replace(iface_stats_resp);
10031 }
10032
10033 fn advance_to_next_telemetry_checkpoint(
10039 &mut self,
10040 test_fut: Pin<&mut impl Future<Output = ()>>,
10041 ) {
10042 let now = fasync::MonotonicInstant::now();
10043 let remaining_interval = TELEMETRY_QUERY_INTERVAL.into_nanos()
10044 - (now.into_nanos() % TELEMETRY_QUERY_INTERVAL.into_nanos());
10045 self.advance_by(zx::MonotonicDuration::from_nanos(remaining_interval), test_fut)
10046 }
10047
10048 fn drain_cobalt_events(&mut self, test_fut: &mut (impl Future + Unpin)) {
10051 let mut made_progress = true;
10052 while made_progress {
10053 let _result = self.advance_test_fut(test_fut);
10054 made_progress = false;
10055 while let Poll::Ready(Some(Ok(req))) =
10056 self.exec.run_until_stalled(&mut self.cobalt_stream.next())
10057 {
10058 self.cobalt_events.append(&mut req.respond_to_metric_req(Ok(())));
10059 made_progress = true;
10060 }
10061 }
10062 }
10063
10064 fn get_logged_metrics(&self, metric_id: u32) -> Vec<MetricEvent> {
10065 self.cobalt_events.iter().filter(|ev| ev.metric_id == metric_id).cloned().collect()
10066 }
10067
10068 fn send_connected_event(&mut self, ap_state: impl Into<client::types::ApState>) {
10069 let event = TelemetryEvent::ConnectResult {
10070 iface_id: IFACE_ID,
10071 policy_connect_reason: Some(
10072 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
10073 ),
10074 result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
10075 multiple_bss_candidates: true,
10076 ap_state: ap_state.into(),
10077 network_is_likely_hidden: true,
10078 };
10079 self.telemetry_sender.send(event);
10080 }
10081
10082 fn clear_cobalt_events(&mut self) {
10085 self.cobalt_events = Vec::new();
10086 }
10087 }
10088
10089 fn respond_iface_counter_stats_req(
10090 executor: &mut fasync::TestExecutor,
10091 telemetry_svc_stream: &mut fidl_fuchsia_wlan_sme::TelemetryRequestStream,
10092 iface_stats_resp: &Option<
10093 Box<dyn Fn() -> fidl_fuchsia_wlan_sme::TelemetryGetIfaceStatsResult>,
10094 >,
10095 ) {
10096 while let Poll::Ready(Ok(Some(request))) =
10097 executor.run_until_stalled(&mut pin!(telemetry_svc_stream.try_next()))
10098 {
10099 match request {
10100 fidl_fuchsia_wlan_sme::TelemetryRequest::GetIfaceStats { responder } => {
10101 let resp = match &iface_stats_resp {
10102 Some(get_resp) => get_resp(),
10103 None => {
10104 let seed = fasync::MonotonicInstant::now().into_nanos() as u64;
10105 Ok(fidl_fuchsia_wlan_stats::IfaceStats {
10106 connection_stats: Some(fake_connection_stats(seed)),
10107 ..Default::default()
10108 })
10109 }
10110 };
10111 let _ = responder.send(resp.as_ref().map_err(|e| *e));
10112 break;
10113 }
10114 fidl_fuchsia_wlan_sme::TelemetryRequest::QueryTelemetrySupport { responder } => {
10115 let _ = responder.send(Ok(&Default::default()));
10116 }
10117 _ => {
10118 panic!("unexpected request: {request:?}");
10119 }
10120 }
10121 }
10122 }
10123
10124 fn respond_iface_histogram_stats_req(
10125 executor: &mut fasync::TestExecutor,
10126 telemetry_svc_stream: &mut fidl_fuchsia_wlan_sme::TelemetryRequestStream,
10127 ) {
10128 while let Poll::Ready(Ok(Some(request))) =
10129 executor.run_until_stalled(&mut pin!(telemetry_svc_stream.try_next()))
10130 {
10131 match request {
10132 fidl_fuchsia_wlan_sme::TelemetryRequest::GetHistogramStats { responder } => {
10133 let _ = responder.send(Ok(&fake_iface_histogram_stats()));
10134 break;
10135 }
10136 fidl_fuchsia_wlan_sme::TelemetryRequest::GetIfaceStats { responder } => {
10137 let seed = fasync::MonotonicInstant::now().into_nanos() as u64;
10138 let stats = fidl_fuchsia_wlan_stats::IfaceStats {
10139 connection_stats: Some(fake_connection_stats(seed)),
10140 ..Default::default()
10141 };
10142 let _ = responder.send(Ok(&stats));
10143 }
10144 fidl_fuchsia_wlan_sme::TelemetryRequest::GetSignalReport { responder } => {
10145 let _ = responder.send(Ok(&fidl_fuchsia_wlan_stats::SignalReport::default()));
10146 }
10147 fidl_fuchsia_wlan_sme::TelemetryRequest::QueryTelemetrySupport { responder } => {
10148 let _ = responder.send(Ok(&Default::default()));
10149 }
10150 _ => {
10151 panic!("unexpected request: {request:?}");
10152 }
10153 }
10154 }
10155 }
10156
10157 #[track_caller]
10159 fn assert_eq_cobalt_events(
10160 mut left: Vec<fidl_fuchsia_metrics::MetricEvent>,
10161 mut right: Vec<fidl_fuchsia_metrics::MetricEvent>,
10162 ) {
10163 left.sort_by(metric_event_cmp);
10164 right.sort_by(metric_event_cmp);
10165 assert_eq!(left, right);
10166 }
10167
10168 fn metric_event_cmp(
10169 left: &fidl_fuchsia_metrics::MetricEvent,
10170 right: &fidl_fuchsia_metrics::MetricEvent,
10171 ) -> std::cmp::Ordering {
10172 match left.metric_id.cmp(&right.metric_id) {
10173 std::cmp::Ordering::Equal => match left.event_codes.len().cmp(&right.event_codes.len())
10174 {
10175 std::cmp::Ordering::Equal => (),
10176 ordering => return ordering,
10177 },
10178 ordering => return ordering,
10179 }
10180
10181 for i in 0..left.event_codes.len() {
10182 match left.event_codes[i].cmp(&right.event_codes[i]) {
10183 std::cmp::Ordering::Equal => (),
10184 ordering => return ordering,
10185 }
10186 }
10187
10188 match (&left.payload, &right.payload) {
10189 (MetricEventPayload::Count(v1), MetricEventPayload::Count(v2)) => v1.cmp(v2),
10190 (MetricEventPayload::IntegerValue(v1), MetricEventPayload::IntegerValue(v2)) => {
10191 v1.cmp(v2)
10192 }
10193 (MetricEventPayload::StringValue(v1), MetricEventPayload::StringValue(v2)) => {
10194 v1.cmp(v2)
10195 }
10196 (MetricEventPayload::Histogram(_), MetricEventPayload::Histogram(_)) => {
10197 unimplemented!()
10198 }
10199 _ => unimplemented!(),
10200 }
10201 }
10202
10203 trait CobaltExt {
10204 fn respond_to_metric_req(
10206 self,
10207 result: Result<(), fidl_fuchsia_metrics::Error>,
10208 ) -> Vec<fidl_fuchsia_metrics::MetricEvent>;
10209 }
10210
10211 impl CobaltExt for MetricEventLoggerRequest {
10212 fn respond_to_metric_req(
10213 self,
10214 result: Result<(), fidl_fuchsia_metrics::Error>,
10215 ) -> Vec<fidl_fuchsia_metrics::MetricEvent> {
10216 match self {
10217 Self::LogOccurrence { metric_id, count, event_codes, responder } => {
10218 assert!(responder.send(result).is_ok());
10219 vec![MetricEvent {
10220 metric_id,
10221 event_codes,
10222 payload: MetricEventPayload::Count(count),
10223 }]
10224 }
10225 Self::LogInteger { metric_id, value, event_codes, responder } => {
10226 assert!(responder.send(result).is_ok());
10227 vec![MetricEvent {
10228 metric_id,
10229 event_codes,
10230 payload: MetricEventPayload::IntegerValue(value),
10231 }]
10232 }
10233 Self::LogIntegerHistogram { metric_id, histogram, event_codes, responder } => {
10234 assert!(responder.send(result).is_ok());
10235 vec![MetricEvent {
10236 metric_id,
10237 event_codes,
10238 payload: MetricEventPayload::Histogram(histogram),
10239 }]
10240 }
10241 Self::LogString { metric_id, string_value, event_codes, responder } => {
10242 assert!(responder.send(result).is_ok());
10243 vec![MetricEvent {
10244 metric_id,
10245 event_codes,
10246 payload: MetricEventPayload::StringValue(string_value),
10247 }]
10248 }
10249 Self::LogMetricEvents { events, responder } => {
10250 assert!(responder.send(result).is_ok());
10251 events
10252 }
10253 }
10254 }
10255 }
10256
10257 fn setup_test() -> (TestHelper, Pin<Box<impl Future<Output = ()>>>) {
10258 let mut exec = fasync::TestExecutor::new_with_fake_time();
10259 exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
10260
10261 let (monitor_svc_proxy, monitor_svc_stream) =
10262 create_proxy_and_stream::<fidl_fuchsia_wlan_device_service::DeviceMonitorMarker>();
10263
10264 let (cobalt_proxy, cobalt_stream) =
10265 create_proxy_and_stream::<fidl_fuchsia_metrics::MetricEventLoggerMarker>();
10266
10267 let inspector = Inspector::default();
10268 let inspect_node = inspector.root().create_child("stats");
10269 let external_inspect_node = inspector.root().create_child("external");
10270 let (defect_sender, _defect_receiver) = mpsc::channel(100);
10271 let (telemetry_sender, test_fut) = serve_telemetry(
10272 monitor_svc_proxy,
10273 cobalt_proxy.clone(),
10274 inspect_node,
10275 external_inspect_node.create_child("stats"),
10276 defect_sender,
10277 );
10278 inspector.root().record(external_inspect_node);
10279 let mut test_fut = Box::pin(test_fut);
10280
10281 assert_eq!(exec.run_until_stalled(&mut test_fut), Poll::Pending);
10282
10283 let test_helper = TestHelper {
10284 telemetry_sender,
10285 inspector,
10286 monitor_svc_stream,
10287 telemetry_svc_streams: vec![],
10288 cobalt_stream,
10289 iface_stats_resp: None,
10290 cobalt_events: vec![],
10291 _defect_receiver,
10292 exec,
10293 };
10294 (test_helper, test_fut)
10295 }
10296
10297 fn fake_connection_stats(nth_req: u64) -> fidl_fuchsia_wlan_stats::ConnectionStats {
10298 fidl_fuchsia_wlan_stats::ConnectionStats {
10299 connection_id: Some(1),
10300 rx_unicast_total: Some(nth_req),
10301 rx_unicast_drop: Some(0),
10302 rx_multicast: Some(2 * nth_req),
10303 tx_total: Some(nth_req),
10304 tx_drop: Some(0),
10305 ..Default::default()
10306 }
10307 }
10308
10309 fn fake_iface_histogram_stats() -> fidl_fuchsia_wlan_stats::IfaceHistogramStats {
10310 fidl_fuchsia_wlan_stats::IfaceHistogramStats {
10311 noise_floor_histograms: Some(fake_noise_floor_histograms()),
10312 rssi_histograms: Some(fake_rssi_histograms()),
10313 rx_rate_index_histograms: Some(fake_rx_rate_index_histograms()),
10314 snr_histograms: Some(fake_snr_histograms()),
10315 ..Default::default()
10316 }
10317 }
10318
10319 fn fake_noise_floor_histograms() -> Vec<fidl_fuchsia_wlan_stats::NoiseFloorHistogram> {
10320 vec![fidl_fuchsia_wlan_stats::NoiseFloorHistogram {
10321 hist_scope: fidl_fuchsia_wlan_stats::HistScope::PerAntenna,
10322 antenna_id: Some(Box::new(fidl_fuchsia_wlan_stats::AntennaId {
10323 freq: fidl_fuchsia_wlan_stats::AntennaFreq::Antenna2G,
10324 index: 0,
10325 })),
10326 noise_floor_samples: vec![
10327 fidl_fuchsia_wlan_stats::HistBucket { bucket_index: 199, num_samples: 0 },
10330 fidl_fuchsia_wlan_stats::HistBucket { bucket_index: 200, num_samples: 999 },
10331 ],
10332 invalid_samples: 44,
10333 }]
10334 }
10335
10336 fn fake_rssi_histograms() -> Vec<fidl_fuchsia_wlan_stats::RssiHistogram> {
10337 vec![fidl_fuchsia_wlan_stats::RssiHistogram {
10338 hist_scope: fidl_fuchsia_wlan_stats::HistScope::PerAntenna,
10339 antenna_id: Some(Box::new(fidl_fuchsia_wlan_stats::AntennaId {
10340 freq: fidl_fuchsia_wlan_stats::AntennaFreq::Antenna2G,
10341 index: 0,
10342 })),
10343 rssi_samples: vec![fidl_fuchsia_wlan_stats::HistBucket {
10344 bucket_index: 230,
10345 num_samples: 999,
10346 }],
10347 invalid_samples: 55,
10348 }]
10349 }
10350
10351 fn fake_rx_rate_index_histograms() -> Vec<fidl_fuchsia_wlan_stats::RxRateIndexHistogram> {
10352 vec![
10353 fidl_fuchsia_wlan_stats::RxRateIndexHistogram {
10354 hist_scope: fidl_fuchsia_wlan_stats::HistScope::Station,
10355 antenna_id: None,
10356 rx_rate_index_samples: vec![fidl_fuchsia_wlan_stats::HistBucket {
10357 bucket_index: 99,
10358 num_samples: 1400,
10359 }],
10360 invalid_samples: 22,
10361 },
10362 fidl_fuchsia_wlan_stats::RxRateIndexHistogram {
10363 hist_scope: fidl_fuchsia_wlan_stats::HistScope::PerAntenna,
10364 antenna_id: Some(Box::new(fidl_fuchsia_wlan_stats::AntennaId {
10365 freq: fidl_fuchsia_wlan_stats::AntennaFreq::Antenna5G,
10366 index: 1,
10367 })),
10368 rx_rate_index_samples: vec![fidl_fuchsia_wlan_stats::HistBucket {
10369 bucket_index: 100,
10370 num_samples: 1500,
10371 }],
10372 invalid_samples: 33,
10373 },
10374 ]
10375 }
10376
10377 fn fake_snr_histograms() -> Vec<fidl_fuchsia_wlan_stats::SnrHistogram> {
10378 vec![fidl_fuchsia_wlan_stats::SnrHistogram {
10379 hist_scope: fidl_fuchsia_wlan_stats::HistScope::PerAntenna,
10380 antenna_id: Some(Box::new(fidl_fuchsia_wlan_stats::AntennaId {
10381 freq: fidl_fuchsia_wlan_stats::AntennaFreq::Antenna2G,
10382 index: 0,
10383 })),
10384 snr_samples: vec![fidl_fuchsia_wlan_stats::HistBucket {
10385 bucket_index: 30,
10386 num_samples: 999,
10387 }],
10388 invalid_samples: 11,
10389 }]
10390 }
10391
10392 fn fake_disconnect_info() -> DisconnectInfo {
10393 let is_sme_reconnecting = false;
10394 let fidl_disconnect_info = generate_disconnect_info(is_sme_reconnecting);
10395 DisconnectInfo {
10396 iface_id: IFACE_ID,
10397 connected_duration: zx::MonotonicDuration::from_hours(6),
10398 is_sme_reconnecting: fidl_disconnect_info.is_sme_reconnecting,
10399 disconnect_source: fidl_disconnect_info.disconnect_source,
10400 previous_connect_reason: client::types::ConnectReason::IdleInterfaceAutoconnect,
10401 ap_state: random_bss_description!(Wpa2).into(),
10402 signals: HistoricalList::new(8),
10403 }
10404 }
10405
10406 fn fake_connect_result(code: fidl_ieee80211::StatusCode) -> fidl_sme::ConnectResult {
10407 fidl_sme::ConnectResult { code, is_credential_rejected: false, is_reconnect: false }
10408 }
10409
10410 #[fuchsia::test]
10411 fn test_error_throttling() {
10412 let exec = fasync::TestExecutor::new_with_fake_time();
10413 exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
10414 let mut error_logger = ThrottledErrorLogger::new(MINUTES_BETWEEN_COBALT_SYSLOG_WARNINGS);
10415
10416 exec.set_fake_time(fasync::MonotonicInstant::after(
10418 fasync::MonotonicDuration::from_minutes(MINUTES_BETWEEN_COBALT_SYSLOG_WARNINGS + 1),
10419 ));
10420
10421 error_logger.throttle_error(Err(format_err!("")));
10424 assert!(!error_logger.suppressed_errors.contains_key(&String::from("")));
10425
10426 error_logger.throttle_error(Err(format_err!("")));
10428 assert_eq!(error_logger.suppressed_errors[&String::from("")], 1);
10429
10430 exec.set_fake_time(fasync::MonotonicInstant::after(
10433 fasync::MonotonicDuration::from_minutes(MINUTES_BETWEEN_COBALT_SYSLOG_WARNINGS + 1),
10434 ));
10435 error_logger.throttle_error(Err(format_err!("")));
10436 assert!(!error_logger.suppressed_errors.contains_key(&String::from("")));
10437
10438 error_logger.throttle_error(Err(format_err!("")));
10440 assert_eq!(error_logger.suppressed_errors[&String::from("")], 1);
10441 }
10442}