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