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