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