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 IFACE_ID: u16 = 1;
4747
4748 macro_rules! assert_data_tree_with_respond_blocking_req {
4751 ($test_helper:expr, $test_fut:expr, $($rest:tt)+) => {{
4752 use {
4753 fuchsia_inspect::reader, diagnostics_assertions::assert_data_tree,
4754 };
4755
4756 let inspector = $test_helper.inspector.clone();
4757 let read_fut = reader::read(&inspector);
4758 let mut read_fut = pin!(read_fut);
4759 loop {
4760 match $test_helper.exec.run_until_stalled(&mut read_fut) {
4761 Poll::Pending => {
4762 $test_helper.drain_cobalt_events(&mut $test_fut);
4765 if let Some(telemetry_svc_stream) = &mut $test_helper.telemetry_svc_stream {
4767 if !telemetry_svc_stream.is_terminated() {
4768 respond_iface_histogram_stats_req(
4769 &mut $test_helper.exec,
4770 telemetry_svc_stream,
4771 );
4772 }
4773 }
4774
4775 }
4776 Poll::Ready(result) => {
4777 let hierarchy = result.expect("failed to get hierarchy");
4778 assert_data_tree!(@executor $test_helper.exec, hierarchy, $($rest)+);
4779 break
4780 }
4781 }
4782 }
4783 }}
4784 }
4785
4786 #[fuchsia::test]
4787 fn test_detect_driver_unresponsive_signal_ind() {
4788 let (mut test_helper, mut test_fut) = setup_test();
4789 test_helper.send_connected_event(random_bss_description!(Wpa2));
4790
4791 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4792 stats: contains {
4793 is_driver_unresponsive: false,
4794 }
4795 });
4796
4797 test_helper.advance_by(
4798 UNRESPONSIVE_FLAG_MIN_DURATION - TELEMETRY_QUERY_INTERVAL,
4799 test_fut.as_mut(),
4800 );
4801 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4802 stats: contains {
4803 is_driver_unresponsive: false,
4804 }
4805 });
4806
4807 let ind = fidl_internal::SignalReportIndication { rssi_dbm: -40, snr_db: 30 };
4809 test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind });
4810
4811 test_helper.advance_by(UNRESPONSIVE_FLAG_MIN_DURATION, test_fut.as_mut());
4812 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4813 stats: contains {
4814 is_driver_unresponsive: false,
4815 }
4816 });
4817
4818 test_helper.advance_by(TELEMETRY_QUERY_INTERVAL, test_fut.as_mut());
4820 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4821 stats: contains {
4822 is_driver_unresponsive: true,
4823 }
4824 });
4825 }
4826
4827 #[fuchsia::test]
4828 fn test_histogram_stats_timeout() {
4829 let mut exec = fasync::TestExecutor::new();
4830
4831 let inspector = Inspector::default();
4832 let external_node = inspector.root().create_child("external");
4833 let external_inspect_node = ExternalInspectNode::new(external_node);
4834
4835 let (telemetry_sender, mut telemetry_receiver) =
4836 mpsc::channel::<TelemetryEvent>(TELEMETRY_EVENT_BUFFER_SIZE);
4837 let (defect_sender, mut defect_receiver) = mpsc::channel(100);
4838
4839 inspect_record_external_data(
4842 &external_inspect_node,
4843 TelemetrySender::new(telemetry_sender),
4844 defect_sender,
4845 );
4846
4847 let fut = reader::read(&inspector);
4849 let mut fut = pin!(fut);
4850 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4851
4852 let (telemetry_proxy, _telemetry_server) =
4856 fidl::endpoints::create_proxy::<fidl_sme::TelemetryMarker>();
4857 assert_matches!(
4858 telemetry_receiver.try_next(),
4859 Ok(Some(TelemetryEvent::QueryStatus {sender})) => {
4860 sender.send(QueryStatusResult {
4861 connection_state: ConnectionStateInfo::Connected {
4862 iface_id: 0,
4863 ap_state: Box::new(random_bss_description!(Wpa2).into()),
4864 telemetry_proxy: Some(telemetry_proxy)
4865 }
4866 }).expect("failed to send query status result")
4867 }
4868 );
4869 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4870
4871 assert!(exec.wake_next_timer().is_some());
4873 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(_));
4874
4875 assert_matches!(
4877 defect_receiver.try_next(),
4878 Ok(Some(Defect::Iface(IfaceFailure::Timeout {
4879 iface_id: 0,
4880 source: TimeoutSource::GetHistogramStats,
4881 })))
4882 );
4883 }
4884
4885 #[fuchsia::test]
4886 fn test_telemetry_timeout() {
4887 let mut exec = fasync::TestExecutor::new();
4888
4889 let (sender, _receiver) = mpsc::channel::<TelemetryEvent>(TELEMETRY_EVENT_BUFFER_SIZE);
4891 let (monitor_svc_proxy, _monitor_svc_stream) =
4892 create_proxy_and_stream::<fidl_fuchsia_wlan_device_service::DeviceMonitorMarker>();
4893 let (cobalt_proxy, _cobalt_stream) =
4894 create_proxy_and_stream::<fidl_fuchsia_metrics::MetricEventLoggerMarker>();
4895 let inspector = Inspector::default();
4896 let inspect_node = inspector.root().create_child("stats");
4897 let external_inspect_node = inspector.root().create_child("external");
4898 let (defect_sender, mut defect_receiver) = mpsc::channel(100);
4899
4900 let mut telemetry = Telemetry::new(
4901 TelemetrySender::new(sender),
4902 monitor_svc_proxy,
4903 cobalt_proxy.clone(),
4904 inspect_node,
4905 external_inspect_node,
4906 defect_sender,
4907 );
4908
4909 let (telemetry_proxy, _telemetry_server) =
4911 fidl::endpoints::create_proxy::<fidl_sme::TelemetryMarker>();
4912 telemetry.connection_state = ConnectionState::Connected(Box::new(ConnectedState {
4913 iface_id: 0,
4914 ap_state: Box::new(random_bss_description!(Wpa2).into()),
4915 telemetry_proxy: Some(telemetry_proxy),
4916
4917 new_connect_start_time: None,
4919 prev_connection_stats: None,
4920 multiple_bss_candidates: false,
4921 network_is_likely_hidden: false,
4922 last_signal_report: fasync::MonotonicInstant::now(),
4923 num_consecutive_get_counter_stats_failures: InspectableU64::new(
4924 0,
4925 &telemetry.inspect_node,
4926 "num_consecutive_get_counter_stats_failures",
4927 ),
4928 is_driver_unresponsive: InspectableBool::new(
4929 false,
4930 &telemetry.inspect_node,
4931 "is_driver_unresponsive",
4932 ),
4933 }));
4934
4935 let fut = telemetry.handle_periodic_telemetry();
4937 let mut fut = pin!(fut);
4938 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4939
4940 assert!(exec.wake_next_timer().is_some());
4942 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
4943
4944 assert_matches!(
4946 defect_receiver.try_next(),
4947 Ok(Some(Defect::Iface(IfaceFailure::Timeout {
4948 iface_id: 0,
4949 source: TimeoutSource::GetIfaceStats,
4950 })))
4951 );
4952 }
4953
4954 #[fuchsia::test]
4955 fn test_logging_num_consecutive_get_iface_stats_failures() {
4956 let (mut test_helper, mut test_fut) = setup_test();
4957 test_helper.set_iface_stats_resp(Box::new(|| Err(zx::sys::ZX_ERR_TIMED_OUT)));
4958 test_helper.send_connected_event(random_bss_description!(Wpa2));
4959
4960 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4961 stats: contains {
4962 num_consecutive_get_counter_stats_failures: 0u64,
4963 }
4964 });
4965
4966 test_helper.advance_by(TELEMETRY_QUERY_INTERVAL * 20i64, test_fut.as_mut());
4967 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4968 stats: contains {
4969 num_consecutive_get_counter_stats_failures: 20u64,
4970 }
4971 });
4972
4973 test_helper.drain_cobalt_events(&mut test_fut);
4975 let logged_metrics =
4976 test_helper.get_logged_metrics(metrics::CONSECUTIVE_COUNTER_STATS_FAILURES_METRIC_ID);
4977 assert_eq!(logged_metrics.len(), 20);
4978
4979 assert_eq!(
4980 logged_metrics[19].payload,
4981 fidl_fuchsia_metrics::MetricEventPayload::IntegerValue(20)
4982 );
4983 }
4984
4985 #[fuchsia::test]
4986 fn test_log_connect_event_correct_shape() {
4987 let (mut test_helper, mut test_fut) = setup_test();
4988 test_helper.send_connected_event(random_bss_description!(Wpa2));
4989
4990 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
4991
4992 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
4993 stats: contains {
4994 connect_events: {
4995 "0": {
4996 "@time": AnyNumericProperty,
4997 multiple_bss_candidates: AnyBoolProperty,
4998 network: {
4999 bssid: &*BSSID_REGEX,
5000 ssid: &*SSID_REGEX,
5001 rssi_dbm: AnyNumericProperty,
5002 snr_db: AnyNumericProperty,
5003 }
5004 }
5005 }
5006 }
5007 });
5008 }
5009
5010 #[fuchsia::test]
5011 fn test_log_connection_status_correct_shape() {
5012 let (mut test_helper, mut test_fut) = setup_test();
5013 test_helper.send_connected_event(random_bss_description!(Wpa2));
5014
5015 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5016
5017 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5018 stats: contains {
5019 connection_status: contains {
5020 status_string: AnyStringProperty,
5021 connected_network: contains {
5022 rssi_dbm: AnyNumericProperty,
5023 snr_db: AnyNumericProperty,
5024 bssid: &*BSSID_REGEX,
5025 ssid: &*SSID_REGEX,
5026 protection: AnyStringProperty,
5027 channel: AnyStringProperty,
5028 is_wmm_assoc: AnyBoolProperty,
5029 }
5030 }
5031 }
5032 });
5033 }
5034
5035 #[allow(clippy::regex_creation_in_loops, reason = "mass allow for https://fxbug.dev/381896734")]
5036 #[fuchsia::test]
5037 fn test_log_disconnect_event_correct_shape() {
5038 let (mut test_helper, mut test_fut) = setup_test();
5039
5040 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5041 track_subsequent_downtime: false,
5042 info: Some(fake_disconnect_info()),
5043 });
5044 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5045
5046 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5047 external: contains {
5048 stats: contains {
5049 disconnect_events: {
5050 "0": {
5051 "@time": AnyNumericProperty,
5052 flattened_reason_code: AnyNumericProperty,
5053 locally_initiated: AnyBoolProperty,
5054 network: {
5055 channel: {
5056 primary: AnyNumericProperty,
5057 }
5058 }
5059 }
5060 }
5061 }
5062 },
5063 stats: contains {
5064 disconnect_events: {
5065 "0": {
5066 "@time": AnyNumericProperty,
5067 connected_duration: AnyNumericProperty,
5068 disconnect_source: Regex::new("^source: [^,]+, reason: [^,]+(?:, mlme_event_name: [^,]+)?$").unwrap(),
5069 network: contains {
5070 rssi_dbm: AnyNumericProperty,
5071 snr_db: AnyNumericProperty,
5072 bssid: &*BSSID_REGEX,
5073 ssid: &*SSID_REGEX,
5074 protection: AnyStringProperty,
5075 channel: AnyStringProperty,
5076 is_wmm_assoc: AnyBoolProperty,
5077 }
5078 }
5079 }
5080 }
5081 });
5082 }
5083
5084 #[fuchsia::test]
5085 fn test_log_disconnect_on_recovery() {
5086 let mut exec = fasync::TestExecutor::new();
5087
5088 let (sender, _receiver) = mpsc::channel::<TelemetryEvent>(TELEMETRY_EVENT_BUFFER_SIZE);
5090 let (monitor_svc_proxy, _monitor_svc_stream) =
5091 create_proxy_and_stream::<fidl_fuchsia_wlan_device_service::DeviceMonitorMarker>();
5092 let (cobalt_1dot1_proxy, mut cobalt_1dot1_stream) =
5093 create_proxy_and_stream::<fidl_fuchsia_metrics::MetricEventLoggerMarker>();
5094 let inspector = Inspector::default();
5095 let inspect_node = inspector.root().create_child("stats");
5096 let external_inspect_node = inspector.root().create_child("external");
5097 let (defect_sender, _defect_receiver) = mpsc::channel(100);
5098
5099 let mut telemetry = Telemetry::new(
5101 TelemetrySender::new(sender),
5102 monitor_svc_proxy,
5103 cobalt_1dot1_proxy.clone(),
5104 inspect_node,
5105 external_inspect_node,
5106 defect_sender,
5107 );
5108
5109 telemetry.connection_state = ConnectionState::Connected(Box::new(ConnectedState {
5110 iface_id: 0,
5111 new_connect_start_time: None,
5112 prev_connection_stats: None,
5113 multiple_bss_candidates: false,
5114 ap_state: Box::new(generate_random_ap_state()),
5115 network_is_likely_hidden: false,
5116 last_signal_report: fasync::MonotonicInstant::now(),
5117 num_consecutive_get_counter_stats_failures: InspectableU64::new(
5118 0,
5119 &telemetry.inspect_node,
5120 "num_consecutive_get_counter_stats_failures",
5121 ),
5122 is_driver_unresponsive: InspectableBool::new(
5123 false,
5124 &telemetry.inspect_node,
5125 "is_driver_unresponsive",
5126 ),
5127 telemetry_proxy: None,
5128 }));
5129
5130 {
5131 let fut = telemetry.handle_telemetry_event(TelemetryEvent::Disconnected {
5133 track_subsequent_downtime: false,
5134 info: None,
5135 });
5136 let mut fut = pin!(fut);
5137
5138 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
5139
5140 assert_matches!(
5142 exec.run_until_stalled(&mut cobalt_1dot1_stream.next()),
5143 Poll::Ready(Some(Ok(fidl_fuchsia_metrics::MetricEventLoggerRequest::LogMetricEvents {
5144 events: _,
5145 responder
5146 }))) => {
5147 responder.send(Ok(())).expect("failed to send response");
5148 }
5149 );
5150
5151 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
5153 }
5154
5155 assert_matches!(
5157 telemetry.connection_state,
5158 ConnectionState::Idle(IdleState { connect_start_time: None })
5159 );
5160 }
5161
5162 #[fuchsia::test]
5163 fn test_stat_cycles() {
5164 let (mut test_helper, mut test_fut) = setup_test();
5165 test_helper.send_connected_event(random_bss_description!(Wpa2));
5166 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5167
5168 test_helper.advance_by(
5169 zx::MonotonicDuration::from_hours(24) - TELEMETRY_QUERY_INTERVAL,
5170 test_fut.as_mut(),
5171 );
5172 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5173 stats: contains {
5174 "1d_counters": contains {
5175 total_duration: (zx::MonotonicDuration::from_hours(24) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5176 connected_duration: (zx::MonotonicDuration::from_hours(24) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5177 },
5178 "7d_counters": contains {
5179 total_duration: (zx::MonotonicDuration::from_hours(24) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5180 connected_duration: (zx::MonotonicDuration::from_hours(24) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5181 },
5182 }
5183 });
5184
5185 test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5186 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5187 stats: contains {
5188 "1d_counters": contains {
5189 total_duration: zx::MonotonicDuration::from_hours(23).into_nanos(),
5192 connected_duration: zx::MonotonicDuration::from_hours(23).into_nanos(),
5193 },
5194 "7d_counters": contains {
5195 total_duration: zx::MonotonicDuration::from_hours(24).into_nanos(),
5196 connected_duration: zx::MonotonicDuration::from_hours(24).into_nanos(),
5197 },
5198 }
5199 });
5200
5201 test_helper.advance_by(zx::MonotonicDuration::from_hours(2), test_fut.as_mut());
5202 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5203 stats: contains {
5204 "1d_counters": contains {
5205 total_duration: zx::MonotonicDuration::from_hours(23).into_nanos(),
5206 connected_duration: zx::MonotonicDuration::from_hours(23).into_nanos(),
5207 },
5208 "7d_counters": contains {
5209 total_duration: zx::MonotonicDuration::from_hours(26).into_nanos(),
5210 connected_duration: zx::MonotonicDuration::from_hours(26).into_nanos(),
5211 },
5212 }
5213 });
5214
5215 let info = fake_disconnect_info();
5217 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5218 track_subsequent_downtime: false,
5219 info: Some(info),
5220 });
5221 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5222
5223 test_helper.advance_by(zx::MonotonicDuration::from_hours(8), test_fut.as_mut());
5224 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5225 stats: contains {
5226 "1d_counters": contains {
5227 total_duration: zx::MonotonicDuration::from_hours(23).into_nanos(),
5228 connected_duration: zx::MonotonicDuration::from_hours(15).into_nanos(),
5230 },
5231 "7d_counters": contains {
5232 total_duration: zx::MonotonicDuration::from_hours(34).into_nanos(),
5233 connected_duration: zx::MonotonicDuration::from_hours(26).into_nanos(),
5234 },
5235 }
5236 });
5237
5238 test_helper.advance_by(zx::MonotonicDuration::from_hours(14), test_fut.as_mut());
5240 test_helper.advance_by(
5241 zx::MonotonicDuration::from_hours(5 * 24) - TELEMETRY_QUERY_INTERVAL,
5242 test_fut.as_mut(),
5243 );
5244 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5245 stats: contains {
5246 "1d_counters": contains {
5247 total_duration: (zx::MonotonicDuration::from_hours(24) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5248 connected_duration: 0i64,
5249 },
5250 "7d_counters": contains {
5251 total_duration: (zx::MonotonicDuration::from_hours(7 * 24) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
5252 connected_duration: zx::MonotonicDuration::from_hours(26).into_nanos(),
5253 },
5254 }
5255 });
5256
5257 test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5259 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5260 stats: contains {
5261 "1d_counters": contains {
5262 total_duration: zx::MonotonicDuration::from_hours(23).into_nanos(),
5263 connected_duration: 0i64,
5264 },
5265 "7d_counters": contains {
5266 total_duration: zx::MonotonicDuration::from_hours(6 * 24).into_nanos(),
5267 connected_duration: zx::MonotonicDuration::from_hours(2).into_nanos(),
5268 },
5269 }
5270 });
5271 }
5272
5273 #[fuchsia::test]
5274 fn test_daily_detailed_stat_cycles() {
5275 let (mut test_helper, mut test_fut) = setup_test();
5276 for _ in 0..10 {
5277 test_helper.send_connected_event(random_bss_description!(Wpa2));
5278 }
5279 test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
5280
5281 let status_codes = test_helper.get_logged_metrics(
5283 metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
5284 );
5285 assert_eq!(status_codes.len(), 1);
5286 assert_eq!(
5287 status_codes[0].event_codes,
5288 vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32]
5289 );
5290 assert_eq!(status_codes[0].payload, MetricEventPayload::Count(10));
5291
5292 test_helper.cobalt_events.clear();
5293
5294 test_helper.send_connected_event(random_bss_description!(Wpa2));
5295 test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
5296
5297 let status_codes = test_helper.get_logged_metrics(
5299 metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
5300 );
5301 assert_eq!(status_codes.len(), 1);
5302 assert_eq!(
5303 status_codes[0].event_codes,
5304 vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32]
5305 );
5306 assert_eq!(status_codes[0].payload, MetricEventPayload::Count(1));
5307 }
5308
5309 #[fuchsia::test]
5310 fn test_total_duration_counters() {
5311 let (mut test_helper, mut test_fut) = setup_test();
5312
5313 test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
5314 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5315 stats: contains {
5316 "1d_counters": contains {
5317 total_duration: zx::MonotonicDuration::from_minutes(30).into_nanos(),
5318 },
5319 "7d_counters": contains {
5320 total_duration: zx::MonotonicDuration::from_minutes(30).into_nanos(),
5321 },
5322 }
5323 });
5324
5325 test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
5326 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5327 stats: contains {
5328 "1d_counters": contains {
5329 total_duration: zx::MonotonicDuration::from_hours(1).into_nanos(),
5330 },
5331 "7d_counters": contains {
5332 total_duration: zx::MonotonicDuration::from_hours(1).into_nanos(),
5333 },
5334 }
5335 });
5336 }
5337
5338 #[fuchsia::test]
5339 fn test_total_duration_time_series() {
5340 let (mut test_helper, mut test_fut) = setup_test();
5341
5342 test_helper.advance_by(zx::MonotonicDuration::from_seconds(25), test_fut.as_mut());
5343 let time_series = test_helper.get_time_series(&mut test_fut);
5344 let total_duration_sec: Vec<_> =
5345 time_series.lock().total_duration_sec.minutely_iter().copied().collect();
5346 assert_eq!(total_duration_sec, vec![15]);
5347
5348 test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5349 let total_duration_sec: Vec<_> =
5350 time_series.lock().total_duration_sec.minutely_iter().copied().collect();
5351 assert_eq!(total_duration_sec, vec![30]);
5352 }
5353
5354 #[fuchsia::test]
5355 fn test_counters_when_idle() {
5356 let (mut test_helper, mut test_fut) = setup_test();
5357
5358 test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
5359 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5360 stats: contains {
5361 "1d_counters": contains {
5362 connected_duration: 0i64,
5363 downtime_duration: 0i64,
5364 downtime_no_saved_neighbor_duration: 0i64,
5365 },
5366 "7d_counters": contains {
5367 connected_duration: 0i64,
5368 downtime_duration: 0i64,
5369 downtime_no_saved_neighbor_duration: 0i64,
5370 },
5371 }
5372 });
5373
5374 test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
5375 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5376 stats: contains {
5377 "1d_counters": contains {
5378 connected_duration: 0i64,
5379 downtime_duration: 0i64,
5380 downtime_no_saved_neighbor_duration: 0i64,
5381 },
5382 "7d_counters": contains {
5383 connected_duration: 0i64,
5384 downtime_duration: 0i64,
5385 downtime_no_saved_neighbor_duration: 0i64,
5386 },
5387 }
5388 });
5389 }
5390
5391 #[fuchsia::test]
5392 fn test_connected_counters_increase_when_connected() {
5393 let (mut test_helper, mut test_fut) = setup_test();
5394 test_helper.send_connected_event(random_bss_description!(Wpa2));
5395 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5396
5397 test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
5398 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5399 stats: contains {
5400 "1d_counters": contains {
5401 connected_duration: zx::MonotonicDuration::from_minutes(30).into_nanos(),
5402 downtime_duration: 0i64,
5403 downtime_no_saved_neighbor_duration: 0i64,
5404 },
5405 "7d_counters": contains {
5406 connected_duration: zx::MonotonicDuration::from_minutes(30).into_nanos(),
5407 downtime_duration: 0i64,
5408 downtime_no_saved_neighbor_duration: 0i64,
5409 },
5410 }
5411 });
5412
5413 test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
5414 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5415 stats: contains {
5416 "1d_counters": contains {
5417 connected_duration: zx::MonotonicDuration::from_hours(1).into_nanos(),
5418 downtime_duration: 0i64,
5419 downtime_no_saved_neighbor_duration: 0i64,
5420 },
5421 "7d_counters": contains {
5422 connected_duration: zx::MonotonicDuration::from_hours(1).into_nanos(),
5423 downtime_duration: 0i64,
5424 downtime_no_saved_neighbor_duration: 0i64,
5425 },
5426 }
5427 });
5428 }
5429
5430 #[fuchsia::test]
5431 fn test_downtime_counter() {
5432 let (mut test_helper, mut test_fut) = setup_test();
5433
5434 let info = fake_disconnect_info();
5436 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5437 track_subsequent_downtime: false,
5438 info: Some(info),
5439 });
5440 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5441
5442 test_helper.advance_by(zx::MonotonicDuration::from_minutes(10), test_fut.as_mut());
5443
5444 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5445 stats: contains {
5446 "1d_counters": contains {
5447 connected_duration: 0i64,
5448 downtime_duration: 0i64,
5449 downtime_no_saved_neighbor_duration: 0i64,
5450 },
5451 "7d_counters": contains {
5452 connected_duration: 0i64,
5453 downtime_duration: 0i64,
5454 downtime_no_saved_neighbor_duration: 0i64,
5455 },
5456 }
5457 });
5458
5459 let info = fake_disconnect_info();
5461 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5462 track_subsequent_downtime: true,
5463 info: Some(info),
5464 });
5465 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5466
5467 test_helper.advance_by(zx::MonotonicDuration::from_minutes(15), test_fut.as_mut());
5468
5469 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5470 stats: contains {
5471 "1d_counters": contains {
5472 connected_duration: 0i64,
5473 downtime_duration: zx::MonotonicDuration::from_minutes(15).into_nanos(),
5474 downtime_no_saved_neighbor_duration: 0i64,
5475 },
5476 "7d_counters": contains {
5477 connected_duration: 0i64,
5478 downtime_duration: zx::MonotonicDuration::from_minutes(15).into_nanos(),
5479 downtime_no_saved_neighbor_duration: 0i64,
5480 },
5481 }
5482 });
5483 }
5484
5485 #[fuchsia::test]
5486 fn test_counters_connect_then_disconnect() {
5487 let (mut test_helper, mut test_fut) = setup_test();
5488 test_helper.send_connected_event(random_bss_description!(Wpa2));
5489 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5490
5491 test_helper.advance_by(zx::MonotonicDuration::from_seconds(5), test_fut.as_mut());
5492
5493 let info = fake_disconnect_info();
5495 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5496 track_subsequent_downtime: true,
5497 info: Some(info),
5498 });
5499 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5500
5501 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5503 stats: contains {
5504 "1d_counters": contains {
5505 connected_duration: 0i64,
5506 downtime_duration: 0i64,
5507 downtime_no_saved_neighbor_duration: 0i64,
5508 },
5509 "7d_counters": contains {
5510 connected_duration: 0i64,
5511 downtime_duration: 0i64,
5512 downtime_no_saved_neighbor_duration: 0i64,
5513 },
5514 }
5515 });
5516
5517 let downtime_start = fasync::MonotonicInstant::now();
5519 test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5520 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5521 stats: contains {
5522 "1d_counters": contains {
5523 connected_duration: zx::MonotonicDuration::from_seconds(5).into_nanos(),
5524 downtime_duration: (fasync::MonotonicInstant::now() - downtime_start).into_nanos(),
5525 downtime_no_saved_neighbor_duration: 0i64,
5526 },
5527 "7d_counters": contains {
5528 connected_duration: zx::MonotonicDuration::from_seconds(5).into_nanos(),
5529 downtime_duration: (fasync::MonotonicInstant::now() - downtime_start).into_nanos(),
5530 downtime_no_saved_neighbor_duration: 0i64,
5531 },
5532 }
5533 });
5534 }
5535
5536 #[fuchsia::test]
5537 fn test_downtime_no_saved_neighbor_duration_counter() {
5538 let (mut test_helper, mut test_fut) = setup_test();
5539 test_helper.send_connected_event(random_bss_description!(Wpa2));
5540 test_helper.drain_cobalt_events(&mut test_fut);
5541
5542 let info = fake_disconnect_info();
5544 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5545 track_subsequent_downtime: true,
5546 info: Some(info),
5547 });
5548 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5549
5550 test_helper.advance_by(zx::MonotonicDuration::from_seconds(5), test_fut.as_mut());
5551 test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
5553 network_selection_type: NetworkSelectionType::Undirected,
5554 num_candidates: Ok(0),
5555 selected_count: 0,
5556 });
5557 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5558
5559 test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5560 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5561 stats: contains {
5562 "1d_counters": contains {
5563 connected_duration: 0i64,
5564 downtime_duration: TELEMETRY_QUERY_INTERVAL.into_nanos(),
5565 downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL - zx::MonotonicDuration::from_seconds(5)).into_nanos(),
5566 },
5567 "7d_counters": contains {
5568 connected_duration: 0i64,
5569 downtime_duration: TELEMETRY_QUERY_INTERVAL.into_nanos(),
5570 downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL - zx::MonotonicDuration::from_seconds(5)).into_nanos(),
5571 },
5572 }
5573 });
5574
5575 test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5576 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5577 stats: contains {
5578 "1d_counters": contains {
5579 connected_duration: 0i64,
5580 downtime_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5581 downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL*2 - zx::MonotonicDuration::from_seconds(5)).into_nanos(),
5582 },
5583 "7d_counters": contains {
5584 connected_duration: 0i64,
5585 downtime_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5586 downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL*2 - zx::MonotonicDuration::from_seconds(5)).into_nanos(),
5587 },
5588 }
5589 });
5590
5591 test_helper.advance_by(zx::MonotonicDuration::from_seconds(5), test_fut.as_mut());
5592 test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
5594 network_selection_type: NetworkSelectionType::Undirected,
5595 num_candidates: Ok(1),
5596 selected_count: 0,
5597 });
5598 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5599
5600 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5602 stats: contains {
5603 "1d_counters": contains {
5604 connected_duration: 0i64,
5605 downtime_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5606 downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL*2 - zx::MonotonicDuration::from_seconds(5)).into_nanos(),
5607 },
5608 "7d_counters": contains {
5609 connected_duration: 0i64,
5610 downtime_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5611 downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL*2 - zx::MonotonicDuration::from_seconds(5)).into_nanos(),
5612 },
5613 }
5614 });
5615
5616 test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5618 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5619 stats: contains {
5620 "1d_counters": contains {
5621 connected_duration: 0i64,
5622 downtime_duration: (TELEMETRY_QUERY_INTERVAL * 3).into_nanos(),
5623 downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5624 },
5625 "7d_counters": contains {
5626 connected_duration: 0i64,
5627 downtime_duration: (TELEMETRY_QUERY_INTERVAL * 3).into_nanos(),
5628 downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5629 },
5630 }
5631 });
5632
5633 let info = fake_disconnect_info();
5635 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5636 track_subsequent_downtime: false,
5637 info: Some(info),
5638 });
5639
5640 test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
5642 network_selection_type: NetworkSelectionType::Undirected,
5643 num_candidates: Ok(0),
5644 selected_count: 0,
5645 });
5646 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5647 test_helper.advance_to_next_telemetry_checkpoint(test_fut.as_mut());
5648
5649 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5651 stats: contains {
5652 "1d_counters": contains {
5653 connected_duration: 0i64,
5654 downtime_duration: (TELEMETRY_QUERY_INTERVAL * 3).into_nanos(),
5655 downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5656 },
5657 "7d_counters": contains {
5658 connected_duration: 0i64,
5659 downtime_duration: (TELEMETRY_QUERY_INTERVAL * 3).into_nanos(),
5660 downtime_no_saved_neighbor_duration: (TELEMETRY_QUERY_INTERVAL * 2).into_nanos(),
5661 },
5662 }
5663 });
5664 }
5665
5666 #[fuchsia::test]
5667 fn test_log_connect_attempt_counters() {
5668 let (mut test_helper, mut test_fut) = setup_test();
5669
5670 for i in 0..10 {
5672 let event = TelemetryEvent::ConnectResult {
5673 iface_id: IFACE_ID,
5674 policy_connect_reason: Some(
5675 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
5676 ),
5677 result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
5678 multiple_bss_candidates: true,
5679 ap_state: random_bss_description!(Wpa1).into(),
5680 network_is_likely_hidden: false,
5681 };
5682 test_helper.telemetry_sender.send(event);
5683
5684 test_helper.drain_cobalt_events(&mut test_fut);
5686 let logged_metrics =
5687 test_helper.get_logged_metrics(metrics::CONNECTION_FAILURES_METRIC_ID);
5688 assert_eq!(logged_metrics.len(), i + 1);
5689 }
5690 test_helper.send_connected_event(random_bss_description!(Wpa2));
5691 test_helper.drain_cobalt_events(&mut test_fut);
5692
5693 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5694 stats: contains {
5695 "1d_counters": contains {
5696 connect_attempts_count: 11u64,
5697 connect_successful_count: 1u64,
5698 },
5699 "7d_counters": contains {
5700 connect_attempts_count: 11u64,
5701 connect_successful_count: 1u64,
5702 },
5703 }
5704 });
5705 }
5706
5707 #[fuchsia::test]
5708 fn test_log_connect_attempt_time_series() {
5709 let (mut test_helper, mut test_fut) = setup_test();
5710
5711 for i in 0..10 {
5713 let event = TelemetryEvent::ConnectResult {
5714 iface_id: IFACE_ID,
5715 policy_connect_reason: Some(
5716 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
5717 ),
5718 result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
5719 multiple_bss_candidates: true,
5720 ap_state: random_bss_description!(Wpa1).into(),
5721 network_is_likely_hidden: false,
5722 };
5723 test_helper.telemetry_sender.send(event);
5724
5725 test_helper.drain_cobalt_events(&mut test_fut);
5727 let logged_metrics =
5728 test_helper.get_logged_metrics(metrics::CONNECTION_FAILURES_METRIC_ID);
5729 assert_eq!(logged_metrics.len(), i + 1);
5730 }
5731 test_helper.send_connected_event(random_bss_description!(Wpa2));
5732 test_helper.drain_cobalt_events(&mut test_fut);
5733 let time_series = test_helper.get_time_series(&mut test_fut);
5734 let connect_attempt_count: Vec<_> =
5735 time_series.lock().connect_attempt_count.minutely_iter().copied().collect();
5736 let connect_successful_count: Vec<_> =
5737 time_series.lock().connect_successful_count.minutely_iter().copied().collect();
5738 assert_eq!(connect_attempt_count, vec![11]);
5739 assert_eq!(connect_successful_count, vec![1]);
5740 }
5741
5742 #[fuchsia::test]
5743 fn test_disconnect_count_counter() {
5744 let (mut test_helper, mut test_fut) = setup_test();
5745 test_helper.send_connected_event(random_bss_description!(Wpa2));
5746 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5747
5748 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5749 stats: contains {
5750 "1d_counters": contains {
5751 disconnect_count: 0u64,
5752 policy_roam_disconnects_count: 0u64,
5753 },
5754 "7d_counters": contains {
5755 disconnect_count: 0u64,
5756 policy_roam_disconnects_count: 0u64,
5757 },
5758 }
5759 });
5760
5761 let info = DisconnectInfo {
5762 disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
5763 reason_code: fidl_ieee80211::ReasonCode::StaLeaving,
5764 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DisassociateIndication,
5765 }),
5766 ..fake_disconnect_info()
5767 };
5768 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5769 track_subsequent_downtime: true,
5770 info: Some(info),
5771 });
5772 test_helper.drain_cobalt_events(&mut test_fut);
5773
5774 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5775 stats: contains {
5776 "1d_counters": contains {
5777 disconnect_count: 1u64,
5778 policy_roam_disconnects_count: 0u64,
5779 total_non_roam_disconnect_count: 1u64,
5780 total_roam_disconnect_count: 0u64,
5781 },
5782 "7d_counters": contains {
5783 disconnect_count: 1u64,
5784 policy_roam_disconnects_count: 0u64,
5785 total_non_roam_disconnect_count: 1u64,
5786 total_roam_disconnect_count: 0u64,
5787 },
5788 }
5789 });
5790
5791 let info = DisconnectInfo {
5792 disconnect_source: fidl_sme::DisconnectSource::User(
5793 fidl_sme::UserDisconnectReason::Startup,
5794 ),
5795 ..fake_disconnect_info()
5796 };
5797 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5798 track_subsequent_downtime: false,
5799 info: Some(info),
5800 });
5801 test_helper.drain_cobalt_events(&mut test_fut);
5802
5803 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5804 stats: contains {
5805 "1d_counters": contains {
5806 disconnect_count: 2u64,
5807 policy_roam_disconnects_count: 0u64,
5808 total_non_roam_disconnect_count: 2u64,
5809 total_roam_disconnect_count: 0u64,
5810 },
5811 "7d_counters": contains {
5812 disconnect_count: 2u64,
5813 policy_roam_disconnects_count: 0u64,
5814 total_non_roam_disconnect_count: 2u64,
5815 total_roam_disconnect_count: 0u64,
5816 },
5817 }
5818 });
5819
5820 let info = DisconnectInfo {
5822 disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
5823 reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
5824 mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
5825 }),
5826 ..fake_disconnect_info()
5827 };
5828 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5829 track_subsequent_downtime: false,
5830 info: Some(info),
5831 });
5832 test_helper.drain_cobalt_events(&mut test_fut);
5833
5834 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5835 stats: contains {
5836 "1d_counters": contains {
5837 disconnect_count: 3u64,
5838 policy_roam_disconnects_count: 0u64,
5839 total_non_roam_disconnect_count: 2u64,
5840 total_roam_disconnect_count: 1u64,
5841 },
5842 "7d_counters": contains {
5843 disconnect_count: 3u64,
5844 policy_roam_disconnects_count: 0u64,
5845 total_non_roam_disconnect_count: 2u64,
5846 total_roam_disconnect_count: 1u64,
5847 },
5848 }
5849 });
5850 }
5851
5852 #[fuchsia::test]
5853 fn test_disconnect_count_time_series() {
5854 let (mut test_helper, mut test_fut) = setup_test();
5855 test_helper.send_connected_event(random_bss_description!(Wpa2));
5856 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5857
5858 let time_series = test_helper.get_time_series(&mut test_fut);
5859 let disconnect_count: Vec<_> =
5860 time_series.lock().disconnect_count.minutely_iter().copied().collect();
5861 assert_eq!(disconnect_count, vec![0]);
5862
5863 let info = DisconnectInfo {
5864 disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
5865 reason_code: fidl_ieee80211::ReasonCode::StaLeaving,
5866 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DisassociateIndication,
5867 }),
5868 ..fake_disconnect_info()
5869 };
5870 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
5871 track_subsequent_downtime: true,
5872 info: Some(info),
5873 });
5874 test_helper.drain_cobalt_events(&mut test_fut);
5875
5876 let time_series = test_helper.get_time_series(&mut test_fut);
5877 let disconnect_count: Vec<_> =
5878 time_series.lock().disconnect_count.minutely_iter().copied().collect();
5879 assert_eq!(disconnect_count, vec![1]);
5880 }
5881
5882 #[fuchsia::test]
5883 fn test_policy_roam_disconnects_count_counter() {
5884 let (mut test_helper, mut test_fut) = setup_test();
5885 test_helper.send_connected_event(random_bss_description!(Wpa2));
5886 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
5887 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5888 stats: contains {
5889 "1d_counters": contains {
5890 disconnect_count: 0u64,
5891 policy_roam_disconnects_count: 0u64,
5892 total_roam_disconnect_count: 0u64,
5893 total_non_roam_disconnect_count: 0u64,
5894 },
5895 "7d_counters": contains {
5896 disconnect_count: 0u64,
5897 policy_roam_disconnects_count: 0u64,
5898 total_roam_disconnect_count: 0u64,
5899 total_non_roam_disconnect_count: 0u64,
5900 },
5901 }
5902 });
5903
5904 let mut roam_result = fidl_sme::RoamResult {
5906 bssid: [1, 1, 1, 1, 1, 1],
5907 status_code: fidl_ieee80211::StatusCode::Success,
5908 original_association_maintained: false,
5909 bss_description: Some(Box::new(random_fidl_bss_description!())),
5910 disconnect_info: None,
5911 is_credential_rejected: false,
5912 };
5913 test_helper.telemetry_sender.send(TelemetryEvent::PolicyInitiatedRoamResult {
5914 iface_id: 1,
5915 result: roam_result.clone(),
5916 updated_ap_state: generate_random_ap_state(),
5917 original_ap_state: Box::new(generate_random_ap_state()),
5918 request: Box::new(generate_policy_roam_request([1, 1, 1, 1, 1, 1].into())),
5919 request_time: fasync::MonotonicInstant::now(),
5920 result_time: fasync::MonotonicInstant::now(),
5921 });
5922 test_helper.drain_cobalt_events(&mut test_fut);
5923 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5924 stats: contains {
5925 "1d_counters": contains {
5926 disconnect_count: 0u64,
5927 policy_roam_disconnects_count: 1u64,
5928 total_roam_disconnect_count: 0u64,
5931 total_non_roam_disconnect_count: 0u64,
5932 },
5933 "7d_counters": contains {
5934 disconnect_count: 0u64,
5935 policy_roam_disconnects_count: 1u64,
5936 total_roam_disconnect_count: 0u64,
5939 total_non_roam_disconnect_count: 0u64,
5940 },
5941 }
5942 });
5943
5944 roam_result.status_code = fidl_ieee80211::StatusCode::RefusedReasonUnspecified;
5946 roam_result.disconnect_info = Some(Box::new(generate_disconnect_info(false)));
5947 test_helper.telemetry_sender.send(TelemetryEvent::PolicyInitiatedRoamResult {
5948 iface_id: 1,
5949 result: roam_result.clone(),
5950 updated_ap_state: generate_random_ap_state(),
5951 original_ap_state: Box::new(generate_random_ap_state()),
5952 request: Box::new(generate_policy_roam_request([1, 1, 1, 1, 1, 1].into())),
5953 request_time: fasync::MonotonicInstant::now(),
5954 result_time: fasync::MonotonicInstant::now(),
5955 });
5956 test_helper.drain_cobalt_events(&mut test_fut);
5957 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5958 stats: contains {
5959 "1d_counters": contains {
5960 disconnect_count: 0u64,
5961 policy_roam_disconnects_count: 2u64,
5962 total_roam_disconnect_count: 0u64,
5965 total_non_roam_disconnect_count: 0u64,
5966 },
5967 "7d_counters": contains {
5968 disconnect_count: 0u64,
5969 policy_roam_disconnects_count: 2u64,
5970 total_roam_disconnect_count: 0u64,
5973 total_non_roam_disconnect_count: 0u64,
5974 },
5975 }
5976 });
5977
5978 roam_result.original_association_maintained = true;
5980 test_helper.telemetry_sender.send(TelemetryEvent::PolicyInitiatedRoamResult {
5981 iface_id: 1,
5982 result: roam_result,
5983 updated_ap_state: generate_random_ap_state(),
5984 original_ap_state: Box::new(generate_random_ap_state()),
5985 request: Box::new(generate_policy_roam_request([1, 1, 1, 1, 1, 1].into())),
5986 request_time: fasync::MonotonicInstant::now(),
5987 result_time: fasync::MonotonicInstant::now(),
5988 });
5989 test_helper.drain_cobalt_events(&mut test_fut);
5990 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
5991 stats: contains {
5992 "1d_counters": contains {
5993 disconnect_count: 0u64,
5994 policy_roam_disconnects_count: 2u64,
5995 total_roam_disconnect_count: 0u64,
5996 total_non_roam_disconnect_count: 0u64,
5997 },
5998 "7d_counters": contains {
5999 disconnect_count: 0u64,
6000 policy_roam_disconnects_count: 2u64,
6001 total_roam_disconnect_count: 0u64,
6002 total_non_roam_disconnect_count: 0u64,
6003 },
6004 }
6005 });
6006 }
6007
6008 #[fuchsia::test]
6009 fn test_connected_duration_time_series() {
6010 let (mut test_helper, mut test_fut) = setup_test();
6011 test_helper.send_connected_event(random_bss_description!(Wpa2));
6012 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6013 test_helper.advance_by(zx::MonotonicDuration::from_seconds(90), test_fut.as_mut());
6014
6015 let time_series = test_helper.get_time_series(&mut test_fut);
6016 let connected_duration_sec: Vec<_> =
6017 time_series.lock().connected_duration_sec.minutely_iter().copied().collect();
6018 assert_eq!(connected_duration_sec, vec![45, 45]);
6019 }
6020
6021 #[fuchsia::test]
6022 fn test_rx_tx_counters_no_issue() {
6023 let (mut test_helper, mut test_fut) = setup_test();
6024 test_helper.send_connected_event(random_bss_description!(Wpa2));
6025 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6026
6027 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6028 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
6029 stats: contains {
6030 get_iface_stats_fail_count: 0u64,
6031 "1d_counters": contains {
6032 tx_high_packet_drop_duration: 0i64,
6033 rx_high_packet_drop_duration: 0i64,
6034 tx_very_high_packet_drop_duration: 0i64,
6035 rx_very_high_packet_drop_duration: 0i64,
6036 no_rx_duration: 0i64,
6037 },
6038 "7d_counters": contains {
6039 tx_high_packet_drop_duration: 0i64,
6040 rx_high_packet_drop_duration: 0i64,
6041 tx_very_high_packet_drop_duration: 0i64,
6042 rx_very_high_packet_drop_duration: 0i64,
6043 no_rx_duration: 0i64,
6044 },
6045 }
6046 });
6047 }
6048
6049 #[fuchsia::test]
6050 fn test_tx_high_packet_drop_duration_counters() {
6051 let (mut test_helper, mut test_fut) = setup_test();
6052 test_helper.set_iface_stats_resp(Box::new(|| {
6053 let seed = fasync::MonotonicInstant::now().into_nanos() as u64;
6054 Ok(fidl_fuchsia_wlan_stats::IfaceStats {
6055 connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
6056 tx_total: Some(10 * seed),
6057 tx_drop: Some(3 * seed),
6058 ..fake_connection_stats(seed)
6059 }),
6060 ..Default::default()
6061 })
6062 }));
6063
6064 test_helper.send_connected_event(random_bss_description!(Wpa2));
6065 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6066
6067 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6068 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
6069 stats: contains {
6070 get_iface_stats_fail_count: 0u64,
6071 "1d_counters": contains {
6072 tx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6075 rx_high_packet_drop_duration: 0i64,
6076 tx_very_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6077 rx_very_high_packet_drop_duration: 0i64,
6078 no_rx_duration: 0i64,
6079 },
6080 "7d_counters": contains {
6081 tx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6082 rx_high_packet_drop_duration: 0i64,
6083 tx_very_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6084 rx_very_high_packet_drop_duration: 0i64,
6085 no_rx_duration: 0i64,
6086 },
6087 }
6088 });
6089 }
6090
6091 #[fuchsia::test]
6092 fn test_rx_high_packet_drop_duration_counters() {
6093 let (mut test_helper, mut test_fut) = setup_test();
6094 test_helper.set_iface_stats_resp(Box::new(|| {
6095 let seed = fasync::MonotonicInstant::now().into_nanos() as u64;
6096 Ok(fidl_fuchsia_wlan_stats::IfaceStats {
6097 connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
6098 rx_unicast_total: Some(10 * seed),
6099 rx_unicast_drop: Some(3 * seed),
6100 ..fake_connection_stats(seed)
6101 }),
6102 ..Default::default()
6103 })
6104 }));
6105
6106 test_helper.send_connected_event(random_bss_description!(Wpa2));
6107 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6108
6109 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6110 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
6111 stats: contains {
6112 get_iface_stats_fail_count: 0u64,
6113 "1d_counters": contains {
6114 rx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6117 tx_high_packet_drop_duration: 0i64,
6118 rx_very_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6119 tx_very_high_packet_drop_duration: 0i64,
6120 no_rx_duration: 0i64,
6121 },
6122 "7d_counters": contains {
6123 rx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6124 tx_high_packet_drop_duration: 0i64,
6125 rx_very_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6126 tx_very_high_packet_drop_duration: 0i64,
6127 no_rx_duration: 0i64,
6128 },
6129 }
6130 });
6131 }
6132
6133 #[fuchsia::test]
6134 fn test_rx_tx_high_but_not_very_high_packet_drop_duration_counters() {
6135 let (mut test_helper, mut test_fut) = setup_test();
6136 test_helper.set_iface_stats_resp(Box::new(|| {
6137 let seed = fasync::MonotonicInstant::now().into_nanos() as u64;
6138 Ok(fidl_fuchsia_wlan_stats::IfaceStats {
6139 connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
6140 rx_unicast_total: Some(100 * seed),
6142 rx_unicast_drop: Some(3 * seed),
6143 tx_total: Some(100 * seed),
6144 tx_drop: Some(3 * seed),
6145 ..fake_connection_stats(seed)
6146 }),
6147 ..Default::default()
6148 })
6149 }));
6150
6151 test_helper.send_connected_event(random_bss_description!(Wpa2));
6152 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6153
6154 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6155 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
6156 stats: contains {
6157 get_iface_stats_fail_count: 0u64,
6158 "1d_counters": contains {
6159 rx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6162 tx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6163 rx_very_high_packet_drop_duration: 0i64,
6165 tx_very_high_packet_drop_duration: 0i64,
6166 no_rx_duration: 0i64,
6167 },
6168 "7d_counters": contains {
6169 rx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6170 tx_high_packet_drop_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6171 rx_very_high_packet_drop_duration: 0i64,
6172 tx_very_high_packet_drop_duration: 0i64,
6173 no_rx_duration: 0i64,
6174 },
6175 }
6176 });
6177 }
6178
6179 #[fuchsia::test]
6180 fn test_rx_tx_reset() {
6181 let (mut test_helper, mut test_fut) = setup_test();
6182 test_helper.set_iface_stats_resp(Box::new(|| {
6183 let seed = (fasync::MonotonicInstant::now() - fasync::MonotonicInstant::from_nanos(0))
6184 .into_seconds() as u64;
6185 Ok(fidl_fuchsia_wlan_stats::IfaceStats {
6186 connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
6187 rx_unicast_total: Some(999999 - seed),
6188 rx_unicast_drop: Some(999999 - seed),
6189 tx_total: Some(999999 - seed),
6190 tx_drop: Some(999999 - seed),
6191 ..fake_connection_stats(seed)
6192 }),
6193 ..Default::default()
6194 })
6195 }));
6196
6197 test_helper.send_connected_event(random_bss_description!(Wpa2));
6198 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6199
6200 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6202 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
6204 stats: contains {
6205 get_iface_stats_fail_count: 0u64,
6206 "1d_counters": contains {
6207 rx_high_packet_drop_duration: 0i64,
6210 tx_high_packet_drop_duration: 0i64,
6211 rx_very_high_packet_drop_duration: 0i64,
6213 tx_very_high_packet_drop_duration: 0i64,
6214 no_rx_duration: 0i64,
6215 },
6216 "7d_counters": contains {
6217 rx_high_packet_drop_duration: 0i64,
6218 tx_high_packet_drop_duration: 0i64,
6219 rx_very_high_packet_drop_duration: 0i64,
6220 tx_very_high_packet_drop_duration: 0i64,
6221 no_rx_duration: 0i64,
6222 },
6223 }
6224 });
6225 }
6226
6227 #[fuchsia::test]
6228 fn test_rx_tx_packet_time_series() {
6229 let (mut test_helper, mut test_fut) = setup_test();
6230 test_helper.set_iface_stats_resp(Box::new(|| {
6231 let seed = (fasync::MonotonicInstant::now()
6232 - fasync::MonotonicInstant::from_nanos(0i64))
6233 .into_seconds() as u64;
6234 Ok(fidl_fuchsia_wlan_stats::IfaceStats {
6235 connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
6236 rx_unicast_total: Some(100 * seed),
6237 rx_unicast_drop: Some(3 * seed),
6238 tx_total: Some(10 * seed),
6239 tx_drop: Some(2 * seed),
6240 ..fake_connection_stats(seed)
6241 }),
6242 ..Default::default()
6243 })
6244 }));
6245
6246 test_helper.send_connected_event(random_bss_description!(Wpa2));
6247 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6248
6249 test_helper.advance_by(zx::MonotonicDuration::from_minutes(2), test_fut.as_mut());
6250
6251 let time_series = test_helper.get_time_series(&mut test_fut);
6252 let rx_unicast_drop_count: Vec<_> =
6253 time_series.lock().rx_unicast_drop_count.minutely_iter().copied().collect();
6254 let rx_unicast_total_count: Vec<_> =
6255 time_series.lock().rx_unicast_total_count.minutely_iter().copied().collect();
6256 let tx_drop_count: Vec<_> =
6257 time_series.lock().tx_drop_count.minutely_iter().copied().collect();
6258 let tx_total_count: Vec<_> =
6259 time_series.lock().tx_total_count.minutely_iter().copied().collect();
6260
6261 assert_eq!(rx_unicast_drop_count, vec![90, 180, 45]);
6267 assert_eq!(rx_unicast_total_count, vec![3000, 6000, 1500]);
6268 assert_eq!(tx_drop_count, vec![60, 120, 30]);
6269 assert_eq!(tx_total_count, vec![300, 600, 150]);
6270 }
6271
6272 #[fuchsia::test]
6273 fn test_no_rx_duration_counters() {
6274 let (mut test_helper, mut test_fut) = setup_test();
6275 test_helper.set_iface_stats_resp(Box::new(|| {
6276 let seed = fasync::MonotonicInstant::now().into_nanos() as u64;
6277 Ok(fidl_fuchsia_wlan_stats::IfaceStats {
6278 connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
6279 rx_unicast_total: Some(10),
6280 ..fake_connection_stats(seed)
6281 }),
6282 ..Default::default()
6283 })
6284 }));
6285
6286 test_helper.send_connected_event(random_bss_description!(Wpa2));
6287 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6288
6289 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6290 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
6291 stats: contains {
6292 get_iface_stats_fail_count: 0u64,
6293 "1d_counters": contains {
6294 no_rx_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6297 rx_high_packet_drop_duration: 0i64,
6298 tx_high_packet_drop_duration: 0i64,
6299 rx_very_high_packet_drop_duration: 0i64,
6300 tx_very_high_packet_drop_duration: 0i64,
6301 },
6302 "7d_counters": contains {
6303 no_rx_duration: (zx::MonotonicDuration::from_hours(1) - TELEMETRY_QUERY_INTERVAL).into_nanos(),
6304 rx_high_packet_drop_duration: 0i64,
6305 tx_high_packet_drop_duration: 0i64,
6306 rx_very_high_packet_drop_duration: 0i64,
6307 tx_very_high_packet_drop_duration: 0i64,
6308 },
6309 }
6310 });
6311 }
6312
6313 #[fuchsia::test]
6314 fn test_no_rx_duration_time_series() {
6315 let (mut test_helper, mut test_fut) = setup_test();
6316 test_helper.set_iface_stats_resp(Box::new(|| {
6317 let seed = fasync::MonotonicInstant::now().into_nanos() as u64;
6318 Ok(fidl_fuchsia_wlan_stats::IfaceStats {
6319 connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
6320 rx_unicast_total: Some(10),
6321 ..fake_connection_stats(seed)
6322 }),
6323 ..Default::default()
6324 })
6325 }));
6326
6327 test_helper.send_connected_event(random_bss_description!(Wpa2));
6328 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6329
6330 test_helper.advance_by(zx::MonotonicDuration::from_seconds(150), test_fut.as_mut());
6331 let time_series = test_helper.get_time_series(&mut test_fut);
6332 let no_rx_duration_sec: Vec<_> =
6333 time_series.lock().no_rx_duration_sec.minutely_iter().copied().collect();
6334 assert_eq!(no_rx_duration_sec, vec![30, 60, 45]);
6335 }
6336
6337 #[fuchsia::test]
6338 fn test_get_iface_stats_fail() {
6339 let (mut test_helper, mut test_fut) = setup_test();
6340 test_helper.set_iface_stats_resp(Box::new(|| Err(zx::sys::ZX_ERR_NOT_SUPPORTED)));
6341
6342 test_helper.send_connected_event(random_bss_description!(Wpa2));
6343 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6344
6345 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6346 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
6347 stats: contains {
6348 get_iface_stats_fail_count: NonZeroUintProperty,
6349 "1d_counters": contains {
6350 no_rx_duration: 0i64,
6351 rx_high_packet_drop_duration: 0i64,
6352 tx_high_packet_drop_duration: 0i64,
6353 rx_very_high_packet_drop_duration: 0i64,
6354 tx_very_high_packet_drop_duration: 0i64,
6355 },
6356 "7d_counters": contains {
6357 no_rx_duration: 0i64,
6358 rx_high_packet_drop_duration: 0i64,
6359 tx_high_packet_drop_duration: 0i64,
6360 rx_very_high_packet_drop_duration: 0i64,
6361 tx_very_high_packet_drop_duration: 0i64,
6362 },
6363 }
6364 });
6365 }
6366
6367 #[fuchsia::test]
6368 fn test_log_signal_histograms_inspect() {
6369 let (mut test_helper, mut test_fut) = setup_test();
6370 test_helper.send_connected_event(random_bss_description!(Wpa2));
6371 test_helper.drain_cobalt_events(&mut test_fut);
6372
6373 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
6375 external: contains {
6376 stats: contains {
6377 connection_status: contains {
6378 histograms: {
6379 antenna0_2Ghz: {
6380 antenna_index: 0u64,
6381 antenna_freq: "2Ghz",
6382 snr_histogram: vec![30i64, 999],
6383 snr_invalid_samples: 11u64,
6384 noise_floor_histogram: vec![-55i64, 999],
6385 noise_floor_invalid_samples: 44u64,
6386 rssi_histogram: vec![-25i64, 999],
6387 rssi_invalid_samples: 55u64,
6388 },
6389 antenna1_5Ghz: {
6390 antenna_index: 1u64,
6391 antenna_freq: "5Ghz",
6392 rx_rate_histogram: vec![100i64, 1500],
6393 rx_rate_invalid_samples: 33u64,
6394 },
6395 }
6396 }
6397 }
6398 }
6399 });
6400 }
6401
6402 #[fuchsia::test]
6403 fn test_log_daily_uptime_ratio_cobalt_metric() {
6404 let (mut test_helper, mut test_fut) = setup_test();
6405 test_helper.send_connected_event(random_bss_description!(Wpa2));
6406 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6407
6408 test_helper.advance_by(zx::MonotonicDuration::from_hours(12), test_fut.as_mut());
6409
6410 let info = fake_disconnect_info();
6411 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6412 track_subsequent_downtime: true,
6413 info: Some(info),
6414 });
6415 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6416
6417 test_helper.advance_by(zx::MonotonicDuration::from_hours(6), test_fut.as_mut());
6418
6419 test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
6421 network_selection_type: NetworkSelectionType::Undirected,
6422 num_candidates: Ok(0),
6423 selected_count: 0,
6424 });
6425
6426 test_helper.advance_by(zx::MonotonicDuration::from_hours(6), test_fut.as_mut());
6427
6428 let uptime_ratios =
6429 test_helper.get_logged_metrics(metrics::CONNECTED_UPTIME_RATIO_METRIC_ID);
6430 assert_eq!(uptime_ratios.len(), 1);
6431 assert_eq!(uptime_ratios[0].payload, MetricEventPayload::IntegerValue(6666));
6433 }
6434
6435 fn connect_and_disconnect_with_source(
6438 test_helper: &mut TestHelper,
6439 mut test_fut: Pin<&mut impl Future<Output = ()>>,
6440 disconnect_source: fidl_sme::DisconnectSource,
6441 ) {
6442 test_helper.send_connected_event(random_bss_description!(Wpa2));
6443 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6444
6445 test_helper.advance_by(zx::MonotonicDuration::from_hours(6), test_fut.as_mut());
6446
6447 let info = DisconnectInfo { disconnect_source, ..fake_disconnect_info() };
6448 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6449 track_subsequent_downtime: true,
6450 info: Some(info),
6451 });
6452 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6453 test_helper.drain_cobalt_events(&mut test_fut);
6454 }
6455
6456 #[fuchsia::test]
6457 fn test_log_daily_disconnect_per_day_connected_cobalt_metric() {
6458 let (mut test_helper, mut test_fut) = setup_test();
6459
6460 let mlme_non_roam_source = fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
6463 reason_code: fidl_ieee80211::ReasonCode::LeavingNetworkDeauth,
6464 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
6465 });
6466 connect_and_disconnect_with_source(
6467 &mut test_helper,
6468 test_fut.as_mut(),
6469 mlme_non_roam_source,
6470 );
6471
6472 let mlme_roam_source = fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
6473 reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
6474 mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamConfirmation,
6475 });
6476 connect_and_disconnect_with_source(&mut test_helper, test_fut.as_mut(), mlme_roam_source);
6477
6478 test_helper.advance_by(zx::MonotonicDuration::from_hours(12), test_fut.as_mut());
6479
6480 let dpdc_ratios =
6481 test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
6482 assert_eq!(dpdc_ratios.len(), 1);
6483 assert_eq!(dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(40_000));
6485
6486 let non_roam_dpdc_ratios = test_helper
6488 .get_logged_metrics(metrics::NON_ROAM_DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
6489 assert_eq!(non_roam_dpdc_ratios.len(), 1);
6490 assert_eq!(non_roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(20_000));
6491
6492 let roam_dpdc_ratios = test_helper
6495 .get_logged_metrics(metrics::POLICY_ROAM_DISCONNECT_COUNT_PER_DAY_CONNECTED_METRIC_ID);
6496 assert_eq!(roam_dpdc_ratios.len(), 1);
6497 assert_eq!(roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6498
6499 let dpdc_ratios_7d =
6500 test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_7D_METRIC_ID);
6501 assert_eq!(dpdc_ratios_7d.len(), 1);
6502 assert_eq!(dpdc_ratios_7d[0].payload, MetricEventPayload::IntegerValue(40_000));
6503
6504 test_helper.cobalt_events.clear();
6506
6507 test_helper.send_connected_event(random_bss_description!(Wpa2));
6509 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6510
6511 test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
6512
6513 let dpdc_ratios =
6515 test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
6516 assert_eq!(dpdc_ratios.len(), 1);
6517 assert_eq!(dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6518
6519 let non_roam_dpdc_ratios = test_helper
6520 .get_logged_metrics(metrics::NON_ROAM_DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
6521 assert_eq!(non_roam_dpdc_ratios.len(), 1);
6522 assert_eq!(non_roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6523
6524 let roam_dpdc_ratios = test_helper
6525 .get_logged_metrics(metrics::POLICY_ROAM_DISCONNECT_COUNT_PER_DAY_CONNECTED_METRIC_ID);
6526 assert_eq!(roam_dpdc_ratios.len(), 1);
6527 assert_eq!(roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6528
6529 let dpdc_ratios_7d =
6530 test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_7D_METRIC_ID);
6531 assert_eq!(dpdc_ratios_7d.len(), 1);
6532 assert_eq!(dpdc_ratios_7d[0].payload, MetricEventPayload::IntegerValue(13_333));
6535 }
6536
6537 #[fuchsia::test]
6538 fn test_log_daily_policy_roam_disconnect_per_day_connected_cobalt_metric() {
6539 let (mut test_helper, mut test_fut) = setup_test();
6540 test_helper.send_connected_event(random_bss_description!(Wpa2));
6541 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6542
6543 let bss_desc = random_fidl_bss_description!();
6545 let roam_result = fidl_sme::RoamResult {
6546 bssid: [1, 1, 1, 1, 1, 1],
6547 status_code: fidl_ieee80211::StatusCode::Success,
6548 original_association_maintained: false,
6549 bss_description: Some(Box::new(bss_desc.clone())),
6550 disconnect_info: None,
6551 is_credential_rejected: false,
6552 };
6553 test_helper.telemetry_sender.send(TelemetryEvent::PolicyInitiatedRoamResult {
6554 iface_id: 1,
6555 result: roam_result,
6556 updated_ap_state: generate_random_ap_state(),
6557 original_ap_state: Box::new(generate_random_ap_state()),
6558 request: Box::new(generate_policy_roam_request([1, 1, 1, 1, 1, 1].into())),
6559 request_time: fasync::MonotonicInstant::now(),
6560 result_time: fasync::MonotonicInstant::now(),
6561 });
6562 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6563 test_helper.advance_by(zx::MonotonicDuration::from_hours(12), test_fut.as_mut());
6564
6565 let bss_desc = random_fidl_bss_description!();
6567 let roam_result = fidl_sme::RoamResult {
6568 bssid: [2, 2, 2, 2, 2, 2],
6569 status_code: fidl_ieee80211::StatusCode::Success,
6570 original_association_maintained: false,
6571 bss_description: Some(Box::new(bss_desc.clone())),
6572 disconnect_info: None,
6573 is_credential_rejected: false,
6574 };
6575 test_helper.telemetry_sender.send(TelemetryEvent::PolicyInitiatedRoamResult {
6576 iface_id: 1,
6577 result: roam_result,
6578 updated_ap_state: generate_random_ap_state(),
6579 original_ap_state: Box::new(generate_random_ap_state()),
6580 request: Box::new(generate_policy_roam_request([2, 2, 2, 2, 2, 2].into())),
6581 request_time: fasync::MonotonicInstant::now(),
6582 result_time: fasync::MonotonicInstant::now(),
6583 });
6584 let info = DisconnectInfo {
6586 disconnect_source: fidl_sme::DisconnectSource::User(
6587 fidl_sme::UserDisconnectReason::Unknown,
6588 ),
6589 ..fake_disconnect_info()
6590 };
6591 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6592 track_subsequent_downtime: true,
6593 info: Some(info),
6594 });
6595 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6596 test_helper.advance_by(zx::MonotonicDuration::from_hours(12), test_fut.as_mut());
6597
6598 let dpdc_ratios =
6599 test_helper.get_logged_metrics(metrics::DISCONNECT_PER_DAY_CONNECTED_METRIC_ID);
6600 assert_eq!(dpdc_ratios.len(), 1);
6601 assert_eq!(dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(20_000));
6604
6605 let roam_dpdc_ratios = test_helper
6607 .get_logged_metrics(metrics::POLICY_ROAM_DISCONNECT_COUNT_PER_DAY_CONNECTED_METRIC_ID);
6608 assert_eq!(roam_dpdc_ratios.len(), 1);
6609 assert_eq!(roam_dpdc_ratios[0].payload, MetricEventPayload::IntegerValue(40_000));
6610 }
6611
6612 #[fuchsia::test]
6613 fn test_log_daily_disconnect_per_day_connected_cobalt_metric_device_high_disconnect() {
6614 let (mut test_helper, mut test_fut) = setup_test();
6615 test_helper.send_connected_event(random_bss_description!(Wpa2));
6616 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6617
6618 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6619 let info = fake_disconnect_info();
6620 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6621 track_subsequent_downtime: true,
6622 info: Some(info),
6623 });
6624 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6625
6626 test_helper.advance_by(zx::MonotonicDuration::from_hours(23), test_fut.as_mut());
6627 }
6628
6629 #[fuchsia::test]
6630 fn test_log_daily_rx_tx_ratio_cobalt_metrics() {
6631 let (mut test_helper, mut test_fut) = setup_test();
6632 test_helper.set_iface_stats_resp(Box::new(|| {
6633 let seed = fasync::MonotonicInstant::now().into_nanos() as u64 / 1_000_000_000;
6634 Ok(fidl_fuchsia_wlan_stats::IfaceStats {
6635 connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
6636 tx_total: Some(10 * seed),
6637 tx_drop: Some(
6641 3 * min(
6642 seed,
6643 (zx::MonotonicDuration::from_hours(3) + TELEMETRY_QUERY_INTERVAL)
6644 .into_seconds() as u64,
6645 ),
6646 ),
6647 rx_unicast_total: Some(
6649 10 * min(seed, zx::MonotonicDuration::from_hours(23).into_seconds() as u64),
6650 ),
6651 rx_unicast_drop: Some(
6653 3 * min(
6654 seed,
6655 (zx::MonotonicDuration::from_hours(4) + TELEMETRY_QUERY_INTERVAL)
6656 .into_seconds() as u64,
6657 ),
6658 ),
6659 ..fake_connection_stats(seed)
6660 }),
6661 ..Default::default()
6662 })
6663 }));
6664
6665 test_helper.send_connected_event(random_bss_description!(Wpa2));
6666 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6667
6668 test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
6669
6670 let high_rx_drop_time_ratios =
6671 test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_HIGH_RX_PACKET_DROP_METRIC_ID);
6672 assert_eq!(high_rx_drop_time_ratios.len(), 1);
6674 assert_eq!(high_rx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(1666));
6675
6676 let high_tx_drop_time_ratios =
6677 test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_HIGH_TX_PACKET_DROP_METRIC_ID);
6678 assert_eq!(high_tx_drop_time_ratios.len(), 1);
6680 assert_eq!(high_tx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(1250));
6681
6682 let very_high_rx_drop_time_ratios = test_helper
6683 .get_logged_metrics(metrics::TIME_RATIO_WITH_VERY_HIGH_RX_PACKET_DROP_METRIC_ID);
6684 assert_eq!(very_high_rx_drop_time_ratios.len(), 1);
6685 assert_eq!(
6686 very_high_rx_drop_time_ratios[0].payload,
6687 MetricEventPayload::IntegerValue(1666)
6688 );
6689
6690 let very_high_tx_drop_time_ratios = test_helper
6691 .get_logged_metrics(metrics::TIME_RATIO_WITH_VERY_HIGH_TX_PACKET_DROP_METRIC_ID);
6692 assert_eq!(very_high_tx_drop_time_ratios.len(), 1);
6693 assert_eq!(
6694 very_high_tx_drop_time_ratios[0].payload,
6695 MetricEventPayload::IntegerValue(1250)
6696 );
6697
6698 let no_rx_time_ratios =
6700 test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_NO_RX_METRIC_ID);
6701 assert_eq!(no_rx_time_ratios.len(), 1);
6702 assert_eq!(no_rx_time_ratios[0].payload, MetricEventPayload::IntegerValue(416));
6703 }
6704
6705 #[fuchsia::test]
6706 fn test_log_daily_rx_tx_ratio_cobalt_metrics_zero() {
6707 let (mut test_helper, mut test_fut) = setup_test();
6710
6711 test_helper.send_connected_event(random_bss_description!(Wpa2));
6712 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6713
6714 test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
6715
6716 let high_rx_drop_time_ratios =
6717 test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_HIGH_RX_PACKET_DROP_METRIC_ID);
6718 assert_eq!(high_rx_drop_time_ratios.len(), 1);
6719 assert_eq!(high_rx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6720
6721 let high_tx_drop_time_ratios =
6722 test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_HIGH_TX_PACKET_DROP_METRIC_ID);
6723 assert_eq!(high_tx_drop_time_ratios.len(), 1);
6724 assert_eq!(high_tx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6725
6726 let very_high_rx_drop_time_ratios = test_helper
6727 .get_logged_metrics(metrics::TIME_RATIO_WITH_VERY_HIGH_RX_PACKET_DROP_METRIC_ID);
6728 assert_eq!(very_high_rx_drop_time_ratios.len(), 1);
6729 assert_eq!(very_high_rx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6730
6731 let very_high_tx_drop_time_ratios = test_helper
6732 .get_logged_metrics(metrics::TIME_RATIO_WITH_VERY_HIGH_TX_PACKET_DROP_METRIC_ID);
6733 assert_eq!(very_high_tx_drop_time_ratios.len(), 1);
6734 assert_eq!(very_high_tx_drop_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6735
6736 let no_rx_time_ratios =
6737 test_helper.get_logged_metrics(metrics::TIME_RATIO_WITH_NO_RX_METRIC_ID);
6738 assert_eq!(no_rx_time_ratios.len(), 1);
6739 assert_eq!(no_rx_time_ratios[0].payload, MetricEventPayload::IntegerValue(0));
6740 }
6741
6742 #[fuchsia::test]
6743 fn test_log_daily_establish_connection_metrics() {
6744 let (mut test_helper, mut test_fut) = setup_test();
6745
6746 for _ in 0..10 {
6748 let event = TelemetryEvent::ConnectResult {
6749 iface_id: IFACE_ID,
6750 policy_connect_reason: Some(
6751 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
6752 ),
6753 result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
6754 multiple_bss_candidates: true,
6755 ap_state: random_bss_description!(Wpa1).into(),
6756 network_is_likely_hidden: true,
6757 };
6758 test_helper.telemetry_sender.send(event);
6759 }
6760 test_helper.send_connected_event(random_bss_description!(Wpa2));
6761
6762 test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
6763
6764 let connection_success_rate =
6765 test_helper.get_logged_metrics(metrics::CONNECTION_SUCCESS_RATE_METRIC_ID);
6766 assert_eq!(connection_success_rate.len(), 1);
6767 assert_eq!(connection_success_rate[0].payload, MetricEventPayload::IntegerValue(909));
6769 }
6770
6771 #[fuchsia::test]
6772 fn test_log_hourly_fleetwide_uptime_cobalt_metrics() {
6773 let (mut test_helper, mut test_fut) = setup_test();
6774
6775 test_helper.send_connected_event(random_bss_description!(Wpa2));
6776 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6777
6778 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6779
6780 let total_wlan_uptime_durs =
6781 test_helper.get_logged_metrics(metrics::TOTAL_WLAN_UPTIME_NEAR_SAVED_NETWORK_METRIC_ID);
6782 assert_eq!(total_wlan_uptime_durs.len(), 1);
6783 assert_eq!(
6784 total_wlan_uptime_durs[0].payload,
6785 MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_hours(1).into_micros())
6786 );
6787
6788 let connected_durs =
6789 test_helper.get_logged_metrics(metrics::TOTAL_CONNECTED_UPTIME_METRIC_ID);
6790 assert_eq!(connected_durs.len(), 1);
6791 assert_eq!(
6792 connected_durs[0].payload,
6793 MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_hours(1).into_micros())
6794 );
6795
6796 test_helper.cobalt_events.clear();
6798
6799 test_helper.advance_by(zx::MonotonicDuration::from_minutes(30), test_fut.as_mut());
6800
6801 let info = fake_disconnect_info();
6802 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
6803 track_subsequent_downtime: true,
6804 info: Some(info),
6805 });
6806 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6807
6808 test_helper.advance_by(zx::MonotonicDuration::from_minutes(15), test_fut.as_mut());
6809
6810 test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
6812 network_selection_type: NetworkSelectionType::Undirected,
6813 num_candidates: Ok(0),
6814 selected_count: 0,
6815 });
6816 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6817
6818 test_helper.advance_by(zx::MonotonicDuration::from_minutes(15), test_fut.as_mut());
6819
6820 let total_wlan_uptime_durs =
6821 test_helper.get_logged_metrics(metrics::TOTAL_WLAN_UPTIME_NEAR_SAVED_NETWORK_METRIC_ID);
6822 assert_eq!(total_wlan_uptime_durs.len(), 1);
6823 assert_eq!(
6825 total_wlan_uptime_durs[0].payload,
6826 MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(45).into_micros())
6827 );
6828
6829 let connected_durs =
6830 test_helper.get_logged_metrics(metrics::TOTAL_CONNECTED_UPTIME_METRIC_ID);
6831 assert_eq!(connected_durs.len(), 1);
6832 assert_eq!(
6833 connected_durs[0].payload,
6834 MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(30).into_micros())
6835 );
6836 }
6837
6838 #[fuchsia::test]
6839 fn test_log_hourly_fleetwide_rx_tx_cobalt_metrics() {
6840 let (mut test_helper, mut test_fut) = setup_test();
6841 test_helper.set_iface_stats_resp(Box::new(|| {
6842 let seed = fasync::MonotonicInstant::now().into_nanos() as u64 / 1_000_000_000;
6843 Ok(fidl_fuchsia_wlan_stats::IfaceStats {
6844 connection_stats: Some(fidl_fuchsia_wlan_stats::ConnectionStats {
6845 tx_total: Some(10 * seed),
6846 tx_drop: Some(
6850 3 * min(
6851 seed,
6852 (zx::MonotonicDuration::from_minutes(10) + TELEMETRY_QUERY_INTERVAL)
6853 .into_seconds() as u64,
6854 ),
6855 ),
6856 rx_unicast_total: Some(
6858 10 * min(
6859 seed,
6860 zx::MonotonicDuration::from_minutes(45).into_seconds() as u64,
6861 ),
6862 ),
6863 rx_unicast_drop: Some(
6865 3 * min(
6866 seed,
6867 (zx::MonotonicDuration::from_minutes(20) + TELEMETRY_QUERY_INTERVAL)
6868 .into_seconds() as u64,
6869 ),
6870 ),
6871 ..fake_connection_stats(seed)
6872 }),
6873 ..Default::default()
6874 })
6875 }));
6876
6877 test_helper.send_connected_event(random_bss_description!(Wpa2));
6878 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
6879
6880 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6881
6882 let rx_high_drop_durs =
6883 test_helper.get_logged_metrics(metrics::TOTAL_TIME_WITH_HIGH_RX_PACKET_DROP_METRIC_ID);
6884 assert_eq!(rx_high_drop_durs.len(), 1);
6885 assert_eq!(
6886 rx_high_drop_durs[0].payload,
6887 MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(20).into_micros())
6888 );
6889
6890 let tx_high_drop_durs =
6891 test_helper.get_logged_metrics(metrics::TOTAL_TIME_WITH_HIGH_TX_PACKET_DROP_METRIC_ID);
6892 assert_eq!(tx_high_drop_durs.len(), 1);
6893 assert_eq!(
6894 tx_high_drop_durs[0].payload,
6895 MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(10).into_micros())
6896 );
6897
6898 let rx_very_high_drop_durs = test_helper
6899 .get_logged_metrics(metrics::TOTAL_TIME_WITH_VERY_HIGH_RX_PACKET_DROP_METRIC_ID);
6900 assert_eq!(rx_very_high_drop_durs.len(), 1);
6901 assert_eq!(
6902 rx_very_high_drop_durs[0].payload,
6903 MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(20).into_micros())
6904 );
6905
6906 let tx_very_high_drop_durs = test_helper
6907 .get_logged_metrics(metrics::TOTAL_TIME_WITH_VERY_HIGH_TX_PACKET_DROP_METRIC_ID);
6908 assert_eq!(tx_very_high_drop_durs.len(), 1);
6909 assert_eq!(
6910 tx_very_high_drop_durs[0].payload,
6911 MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(10).into_micros())
6912 );
6913
6914 let no_rx_durs = test_helper.get_logged_metrics(metrics::TOTAL_TIME_WITH_NO_RX_METRIC_ID);
6915 assert_eq!(no_rx_durs.len(), 1);
6916 assert_eq!(
6917 no_rx_durs[0].payload,
6918 MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(15).into_micros())
6919 );
6920 }
6921
6922 #[fuchsia::test]
6923 fn test_log_rssi_hourly() {
6924 let (mut test_helper, mut test_fut) = setup_test();
6925
6926 test_helper.send_connected_event(random_bss_description!(Wpa2));
6928
6929 let ind_1 = fidl_internal::SignalReportIndication { rssi_dbm: -50, snr_db: 30 };
6931 let ind_2 = fidl_internal::SignalReportIndication { rssi_dbm: -61, snr_db: 40 };
6932 test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_1 });
6933 test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_1 });
6934 test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_2 });
6935
6936 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6938 test_helper.drain_cobalt_events(&mut test_fut);
6939
6940 let metrics = test_helper.get_logged_metrics(metrics::CONNECTION_RSSI_METRIC_ID);
6941 assert_eq!(metrics.len(), 1);
6942 assert_matches!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => {
6943 assert_eq!(buckets.len(), 2);
6944 assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket{index: 79, count: 2}));
6945 assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket{index: 68, count: 1}));
6946 });
6947 test_helper.clear_cobalt_events();
6948
6949 let ind_3 = fidl_internal::SignalReportIndication { rssi_dbm: -75, snr_db: 30 };
6951 test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_3 });
6952 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6953
6954 test_helper.drain_cobalt_events(&mut test_fut);
6957
6958 let metrics = test_helper.get_logged_metrics(metrics::CONNECTION_RSSI_METRIC_ID);
6959 assert_eq!(metrics.len(), 1);
6960 let buckets =
6961 assert_matches!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => buckets);
6962 assert_eq!(buckets.len(), 1);
6963 assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 54, count: 1 }));
6964 }
6965
6966 #[fuchsia::test]
6967 fn test_log_rssi_velocity_hourly() {
6968 let (mut test_helper, mut test_fut) = setup_test();
6969
6970 test_helper.send_connected_event(random_bss_description!(Wpa2));
6972
6973 let rssi_velocity_1 = -2.0;
6975 let rssi_velocity_2 = 2.0;
6976 test_helper
6977 .telemetry_sender
6978 .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: rssi_velocity_1 });
6979 test_helper
6980 .telemetry_sender
6981 .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: rssi_velocity_2 });
6982 test_helper
6983 .telemetry_sender
6984 .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: rssi_velocity_2 });
6985
6986 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
6988 test_helper.drain_cobalt_events(&mut test_fut);
6989
6990 let metrics = test_helper.get_logged_metrics(metrics::RSSI_VELOCITY_METRIC_ID);
6991 assert_eq!(metrics.len(), 1);
6992 assert_matches!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => {
6993 assert_eq!(buckets.len(), 2);
6995 assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket{index: 9, count: 1}));
6996 assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket{index: 13, count: 2}));
6997 });
6998 test_helper.clear_cobalt_events();
6999
7000 let rssi_velocity_3 = 3.0;
7002 test_helper
7003 .telemetry_sender
7004 .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: rssi_velocity_3 });
7005 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
7006
7007 test_helper.drain_cobalt_events(&mut test_fut);
7010
7011 let metrics = test_helper.get_logged_metrics(metrics::RSSI_VELOCITY_METRIC_ID);
7012 assert_eq!(metrics.len(), 1);
7013 assert_eq!(
7014 metrics[0].payload,
7015 MetricEventPayload::Histogram(vec![fidl_fuchsia_metrics::HistogramBucket {
7016 index: 14,
7017 count: 1
7018 }])
7019 );
7020 }
7021
7022 #[fuchsia::test]
7023 fn test_log_rssi_histogram_bounds() {
7024 let (mut test_helper, mut test_fut) = setup_test();
7025
7026 test_helper.send_connected_event(random_bss_description!(Wpa2));
7028
7029 let ind_min = fidl_internal::SignalReportIndication { rssi_dbm: -128, snr_db: 30 };
7030 let ind_max = fidl_internal::SignalReportIndication { rssi_dbm: 0, snr_db: 30 };
7032 let ind_overflow_1 = fidl_internal::SignalReportIndication { rssi_dbm: 1, snr_db: 30 };
7033 let ind_overflow_2 = fidl_internal::SignalReportIndication { rssi_dbm: 127, snr_db: 30 };
7034 test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_min });
7036 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_max });
7039 test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_overflow_1 });
7040 test_helper.telemetry_sender.send(TelemetryEvent::OnSignalReport { ind: ind_overflow_2 });
7041 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
7042
7043 test_helper.drain_cobalt_events(&mut test_fut);
7045 let metrics = test_helper.get_logged_metrics(metrics::CONNECTION_RSSI_METRIC_ID);
7047 assert_eq!(metrics.len(), 1);
7048 let buckets =
7049 assert_matches!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => buckets);
7050 assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 1, count: 3 }));
7051 assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 129, count: 1 }));
7052 assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 130, count: 2 }));
7053 }
7054
7055 #[fuchsia::test]
7056 fn test_log_rssi_velocity_histogram_bounds() {
7057 let (mut test_helper, mut test_fut) = setup_test();
7058
7059 test_helper.send_connected_event(random_bss_description!(Wpa2));
7061
7062 test_helper
7064 .telemetry_sender
7065 .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: -11.0 });
7066 test_helper
7067 .telemetry_sender
7068 .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: -15.0 });
7069 test_helper
7070 .telemetry_sender
7071 .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: 11.0 });
7072 test_helper
7073 .telemetry_sender
7074 .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: 20.0 });
7075 test_helper
7076 .telemetry_sender
7077 .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: -10.0 });
7078 test_helper
7079 .telemetry_sender
7080 .send(TelemetryEvent::OnSignalVelocityUpdate { rssi_velocity: 10.0 });
7081 test_helper.advance_by(zx::MonotonicDuration::from_hours(1), test_fut.as_mut());
7082
7083 test_helper.drain_cobalt_events(&mut test_fut);
7085
7086 let metrics = test_helper.get_logged_metrics(metrics::RSSI_VELOCITY_METRIC_ID);
7088 assert_eq!(metrics.len(), 1);
7089 let buckets =
7090 assert_matches!(&metrics[0].payload, MetricEventPayload::Histogram(buckets) => buckets);
7091 assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 1, count: 1 }));
7093 assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 21, count: 1 }));
7094 assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 0, count: 2 }));
7095 assert!(buckets.contains(&fidl_fuchsia_metrics::HistogramBucket { index: 22, count: 2 }));
7096 }
7097
7098 #[fuchsia::test]
7099 fn test_log_short_duration_connection_metrics() {
7100 let (mut test_helper, mut test_fut) = setup_test();
7101 let now = fasync::MonotonicInstant::now();
7102 test_helper.send_connected_event(random_bss_description!(Wpa2));
7103 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
7104
7105 let channel = generate_random_channel();
7106 let ap_state = random_bss_description!(Wpa2, channel: channel).into();
7107 let mut signals = HistoricalList::new(5);
7108 signals.add(client::types::TimestampedSignal {
7109 signal: client::types::Signal { rssi_dbm: -30, snr_db: 60 },
7110 time: now,
7111 });
7112 signals.add(client::types::TimestampedSignal {
7113 signal: client::types::Signal { rssi_dbm: -30, snr_db: 60 },
7114 time: now,
7115 });
7116 let info = DisconnectInfo {
7118 connected_duration: METRICS_SHORT_CONNECT_DURATION
7119 - zx::MonotonicDuration::from_seconds(1),
7120 disconnect_source: fidl_sme::DisconnectSource::User(
7121 fidl_sme::UserDisconnectReason::FidlConnectRequest,
7122 ),
7123 ap_state,
7124 signals,
7125 ..fake_disconnect_info()
7126 };
7127 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7128 track_subsequent_downtime: true,
7129 info: Some(info.clone()),
7130 });
7131
7132 test_helper.send_connected_event(random_bss_description!(Wpa2));
7133 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
7134
7135 let info = DisconnectInfo {
7137 disconnect_source: fidl_sme::DisconnectSource::User(
7138 fidl_sme::UserDisconnectReason::NetworkUnsaved,
7139 ),
7140 ..info
7141 };
7142 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7143 track_subsequent_downtime: true,
7144 info: Some(info.clone()),
7145 });
7146
7147 test_helper.send_connected_event(random_bss_description!(Wpa2));
7148 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
7149
7150 let info = DisconnectInfo {
7152 connected_duration: METRICS_SHORT_CONNECT_DURATION
7153 + zx::MonotonicDuration::from_seconds(1),
7154 ..info
7155 };
7156 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7157 track_subsequent_downtime: true,
7158 info: Some(info.clone()),
7159 });
7160
7161 test_helper.drain_cobalt_events(&mut test_fut);
7162
7163 let logged_metrics = test_helper.get_logged_metrics(
7164 metrics::POLICY_FIDL_CONNECTION_ATTEMPTS_DURING_SHORT_CONNECTION_METRIC_ID,
7165 );
7166 assert_eq!(logged_metrics.len(), 2);
7167
7168 let logged_metrics = test_helper.get_logged_metrics(
7169 metrics::POLICY_FIDL_CONNECTION_ATTEMPTS_DURING_SHORT_CONNECTION_DETAILED_METRIC_ID,
7170 );
7171 assert_eq!(logged_metrics.len(), 2);
7172 assert_eq!(logged_metrics[0].event_codes, vec![info.previous_connect_reason as u32]);
7173
7174 let logged_metrics =
7175 test_helper.get_logged_metrics(metrics::CONNECTION_SCORE_AVERAGE_METRIC_ID);
7176 assert_eq!(logged_metrics.len(), 2);
7177 assert_eq!(
7178 logged_metrics[0].event_codes,
7179 vec![metrics::ConnectionScoreAverageMetricDimensionDuration::ShortDuration as u32]
7180 );
7181 assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(100));
7182 }
7183
7184 #[fuchsia::test]
7185 fn test_log_disconnect_cobalt_metrics() {
7186 let (mut test_helper, mut test_fut) = setup_test();
7187 test_helper.advance_by(zx::MonotonicDuration::from_hours(3), test_fut.as_mut());
7188 test_helper.send_connected_event(random_bss_description!(Wpa2));
7189 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
7190
7191 test_helper.advance_by(zx::MonotonicDuration::from_hours(5), test_fut.as_mut());
7192
7193 let primary_channel = 8;
7194 let channel = Channel::new(primary_channel, Cbw::Cbw20);
7195 let ap_state: client::types::ApState =
7196 random_bss_description!(Wpa2, channel: channel).into();
7197 let info = DisconnectInfo {
7198 connected_duration: zx::MonotonicDuration::from_hours(5),
7199 disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
7200 reason_code: fidl_ieee80211::ReasonCode::LeavingNetworkDeauth,
7201 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
7202 }),
7203 ap_state: ap_state.clone(),
7204 ..fake_disconnect_info()
7205 };
7206 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7207 track_subsequent_downtime: true,
7208 info: Some(info),
7209 });
7210 test_helper.drain_cobalt_events(&mut test_fut);
7211
7212 let policy_disconnection_reasons =
7213 test_helper.get_logged_metrics(metrics::POLICY_DISCONNECTION_MIGRATED_METRIC_ID);
7214 assert_eq!(policy_disconnection_reasons.len(), 1);
7215 assert_eq!(policy_disconnection_reasons[0].payload, MetricEventPayload::Count(1));
7216 assert_eq!(
7217 policy_disconnection_reasons[0].event_codes,
7218 vec![client::types::DisconnectReason::DisconnectDetectedFromSme as u32]
7219 );
7220
7221 let disconnect_counts =
7222 test_helper.get_logged_metrics(metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID);
7223 assert_eq!(disconnect_counts.len(), 1);
7224 assert_eq!(disconnect_counts[0].payload, MetricEventPayload::Count(1));
7225
7226 let breakdowns_by_device_uptime = test_helper
7227 .get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_DEVICE_UPTIME_METRIC_ID);
7228 assert_eq!(breakdowns_by_device_uptime.len(), 1);
7229 assert_eq!(breakdowns_by_device_uptime[0].event_codes, vec![
7230 metrics::DisconnectBreakdownByDeviceUptimeMetricDimensionDeviceUptime::LessThan12Hours as u32,
7231 ]);
7232 assert_eq!(breakdowns_by_device_uptime[0].payload, MetricEventPayload::Count(1));
7233
7234 let breakdowns_by_connected_duration = test_helper
7235 .get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_CONNECTED_DURATION_METRIC_ID);
7236 assert_eq!(breakdowns_by_connected_duration.len(), 1);
7237 assert_eq!(breakdowns_by_connected_duration[0].event_codes, vec![
7238 metrics::DisconnectBreakdownByConnectedDurationMetricDimensionConnectedDuration::LessThan6Hours as u32,
7239 ]);
7240 assert_eq!(breakdowns_by_connected_duration[0].payload, MetricEventPayload::Count(1));
7241
7242 let breakdowns_by_reason =
7243 test_helper.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID);
7244 assert_eq!(breakdowns_by_reason.len(), 1);
7245 assert_eq!(
7246 breakdowns_by_reason[0].event_codes,
7247 vec![3u32, metrics::ConnectivityWlanMetricDimensionDisconnectSource::Mlme as u32,]
7248 );
7249 assert_eq!(breakdowns_by_reason[0].payload, MetricEventPayload::Count(1));
7250
7251 let breakdowns_by_channel = test_helper
7252 .get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID);
7253 assert_eq!(breakdowns_by_channel.len(), 1);
7254 assert_eq!(breakdowns_by_channel[0].event_codes, vec![channel.primary as u32]);
7255 assert_eq!(breakdowns_by_channel[0].payload, MetricEventPayload::Count(1));
7256
7257 let breakdowns_by_channel_band =
7258 test_helper.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID);
7259 assert_eq!(breakdowns_by_channel_band.len(), 1);
7260 assert_eq!(
7261 breakdowns_by_channel_band[0].event_codes,
7262 vec![
7263 metrics::DisconnectBreakdownByChannelBandMetricDimensionChannelBand::Band2Dot4Ghz
7264 as u32
7265 ]
7266 );
7267 assert_eq!(breakdowns_by_channel_band[0].payload, MetricEventPayload::Count(1));
7268
7269 let breakdowns_by_is_multi_bss =
7270 test_helper.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID);
7271 assert_eq!(breakdowns_by_is_multi_bss.len(), 1);
7272 assert_eq!(
7273 breakdowns_by_is_multi_bss[0].event_codes,
7274 vec![metrics::DisconnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::Yes as u32]
7275 );
7276 assert_eq!(breakdowns_by_is_multi_bss[0].payload, MetricEventPayload::Count(1));
7277
7278 let breakdowns_by_security_type = test_helper
7279 .get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID);
7280 assert_eq!(breakdowns_by_security_type.len(), 1);
7281 assert_eq!(
7282 breakdowns_by_security_type[0].event_codes,
7283 vec![
7284 metrics::DisconnectBreakdownBySecurityTypeMetricDimensionSecurityType::Wpa2Personal
7285 as u32
7286 ]
7287 );
7288 assert_eq!(breakdowns_by_security_type[0].payload, MetricEventPayload::Count(1));
7289
7290 let connected_duration_before_disconnect =
7293 test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_DISCONNECT_METRIC_ID);
7294 assert_eq!(connected_duration_before_disconnect.len(), 1);
7295 assert_eq!(
7296 connected_duration_before_disconnect[0].payload,
7297 MetricEventPayload::IntegerValue(300)
7298 );
7299 let connected_duration_before_non_roam_disconnect = test_helper
7300 .get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_NON_ROAM_DISCONNECT_METRIC_ID);
7301 assert_eq!(connected_duration_before_non_roam_disconnect.len(), 1);
7302 assert_eq!(
7303 connected_duration_before_non_roam_disconnect[0].payload,
7304 MetricEventPayload::IntegerValue(300)
7305 );
7306 let connected_duration_before_roam_attempt = test_helper.get_logged_metrics(
7307 metrics::POLICY_ROAM_CONNECTED_DURATION_BEFORE_ROAM_ATTEMPT_METRIC_ID,
7308 );
7309 assert_eq!(connected_duration_before_roam_attempt.len(), 0);
7310
7311 let network_disconnect_counts =
7314 test_helper.get_logged_metrics(metrics::NETWORK_DISCONNECT_COUNTS_METRIC_ID);
7315 assert_eq!(network_disconnect_counts.len(), 1);
7316 assert_eq!(network_disconnect_counts[0].payload, MetricEventPayload::Count(1));
7317
7318 let non_roam_disconnect_counts =
7319 test_helper.get_logged_metrics(metrics::NON_ROAM_DISCONNECT_COUNTS_METRIC_ID);
7320 assert_eq!(non_roam_disconnect_counts.len(), 1);
7321 assert_eq!(non_roam_disconnect_counts[0].payload, MetricEventPayload::Count(1));
7322
7323 let roam_disconnect_counts =
7324 test_helper.get_logged_metrics(metrics::POLICY_ROAM_DISCONNECT_COUNT_METRIC_ID);
7325 assert!(roam_disconnect_counts.is_empty());
7326
7327 test_helper.clear_cobalt_events();
7329
7330 test_helper.advance_by(zx::MonotonicDuration::from_minutes(1), test_fut.as_mut());
7332 test_helper.send_connected_event(random_bss_description!(Wpa2));
7333 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
7334
7335 test_helper.advance_by(zx::MonotonicDuration::from_hours(6), test_fut.as_mut());
7336
7337 let info = DisconnectInfo {
7339 connected_duration: zx::MonotonicDuration::from_hours(6),
7340 disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
7341 reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
7342 mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
7343 }),
7344 ap_state,
7345 ..fake_disconnect_info()
7346 };
7347 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7348 track_subsequent_downtime: true,
7349 info: Some(info),
7350 });
7351 test_helper.drain_cobalt_events(&mut test_fut);
7352
7353 let connected_duration_before_disconnect =
7356 test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_DISCONNECT_METRIC_ID);
7357 assert_eq!(connected_duration_before_disconnect.len(), 1);
7358 assert_eq!(
7359 connected_duration_before_disconnect[0].payload,
7360 MetricEventPayload::IntegerValue(360)
7361 );
7362
7363 let connected_duration_before_non_roam_disconnect = test_helper
7364 .get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_NON_ROAM_DISCONNECT_METRIC_ID);
7365 assert!(connected_duration_before_non_roam_disconnect.is_empty());
7366
7367 let connected_duration_before_roam_attempt = test_helper.get_logged_metrics(
7371 metrics::POLICY_ROAM_CONNECTED_DURATION_BEFORE_ROAM_ATTEMPT_METRIC_ID,
7372 );
7373 assert!(connected_duration_before_roam_attempt.is_empty());
7374
7375 let network_disconnect_counts =
7378 test_helper.get_logged_metrics(metrics::NETWORK_DISCONNECT_COUNTS_METRIC_ID);
7379 assert_eq!(network_disconnect_counts.len(), 1);
7380 assert_eq!(network_disconnect_counts[0].payload, MetricEventPayload::Count(1));
7381
7382 let non_roam_disconnect_counts =
7383 test_helper.get_logged_metrics(metrics::NON_ROAM_DISCONNECT_COUNTS_METRIC_ID);
7384 assert!(non_roam_disconnect_counts.is_empty());
7385
7386 let roam_disconnect_counts =
7388 test_helper.get_logged_metrics(metrics::POLICY_ROAM_DISCONNECT_COUNT_METRIC_ID);
7389 assert!(roam_disconnect_counts.is_empty());
7390 }
7391
7392 #[fuchsia::test]
7393 fn test_log_user_disconnect_cobalt_metrics() {
7394 let (mut test_helper, mut test_fut) = setup_test();
7395 test_helper.advance_by(zx::MonotonicDuration::from_hours(3), test_fut.as_mut());
7396 test_helper.send_connected_event(random_bss_description!(Wpa2));
7397 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
7398
7399 const DUR_MIN: i64 = 250;
7400 test_helper.advance_by(zx::MonotonicDuration::from_minutes(DUR_MIN), test_fut.as_mut());
7401
7402 let info = DisconnectInfo {
7404 connected_duration: zx::MonotonicDuration::from_minutes(DUR_MIN),
7405 disconnect_source: fidl_sme::DisconnectSource::User(
7406 fidl_sme::UserDisconnectReason::FidlConnectRequest,
7407 ),
7408 ..fake_disconnect_info()
7409 };
7410 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7411 track_subsequent_downtime: true,
7412 info: Some(info),
7413 });
7414 test_helper.drain_cobalt_events(&mut test_fut);
7415
7416 let roam_connected_duration = test_helper.get_logged_metrics(
7418 metrics::POLICY_ROAM_CONNECTED_DURATION_BEFORE_ROAM_ATTEMPT_METRIC_ID,
7419 );
7420 assert_eq!(roam_connected_duration.len(), 0);
7421
7422 let non_roam_connected_duration = test_helper
7424 .get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_NON_ROAM_DISCONNECT_METRIC_ID);
7425 assert_eq!(non_roam_connected_duration.len(), 1);
7426
7427 let roam_disconnect_counts =
7428 test_helper.get_logged_metrics(metrics::POLICY_ROAM_DISCONNECT_COUNT_METRIC_ID);
7429 assert!(roam_disconnect_counts.is_empty());
7430
7431 let non_roam_disconnect_counts =
7432 test_helper.get_logged_metrics(metrics::NON_ROAM_DISCONNECT_COUNTS_METRIC_ID);
7433 assert!(!non_roam_disconnect_counts.is_empty());
7434
7435 let total_connected_duration =
7437 test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_BEFORE_DISCONNECT_METRIC_ID);
7438 assert_eq!(total_connected_duration.len(), 1);
7439 assert_eq!(total_connected_duration[0].payload, MetricEventPayload::IntegerValue(DUR_MIN));
7440
7441 let total_disconnect_counts =
7442 test_helper.get_logged_metrics(metrics::NETWORK_DISCONNECT_COUNTS_METRIC_ID);
7443 assert_eq!(total_disconnect_counts.len(), 1);
7444 assert_eq!(total_disconnect_counts[0].payload, MetricEventPayload::Count(1));
7445 }
7446
7447 #[fuchsia::test]
7448 fn test_log_saved_networks_count() {
7449 let (mut test_helper, mut test_fut) = setup_test();
7450
7451 let event = TelemetryEvent::SavedNetworkCount {
7452 saved_network_count: 4,
7453 config_count_per_saved_network: vec![1, 1],
7454 };
7455 test_helper.telemetry_sender.send(event);
7456 test_helper.drain_cobalt_events(&mut test_fut);
7457
7458 let saved_networks_count =
7459 test_helper.get_logged_metrics(metrics::SAVED_NETWORKS_MIGRATED_METRIC_ID);
7460 assert_eq!(saved_networks_count.len(), 1);
7461 assert_eq!(
7462 saved_networks_count[0].event_codes,
7463 vec![metrics::SavedNetworksMigratedMetricDimensionSavedNetworks::TwoToFour as u32]
7464 );
7465
7466 let config_count = test_helper
7467 .get_logged_metrics(metrics::SAVED_CONFIGURATIONS_FOR_SAVED_NETWORK_MIGRATED_METRIC_ID);
7468 assert_eq!(config_count.len(), 2);
7469 assert_eq!(
7470 config_count[0].event_codes,
7471 vec![metrics::SavedConfigurationsForSavedNetworkMigratedMetricDimensionSavedConfigurations::One as u32]
7472 );
7473 assert_eq!(
7474 config_count[1].event_codes,
7475 vec![metrics::SavedConfigurationsForSavedNetworkMigratedMetricDimensionSavedConfigurations::One as u32]
7476 );
7477 }
7478
7479 #[fuchsia::test]
7480 fn test_log_network_selection_scan_interval() {
7481 let (mut test_helper, mut test_fut) = setup_test();
7482
7483 let duration = zx::MonotonicDuration::from_seconds(rand::random_range(0..100));
7484
7485 let event = TelemetryEvent::NetworkSelectionScanInterval { time_since_last_scan: duration };
7486 test_helper.telemetry_sender.send(event);
7487 test_helper.drain_cobalt_events(&mut test_fut);
7488
7489 let last_scan_age = test_helper
7490 .get_logged_metrics(metrics::LAST_SCAN_AGE_WHEN_SCAN_REQUESTED_MIGRATED_METRIC_ID);
7491 assert_eq!(last_scan_age.len(), 1);
7492 assert_eq!(
7493 last_scan_age[0].payload,
7494 fidl_fuchsia_metrics::MetricEventPayload::IntegerValue(duration.into_micros())
7495 );
7496 }
7497
7498 #[fuchsia::test]
7499 fn test_log_connection_selection_scan_results() {
7500 let (mut test_helper, mut test_fut) = setup_test();
7501
7502 let event = TelemetryEvent::ConnectionSelectionScanResults {
7503 saved_network_count: 4,
7504 saved_network_count_found_by_active_scan: 1,
7505 bss_count_per_saved_network: vec![10, 10],
7506 };
7507 test_helper.telemetry_sender.send(event);
7508 test_helper.drain_cobalt_events(&mut test_fut);
7509
7510 let saved_networks_count =
7511 test_helper.get_logged_metrics(metrics::SCAN_RESULTS_RECEIVED_MIGRATED_METRIC_ID);
7512 assert_eq!(saved_networks_count.len(), 1);
7513 assert_eq!(
7514 saved_networks_count[0].event_codes,
7515 vec![
7516 metrics::ScanResultsReceivedMigratedMetricDimensionSavedNetworksCount::TwoToFour
7517 as u32
7518 ]
7519 );
7520
7521 let active_scanned_network = test_helper.get_logged_metrics(
7522 metrics::SAVED_NETWORK_IN_SCAN_RESULT_WITH_ACTIVE_SCAN_MIGRATED_METRIC_ID,
7523 );
7524 assert_eq!(active_scanned_network.len(), 1);
7525 assert_eq!(
7526 active_scanned_network[0].event_codes,
7527 vec![metrics::SavedNetworkInScanResultWithActiveScanMigratedMetricDimensionActiveScanSsidsObserved::One as u32]
7528 );
7529
7530 let bss_count = test_helper
7531 .get_logged_metrics(metrics::SAVED_NETWORK_IN_SCAN_RESULT_MIGRATED_METRIC_ID);
7532 assert_eq!(bss_count.len(), 2);
7533 assert_eq!(
7534 bss_count[0].event_codes,
7535 vec![
7536 metrics::SavedNetworkInScanResultMigratedMetricDimensionBssCount::FiveToTen as u32
7537 ]
7538 );
7539 assert_eq!(
7540 bss_count[1].event_codes,
7541 vec![
7542 metrics::SavedNetworkInScanResultMigratedMetricDimensionBssCount::FiveToTen as u32
7543 ]
7544 );
7545 }
7546
7547 #[fuchsia::test]
7548 fn test_log_establish_connection_cobalt_metrics() {
7549 let (mut test_helper, mut test_fut) = setup_test();
7550
7551 let primary_channel = 8;
7552 let channel = Channel::new(primary_channel, Cbw::Cbw20);
7553 let ap_state = random_bss_description!(Wpa2,
7554 bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
7555 channel: channel,
7556 rssi_dbm: -50,
7557 snr_db: 25,
7558 )
7559 .into();
7560 let event = TelemetryEvent::ConnectResult {
7561 iface_id: IFACE_ID,
7562 policy_connect_reason: Some(client::types::ConnectReason::FidlConnectRequest),
7563 result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
7564 multiple_bss_candidates: true,
7565 ap_state,
7566 network_is_likely_hidden: true,
7567 };
7568 test_helper.telemetry_sender.send(event);
7569 test_helper.drain_cobalt_events(&mut test_fut);
7570
7571 let policy_connect_reasons =
7572 test_helper.get_logged_metrics(metrics::POLICY_CONNECTION_ATTEMPT_MIGRATED_METRIC_ID);
7573 assert_eq!(policy_connect_reasons.len(), 1);
7574 assert_eq!(
7575 policy_connect_reasons[0].event_codes,
7576 vec![client::types::ConnectReason::FidlConnectRequest as u32]
7577 );
7578 assert_eq!(policy_connect_reasons[0].payload, MetricEventPayload::Count(1));
7579
7580 let breakdowns_by_status_code = test_helper
7581 .get_logged_metrics(metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID);
7582 assert_eq!(breakdowns_by_status_code.len(), 1);
7583 assert_eq!(
7584 breakdowns_by_status_code[0].event_codes,
7585 vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32]
7586 );
7587 assert_eq!(breakdowns_by_status_code[0].payload, MetricEventPayload::Count(1));
7588
7589 let breakdowns_by_user_wait_time = test_helper
7590 .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
7591 assert_eq!(breakdowns_by_user_wait_time.len(), 0);
7594
7595 let breakdowns_by_is_multi_bss = test_helper
7596 .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID);
7597 assert_eq!(breakdowns_by_is_multi_bss.len(), 1);
7598 assert_eq!(
7599 breakdowns_by_is_multi_bss[0].event_codes,
7600 vec![
7601 metrics::SuccessfulConnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::Yes
7602 as u32
7603 ]
7604 );
7605 assert_eq!(breakdowns_by_is_multi_bss[0].payload, MetricEventPayload::Count(1));
7606
7607 let breakdowns_by_security_type = test_helper
7608 .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID);
7609 assert_eq!(breakdowns_by_security_type.len(), 1);
7610 assert_eq!(
7611 breakdowns_by_security_type[0].event_codes,
7612 vec![
7613 metrics::SuccessfulConnectBreakdownBySecurityTypeMetricDimensionSecurityType::Wpa2Personal
7614 as u32
7615 ]
7616 );
7617 assert_eq!(breakdowns_by_security_type[0].payload, MetricEventPayload::Count(1));
7618
7619 let breakdowns_by_channel = test_helper
7620 .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID);
7621 assert_eq!(breakdowns_by_channel.len(), 1);
7622 assert_eq!(breakdowns_by_channel[0].event_codes, vec![primary_channel as u32]);
7623 assert_eq!(breakdowns_by_channel[0].payload, MetricEventPayload::Count(1));
7624
7625 let breakdowns_by_channel_band = test_helper
7626 .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID);
7627 assert_eq!(breakdowns_by_channel_band.len(), 1);
7628 assert_eq!(breakdowns_by_channel_band[0].event_codes, vec![
7629 metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band2Dot4Ghz as u32
7630 ]);
7631 assert_eq!(breakdowns_by_channel_band[0].payload, MetricEventPayload::Count(1));
7632
7633 let fidl_connect_count =
7634 test_helper.get_logged_metrics(metrics::POLICY_CONNECTION_ATTEMPTS_METRIC_ID);
7635 assert_eq!(fidl_connect_count.len(), 1);
7636 assert_eq!(fidl_connect_count[0].payload, MetricEventPayload::Count(1));
7637
7638 let network_is_likely_hidden =
7639 test_helper.get_logged_metrics(metrics::CONNECT_TO_LIKELY_HIDDEN_NETWORK_METRIC_ID);
7640 assert_eq!(network_is_likely_hidden.len(), 1);
7641 assert_eq!(network_is_likely_hidden[0].payload, MetricEventPayload::Count(1));
7642 }
7643
7644 #[fuchsia::test]
7645 fn test_log_connect_attempt_breakdown_by_failed_status_code() {
7646 let (mut test_helper, mut test_fut) = setup_test();
7647
7648 let event = TelemetryEvent::ConnectResult {
7649 iface_id: IFACE_ID,
7650 policy_connect_reason: None,
7651 result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedCapabilitiesMismatch),
7652 multiple_bss_candidates: true,
7653 ap_state: random_bss_description!(Wpa2).into(),
7654 network_is_likely_hidden: true,
7655 };
7656 test_helper.telemetry_sender.send(event);
7657 test_helper.drain_cobalt_events(&mut test_fut);
7658
7659 let breakdowns_by_status_code = test_helper
7660 .get_logged_metrics(metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID);
7661 assert_eq!(breakdowns_by_status_code.len(), 1);
7662 assert_eq!(
7663 breakdowns_by_status_code[0].event_codes,
7664 vec![fidl_ieee80211::StatusCode::RefusedCapabilitiesMismatch.into_primitive() as u32]
7665 );
7666 }
7667
7668 #[fuchsia::test]
7669 fn test_log_establish_connection_status_code_cobalt_metrics_normal_device() {
7670 let (mut test_helper, mut test_fut) = setup_test();
7671 for _ in 0..3 {
7672 let event = TelemetryEvent::ConnectResult {
7673 iface_id: IFACE_ID,
7674 policy_connect_reason: Some(
7675 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
7676 ),
7677 result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
7678 multiple_bss_candidates: true,
7679 ap_state: random_bss_description!(Wpa1).into(),
7680 network_is_likely_hidden: true,
7681 };
7682 test_helper.telemetry_sender.send(event);
7683 }
7684 test_helper.send_connected_event(random_bss_description!(Wpa2));
7685 test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
7686
7687 let status_codes = test_helper.get_logged_metrics(
7688 metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
7689 );
7690 assert_eq!(status_codes.len(), 2);
7691 assert_eq_cobalt_events(
7692 status_codes,
7693 vec![
7694 MetricEvent {
7695 metric_id:
7696 metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
7697 event_codes: vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32],
7698 payload: MetricEventPayload::Count(1),
7699 },
7700 MetricEvent {
7701 metric_id:
7702 metrics::CONNECT_ATTEMPT_ON_NORMAL_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
7703 event_codes: vec![
7704 fidl_ieee80211::StatusCode::RefusedReasonUnspecified.into_primitive()
7705 as u32,
7706 ],
7707 payload: MetricEventPayload::Count(3),
7708 },
7709 ],
7710 );
7711 }
7712
7713 #[fuchsia::test]
7714 fn test_log_establish_connection_status_code_cobalt_metrics_bad_device() {
7715 let (mut test_helper, mut test_fut) = setup_test();
7716 for _ in 0..10 {
7717 let event = TelemetryEvent::ConnectResult {
7718 iface_id: IFACE_ID,
7719 policy_connect_reason: Some(
7720 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
7721 ),
7722 result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
7723 multiple_bss_candidates: true,
7724 ap_state: random_bss_description!(Wpa1).into(),
7725 network_is_likely_hidden: true,
7726 };
7727 test_helper.telemetry_sender.send(event);
7728 }
7729 test_helper.send_connected_event(random_bss_description!(Wpa2));
7730 test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
7731
7732 let status_codes = test_helper.get_logged_metrics(
7733 metrics::CONNECT_ATTEMPT_ON_BAD_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
7734 );
7735 assert_eq!(status_codes.len(), 2);
7736 assert_eq_cobalt_events(
7737 status_codes,
7738 vec![
7739 MetricEvent {
7740 metric_id:
7741 metrics::CONNECT_ATTEMPT_ON_BAD_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
7742 event_codes: vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32],
7743 payload: MetricEventPayload::Count(1),
7744 },
7745 MetricEvent {
7746 metric_id:
7747 metrics::CONNECT_ATTEMPT_ON_BAD_DEVICE_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
7748 event_codes: vec![
7749 fidl_ieee80211::StatusCode::RefusedReasonUnspecified.into_primitive()
7750 as u32,
7751 ],
7752 payload: MetricEventPayload::Count(10),
7753 },
7754 ],
7755 );
7756 }
7757
7758 #[fuchsia::test]
7759 fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_no_reset() {
7760 let (mut test_helper, mut test_fut) = setup_test();
7761
7762 test_helper
7763 .telemetry_sender
7764 .send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
7765 test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
7766 test_helper
7767 .telemetry_sender
7768 .send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
7769 test_helper.advance_by(zx::MonotonicDuration::from_seconds(4), test_fut.as_mut());
7770 test_helper.send_connected_event(random_bss_description!(Wpa2));
7771 test_helper.drain_cobalt_events(&mut test_fut);
7772
7773 let breakdowns_by_user_wait_time = test_helper
7774 .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
7775 assert_eq!(breakdowns_by_user_wait_time.len(), 1);
7776 assert_eq!(
7777 breakdowns_by_user_wait_time[0].event_codes,
7778 vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan8Seconds as u32]
7781 );
7782 }
7783
7784 #[fuchsia::test]
7785 fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_with_reset() {
7786 let (mut test_helper, mut test_fut) = setup_test();
7787
7788 test_helper
7789 .telemetry_sender
7790 .send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
7791 test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
7792 test_helper
7793 .telemetry_sender
7794 .send(TelemetryEvent::StartEstablishConnection { reset_start_time: true });
7795 test_helper.advance_by(zx::MonotonicDuration::from_seconds(4), test_fut.as_mut());
7796 test_helper.send_connected_event(random_bss_description!(Wpa2));
7797 test_helper.drain_cobalt_events(&mut test_fut);
7798
7799 let breakdowns_by_user_wait_time = test_helper
7800 .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
7801 assert_eq!(breakdowns_by_user_wait_time.len(), 1);
7802 assert_eq!(
7803 breakdowns_by_user_wait_time[0].event_codes,
7804 vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan5Seconds as u32]
7806 );
7807 }
7808
7809 #[fuchsia::test]
7810 fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_with_clear() {
7811 let (mut test_helper, mut test_fut) = setup_test();
7812
7813 test_helper
7814 .telemetry_sender
7815 .send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
7816 test_helper.advance_by(zx::MonotonicDuration::from_seconds(10), test_fut.as_mut());
7817 test_helper.telemetry_sender.send(TelemetryEvent::ClearEstablishConnectionStartTime);
7818
7819 test_helper.advance_by(zx::MonotonicDuration::from_seconds(30), test_fut.as_mut());
7820
7821 test_helper
7822 .telemetry_sender
7823 .send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
7824 test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
7825 test_helper.send_connected_event(random_bss_description!(Wpa2));
7826 test_helper.drain_cobalt_events(&mut test_fut);
7827
7828 let breakdowns_by_user_wait_time = test_helper
7829 .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
7830 assert_eq!(breakdowns_by_user_wait_time.len(), 1);
7831 assert_eq!(
7832 breakdowns_by_user_wait_time[0].event_codes,
7833 vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan3Seconds as u32]
7835 );
7836 }
7837
7838 #[test_case(
7839 (true, random_bss_description!(Wpa2)),
7840 (false, random_bss_description!(Wpa2)),
7841 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID,
7842 metrics::SuccessfulConnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::Yes as u32,
7843 metrics::SuccessfulConnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::No as u32;
7844 "breakdown_by_is_multi_bss"
7845 )]
7846 #[test_case(
7847 (false, random_bss_description!(Wpa1)),
7848 (false, random_bss_description!(Wpa2)),
7849 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID,
7850 metrics::SuccessfulConnectBreakdownBySecurityTypeMetricDimensionSecurityType::Wpa1 as u32,
7851 metrics::SuccessfulConnectBreakdownBySecurityTypeMetricDimensionSecurityType::Wpa2Personal as u32;
7852 "breakdown_by_security_type"
7853 )]
7854 #[test_case(
7855 (false, random_bss_description!(Wpa2, channel: Channel::new(6, Cbw::Cbw20))),
7856 (false, random_bss_description!(Wpa2, channel: Channel::new(157, Cbw::Cbw40))),
7857 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
7858 6,
7859 157;
7860 "breakdown_by_primary_channel"
7861 )]
7862 #[test_case(
7863 (false, random_bss_description!(Wpa2, channel: Channel::new(6, Cbw::Cbw20))),
7864 (false, random_bss_description!(Wpa2, channel: Channel::new(157, Cbw::Cbw40))),
7865 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
7866 metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band2Dot4Ghz as u32,
7867 metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band5Ghz as u32;
7868 "breakdown_by_channel_band"
7869 )]
7870 #[test_case(
7871 (false, random_bss_description!(Wpa2, rssi_dbm: -79)),
7872 (false, random_bss_description!(Wpa2, rssi_dbm: -40)),
7873 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_RSSI_BUCKET_METRIC_ID,
7874 metrics::ConnectivityWlanMetricDimensionRssiBucket::From79To77 as u32,
7875 metrics::ConnectivityWlanMetricDimensionRssiBucket::From50To35 as u32;
7876 "breakdown_by_rssi_bucket"
7877 )]
7878 #[test_case(
7879 (false, random_bss_description!(Wpa2, snr_db: 11)),
7880 (false, random_bss_description!(Wpa2, snr_db: 35)),
7881 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SNR_BUCKET_METRIC_ID,
7882 metrics::ConnectivityWlanMetricDimensionSnrBucket::From11To15 as u32,
7883 metrics::ConnectivityWlanMetricDimensionSnrBucket::From26To40 as u32;
7884 "breakdown_by_snr_bucket"
7885 )]
7886 #[fuchsia::test(add_test_attr = false)]
7887 fn test_log_daily_connect_success_rate_breakdown_cobalt_metrics(
7888 first_connect_result_params: (bool, BssDescription),
7889 second_connect_result_params: (bool, BssDescription),
7890 metric_id: u32,
7891 event_code_1: u32,
7892 event_code_2: u32,
7893 ) {
7894 let (mut test_helper, mut test_fut) = setup_test();
7895
7896 for i in 0..3 {
7897 let code = if i == 0 {
7898 fidl_ieee80211::StatusCode::Success
7899 } else {
7900 fidl_ieee80211::StatusCode::RefusedReasonUnspecified
7901 };
7902 let event = TelemetryEvent::ConnectResult {
7903 iface_id: IFACE_ID,
7904 policy_connect_reason: Some(
7905 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
7906 ),
7907 result: fake_connect_result(code),
7908 multiple_bss_candidates: first_connect_result_params.0,
7909 ap_state: first_connect_result_params.1.clone().into(),
7910 network_is_likely_hidden: true,
7911 };
7912 test_helper.telemetry_sender.send(event);
7913 }
7914 for i in 0..2 {
7915 let code = if i == 0 {
7916 fidl_ieee80211::StatusCode::Success
7917 } else {
7918 fidl_ieee80211::StatusCode::RefusedReasonUnspecified
7919 };
7920 let event = TelemetryEvent::ConnectResult {
7921 iface_id: IFACE_ID,
7922 policy_connect_reason: Some(
7923 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
7924 ),
7925 result: fake_connect_result(code),
7926 multiple_bss_candidates: second_connect_result_params.0,
7927 ap_state: second_connect_result_params.1.clone().into(),
7928 network_is_likely_hidden: true,
7929 };
7930 test_helper.telemetry_sender.send(event);
7931 }
7932
7933 test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
7934
7935 let metrics = test_helper.get_logged_metrics(metric_id);
7936 assert_eq!(metrics.len(), 2);
7937 assert_eq_cobalt_events(
7938 metrics,
7939 vec![
7940 MetricEvent {
7941 metric_id,
7942 event_codes: vec![event_code_1],
7943 payload: MetricEventPayload::IntegerValue(3333), },
7945 MetricEvent {
7946 metric_id,
7947 event_codes: vec![event_code_2],
7948 payload: MetricEventPayload::IntegerValue(5000), },
7950 ],
7951 );
7952 }
7953
7954 #[fuchsia::test]
7955 fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_while_connected() {
7956 let (mut test_helper, mut test_fut) = setup_test();
7957 test_helper.send_connected_event(random_bss_description!(Wpa2));
7958 test_helper.drain_cobalt_events(&mut test_fut);
7959 test_helper.cobalt_events.clear();
7960
7961 test_helper
7962 .telemetry_sender
7963 .send(TelemetryEvent::StartEstablishConnection { reset_start_time: true });
7964 test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
7965 let info = fake_disconnect_info();
7966 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7967 track_subsequent_downtime: false,
7968 info: Some(info),
7969 });
7970 test_helper.advance_by(zx::MonotonicDuration::from_seconds(4), test_fut.as_mut());
7971 test_helper.send_connected_event(random_bss_description!(Wpa2));
7972 test_helper.drain_cobalt_events(&mut test_fut);
7973
7974 let breakdowns_by_user_wait_time = test_helper
7975 .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
7976 assert_eq!(breakdowns_by_user_wait_time.len(), 1);
7977 assert_eq!(
7978 breakdowns_by_user_wait_time[0].event_codes,
7979 vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan8Seconds as u32]
7982 );
7983 }
7984
7985 #[fuchsia::test]
7986 fn test_log_establish_connection_cobalt_metrics_user_wait_time_tracked_with_clear_while_connected()
7987 {
7988 let (mut test_helper, mut test_fut) = setup_test();
7989 test_helper.send_connected_event(random_bss_description!(Wpa2));
7990 test_helper.drain_cobalt_events(&mut test_fut);
7991 test_helper.cobalt_events.clear();
7992
7993 test_helper
7994 .telemetry_sender
7995 .send(TelemetryEvent::StartEstablishConnection { reset_start_time: true });
7996 test_helper.telemetry_sender.send(TelemetryEvent::ClearEstablishConnectionStartTime);
7997 let info = fake_disconnect_info();
7998 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
7999 track_subsequent_downtime: false,
8000 info: Some(info),
8001 });
8002 test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
8003 test_helper
8004 .telemetry_sender
8005 .send(TelemetryEvent::StartEstablishConnection { reset_start_time: false });
8006 test_helper.advance_by(zx::MonotonicDuration::from_seconds(4), test_fut.as_mut());
8007 test_helper.send_connected_event(random_bss_description!(Wpa2));
8008 test_helper.drain_cobalt_events(&mut test_fut);
8009
8010 let breakdowns_by_user_wait_time = test_helper
8011 .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
8012 assert_eq!(breakdowns_by_user_wait_time.len(), 1);
8013 assert_eq!(
8014 breakdowns_by_user_wait_time[0].event_codes,
8015 vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan5Seconds as u32]
8017 );
8018 }
8019
8020 #[fuchsia::test]
8021 fn test_log_establish_connection_cobalt_metrics_user_wait_time_logged_for_sme_reconnecting() {
8022 let (mut test_helper, mut test_fut) = setup_test();
8023 test_helper.send_connected_event(random_bss_description!(Wpa2));
8024 test_helper.drain_cobalt_events(&mut test_fut);
8025 test_helper.cobalt_events.clear();
8026
8027 let info = DisconnectInfo { is_sme_reconnecting: true, ..fake_disconnect_info() };
8028 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
8029 track_subsequent_downtime: false,
8030 info: Some(info),
8031 });
8032 test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
8033 test_helper.send_connected_event(random_bss_description!(Wpa2));
8034 test_helper.drain_cobalt_events(&mut test_fut);
8035
8036 let breakdowns_by_user_wait_time = test_helper
8037 .get_logged_metrics(metrics::SUCCESSFUL_CONNECT_BREAKDOWN_BY_USER_WAIT_TIME_METRIC_ID);
8038 assert_eq!(breakdowns_by_user_wait_time.len(), 1);
8039 assert_eq!(
8040 breakdowns_by_user_wait_time[0].event_codes,
8041 vec![metrics::ConnectivityWlanMetricDimensionWaitTime::LessThan3Seconds as u32]
8042 );
8043 }
8044
8045 #[fuchsia::test]
8046 fn test_log_downtime_cobalt_metrics() {
8047 let (mut test_helper, mut test_fut) = setup_test();
8048 test_helper.send_connected_event(random_bss_description!(Wpa2));
8049 test_helper.drain_cobalt_events(&mut test_fut);
8050
8051 let info = DisconnectInfo {
8052 disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
8053 reason_code: fidl_ieee80211::ReasonCode::LeavingNetworkDeauth,
8054 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
8055 }),
8056 ..fake_disconnect_info()
8057 };
8058 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
8059 track_subsequent_downtime: true,
8060 info: Some(info),
8061 });
8062 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8063
8064 test_helper.advance_by(zx::MonotonicDuration::from_minutes(42), test_fut.as_mut());
8065 test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
8067 network_selection_type: NetworkSelectionType::Undirected,
8068 num_candidates: Ok(0),
8069 selected_count: 0,
8070 });
8071 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8072
8073 test_helper.advance_by(zx::MonotonicDuration::from_minutes(5), test_fut.as_mut());
8074 test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
8076 network_selection_type: NetworkSelectionType::Undirected,
8077 num_candidates: Ok(5),
8078 selected_count: 1,
8079 });
8080 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8081
8082 test_helper.advance_by(zx::MonotonicDuration::from_minutes(7), test_fut.as_mut());
8083 test_helper.send_connected_event(random_bss_description!(Wpa2));
8085 test_helper.drain_cobalt_events(&mut test_fut);
8086
8087 let breakdowns_by_reason = test_helper
8088 .get_logged_metrics(metrics::DOWNTIME_BREAKDOWN_BY_DISCONNECT_REASON_METRIC_ID);
8089 assert_eq!(breakdowns_by_reason.len(), 1);
8090 assert_eq!(
8091 breakdowns_by_reason[0].event_codes,
8092 vec![3u32, metrics::ConnectivityWlanMetricDimensionDisconnectSource::Mlme as u32,]
8093 );
8094 assert_eq!(
8095 breakdowns_by_reason[0].payload,
8096 MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_minutes(49).into_micros())
8097 );
8098 }
8099
8100 #[fuchsia::test]
8101 fn test_log_reconnect_cobalt_metrics() {
8102 let (mut test_helper, mut test_fut) = setup_test();
8103 test_helper.send_connected_event(random_bss_description!(Wpa2));
8104 test_helper.drain_cobalt_events(&mut test_fut);
8105
8106 let info = DisconnectInfo {
8108 disconnect_source: fidl_sme::DisconnectSource::User(
8109 fidl_sme::UserDisconnectReason::ProactiveNetworkSwitch,
8110 ),
8111 ..fake_disconnect_info()
8112 };
8113 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
8114 track_subsequent_downtime: true,
8115 info: Some(info),
8116 });
8117 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8118
8119 test_helper.advance_by(zx::MonotonicDuration::from_seconds(3), test_fut.as_mut());
8120 test_helper.send_connected_event(random_bss_description!(Wpa2));
8122 test_helper.drain_cobalt_events(&mut test_fut);
8123
8124 let metrics =
8126 test_helper.get_logged_metrics(metrics::NON_ROAM_RECONNECT_DURATION_METRIC_ID);
8127 assert_eq!(metrics.len(), 1);
8128 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(3_000_000));
8129 assert!(
8130 test_helper
8131 .get_logged_metrics(metrics::POLICY_ROAM_RECONNECT_DURATION_METRIC_ID)
8132 .is_empty()
8133 );
8134
8135 test_helper.clear_cobalt_events();
8137 let info = DisconnectInfo {
8138 disconnect_source: fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
8139 reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
8140 mlme_event_name: fidl_sme::DisconnectMlmeEventName::RoamResultIndication,
8141 }),
8142 ..fake_disconnect_info()
8143 };
8144 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
8145 track_subsequent_downtime: true,
8146 info: Some(info),
8147 });
8148 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8149 test_helper.advance_by(zx::MonotonicDuration::from_seconds(1), test_fut.as_mut());
8150 test_helper.send_connected_event(random_bss_description!(Wpa2));
8152 test_helper.drain_cobalt_events(&mut test_fut);
8153
8154 assert!(
8157 test_helper
8158 .get_logged_metrics(metrics::NON_ROAM_RECONNECT_DURATION_METRIC_ID)
8159 .is_empty()
8160 );
8161 assert!(
8165 test_helper
8166 .get_logged_metrics(metrics::POLICY_ROAM_RECONNECT_DURATION_METRIC_ID)
8167 .is_empty()
8168 );
8169 }
8170
8171 #[fuchsia::test]
8172 fn test_log_device_connected_cobalt_metrics() {
8173 let (mut test_helper, mut test_fut) = setup_test();
8174
8175 let wmm_info = vec![0x80]; #[rustfmt::skip]
8177 let rm_enabled_capabilities = vec![
8178 0x03, 0x00, 0x00, 0x00, 0x00,
8180 ];
8181 #[rustfmt::skip]
8182 let ext_capabilities = vec![
8183 0x04, 0x00,
8184 0x08, 0x00, 0x00, 0x00, 0x00, 0x40
8186 ];
8187 let bss_description = random_bss_description!(Wpa2,
8188 channel: Channel::new(157, Cbw::Cbw40),
8189 ies_overrides: IesOverrides::new()
8190 .remove(IeType::WMM_PARAM)
8191 .set(IeType::WMM_INFO, wmm_info)
8192 .set(IeType::RM_ENABLED_CAPABILITIES, rm_enabled_capabilities)
8193 .set(IeType::MOBILITY_DOMAIN, vec![0x00; 3])
8194 .set(IeType::EXT_CAPABILITIES, ext_capabilities),
8195 bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
8196 );
8197 test_helper.send_connected_event(bss_description);
8198 test_helper.drain_cobalt_events(&mut test_fut);
8199
8200 let num_devices_connected =
8201 test_helper.get_logged_metrics(metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID);
8202 assert_eq!(num_devices_connected.len(), 1);
8203 assert_eq!(num_devices_connected[0].payload, MetricEventPayload::Count(1));
8204
8205 let connected_security_type =
8206 test_helper.get_logged_metrics(metrics::CONNECTED_NETWORK_SECURITY_TYPE_METRIC_ID);
8207 assert_eq!(connected_security_type.len(), 1);
8208 assert_eq!(
8209 connected_security_type[0].event_codes,
8210 vec![
8211 metrics::ConnectedNetworkSecurityTypeMetricDimensionSecurityType::Wpa2Personal
8212 as u32
8213 ]
8214 );
8215 assert_eq!(connected_security_type[0].payload, MetricEventPayload::Count(1));
8216
8217 let connected_apsd = test_helper
8218 .get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_APSD_METRIC_ID);
8219 assert_eq!(connected_apsd.len(), 1);
8220 assert_eq!(connected_apsd[0].payload, MetricEventPayload::Count(1));
8221
8222 let connected_link_measurement = test_helper.get_logged_metrics(
8223 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_LINK_MEASUREMENT_METRIC_ID,
8224 );
8225 assert_eq!(connected_link_measurement.len(), 1);
8226 assert_eq!(connected_link_measurement[0].payload, MetricEventPayload::Count(1));
8227
8228 let connected_neighbor_report = test_helper.get_logged_metrics(
8229 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_NEIGHBOR_REPORT_METRIC_ID,
8230 );
8231 assert_eq!(connected_neighbor_report.len(), 1);
8232 assert_eq!(connected_neighbor_report[0].payload, MetricEventPayload::Count(1));
8233
8234 let connected_ft = test_helper
8235 .get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_FT_METRIC_ID);
8236 assert_eq!(connected_ft.len(), 1);
8237 assert_eq!(connected_ft[0].payload, MetricEventPayload::Count(1));
8238
8239 let connected_bss_transition_mgmt = test_helper.get_logged_metrics(
8240 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_BSS_TRANSITION_MANAGEMENT_METRIC_ID,
8241 );
8242 assert_eq!(connected_bss_transition_mgmt.len(), 1);
8243 assert_eq!(connected_bss_transition_mgmt[0].payload, MetricEventPayload::Count(1));
8244
8245 let breakdown_by_is_multi_bss = test_helper.get_logged_metrics(
8246 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID,
8247 );
8248 assert_eq!(breakdown_by_is_multi_bss.len(), 1);
8249 assert_eq!(
8250 breakdown_by_is_multi_bss[0].event_codes,
8251 vec![
8252 metrics::SuccessfulConnectBreakdownByIsMultiBssMetricDimensionIsMultiBss::Yes
8253 as u32
8254 ]
8255 );
8256 assert_eq!(breakdown_by_is_multi_bss[0].payload, MetricEventPayload::Count(1));
8257
8258 let breakdown_by_primary_channel = test_helper.get_logged_metrics(
8259 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
8260 );
8261 assert_eq!(breakdown_by_primary_channel.len(), 1);
8262 assert_eq!(breakdown_by_primary_channel[0].event_codes, vec![157]);
8263 assert_eq!(breakdown_by_primary_channel[0].payload, MetricEventPayload::Count(1));
8264
8265 let breakdown_by_channel_band = test_helper.get_logged_metrics(
8266 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
8267 );
8268 assert_eq!(breakdown_by_channel_band.len(), 1);
8269 assert_eq!(
8270 breakdown_by_channel_band[0].event_codes,
8271 vec![
8272 metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band5Ghz
8273 as u32
8274 ]
8275 );
8276 assert_eq!(breakdown_by_channel_band[0].payload, MetricEventPayload::Count(1));
8277
8278 let ap_oui_connected =
8279 test_helper.get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID);
8280 assert_eq!(ap_oui_connected.len(), 1);
8281 assert_eq!(
8282 ap_oui_connected[0].payload,
8283 MetricEventPayload::StringValue("00F620".to_string())
8284 );
8285
8286 let network_is_likely_hidden =
8287 test_helper.get_logged_metrics(metrics::CONNECT_TO_LIKELY_HIDDEN_NETWORK_METRIC_ID);
8288 assert_eq!(network_is_likely_hidden.len(), 1);
8289 assert_eq!(network_is_likely_hidden[0].payload, MetricEventPayload::Count(1));
8290 }
8291
8292 #[fuchsia::test]
8293 fn test_log_device_connected_cobalt_metrics_ap_features_not_supported() {
8294 let (mut test_helper, mut test_fut) = setup_test();
8295
8296 let bss_description = random_bss_description!(Wpa2,
8297 ies_overrides: IesOverrides::new()
8298 .remove(IeType::WMM_PARAM)
8299 .remove(IeType::WMM_INFO)
8300 .remove(IeType::RM_ENABLED_CAPABILITIES)
8301 .remove(IeType::MOBILITY_DOMAIN)
8302 .remove(IeType::EXT_CAPABILITIES)
8303 );
8304 test_helper.send_connected_event(bss_description);
8305 test_helper.drain_cobalt_events(&mut test_fut);
8306
8307 let connected_apsd = test_helper
8308 .get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_APSD_METRIC_ID);
8309 assert_eq!(connected_apsd.len(), 0);
8310
8311 let connected_link_measurement = test_helper.get_logged_metrics(
8312 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_LINK_MEASUREMENT_METRIC_ID,
8313 );
8314 assert_eq!(connected_link_measurement.len(), 0);
8315
8316 let connected_neighbor_report = test_helper.get_logged_metrics(
8317 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_NEIGHBOR_REPORT_METRIC_ID,
8318 );
8319 assert_eq!(connected_neighbor_report.len(), 0);
8320
8321 let connected_ft = test_helper
8322 .get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_FT_METRIC_ID);
8323 assert_eq!(connected_ft.len(), 0);
8324
8325 let connected_bss_transition_mgmt = test_helper.get_logged_metrics(
8326 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_BSS_TRANSITION_MANAGEMENT_METRIC_ID,
8327 );
8328 assert_eq!(connected_bss_transition_mgmt.len(), 0);
8329 }
8330
8331 #[test_case(metrics::CONNECT_TO_LIKELY_HIDDEN_NETWORK_METRIC_ID, None; "connect_to_likely_hidden_network")]
8332 #[test_case(metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID, None; "number_of_connected_devices")]
8333 #[test_case(metrics::CONNECTED_NETWORK_SECURITY_TYPE_METRIC_ID, None; "breakdown_by_security_type")]
8334 #[test_case(metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_IS_MULTI_BSS_METRIC_ID, None; "breakdown_by_is_multi_bss")]
8335 #[test_case(metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID, None; "breakdown_by_primary_channel")]
8336 #[test_case(metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID, None; "breakdown_by_channel_band")]
8337 #[test_case(metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID,
8338 Some(vec![
8339 MetricEvent {
8340 metric_id: metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID,
8341 event_codes: vec![],
8342 payload: MetricEventPayload::StringValue("00F620".to_string()),
8343 },
8344 ]); "number_of_devices_connected_to_specific_oui")]
8345 #[fuchsia::test(add_test_attr = false)]
8346 fn test_log_device_connected_cobalt_metrics_on_disconnect_and_periodically(
8347 metric_id: u32,
8348 payload: Option<Vec<MetricEvent>>,
8349 ) {
8350 let (mut test_helper, mut test_fut) = setup_test();
8351
8352 let bss_description = random_bss_description!(Wpa2,
8353 bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
8354 );
8355 test_helper.send_connected_event(bss_description);
8356 test_helper.drain_cobalt_events(&mut test_fut);
8357 test_helper.cobalt_events.clear();
8358
8359 test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
8360
8361 let metrics = test_helper.get_logged_metrics(metric_id);
8364 assert!(!metrics.is_empty());
8365
8366 if let Some(payload) = payload {
8367 assert_eq_cobalt_events(metrics, payload)
8368 }
8369
8370 test_helper.cobalt_events.clear();
8371
8372 let info = fake_disconnect_info();
8373 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
8374 track_subsequent_downtime: false,
8375 info: Some(info),
8376 });
8377 test_helper.drain_cobalt_events(&mut test_fut);
8378
8379 let metrics = test_helper.get_logged_metrics(metric_id);
8381 assert_eq!(metrics.len(), 1);
8382 }
8383
8384 #[fuchsia::test]
8385 fn test_log_device_connected_cobalt_metrics_on_channel_switched() {
8386 let (mut test_helper, mut test_fut) = setup_test();
8387 let bss_description = random_bss_description!(Wpa2,
8388 channel: Channel::new(4, Cbw::Cbw20),
8389 );
8390 test_helper.send_connected_event(bss_description);
8391 test_helper.drain_cobalt_events(&mut test_fut);
8392
8393 let breakdown_by_primary_channel = test_helper.get_logged_metrics(
8394 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
8395 );
8396 assert_eq!(breakdown_by_primary_channel.len(), 1);
8397 assert_eq!(breakdown_by_primary_channel[0].event_codes, vec![4]);
8398 assert_eq!(breakdown_by_primary_channel[0].payload, MetricEventPayload::Count(1));
8399
8400 let breakdown_by_channel_band = test_helper.get_logged_metrics(
8401 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
8402 );
8403 assert_eq!(breakdown_by_channel_band.len(), 1);
8404 assert_eq!(
8405 breakdown_by_channel_band[0].event_codes,
8406 vec![
8407 metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band2Dot4Ghz
8408 as u32
8409 ]
8410 );
8411 assert_eq!(breakdown_by_channel_band[0].payload, MetricEventPayload::Count(1));
8412
8413 test_helper.cobalt_events.clear();
8415
8416 test_helper.telemetry_sender.send(TelemetryEvent::OnChannelSwitched {
8417 info: fidl_internal::ChannelSwitchInfo { new_channel: 157 },
8418 });
8419 test_helper.drain_cobalt_events(&mut test_fut);
8420
8421 let breakdown_by_primary_channel = test_helper.get_logged_metrics(
8424 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
8425 );
8426 assert_eq!(breakdown_by_primary_channel.len(), 1);
8427 assert_eq!(breakdown_by_primary_channel[0].event_codes, vec![157]);
8428 assert_eq!(breakdown_by_primary_channel[0].payload, MetricEventPayload::Count(1));
8429
8430 let breakdown_by_channel_band = test_helper.get_logged_metrics(
8431 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
8432 );
8433 assert_eq!(breakdown_by_channel_band.len(), 1);
8434 assert_eq!(
8435 breakdown_by_channel_band[0].event_codes,
8436 vec![
8437 metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band5Ghz
8438 as u32
8439 ]
8440 );
8441 assert_eq!(breakdown_by_channel_band[0].payload, MetricEventPayload::Count(1));
8442 }
8443
8444 #[fuchsia::test]
8445 fn test_active_scan_requested_metric() {
8446 let (mut test_helper, mut test_fut) = setup_test();
8447
8448 test_helper
8449 .telemetry_sender
8450 .send(TelemetryEvent::ActiveScanRequested { num_ssids_requested: 4 });
8451
8452 test_helper.drain_cobalt_events(&mut test_fut);
8453 let metrics = test_helper.get_logged_metrics(
8454 metrics::ACTIVE_SCAN_REQUESTED_FOR_NETWORK_SELECTION_MIGRATED_METRIC_ID,
8455 );
8456 assert_eq!(metrics.len(), 1);
8457 assert_eq!(metrics[0].event_codes, vec![metrics::ActiveScanRequestedForNetworkSelectionMigratedMetricDimensionActiveScanSsidsRequested::TwoToFour as u32]);
8458 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8459 }
8460
8461 #[fuchsia::test]
8462 fn test_log_device_performed_roaming_scan() {
8463 let (mut test_helper, mut test_fut) = setup_test();
8464
8465 test_helper.telemetry_sender.send(TelemetryEvent::PolicyRoamScan {
8467 reasons: vec![RoamReason::RssiBelowThreshold, RoamReason::SnrBelowThreshold],
8468 });
8469 test_helper.drain_cobalt_events(&mut test_fut);
8470
8471 let metrics = test_helper.get_logged_metrics(metrics::POLICY_ROAM_SCAN_COUNT_METRIC_ID);
8473 assert_eq!(metrics.len(), 1);
8474 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8475
8476 let metrics = test_helper
8478 .get_logged_metrics(metrics::POLICY_ROAM_SCAN_COUNT_BY_ROAM_REASON_METRIC_ID);
8479 assert_eq!(metrics.len(), 2);
8480 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8481 assert_eq!(
8482 metrics[0].event_codes,
8483 vec![convert::convert_roam_reason_dimension(RoamReason::RssiBelowThreshold) as u32]
8484 );
8485 assert_eq!(metrics[1].payload, MetricEventPayload::Count(1));
8486 assert_eq!(
8487 metrics[1].event_codes,
8488 vec![convert::convert_roam_reason_dimension(RoamReason::SnrBelowThreshold) as u32]
8489 );
8490 }
8491
8492 #[fuchsia::test]
8493 fn test_log_policy_roam_attempt() {
8494 let (mut test_helper, mut test_fut) = setup_test();
8495
8496 let candidate = generate_random_scanned_candidate();
8498 test_helper.telemetry_sender.send(TelemetryEvent::PolicyRoamAttempt {
8499 request: PolicyRoamRequest {
8500 candidate,
8501 reasons: vec![RoamReason::RssiBelowThreshold, RoamReason::SnrBelowThreshold],
8502 },
8503 connected_duration: zx::Duration::from_hours(1),
8504 });
8505 test_helper.drain_cobalt_events(&mut test_fut);
8506
8507 let metrics = test_helper.get_logged_metrics(metrics::POLICY_ROAM_ATTEMPT_COUNT_METRIC_ID);
8508 assert_eq!(metrics.len(), 1);
8509 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8510
8511 let metrics = test_helper
8513 .get_logged_metrics(metrics::POLICY_ROAM_ATTEMPT_COUNT_BY_ROAM_REASON_METRIC_ID);
8514 assert_eq!(metrics.len(), 2);
8515 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8516 assert_eq!(
8517 metrics[0].event_codes,
8518 vec![convert::convert_roam_reason_dimension(RoamReason::RssiBelowThreshold) as u32]
8519 );
8520 assert_eq!(metrics[1].payload, MetricEventPayload::Count(1));
8521 assert_eq!(
8522 metrics[1].event_codes,
8523 vec![convert::convert_roam_reason_dimension(RoamReason::SnrBelowThreshold) as u32]
8524 );
8525
8526 let metrics = test_helper.get_logged_metrics(
8528 metrics::POLICY_ROAM_CONNECTED_DURATION_BEFORE_ROAM_ATTEMPT_METRIC_ID,
8529 );
8530 assert_eq!(metrics.len(), 2);
8531 assert_eq!(metrics.len(), 2);
8532 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(60));
8533 assert_eq!(
8534 metrics[0].event_codes,
8535 vec![convert::convert_roam_reason_dimension(RoamReason::RssiBelowThreshold) as u32]
8536 );
8537 assert_eq!(metrics[1].payload, MetricEventPayload::IntegerValue(60));
8538 assert_eq!(
8539 metrics[1].event_codes,
8540 vec![convert::convert_roam_reason_dimension(RoamReason::SnrBelowThreshold) as u32]
8541 );
8542 }
8543
8544 fn log_policy_roam_attempt_and_result(
8546 test_helper: &mut TestHelper,
8547 is_success: bool,
8548 reasons: Vec<RoamReason>,
8549 ) {
8550 let status_code = if is_success {
8551 fidl_ieee80211::StatusCode::Success
8552 } else {
8553 fidl_ieee80211::StatusCode::RefusedReasonUnspecified
8554 };
8555
8556 let request = PolicyRoamRequest { candidate: generate_random_scanned_candidate(), reasons };
8558 let event = TelemetryEvent::PolicyRoamAttempt {
8559 request: request.clone(),
8560 connected_duration: zx::MonotonicDuration::from_hours(1),
8561 };
8562 test_helper.telemetry_sender.send(event);
8563
8564 let result = fidl_sme::RoamResult {
8566 bssid: [1, 1, 1, 1, 1, 1],
8567 status_code,
8568 original_association_maintained: false,
8569 bss_description: Some(Box::new(random_fidl_bss_description!())),
8570 disconnect_info: None,
8571 is_credential_rejected: false,
8572 };
8573
8574 let event = TelemetryEvent::PolicyInitiatedRoamResult {
8575 iface_id: IFACE_ID,
8576 result,
8577 updated_ap_state: random_bss_description!().into(),
8578 original_ap_state: Box::new(random_bss_description!().into()),
8579 request: Box::new(request.clone()),
8580 request_time: fasync::MonotonicInstant::now(),
8581 result_time: fasync::MonotonicInstant::now(),
8582 };
8583 test_helper.telemetry_sender.send(event);
8584 }
8585
8586 #[fuchsia::test]
8587 fn test_log_policy_roam_success_rate_cobalt_metrics() {
8588 let (mut test_helper, mut test_fut) = setup_test();
8589 test_helper.send_connected_event(random_bss_description!(Wpa1));
8590
8591 log_policy_roam_attempt_and_result(
8593 &mut test_helper,
8594 true,
8595 vec![RoamReason::RssiBelowThreshold],
8596 );
8597 log_policy_roam_attempt_and_result(
8598 &mut test_helper,
8599 true,
8600 vec![RoamReason::RssiBelowThreshold],
8601 );
8602
8603 log_policy_roam_attempt_and_result(
8605 &mut test_helper,
8606 false,
8607 vec![RoamReason::RssiBelowThreshold],
8608 );
8609
8610 test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
8611
8612 let metrics = test_helper.get_logged_metrics(metrics::POLICY_ROAM_SUCCESS_RATE_METRIC_ID);
8613 assert_eq!(metrics.len(), 1);
8614 assert_eq_cobalt_events(
8615 metrics,
8616 vec![MetricEvent {
8617 metric_id: metrics::POLICY_ROAM_SUCCESS_RATE_METRIC_ID,
8618 event_codes: vec![],
8619 payload: MetricEventPayload::IntegerValue(6666), }],
8621 );
8622 }
8623
8624 #[fuchsia::test]
8625 fn test_log_policy_roam_success_rate_by_roam_reason_cobalt_metrics() {
8626 let (mut test_helper, mut test_fut) = setup_test();
8627 test_helper.send_connected_event(random_bss_description!(Wpa1));
8628
8629 log_policy_roam_attempt_and_result(
8631 &mut test_helper,
8632 true,
8633 vec![RoamReason::RssiBelowThreshold],
8634 );
8635 log_policy_roam_attempt_and_result(
8636 &mut test_helper,
8637 true,
8638 vec![RoamReason::RssiBelowThreshold, RoamReason::SnrBelowThreshold],
8639 );
8640
8641 log_policy_roam_attempt_and_result(
8643 &mut test_helper,
8644 false,
8645 vec![RoamReason::RssiBelowThreshold],
8646 );
8647
8648 test_helper.advance_by(zx::MonotonicDuration::from_hours(24), test_fut.as_mut());
8649
8650 let metrics = test_helper
8651 .get_logged_metrics(metrics::POLICY_ROAM_SUCCESS_RATE_BY_ROAM_REASON_METRIC_ID);
8652 assert_eq!(metrics.len(), 2);
8653 assert_eq_cobalt_events(
8654 metrics,
8655 vec![
8656 MetricEvent {
8657 metric_id: metrics::POLICY_ROAM_SUCCESS_RATE_BY_ROAM_REASON_METRIC_ID,
8658 event_codes: vec![convert::convert_roam_reason_dimension(
8659 RoamReason::RssiBelowThreshold,
8660 ) as u32],
8661 payload: MetricEventPayload::IntegerValue(6666), },
8663 MetricEvent {
8664 metric_id: metrics::POLICY_ROAM_SUCCESS_RATE_BY_ROAM_REASON_METRIC_ID,
8665 event_codes: vec![convert::convert_roam_reason_dimension(
8666 RoamReason::SnrBelowThreshold,
8667 ) as u32],
8668 payload: MetricEventPayload::IntegerValue(10000), },
8670 ],
8671 );
8672 }
8673
8674 #[fuchsia::test]
8675 fn test_connection_enabled_duration_metric() {
8676 let (mut test_helper, mut test_fut) = setup_test();
8677
8678 test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
8679 assert_eq!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8680 test_helper.advance_by(zx::MonotonicDuration::from_seconds(10), test_fut.as_mut());
8681 test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
8682
8683 test_helper.drain_cobalt_events(&mut test_fut);
8684 let metrics = test_helper
8685 .get_logged_metrics(metrics::CLIENT_CONNECTIONS_ENABLED_DURATION_MIGRATED_METRIC_ID);
8686 assert_eq!(metrics.len(), 1);
8687 assert_eq!(
8688 metrics[0].payload,
8689 MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_seconds(10).into_micros())
8690 );
8691 }
8692
8693 #[fuchsia::test]
8694 fn test_restart_metric_start_client_connections_request_sent_first() {
8695 let (mut test_helper, mut test_fut) = setup_test();
8696
8697 test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
8700 test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
8701 test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
8702 test_helper.advance_by(zx::MonotonicDuration::from_seconds(1), test_fut.as_mut());
8703 test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
8704
8705 test_helper.drain_cobalt_events(&mut test_fut);
8707 let metrics =
8708 test_helper.get_logged_metrics(metrics::CLIENT_CONNECTIONS_STOP_AND_START_METRIC_ID);
8709 assert_eq!(metrics.len(), 1);
8710 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8711 }
8712
8713 #[fuchsia::test]
8714 fn test_restart_metric_stop_client_connections_request_sent_first() {
8715 let (mut test_helper, mut test_fut) = setup_test();
8716
8717 test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
8719 test_helper.advance_by(zx::MonotonicDuration::from_seconds(3), test_fut.as_mut());
8720 test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
8721 test_helper.drain_cobalt_events(&mut test_fut);
8723 let metrics =
8724 test_helper.get_logged_metrics(metrics::CLIENT_CONNECTIONS_STOP_AND_START_METRIC_ID);
8725 assert_eq!(metrics.len(), 1);
8726 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8727
8728 test_helper.advance_by(zx::MonotonicDuration::from_seconds(20), test_fut.as_mut());
8730 test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
8731 test_helper.advance_by(zx::MonotonicDuration::from_seconds(1), test_fut.as_mut());
8732 test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
8733 test_helper.drain_cobalt_events(&mut test_fut);
8735 let metrics =
8736 test_helper.get_logged_metrics(metrics::CLIENT_CONNECTIONS_STOP_AND_START_METRIC_ID);
8737 assert_eq!(metrics.len(), 2);
8738 assert_eq!(metrics[1].payload, MetricEventPayload::Count(1));
8739 }
8740
8741 #[fuchsia::test]
8742 fn test_restart_metric_stop_client_connections_request_long_time_not_counted() {
8743 let (mut test_helper, mut test_fut) = setup_test();
8744
8745 test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
8747 test_helper.advance_by(zx::MonotonicDuration::from_seconds(30), test_fut.as_mut());
8748 test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
8749 test_helper.advance_by(zx::MonotonicDuration::from_seconds(2), test_fut.as_mut());
8750 test_helper.drain_cobalt_events(&mut test_fut);
8752 let metrics =
8753 test_helper.get_logged_metrics(metrics::CLIENT_CONNECTIONS_STOP_AND_START_METRIC_ID);
8754 assert!(metrics.is_empty());
8755
8756 test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
8758 test_helper.advance_by(zx::MonotonicDuration::from_seconds(1), test_fut.as_mut());
8759 test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
8760 test_helper.drain_cobalt_events(&mut test_fut);
8762 let metrics =
8763 test_helper.get_logged_metrics(metrics::CLIENT_CONNECTIONS_STOP_AND_START_METRIC_ID);
8764 assert_eq!(metrics.len(), 1);
8765 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
8766 }
8767
8768 #[fuchsia::test]
8769 fn test_restart_metric_extra_stop_client_connections_ignored() {
8770 let (mut test_helper, mut test_fut) = setup_test();
8771
8772 test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
8774 test_helper.advance_by(zx::MonotonicDuration::from_seconds(10), test_fut.as_mut());
8775
8776 test_helper.telemetry_sender.send(TelemetryEvent::StopClientConnectionsRequest);
8779 test_helper.advance_by(zx::MonotonicDuration::from_seconds(1), test_fut.as_mut());
8780 test_helper.telemetry_sender.send(TelemetryEvent::StartClientConnectionsRequest);
8781
8782 test_helper.drain_cobalt_events(&mut test_fut);
8783 let metrics =
8784 test_helper.get_logged_metrics(metrics::CLIENT_CONNECTIONS_STOP_AND_START_METRIC_ID);
8785 assert!(metrics.is_empty());
8786 }
8787
8788 #[fuchsia::test]
8789 fn test_stop_ap_metric() {
8790 let (mut test_helper, mut test_fut) = setup_test();
8791
8792 test_helper.telemetry_sender.send(TelemetryEvent::StopAp {
8793 enabled_duration: zx::MonotonicDuration::from_seconds(50),
8794 });
8795
8796 test_helper.drain_cobalt_events(&mut test_fut);
8797 let metrics = test_helper
8798 .get_logged_metrics(metrics::ACCESS_POINT_ENABLED_DURATION_MIGRATED_METRIC_ID);
8799 assert_eq!(metrics.len(), 1);
8800 assert_eq!(
8801 metrics[0].payload,
8802 MetricEventPayload::IntegerValue(zx::MonotonicDuration::from_seconds(50).into_micros())
8803 );
8804 }
8805
8806 #[derive(PartialEq)]
8807 enum CreateMetricsLoggerFailureMode {
8808 None,
8809 FactoryRequest,
8810 ApiFailure,
8811 }
8812
8813 #[test_case(CreateMetricsLoggerFailureMode::None)]
8814 #[test_case(CreateMetricsLoggerFailureMode::FactoryRequest)]
8815 #[test_case(CreateMetricsLoggerFailureMode::ApiFailure)]
8816 #[fuchsia::test]
8817 fn test_create_metrics_logger(failure_mode: CreateMetricsLoggerFailureMode) {
8818 let mut exec = fasync::TestExecutor::new();
8819 let (factory_proxy, mut factory_stream) = fidl::endpoints::create_proxy_and_stream::<
8820 fidl_fuchsia_metrics::MetricEventLoggerFactoryMarker,
8821 >();
8822
8823 let fut = create_metrics_logger(&factory_proxy);
8824 let mut fut = pin!(fut);
8825
8826 if failure_mode == CreateMetricsLoggerFailureMode::FactoryRequest {
8828 drop(factory_stream);
8829 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(Err(_)));
8830 return;
8831 }
8832
8833 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
8836
8837 let request = exec.run_until_stalled(&mut factory_stream.next());
8838 assert_matches!(
8839 request,
8840 Poll::Ready(Some(Ok(fidl_fuchsia_metrics::MetricEventLoggerFactoryRequest::CreateMetricEventLogger {
8841 project_spec: fidl_fuchsia_metrics::ProjectSpec {
8842 customer_id: None,
8843 project_id: Some(metrics::PROJECT_ID),
8844 ..
8845 },
8846 responder,
8847 ..
8848 }))) => {
8849 match failure_mode {
8850 CreateMetricsLoggerFailureMode::FactoryRequest => panic!("The factory request failure should have been handled already."),
8851 CreateMetricsLoggerFailureMode::None => responder.send(Ok(())).expect("failed to send response"),
8852 CreateMetricsLoggerFailureMode::ApiFailure => responder.send(Err(fidl_fuchsia_metrics::Error::InvalidArguments)).expect("failed to send response"),
8853 }
8854 }
8855 );
8856
8857 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(result) => {
8860 match failure_mode {
8861 CreateMetricsLoggerFailureMode::FactoryRequest => panic!("The factory request failure should have been handled already."),
8862 CreateMetricsLoggerFailureMode::None => assert_matches!(result, Ok(_)),
8863 CreateMetricsLoggerFailureMode::ApiFailure => assert_matches!(result, Err(_))
8864 }
8865 });
8866 }
8867
8868 #[fuchsia::test]
8869 fn test_log_iface_creation_failure() {
8870 let (mut test_helper, mut test_fut) = setup_test();
8871
8872 test_helper.telemetry_sender.send(TelemetryEvent::IfaceCreationResult(Err(())));
8874
8875 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8877
8878 test_helper.drain_cobalt_events(&mut test_fut);
8880 let logged_metrics =
8881 test_helper.get_logged_metrics(metrics::INTERFACE_CREATION_FAILURE_METRIC_ID);
8882 assert_eq!(logged_metrics.len(), 1);
8883 }
8884
8885 #[fuchsia::test]
8886 fn test_log_iface_destruction_failure() {
8887 let (mut test_helper, mut test_fut) = setup_test();
8888
8889 test_helper.telemetry_sender.send(TelemetryEvent::IfaceDestructionResult(Err(())));
8891
8892 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8894
8895 test_helper.drain_cobalt_events(&mut test_fut);
8897 let logged_metrics =
8898 test_helper.get_logged_metrics(metrics::INTERFACE_DESTRUCTION_FAILURE_METRIC_ID);
8899 assert_eq!(logged_metrics.len(), 1);
8900 }
8901
8902 #[test_case(ScanIssue::ScanFailure, metrics::CLIENT_SCAN_FAILURE_METRIC_ID)]
8903 #[test_case(ScanIssue::AbortedScan, metrics::ABORTED_SCAN_METRIC_ID)]
8904 #[test_case(ScanIssue::EmptyScanResults, metrics::EMPTY_SCAN_RESULTS_METRIC_ID)]
8905 #[fuchsia::test(add_test_attr = false)]
8906 fn test_scan_defect_metrics(scan_issue: ScanIssue, expected_metric_id: u32) {
8907 let (mut test_helper, mut test_fut) = setup_test();
8908
8909 let event = TelemetryEvent::ScanEvent {
8910 inspect_data: ScanEventInspectData::new(),
8911 scan_defects: vec![scan_issue],
8912 };
8913
8914 test_helper.telemetry_sender.send(event);
8916
8917 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8919
8920 test_helper.drain_cobalt_events(&mut test_fut);
8922 let logged_metrics = test_helper.get_logged_metrics(expected_metric_id);
8923 assert_eq!(logged_metrics.len(), 1);
8924 }
8925
8926 #[fuchsia::test]
8927 fn test_log_ap_start_failure() {
8928 let (mut test_helper, mut test_fut) = setup_test();
8929
8930 test_helper.telemetry_sender.send(TelemetryEvent::StartApResult(Err(())));
8932
8933 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8935
8936 test_helper.drain_cobalt_events(&mut test_fut);
8938 let logged_metrics = test_helper.get_logged_metrics(metrics::AP_START_FAILURE_METRIC_ID);
8939 assert_eq!(logged_metrics.len(), 1);
8940 }
8941
8942 #[test_case(
8943 RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset),
8944 metrics::RecoveryOccurrenceMetricDimensionReason::InterfaceCreationFailure ;
8945 "log recovery event for iface creation failure"
8946 )]
8947 #[test_case(
8948 RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset),
8949 metrics::RecoveryOccurrenceMetricDimensionReason::InterfaceDestructionFailure ;
8950 "log recovery event for iface destruction failure"
8951 )]
8952 #[test_case(
8953 RecoveryReason::ConnectFailure(ClientRecoveryMechanism::Disconnect),
8954 metrics::RecoveryOccurrenceMetricDimensionReason::ClientConnectionFailure ;
8955 "log recovery event for connect failure"
8956 )]
8957 #[test_case(
8958 RecoveryReason::StartApFailure(ApRecoveryMechanism::StopAp),
8959 metrics::RecoveryOccurrenceMetricDimensionReason::ApStartFailure ;
8960 "log recovery event for start AP failure"
8961 )]
8962 #[test_case(
8963 RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect),
8964 metrics::RecoveryOccurrenceMetricDimensionReason::ScanFailure ;
8965 "log recovery event for scan failure"
8966 )]
8967 #[test_case(
8968 RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect),
8969 metrics::RecoveryOccurrenceMetricDimensionReason::ScanCancellation ;
8970 "log recovery event for scan cancellation"
8971 )]
8972 #[test_case(
8973 RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect),
8974 metrics::RecoveryOccurrenceMetricDimensionReason::ScanResultsEmpty ;
8975 "log recovery event for empty scan results"
8976 )]
8977 #[fuchsia::test(add_test_attr = false)]
8978 fn test_log_recovery_occurrence(
8979 reason: RecoveryReason,
8980 expected_dimension: metrics::RecoveryOccurrenceMetricDimensionReason,
8981 ) {
8982 let (mut test_helper, mut test_fut) = setup_test();
8983
8984 test_helper.telemetry_sender.send(TelemetryEvent::RecoveryEvent { reason });
8986
8987 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
8989
8990 assert_matches!(
8992 test_helper.exec.run_until_stalled(&mut test_helper.cobalt_stream.next()),
8993 Poll::Ready(Some(Ok(fidl_fuchsia_metrics::MetricEventLoggerRequest::LogOccurrence {
8994 metric_id, event_codes, responder, ..
8995 }))) => {
8996 assert_eq!(metric_id, metrics::RECOVERY_OCCURRENCE_METRIC_ID);
8997 assert_eq!(event_codes, vec![expected_dimension.as_event_code()]);
8998
8999 assert!(responder.send(Ok(())).is_ok());
9000 });
9001 }
9002
9003 #[test_case(
9004 RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset),
9005 RecoveryOutcome::Success,
9006 metrics::INTERFACE_CREATION_RECOVERY_OUTCOME_METRIC_ID,
9007 vec![RecoveryOutcome::Success as u32] ;
9008 "create iface fixed by resetting PHY"
9009 )]
9010 #[test_case(
9011 RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset),
9012 RecoveryOutcome::Failure,
9013 metrics::INTERFACE_CREATION_RECOVERY_OUTCOME_METRIC_ID,
9014 vec![RecoveryOutcome::Failure as u32] ;
9015 "create iface not fixed by resetting PHY"
9016 )]
9017 #[test_case(
9018 RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset),
9019 RecoveryOutcome::Success,
9020 metrics::INTERFACE_DESTRUCTION_RECOVERY_OUTCOME_METRIC_ID,
9021 vec![RecoveryOutcome::Success as u32] ;
9022 "destroy iface fixed by resetting PHY"
9023 )]
9024 #[test_case(
9025 RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset),
9026 RecoveryOutcome::Failure,
9027 metrics::INTERFACE_DESTRUCTION_RECOVERY_OUTCOME_METRIC_ID,
9028 vec![RecoveryOutcome::Failure as u32] ;
9029 "destroy iface not fixed by resetting PHY"
9030 )]
9031 #[test_case(
9032 RecoveryReason::ConnectFailure(ClientRecoveryMechanism::Disconnect),
9033 RecoveryOutcome::Success,
9034 metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9035 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9036 "connect works after disconnecting"
9037 )]
9038 #[test_case(
9039 RecoveryReason::ConnectFailure(ClientRecoveryMechanism::DestroyIface),
9040 RecoveryOutcome::Success,
9041 metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9042 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9043 "connect works after destroying iface"
9044 )]
9045 #[test_case(
9046 RecoveryReason::ConnectFailure(ClientRecoveryMechanism::PhyReset),
9047 RecoveryOutcome::Success,
9048 metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9049 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9050 "connect works after resetting PHY"
9051 )]
9052 #[test_case(
9053 RecoveryReason::ConnectFailure(ClientRecoveryMechanism::Disconnect),
9054 RecoveryOutcome::Failure,
9055 metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9056 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9057 "connect still fails after disconnecting"
9058 )]
9059 #[test_case(
9060 RecoveryReason::ConnectFailure(ClientRecoveryMechanism::DestroyIface),
9061 RecoveryOutcome::Failure,
9062 metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9063 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9064 "connect still fails after destroying iface"
9065 )]
9066 #[test_case(
9067 RecoveryReason::ConnectFailure(ClientRecoveryMechanism::PhyReset),
9068 RecoveryOutcome::Failure,
9069 metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9070 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9071 "connect still fails after resetting PHY"
9072 )]
9073 #[test_case(
9074 RecoveryReason::StartApFailure(ApRecoveryMechanism::StopAp),
9075 RecoveryOutcome::Success,
9076 metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
9077 vec![RecoveryOutcome::Success as u32, ApRecoveryMechanism::StopAp as u32] ;
9078 "start AP works after stopping AP"
9079 )]
9080 #[test_case(
9081 RecoveryReason::StartApFailure(ApRecoveryMechanism::DestroyIface),
9082 RecoveryOutcome::Success,
9083 metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
9084 vec![RecoveryOutcome::Success as u32, ApRecoveryMechanism::DestroyIface as u32] ;
9085 "start AP works after destroying iface"
9086 )]
9087 #[test_case(
9088 RecoveryReason::StartApFailure(ApRecoveryMechanism::ResetPhy),
9089 RecoveryOutcome::Success,
9090 metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
9091 vec![RecoveryOutcome::Success as u32, ApRecoveryMechanism::ResetPhy as u32] ;
9092 "start AP works after resetting PHY"
9093 )]
9094 #[test_case(
9095 RecoveryReason::StartApFailure(ApRecoveryMechanism::StopAp),
9096 RecoveryOutcome::Failure,
9097 metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
9098 vec![RecoveryOutcome::Failure as u32, ApRecoveryMechanism::StopAp as u32] ;
9099 "start AP still fails after stopping AP"
9100 )]
9101 #[test_case(
9102 RecoveryReason::StartApFailure(ApRecoveryMechanism::DestroyIface),
9103 RecoveryOutcome::Failure,
9104 metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
9105 vec![RecoveryOutcome::Failure as u32, ApRecoveryMechanism::DestroyIface as u32] ;
9106 "start AP still fails after destroying iface"
9107 )]
9108 #[test_case(
9109 RecoveryReason::StartApFailure(ApRecoveryMechanism::ResetPhy),
9110 RecoveryOutcome::Failure,
9111 metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
9112 vec![RecoveryOutcome::Failure as u32, ApRecoveryMechanism::ResetPhy as u32] ;
9113 "start AP still fails after resetting PHY"
9114 )]
9115 #[test_case(
9116 RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect),
9117 RecoveryOutcome::Success,
9118 metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9119 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9120 "scan works after disconnecting"
9121 )]
9122 #[test_case(
9123 RecoveryReason::ScanFailure(ClientRecoveryMechanism::DestroyIface),
9124 RecoveryOutcome::Success,
9125 metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9126 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9127 "scan works after destroying iface"
9128 )]
9129 #[test_case(
9130 RecoveryReason::ScanFailure(ClientRecoveryMechanism::PhyReset),
9131 RecoveryOutcome::Success,
9132 metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9133 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9134 "scan works after resetting PHY"
9135 )]
9136 #[test_case(
9137 RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect),
9138 RecoveryOutcome::Failure,
9139 metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9140 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9141 "scan still fails after disconnecting"
9142 )]
9143 #[test_case(
9144 RecoveryReason::ScanFailure(ClientRecoveryMechanism::DestroyIface),
9145 RecoveryOutcome::Failure,
9146 metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9147 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9148 "scan still fails after destroying iface"
9149 )]
9150 #[test_case(
9151 RecoveryReason::ScanFailure(ClientRecoveryMechanism::PhyReset),
9152 RecoveryOutcome::Failure,
9153 metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9154 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9155 "scan still fails after resetting PHY"
9156 )]
9157 #[test_case(
9158 RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect),
9159 RecoveryOutcome::Success,
9160 metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
9161 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9162 "scan is no longer cancelled after disconnecting"
9163 )]
9164 #[test_case(
9165 RecoveryReason::ScanCancellation(ClientRecoveryMechanism::DestroyIface),
9166 RecoveryOutcome::Success,
9167 metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
9168 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9169 "scan is no longer cancelled after destroying iface"
9170 )]
9171 #[test_case(
9172 RecoveryReason::ScanCancellation(ClientRecoveryMechanism::PhyReset),
9173 RecoveryOutcome::Success,
9174 metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
9175 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9176 "scan is no longer cancelled after resetting PHY"
9177 )]
9178 #[test_case(
9179 RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect),
9180 RecoveryOutcome::Failure,
9181 metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
9182 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9183 "scan is still cancelled after disconnect"
9184 )]
9185 #[test_case(
9186 RecoveryReason::ScanCancellation(ClientRecoveryMechanism::DestroyIface),
9187 RecoveryOutcome::Failure,
9188 metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
9189 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9190 "scan is still cancelled after destroying iface"
9191 )]
9192 #[test_case(
9193 RecoveryReason::ScanCancellation(ClientRecoveryMechanism::PhyReset),
9194 RecoveryOutcome::Failure,
9195 metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
9196 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9197 "scan is still cancelled after resetting PHY"
9198 )]
9199 #[test_case(
9200 RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect),
9201 RecoveryOutcome::Success,
9202 metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9203 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9204 "scan results not empty after disconnect"
9205 )]
9206 #[test_case(
9207 RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::DestroyIface),
9208 RecoveryOutcome::Success,
9209 metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9210 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9211 "scan results not empty after destroy iface"
9212 )]
9213 #[test_case(
9214 RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::PhyReset),
9215 RecoveryOutcome::Success,
9216 metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9217 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9218 "scan results not empty after PHY reset"
9219 )]
9220 #[test_case(
9221 RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect),
9222 RecoveryOutcome::Failure,
9223 metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9224 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9225 "scan results still empty after disconnect"
9226 )]
9227 #[test_case(
9228 RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::DestroyIface),
9229 RecoveryOutcome::Failure,
9230 metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9231 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9232 "scan results still empty after destroy iface"
9233 )]
9234 #[test_case(
9235 RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::PhyReset),
9236 RecoveryOutcome::Failure,
9237 metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9238 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9239 "scan results still empty after PHY reset"
9240 )]
9241 #[fuchsia::test(add_test_attr = false)]
9242 fn test_log_post_recovery_result(
9243 reason: RecoveryReason,
9244 outcome: RecoveryOutcome,
9245 expected_metric_id: u32,
9246 expected_event_codes: Vec<u32>,
9247 ) {
9248 let mut exec = fasync::TestExecutor::new();
9249
9250 let (cobalt_proxy, mut cobalt_stream) =
9252 create_proxy_and_stream::<fidl_fuchsia_metrics::MetricEventLoggerMarker>();
9253
9254 let inspector = Inspector::default();
9255 let inspect_node = inspector.root().create_child("stats");
9256
9257 let mut stats_logger = StatsLogger::new(cobalt_proxy, &inspect_node);
9258
9259 let fut = stats_logger.log_post_recovery_result(reason, outcome);
9261 let mut fut = pin!(fut);
9262 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Pending);
9263
9264 assert_matches!(
9266 exec.run_until_stalled(&mut cobalt_stream.next()),
9267 Poll::Ready(Some(Ok(fidl_fuchsia_metrics::MetricEventLoggerRequest::LogOccurrence {
9268 metric_id, event_codes, responder, ..
9269 }))) => {
9270 assert_eq!(metric_id, expected_metric_id);
9271 assert_eq!(event_codes, expected_event_codes);
9272
9273 assert!(responder.send(Ok(())).is_ok());
9274 });
9275
9276 assert_matches!(exec.run_until_stalled(&mut fut), Poll::Ready(()));
9278 }
9279
9280 #[fuchsia::test]
9281 fn test_post_recovery_connect_success() {
9282 let (mut test_helper, mut test_fut) = setup_test();
9283
9284 let reason = RecoveryReason::ConnectFailure(ClientRecoveryMechanism::PhyReset);
9286 let event = TelemetryEvent::RecoveryEvent { reason };
9287 test_helper.telemetry_sender.send(event);
9288
9289 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9291
9292 test_helper.drain_cobalt_events(&mut test_fut);
9294 let logged_metrics = test_helper.get_logged_metrics(metrics::RECOVERY_OCCURRENCE_METRIC_ID);
9295 assert_eq!(logged_metrics.len(), 1);
9296
9297 assert_eq!(
9299 logged_metrics[0].event_codes,
9300 vec![
9301 metrics::RecoveryOccurrenceMetricDimensionReason::ClientConnectionFailure
9302 .as_event_code()
9303 ]
9304 );
9305
9306 test_helper.telemetry_sender.send(TelemetryEvent::ConnectResult {
9308 iface_id: IFACE_ID,
9309 policy_connect_reason: Some(
9310 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
9311 ),
9312 result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
9313 multiple_bss_candidates: true,
9314 ap_state: random_bss_description!(Wpa1).into(),
9315 network_is_likely_hidden: false,
9316 });
9317
9318 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9320
9321 test_helper.drain_cobalt_events(&mut test_fut);
9323 let logged_metrics =
9324 test_helper.get_logged_metrics(metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID);
9325 assert_eq!(
9326 logged_metrics[0].event_codes,
9327 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32]
9328 );
9329
9330 test_helper.telemetry_sender.send(TelemetryEvent::ConnectResult {
9332 iface_id: IFACE_ID,
9333 policy_connect_reason: Some(
9334 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
9335 ),
9336 result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
9337 multiple_bss_candidates: true,
9338 ap_state: random_bss_description!(Wpa1).into(),
9339 network_is_likely_hidden: false,
9340 });
9341
9342 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9343
9344 test_helper.cobalt_events = Vec::new();
9345 test_helper.drain_cobalt_events(&mut test_fut);
9346 let logged_metrics =
9347 test_helper.get_logged_metrics(metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID);
9348 assert!(logged_metrics.is_empty());
9349 }
9350
9351 #[fuchsia::test]
9352 fn test_post_recovery_connect_failure() {
9353 let (mut test_helper, mut test_fut) = setup_test();
9354
9355 let reason = RecoveryReason::ConnectFailure(ClientRecoveryMechanism::PhyReset);
9357 let event = TelemetryEvent::RecoveryEvent { reason };
9358 test_helper.telemetry_sender.send(event);
9359
9360 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9362
9363 test_helper.drain_cobalt_events(&mut test_fut);
9365 let logged_metrics = test_helper.get_logged_metrics(metrics::RECOVERY_OCCURRENCE_METRIC_ID);
9366 assert_eq!(logged_metrics.len(), 1);
9367
9368 assert_eq!(
9370 logged_metrics[0].event_codes,
9371 vec![
9372 metrics::RecoveryOccurrenceMetricDimensionReason::ClientConnectionFailure
9373 .as_event_code()
9374 ]
9375 );
9376
9377 test_helper.telemetry_sender.send(TelemetryEvent::ConnectResult {
9379 iface_id: IFACE_ID,
9380 policy_connect_reason: Some(
9381 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
9382 ),
9383 result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
9384 multiple_bss_candidates: true,
9385 ap_state: random_bss_description!(Wpa1).into(),
9386 network_is_likely_hidden: false,
9387 });
9388
9389 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9391
9392 test_helper.drain_cobalt_events(&mut test_fut);
9394 let logged_metrics =
9395 test_helper.get_logged_metrics(metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID);
9396 assert_eq!(
9397 logged_metrics[0].event_codes,
9398 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32]
9399 );
9400
9401 test_helper.telemetry_sender.send(TelemetryEvent::ConnectResult {
9403 iface_id: IFACE_ID,
9404 policy_connect_reason: Some(
9405 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
9406 ),
9407 result: fake_connect_result(fidl_ieee80211::StatusCode::RefusedReasonUnspecified),
9408 multiple_bss_candidates: true,
9409 ap_state: random_bss_description!(Wpa1).into(),
9410 network_is_likely_hidden: false,
9411 });
9412
9413 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9414
9415 test_helper.cobalt_events = Vec::new();
9416 test_helper.drain_cobalt_events(&mut test_fut);
9417 let logged_metrics =
9418 test_helper.get_logged_metrics(metrics::CONNECT_FAILURE_RECOVERY_OUTCOME_METRIC_ID);
9419 assert!(logged_metrics.is_empty());
9420 }
9421
9422 fn test_generic_post_recovery_event(
9423 recovery_event: TelemetryEvent,
9424 post_recovery_event: TelemetryEvent,
9425 duplicate_check_event: TelemetryEvent,
9426 expected_metric_id: u32,
9427 dimensions: Vec<u32>,
9428 ) {
9429 let (mut test_helper, mut test_fut) = setup_test();
9430 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(1));
9431
9432 test_helper.telemetry_sender.send(recovery_event);
9434 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9435
9436 test_helper.telemetry_sender.send(post_recovery_event);
9438 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9439
9440 test_helper.drain_cobalt_events(&mut test_fut);
9442 let logged_metrics = test_helper.get_logged_metrics(expected_metric_id);
9443
9444 assert_eq!(logged_metrics.len(), 1);
9445 assert_eq!(logged_metrics[0].event_codes, dimensions);
9446
9447 test_helper.cobalt_events = Vec::new();
9449 test_helper.telemetry_sender.send(duplicate_check_event);
9450 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9451 let logged_metrics = test_helper.get_logged_metrics(expected_metric_id);
9452 assert!(logged_metrics.is_empty());
9453
9454 if dimensions[0] == RecoveryOutcome::Success.as_event_code() {
9457 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
9458 stats: contains {
9459 last_successful_recovery: 1_u64,
9460 successful_recoveries: 1_u64
9461 }
9462 });
9463 } else {
9464 assert_data_tree_with_respond_blocking_req!(test_helper, test_fut, root: contains {
9465 stats: contains {
9466 last_successful_recovery: 0_u64,
9467 successful_recoveries: 0_u64
9468 }
9469 });
9470 }
9471 }
9472
9473 #[test_case(
9474 TelemetryEvent::RecoveryEvent {
9475 reason: RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect)
9476 },
9477 TelemetryEvent::ScanEvent {
9478 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9479 scan_defects: vec![]
9480 },
9481 TelemetryEvent::ScanEvent {
9482 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9483 scan_defects: vec![]
9484 },
9485 metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9486 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9487 "Scan succeeds after recovery with no other defects"
9488 )]
9489 #[test_case(
9490 TelemetryEvent::RecoveryEvent {
9491 reason: RecoveryReason::ScanFailure(ClientRecoveryMechanism::Disconnect)
9492 },
9493 TelemetryEvent::ScanEvent {
9494 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9495 scan_defects: vec![ScanIssue::ScanFailure]
9496 },
9497 TelemetryEvent::ScanEvent {
9498 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9499 scan_defects: vec![ScanIssue::ScanFailure]
9500 },
9501 metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9502 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9503 "Scan still fails following recovery"
9504 )]
9505 #[test_case(
9506 TelemetryEvent::RecoveryEvent {
9507 reason: RecoveryReason::ScanFailure(ClientRecoveryMechanism::DestroyIface)
9508 },
9509 TelemetryEvent::ScanEvent {
9510 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9511 scan_defects: vec![ScanIssue::AbortedScan]
9512 },
9513 TelemetryEvent::ScanEvent {
9514 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9515 scan_defects: vec![ScanIssue::AbortedScan]
9516 },
9517 metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9518 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9519 "Scan succeeds after recovery but the scan was cancelled"
9520 )]
9521 #[test_case(
9522 TelemetryEvent::RecoveryEvent {
9523 reason: RecoveryReason::ScanFailure(ClientRecoveryMechanism::PhyReset)
9524 },
9525 TelemetryEvent::ScanEvent {
9526 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9527 scan_defects: vec![ScanIssue::EmptyScanResults]
9528 },
9529 TelemetryEvent::ScanEvent {
9530 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9531 scan_defects: vec![ScanIssue::EmptyScanResults]
9532 },
9533 metrics::SCAN_FAILURE_RECOVERY_OUTCOME_METRIC_ID,
9534 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9535 "Scan succeeds after recovery but the results are empty"
9536 )]
9537 #[test_case(
9538 TelemetryEvent::RecoveryEvent {
9539 reason: RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect)
9540 },
9541 TelemetryEvent::ScanEvent {
9542 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9543 scan_defects: vec![]
9544 },
9545 TelemetryEvent::ScanEvent {
9546 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9547 scan_defects: vec![]
9548 },
9549 metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
9550 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9551 "Scan no longer cancelled after recovery"
9552 )]
9553 #[test_case(
9554 TelemetryEvent::RecoveryEvent {
9555 reason: RecoveryReason::ScanCancellation(ClientRecoveryMechanism::Disconnect)
9556 },
9557 TelemetryEvent::ScanEvent {
9558 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9559 scan_defects: vec![ScanIssue::ScanFailure]
9560 },
9561 TelemetryEvent::ScanEvent {
9562 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9563 scan_defects: vec![ScanIssue::ScanFailure]
9564 },
9565 metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
9566 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9567 "Scan not cancelled after recovery but fails instead"
9568 )]
9569 #[test_case(
9570 TelemetryEvent::RecoveryEvent {
9571 reason: RecoveryReason::ScanCancellation(ClientRecoveryMechanism::DestroyIface)
9572 },
9573 TelemetryEvent::ScanEvent {
9574 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9575 scan_defects: vec![ScanIssue::AbortedScan]
9576 },
9577 TelemetryEvent::ScanEvent {
9578 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9579 scan_defects: vec![ScanIssue::AbortedScan]
9580 },
9581 metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
9582 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9583 "Scan still cancelled after recovery"
9584 )]
9585 #[test_case(
9586 TelemetryEvent::RecoveryEvent {
9587 reason: RecoveryReason::ScanCancellation(ClientRecoveryMechanism::PhyReset)
9588 },
9589 TelemetryEvent::ScanEvent {
9590 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9591 scan_defects: vec![ScanIssue::EmptyScanResults]
9592 },
9593 TelemetryEvent::ScanEvent {
9594 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9595 scan_defects: vec![ScanIssue::EmptyScanResults]
9596 },
9597 metrics::SCAN_CANCELLATION_RECOVERY_OUTCOME_METRIC_ID,
9598 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9599 "Scan not cancelled after recovery but results are empty"
9600 )]
9601 #[test_case(
9602 TelemetryEvent::RecoveryEvent {
9603 reason: RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect)
9604 },
9605 TelemetryEvent::ScanEvent {
9606 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9607 scan_defects: vec![]
9608 },
9609 TelemetryEvent::ScanEvent {
9610 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9611 scan_defects: vec![]
9612 },
9613 metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9614 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9615 "Scan results not empty after recovery and no other errors"
9616 )]
9617 #[test_case(
9618 TelemetryEvent::RecoveryEvent {
9619 reason: RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::Disconnect)
9620 },
9621 TelemetryEvent::ScanEvent {
9622 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9623 scan_defects: vec![ScanIssue::ScanFailure]
9624 },
9625 TelemetryEvent::ScanEvent {
9626 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9627 scan_defects: vec![ScanIssue::ScanFailure]
9628 },
9629 metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9630 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::Disconnect as u32] ;
9631 "Scan results no longer empty after recovery, but scan fails"
9632 )]
9633 #[test_case(
9634 TelemetryEvent::RecoveryEvent {
9635 reason: RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::DestroyIface)
9636 },
9637 TelemetryEvent::ScanEvent {
9638 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9639 scan_defects: vec![ScanIssue::AbortedScan]
9640 },
9641 TelemetryEvent::ScanEvent {
9642 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9643 scan_defects: vec![ScanIssue::AbortedScan]
9644 },
9645 metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9646 vec![RecoveryOutcome::Success as u32, ClientRecoveryMechanism::DestroyIface as u32] ;
9647 "Scan results not empty after recovery but scan is cancelled"
9648 )]
9649 #[test_case(
9650 TelemetryEvent::RecoveryEvent {
9651 reason: RecoveryReason::ScanResultsEmpty(ClientRecoveryMechanism::PhyReset)
9652 },
9653 TelemetryEvent::ScanEvent {
9654 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9655 scan_defects: vec![ScanIssue::EmptyScanResults]
9656 },
9657 TelemetryEvent::ScanEvent {
9658 inspect_data: ScanEventInspectData { unknown_protection_ies: vec![] },
9659 scan_defects: vec![ScanIssue::EmptyScanResults]
9660 },
9661 metrics::EMPTY_SCAN_RESULTS_RECOVERY_OUTCOME_METRIC_ID,
9662 vec![RecoveryOutcome::Failure as u32, ClientRecoveryMechanism::PhyReset as u32] ;
9663 "Scan results still empty after recovery"
9664 )]
9665 #[fuchsia::test(add_test_attr = false)]
9666 fn test_post_recovery_scan_metrics(
9667 recovery_event: TelemetryEvent,
9668 post_recovery_event: TelemetryEvent,
9669 duplicate_check_event: TelemetryEvent,
9670 expected_metric_id: u32,
9671 dimensions: Vec<u32>,
9672 ) {
9673 test_generic_post_recovery_event(
9674 recovery_event,
9675 post_recovery_event,
9676 duplicate_check_event,
9677 expected_metric_id,
9678 dimensions,
9679 );
9680 }
9681
9682 #[test_case(
9683 TelemetryEvent::RecoveryEvent {
9684 reason: RecoveryReason::StartApFailure(ApRecoveryMechanism::ResetPhy)
9685 },
9686 TelemetryEvent::StartApResult(Err(())),
9687 TelemetryEvent::StartApResult(Err(())),
9688 metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
9689 vec![RecoveryOutcome::Failure as u32, ApRecoveryMechanism::ResetPhy as u32] ;
9690 "start AP still does not work after recovery"
9691 )]
9692 #[test_case(
9693 TelemetryEvent::RecoveryEvent {
9694 reason: RecoveryReason::StartApFailure(ApRecoveryMechanism::ResetPhy)
9695 },
9696 TelemetryEvent::StartApResult(Ok(())),
9697 TelemetryEvent::StartApResult(Ok(())),
9698 metrics::START_ACCESS_POINT_RECOVERY_OUTCOME_METRIC_ID,
9699 vec![RecoveryOutcome::Success as u32, ApRecoveryMechanism::ResetPhy as u32] ;
9700 "start AP works after recovery"
9701 )]
9702 #[fuchsia::test(add_test_attr = false)]
9703 fn test_post_recovery_start_ap(
9704 recovery_event: TelemetryEvent,
9705 post_recovery_event: TelemetryEvent,
9706 duplicate_check_event: TelemetryEvent,
9707 expected_metric_id: u32,
9708 dimensions: Vec<u32>,
9709 ) {
9710 test_generic_post_recovery_event(
9711 recovery_event,
9712 post_recovery_event,
9713 duplicate_check_event,
9714 expected_metric_id,
9715 dimensions,
9716 );
9717 }
9718
9719 #[test_case(
9720 TelemetryEvent::RecoveryEvent {
9721 reason: RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset)
9722 },
9723 TelemetryEvent::IfaceCreationResult(Err(())),
9724 TelemetryEvent::IfaceCreationResult(Err(())),
9725 metrics::INTERFACE_CREATION_RECOVERY_OUTCOME_METRIC_ID,
9726 vec![RecoveryOutcome::Failure as u32] ;
9727 "create iface still does not work after recovery"
9728 )]
9729 #[test_case(
9730 TelemetryEvent::RecoveryEvent {
9731 reason: RecoveryReason::CreateIfaceFailure(PhyRecoveryMechanism::PhyReset)
9732 },
9733 TelemetryEvent::IfaceCreationResult(Ok(())),
9734 TelemetryEvent::IfaceCreationResult(Ok(())),
9735 metrics::INTERFACE_CREATION_RECOVERY_OUTCOME_METRIC_ID,
9736 vec![RecoveryOutcome::Success as u32] ;
9737 "create iface works after recovery"
9738 )]
9739 #[fuchsia::test(add_test_attr = false)]
9740 fn test_post_recovery_create_iface(
9741 recovery_event: TelemetryEvent,
9742 post_recovery_event: TelemetryEvent,
9743 duplicate_check_event: TelemetryEvent,
9744 expected_metric_id: u32,
9745 dimensions: Vec<u32>,
9746 ) {
9747 test_generic_post_recovery_event(
9748 recovery_event,
9749 post_recovery_event,
9750 duplicate_check_event,
9751 expected_metric_id,
9752 dimensions,
9753 );
9754 }
9755
9756 #[test_case(
9757 TelemetryEvent::RecoveryEvent {
9758 reason: RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset)
9759 },
9760 TelemetryEvent::IfaceDestructionResult(Err(())),
9761 TelemetryEvent::IfaceDestructionResult(Err(())),
9762 metrics::INTERFACE_DESTRUCTION_RECOVERY_OUTCOME_METRIC_ID,
9763 vec![RecoveryOutcome::Failure as u32] ;
9764 "destroy iface does not work after recovery"
9765 )]
9766 #[test_case(
9767 TelemetryEvent::RecoveryEvent {
9768 reason: RecoveryReason::DestroyIfaceFailure(PhyRecoveryMechanism::PhyReset)
9769 },
9770 TelemetryEvent::IfaceDestructionResult(Ok(())),
9771 TelemetryEvent::IfaceDestructionResult(Ok(())),
9772 metrics::INTERFACE_DESTRUCTION_RECOVERY_OUTCOME_METRIC_ID,
9773 vec![RecoveryOutcome::Success as u32] ;
9774 "destroy iface works after recovery"
9775 )]
9776 #[fuchsia::test(add_test_attr = false)]
9777 fn test_post_recovery_destroy_iface(
9778 recovery_event: TelemetryEvent,
9779 post_recovery_event: TelemetryEvent,
9780 duplicate_check_event: TelemetryEvent,
9781 expected_metric_id: u32,
9782 dimensions: Vec<u32>,
9783 ) {
9784 test_generic_post_recovery_event(
9785 recovery_event,
9786 post_recovery_event,
9787 duplicate_check_event,
9788 expected_metric_id,
9789 dimensions,
9790 );
9791 }
9792
9793 #[test_case(
9794 TelemetryEvent::RecoveryEvent {
9795 reason: RecoveryReason::Timeout(TimeoutRecoveryMechanism::PhyReset)
9796 },
9797 TelemetryEvent::ConnectResult {
9798 iface_id: IFACE_ID,
9799 policy_connect_reason: Some(
9800 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
9801 ),
9802 result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
9803 multiple_bss_candidates: true,
9804 ap_state: random_bss_description!(Wpa2).into(),
9805 network_is_likely_hidden: true,
9806 },
9807 TelemetryEvent::ConnectResult {
9808 iface_id: IFACE_ID,
9809 policy_connect_reason: Some(
9810 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
9811 ),
9812 result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
9813 multiple_bss_candidates: true,
9814 ap_state: random_bss_description!(Wpa2).into(),
9815 network_is_likely_hidden: true,
9816 },
9817 metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
9818 vec![RecoveryOutcome::Success as u32, TimeoutRecoveryMechanism::PhyReset as u32] ;
9819 "Connect works after recovery"
9820 )]
9821 #[test_case(
9822 TelemetryEvent::RecoveryEvent {
9823 reason: RecoveryReason::Timeout(TimeoutRecoveryMechanism::PhyReset)
9824 },
9825 TelemetryEvent::Disconnected {
9826 track_subsequent_downtime: false,
9827 info: Some(fake_disconnect_info()),
9828 },
9829 TelemetryEvent::Disconnected {
9830 track_subsequent_downtime: false,
9831 info: Some(fake_disconnect_info()),
9832 },
9833 metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
9834 vec![RecoveryOutcome::Success as u32, TimeoutRecoveryMechanism::PhyReset as u32] ;
9835 "Disconnect works after recovery"
9836 )]
9837 #[test_case(
9838 TelemetryEvent::RecoveryEvent {
9839 reason: RecoveryReason::Timeout(TimeoutRecoveryMechanism::PhyReset)
9840 },
9841 TelemetryEvent::StopAp { enabled_duration: zx::MonotonicDuration::from_seconds(0) },
9842 TelemetryEvent::StopAp { enabled_duration: zx::MonotonicDuration::from_seconds(0) },
9843 metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
9844 vec![RecoveryOutcome::Success as u32, TimeoutRecoveryMechanism::PhyReset as u32] ;
9845 "Stop AP works after recovery"
9846 )]
9847 #[test_case(
9848 TelemetryEvent::RecoveryEvent {
9849 reason: RecoveryReason::Timeout(TimeoutRecoveryMechanism::PhyReset)
9850 },
9851 TelemetryEvent::StartApResult(Ok(())),
9852 TelemetryEvent::StartApResult(Ok(())),
9853 metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
9854 vec![RecoveryOutcome::Success as u32, TimeoutRecoveryMechanism::PhyReset as u32] ;
9855 "Start AP works after recovery"
9856 )]
9857 #[test_case(
9858 TelemetryEvent::RecoveryEvent {
9859 reason: RecoveryReason::Timeout(TimeoutRecoveryMechanism::DestroyIface)
9860 },
9861 TelemetryEvent::ScanEvent {
9862 inspect_data: ScanEventInspectData::default(),
9863 scan_defects: vec![]
9864 },
9865 TelemetryEvent::ScanEvent {
9866 inspect_data: ScanEventInspectData::default(),
9867 scan_defects: vec![]
9868 },
9869 metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
9870 vec![RecoveryOutcome::Success as u32, TimeoutRecoveryMechanism::DestroyIface as u32] ;
9871 "Scan works after timeout recovery"
9872 )]
9873 #[test_case(
9874 TelemetryEvent::RecoveryEvent {
9875 reason: RecoveryReason::Timeout(TimeoutRecoveryMechanism::PhyReset)
9876 },
9877 TelemetryEvent::SmeTimeout { source: TimeoutSource::Scan },
9878 TelemetryEvent::SmeTimeout { source: TimeoutSource::Scan },
9879 metrics::TIMEOUT_RECOVERY_OUTCOME_METRIC_ID,
9880 vec![RecoveryOutcome::Failure as u32, TimeoutRecoveryMechanism::PhyReset as u32] ;
9881 "SME timeout after recovery"
9882 )]
9883 #[fuchsia::test(add_test_attr = false)]
9884 fn test_post_recovery_timeout(
9885 recovery_event: TelemetryEvent,
9886 post_recovery_event: TelemetryEvent,
9887 duplicate_check_event: TelemetryEvent,
9888 expected_metric_id: u32,
9889 dimensions: Vec<u32>,
9890 ) {
9891 test_generic_post_recovery_event(
9892 recovery_event,
9893 post_recovery_event,
9894 duplicate_check_event,
9895 expected_metric_id,
9896 dimensions,
9897 );
9898 }
9899
9900 #[fuchsia::test]
9901 fn test_log_scan_request_fulfillment_time() {
9902 let (mut test_helper, mut test_fut) = setup_test();
9903
9904 let duration = zx::MonotonicDuration::from_seconds(15);
9906 test_helper.telemetry_sender.send(TelemetryEvent::ScanRequestFulfillmentTime {
9907 duration,
9908 reason: client::scan::ScanReason::ClientRequest,
9909 });
9910
9911 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9913
9914 test_helper.drain_cobalt_events(&mut test_fut);
9916 let logged_metrics = test_helper
9917 .get_logged_metrics(metrics::SUCCESSFUL_SCAN_REQUEST_FULFILLMENT_TIME_METRIC_ID);
9918 assert_eq!(logged_metrics.len(), 1);
9919 assert_eq!(
9920 logged_metrics[0].event_codes,
9921 vec![
9922 metrics::ConnectivityWlanMetricDimensionScanFulfillmentTime::LessThanTwentyOneSeconds as u32,
9923 metrics::ConnectivityWlanMetricDimensionScanReason::ClientRequest as u32
9924 ]
9925 );
9926 }
9927
9928 #[fuchsia::test]
9929 fn test_log_scan_queue_statistics() {
9930 let (mut test_helper, mut test_fut) = setup_test();
9931
9932 test_helper.telemetry_sender.send(TelemetryEvent::ScanQueueStatistics {
9934 fulfilled_requests: 4,
9935 remaining_requests: 12,
9936 });
9937
9938 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
9940
9941 test_helper.drain_cobalt_events(&mut test_fut);
9943 let logged_metrics = test_helper
9944 .get_logged_metrics(metrics::SCAN_QUEUE_STATISTICS_AFTER_COMPLETED_SCAN_METRIC_ID);
9945 assert_eq!(logged_metrics.len(), 1);
9946 assert_eq!(
9947 logged_metrics[0].event_codes,
9948 vec![
9949 metrics::ConnectivityWlanMetricDimensionScanRequestsFulfilled::Four as u32,
9950 metrics::ConnectivityWlanMetricDimensionScanRequestsRemaining::TenToFourteen as u32
9951 ]
9952 );
9953 }
9954
9955 #[fuchsia::test]
9956 fn test_log_post_connection_score_deltas_by_signal_and_post_connection_rssi_deltas() {
9957 let (mut test_helper, mut test_fut) = setup_test();
9958 let connect_time = fasync::MonotonicInstant::from_nanos(31_000_000_000);
9959
9960 let signals_deque: VecDeque<client::types::TimestampedSignal> = VecDeque::from_iter([
9961 client::types::TimestampedSignal {
9962 signal: client::types::Signal { rssi_dbm: -70, snr_db: 10 },
9963 time: connect_time + zx::MonotonicDuration::from_millis(500),
9964 },
9965 client::types::TimestampedSignal {
9966 signal: client::types::Signal { rssi_dbm: -50, snr_db: 30 },
9967 time: connect_time + zx::MonotonicDuration::from_seconds(4),
9968 },
9969 client::types::TimestampedSignal {
9970 signal: client::types::Signal { rssi_dbm: -30, snr_db: 60 },
9971 time: connect_time + zx::MonotonicDuration::from_seconds(9),
9972 },
9973 client::types::TimestampedSignal {
9974 signal: client::types::Signal { rssi_dbm: -10, snr_db: 80 },
9975 time: connect_time + zx::MonotonicDuration::from_seconds(20),
9976 },
9977 ]);
9978 let signals = HistoricalList(signals_deque);
9979 let signal_at_connect = client::types::Signal { rssi_dbm: -90, snr_db: 0 };
9980
9981 test_helper.telemetry_sender.send(TelemetryEvent::PostConnectionSignals {
9982 connect_time,
9983 signal_at_connect,
9984 signals,
9985 });
9986
9987 test_helper.drain_cobalt_events(&mut test_fut);
9989 let logged_metrics = test_helper.get_logged_metrics(
9990 metrics::AVERAGE_SCORE_DELTA_AFTER_CONNECTION_BY_INITIAL_SCORE_METRIC_ID,
9991 );
9992
9993 use metrics::AverageScoreDeltaAfterConnectionByInitialScoreMetricDimensionTimeSinceConnect as DurationDimension;
9994
9995 assert_eq!(logged_metrics.len(), 4);
9997
9998 let mut prev_score = 0;
9999 assert_eq!(logged_metrics[0].event_codes[1], DurationDimension::OneSecond as u32);
10001 assert_matches!(&logged_metrics[0].payload, MetricEventPayload::IntegerValue(delta) => {
10002 assert_gt!(*delta, prev_score);
10003 prev_score = *delta;
10004 });
10005
10006 assert_eq!(logged_metrics[1].event_codes[1], DurationDimension::FiveSeconds as u32);
10008 assert_matches!(&logged_metrics[1].payload, MetricEventPayload::IntegerValue(delta) => {
10009 assert_gt!(*delta, prev_score);
10010 prev_score = *delta;
10011 });
10012 assert_eq!(logged_metrics[2].event_codes[1], DurationDimension::TenSeconds as u32);
10014 assert_matches!(&logged_metrics[2].payload, MetricEventPayload::IntegerValue(delta) => {
10015 assert_gt!(*delta, prev_score);
10016 prev_score = *delta;
10017 });
10018 assert_eq!(logged_metrics[3].event_codes[1], DurationDimension::ThirtySeconds as u32);
10020 assert_matches!(&logged_metrics[3].payload, MetricEventPayload::IntegerValue(delta) => {
10021 assert_gt!(*delta, prev_score);
10022 });
10023
10024 test_helper.drain_cobalt_events(&mut test_fut);
10026 let logged_metrics = test_helper.get_logged_metrics(
10027 metrics::AVERAGE_RSSI_DELTA_AFTER_CONNECTION_BY_INITIAL_RSSI_METRIC_ID,
10028 );
10029 assert_eq!(logged_metrics.len(), 4);
10031
10032 assert_eq!(logged_metrics[0].event_codes[1], DurationDimension::OneSecond as u32);
10034 assert_matches!(&logged_metrics[0].payload, MetricEventPayload::IntegerValue(delta) => {
10035 assert_eq!(*delta, 10);
10036 });
10037
10038 assert_eq!(logged_metrics[1].event_codes[1], DurationDimension::FiveSeconds as u32);
10040 assert_matches!(&logged_metrics[1].payload, MetricEventPayload::IntegerValue(delta) => {
10041 assert_eq!(*delta, 20);
10042 });
10043 assert_eq!(logged_metrics[2].event_codes[1], DurationDimension::TenSeconds as u32);
10045 assert_matches!(&logged_metrics[2].payload, MetricEventPayload::IntegerValue(delta) => {
10046 assert_eq!(*delta, 30);
10047 });
10048 assert_eq!(logged_metrics[3].event_codes[1], DurationDimension::ThirtySeconds as u32);
10050 assert_matches!(&logged_metrics[3].payload, MetricEventPayload::IntegerValue(delta) => {
10051 assert_eq!(*delta, 40);
10052 });
10053 }
10054
10055 #[fuchsia::test]
10056 fn test_log_pre_disconnect_score_deltas_by_signal_and_pre_disconnect_rssi_deltas() {
10057 let (mut test_helper, mut test_fut) = setup_test();
10058 let final_score_time = fasync::MonotonicInstant::from_nanos(31_000_000_000);
10060
10061 let signals_deque: VecDeque<client::types::TimestampedSignal> = VecDeque::from_iter([
10062 client::types::TimestampedSignal {
10063 signal: client::types::Signal { rssi_dbm: -10, snr_db: 80 },
10064 time: final_score_time - zx::MonotonicDuration::from_seconds(20),
10065 },
10066 client::types::TimestampedSignal {
10067 signal: client::types::Signal { rssi_dbm: -30, snr_db: 60 },
10068 time: final_score_time - zx::MonotonicDuration::from_seconds(9),
10069 },
10070 client::types::TimestampedSignal {
10071 signal: client::types::Signal { rssi_dbm: -50, snr_db: 30 },
10072 time: final_score_time - zx::MonotonicDuration::from_seconds(4),
10073 },
10074 client::types::TimestampedSignal {
10075 signal: client::types::Signal { rssi_dbm: -70, snr_db: 10 },
10076 time: final_score_time - zx::MonotonicDuration::from_millis(500),
10077 },
10078 client::types::TimestampedSignal {
10079 signal: client::types::Signal { rssi_dbm: -90, snr_db: 0 },
10080 time: final_score_time,
10081 },
10082 ]);
10083 let signals = HistoricalList(signals_deque);
10084
10085 let disconnect_info = DisconnectInfo {
10086 connected_duration: AVERAGE_SCORE_DELTA_MINIMUM_DURATION,
10087 signals,
10088 ..fake_disconnect_info()
10089 };
10090 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
10091 track_subsequent_downtime: false,
10092 info: Some(disconnect_info),
10093 });
10094
10095 test_helper.drain_cobalt_events(&mut test_fut);
10097 let logged_metrics = test_helper.get_logged_metrics(
10098 metrics::AVERAGE_SCORE_DELTA_BEFORE_DISCONNECT_BY_FINAL_SCORE_METRIC_ID,
10099 );
10100
10101 use metrics::AverageScoreDeltaBeforeDisconnectByFinalScoreMetricDimensionTimeUntilDisconnect as DurationDimension;
10102
10103 assert_eq!(logged_metrics.len(), 4);
10105
10106 let mut prev_score = 0;
10107 assert_eq!(logged_metrics[0].event_codes[1], DurationDimension::OneSecond as u32);
10109 assert_matches!(&logged_metrics[0].payload, MetricEventPayload::IntegerValue(delta) => {
10110 assert_gt!(*delta, prev_score);
10111 prev_score = *delta;
10112 });
10113
10114 assert_eq!(logged_metrics[1].event_codes[1], DurationDimension::FiveSeconds as u32);
10116 assert_matches!(&logged_metrics[1].payload, MetricEventPayload::IntegerValue(delta) => {
10117 assert_gt!(*delta, prev_score);
10118 prev_score = *delta;
10119 });
10120 assert_eq!(logged_metrics[2].event_codes[1], DurationDimension::TenSeconds as u32);
10122 assert_matches!(&logged_metrics[2].payload, MetricEventPayload::IntegerValue(delta) => {
10123 assert_gt!(*delta, prev_score);
10124 prev_score = *delta;
10125 });
10126 assert_eq!(logged_metrics[3].event_codes[1], DurationDimension::ThirtySeconds as u32);
10128 assert_matches!(&logged_metrics[3].payload, MetricEventPayload::IntegerValue(delta) => {
10129 assert_gt!(*delta, prev_score);
10130 });
10131
10132 test_helper.drain_cobalt_events(&mut test_fut);
10134 let logged_metrics = test_helper.get_logged_metrics(
10135 metrics::AVERAGE_RSSI_DELTA_BEFORE_DISCONNECT_BY_FINAL_RSSI_METRIC_ID,
10136 );
10137 assert_eq!(logged_metrics.len(), 4);
10139
10140 assert_eq!(logged_metrics[0].event_codes[1], DurationDimension::OneSecond as u32);
10142 assert_matches!(&logged_metrics[0].payload, MetricEventPayload::IntegerValue(delta) => {
10143 assert_eq!(*delta, 10);
10144 });
10145
10146 assert_eq!(logged_metrics[1].event_codes[1], DurationDimension::FiveSeconds as u32);
10148 assert_matches!(&logged_metrics[1].payload, MetricEventPayload::IntegerValue(delta) => {
10149 assert_eq!(*delta, 20);
10150 });
10151 assert_eq!(logged_metrics[2].event_codes[1], DurationDimension::TenSeconds as u32);
10153 assert_matches!(&logged_metrics[2].payload, MetricEventPayload::IntegerValue(delta) => {
10154 assert_eq!(*delta, 30);
10155 });
10156 assert_eq!(logged_metrics[3].event_codes[1], DurationDimension::ThirtySeconds as u32);
10158 assert_matches!(&logged_metrics[3].payload, MetricEventPayload::IntegerValue(delta) => {
10159 assert_eq!(*delta, 40);
10160 });
10161
10162 let disconnect_info = DisconnectInfo {
10164 connected_duration: AVERAGE_SCORE_DELTA_MINIMUM_DURATION
10165 - zx::MonotonicDuration::from_seconds(1),
10166 ..fake_disconnect_info()
10167 };
10168 test_helper.telemetry_sender.send(TelemetryEvent::Disconnected {
10169 track_subsequent_downtime: false,
10170 info: Some(disconnect_info),
10171 });
10172 test_helper.drain_cobalt_events(&mut test_fut);
10173
10174 let logged_metrics = test_helper.get_logged_metrics(
10176 metrics::AVERAGE_SCORE_DELTA_BEFORE_DISCONNECT_BY_FINAL_SCORE_METRIC_ID,
10177 );
10178 assert_eq!(logged_metrics.len(), 4);
10179 let logged_metrics = test_helper.get_logged_metrics(
10180 metrics::AVERAGE_RSSI_DELTA_BEFORE_DISCONNECT_BY_FINAL_RSSI_METRIC_ID,
10181 );
10182 assert_eq!(logged_metrics.len(), 4);
10183 }
10184
10185 #[fuchsia::test]
10186 fn test_log_network_selection_metrics() {
10187 let (mut test_helper, mut test_fut) = setup_test();
10188
10189 test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
10191 network_selection_type: NetworkSelectionType::Undirected,
10192 num_candidates: Ok(3),
10193 selected_count: 2,
10194 });
10195
10196 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
10198 test_helper.drain_cobalt_events(&mut test_fut);
10199
10200 let logged_metrics =
10202 test_helper.get_logged_metrics(metrics::NETWORK_SELECTION_COUNT_METRIC_ID);
10203 assert_eq!(logged_metrics.len(), 1);
10204 assert_eq!(logged_metrics[0].payload, MetricEventPayload::Count(1));
10205
10206 let logged_metrics =
10208 test_helper.get_logged_metrics(metrics::NUM_NETWORKS_SELECTED_METRIC_ID);
10209 assert_eq!(logged_metrics.len(), 1);
10210 assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(2));
10211
10212 test_helper.telemetry_sender.send(TelemetryEvent::NetworkSelectionDecision {
10214 network_selection_type: NetworkSelectionType::Undirected,
10215 num_candidates: Ok(0),
10216 selected_count: 0,
10217 });
10218
10219 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
10221 test_helper.drain_cobalt_events(&mut test_fut);
10222
10223 let logged_metrics =
10225 test_helper.get_logged_metrics(metrics::NETWORK_SELECTION_COUNT_METRIC_ID);
10226 assert_eq!(logged_metrics.len(), 2);
10227
10228 let logged_metrics =
10231 test_helper.get_logged_metrics(metrics::NUM_NETWORKS_SELECTED_METRIC_ID);
10232 assert_eq!(logged_metrics.len(), 1);
10233 }
10234
10235 #[fuchsia::test]
10236 fn test_log_bss_selection_metrics() {
10237 let (mut test_helper, mut test_fut) = setup_test();
10238
10239 let selected_candidate_2g = client::types::ScannedCandidate {
10241 bss: client::types::Bss {
10242 channel: client::types::WlanChan::new(1, wlan_common::channel::Cbw::Cbw20),
10243 ..generate_random_bss()
10244 },
10245 ..generate_random_scanned_candidate()
10246 };
10247 let candidate_2g = client::types::ScannedCandidate {
10248 bss: client::types::Bss {
10249 channel: client::types::WlanChan::new(1, wlan_common::channel::Cbw::Cbw20),
10250 ..generate_random_bss()
10251 },
10252 ..generate_random_scanned_candidate()
10253 };
10254 let candidate_5g = client::types::ScannedCandidate {
10255 bss: client::types::Bss {
10256 channel: client::types::WlanChan::new(36, wlan_common::channel::Cbw::Cbw40),
10257 ..generate_random_bss()
10258 },
10259 ..generate_random_scanned_candidate()
10260 };
10261 let scored_candidates =
10262 vec![(selected_candidate_2g.clone(), 70), (candidate_2g, 60), (candidate_5g, 50)];
10263
10264 test_helper.telemetry_sender.send(TelemetryEvent::BssSelectionResult {
10265 reason: client::types::ConnectReason::FidlConnectRequest,
10266 scored_candidates: scored_candidates.clone(),
10267 selected_candidate: Some((selected_candidate_2g, 70)),
10268 });
10269
10270 test_helper.drain_cobalt_events(&mut test_fut);
10271
10272 let fidl_connect_event_code = vec![
10273 metrics::PolicyConnectionAttemptMigratedMetricDimensionReason::FidlConnectRequest
10274 as u32,
10275 ];
10276 let logged_metrics = test_helper.get_logged_metrics(metrics::BSS_SELECTION_COUNT_METRIC_ID);
10278 assert_eq!(logged_metrics.len(), 1);
10279 assert_eq!(logged_metrics[0].event_codes, Vec::<u32>::new());
10280 assert_eq!(logged_metrics[0].payload, MetricEventPayload::Count(1));
10281
10282 let logged_metrics =
10283 test_helper.get_logged_metrics(metrics::BSS_SELECTION_COUNT_DETAILED_METRIC_ID);
10284 assert_eq!(logged_metrics.len(), 1);
10285 assert_eq!(logged_metrics[0].event_codes, fidl_connect_event_code);
10286 assert_eq!(logged_metrics[0].payload, MetricEventPayload::Count(1));
10287
10288 let logged_metrics =
10290 test_helper.get_logged_metrics(metrics::NUM_BSS_CONSIDERED_IN_SELECTION_METRIC_ID);
10291 assert_eq!(logged_metrics.len(), 1);
10292 assert_eq!(logged_metrics[0].event_codes, Vec::<u32>::new());
10293 assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(3));
10294
10295 let logged_metrics = test_helper
10296 .get_logged_metrics(metrics::NUM_BSS_CONSIDERED_IN_SELECTION_DETAILED_METRIC_ID);
10297 assert_eq!(logged_metrics.len(), 1);
10298 assert_eq!(logged_metrics[0].event_codes, fidl_connect_event_code);
10299 assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(3));
10300
10301 let logged_metrics = test_helper.get_logged_metrics(metrics::BSS_CANDIDATE_SCORE_METRIC_ID);
10303 assert_eq!(logged_metrics.len(), 3);
10304 for i in 0..3 {
10305 assert_eq!(
10306 logged_metrics[i].payload,
10307 MetricEventPayload::IntegerValue(scored_candidates[i].1 as i64)
10308 )
10309 }
10310
10311 let logged_metrics = test_helper
10313 .get_logged_metrics(metrics::NUM_NETWORKS_REPRESENTED_IN_BSS_SELECTION_METRIC_ID);
10314 assert_eq!(logged_metrics.len(), 1);
10315 assert_eq!(logged_metrics[0].event_codes, fidl_connect_event_code);
10316 assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(3));
10317
10318 let logged_metrics = test_helper.get_logged_metrics(metrics::SELECTED_BSS_SCORE_METRIC_ID);
10320 assert_eq!(logged_metrics.len(), 1);
10321 assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(70));
10322
10323 let logged_metrics =
10325 test_helper.get_logged_metrics(metrics::RUNNER_UP_CANDIDATE_SCORE_DELTA_METRIC_ID);
10326 assert_eq!(logged_metrics.len(), 1);
10327 assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(10));
10328
10329 let logged_metrics =
10331 test_helper.get_logged_metrics(metrics::BEST_CANDIDATES_GHZ_SCORE_DELTA_METRIC_ID);
10332 assert_eq!(logged_metrics.len(), 1);
10333 assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(-20));
10334
10335 let logged_metrics =
10337 test_helper.get_logged_metrics(metrics::GHZ_BANDS_AVAILABLE_IN_BSS_SELECTION_METRIC_ID);
10338 assert_eq!(logged_metrics.len(), 1);
10339 assert_eq!(
10340 logged_metrics[0].event_codes,
10341 vec![metrics::GhzBandsAvailableInBssSelectionMetricDimensionBands::MultiBand as u32]
10342 );
10343 assert_eq!(logged_metrics[0].payload, MetricEventPayload::Count(1));
10344 }
10345
10346 #[fuchsia::test]
10347 fn test_log_bss_selection_metrics_none_selected() {
10348 let (mut test_helper, mut test_fut) = setup_test();
10349
10350 test_helper.telemetry_sender.send(TelemetryEvent::BssSelectionResult {
10351 reason: client::types::ConnectReason::FidlConnectRequest,
10352 scored_candidates: vec![],
10353 selected_candidate: None,
10354 });
10355
10356 test_helper.drain_cobalt_events(&mut test_fut);
10357
10358 assert!(!test_helper.get_logged_metrics(metrics::BSS_SELECTION_COUNT_METRIC_ID).is_empty());
10360 assert!(
10361 !test_helper
10362 .get_logged_metrics(metrics::BSS_SELECTION_COUNT_DETAILED_METRIC_ID)
10363 .is_empty()
10364 );
10365 assert!(
10366 !test_helper
10367 .get_logged_metrics(metrics::NUM_BSS_CONSIDERED_IN_SELECTION_METRIC_ID)
10368 .is_empty()
10369 );
10370 assert!(
10371 !test_helper
10372 .get_logged_metrics(metrics::NUM_BSS_CONSIDERED_IN_SELECTION_DETAILED_METRIC_ID)
10373 .is_empty()
10374 );
10375 assert!(test_helper.get_logged_metrics(metrics::BSS_CANDIDATE_SCORE_METRIC_ID).is_empty());
10376 assert!(
10377 test_helper
10378 .get_logged_metrics(metrics::NUM_NETWORKS_REPRESENTED_IN_BSS_SELECTION_METRIC_ID)
10379 .is_empty()
10380 );
10381 assert!(
10382 test_helper
10383 .get_logged_metrics(metrics::RUNNER_UP_CANDIDATE_SCORE_DELTA_METRIC_ID)
10384 .is_empty()
10385 );
10386 assert!(
10387 test_helper
10388 .get_logged_metrics(metrics::NUM_NETWORKS_REPRESENTED_IN_BSS_SELECTION_METRIC_ID)
10389 .is_empty()
10390 );
10391 assert!(
10392 test_helper
10393 .get_logged_metrics(metrics::BEST_CANDIDATES_GHZ_SCORE_DELTA_METRIC_ID)
10394 .is_empty()
10395 );
10396 assert!(
10397 test_helper
10398 .get_logged_metrics(metrics::GHZ_BANDS_AVAILABLE_IN_BSS_SELECTION_METRIC_ID)
10399 .is_empty()
10400 );
10401 }
10402
10403 #[fuchsia::test]
10404 fn test_log_bss_selection_metrics_runner_up_delta_not_recorded() {
10405 let (mut test_helper, mut test_fut) = setup_test();
10406
10407 let scored_candidates = vec![
10408 (generate_random_scanned_candidate(), 90),
10409 (generate_random_scanned_candidate(), 60),
10410 (generate_random_scanned_candidate(), 50),
10411 ];
10412
10413 test_helper.telemetry_sender.send(TelemetryEvent::BssSelectionResult {
10414 reason: client::types::ConnectReason::FidlConnectRequest,
10415 scored_candidates,
10416 selected_candidate: Some((generate_random_scanned_candidate(), 60)),
10418 });
10419
10420 test_helper.drain_cobalt_events(&mut test_fut);
10421
10422 assert!(
10424 test_helper
10425 .get_logged_metrics(metrics::RUNNER_UP_CANDIDATE_SCORE_DELTA_METRIC_ID)
10426 .is_empty()
10427 );
10428 }
10429
10430 #[fuchsia::test]
10431 fn test_log_connection_score_average_long_duration() {
10432 let (mut test_helper, mut test_fut) = setup_test();
10433 let now = fasync::MonotonicInstant::now();
10434 let signals = vec![
10435 client::types::TimestampedSignal {
10436 signal: client::types::Signal { rssi_dbm: -60, snr_db: 30 },
10437 time: now,
10438 },
10439 client::types::TimestampedSignal {
10440 signal: client::types::Signal { rssi_dbm: -60, snr_db: 30 },
10441 time: now,
10442 },
10443 client::types::TimestampedSignal {
10444 signal: client::types::Signal { rssi_dbm: -80, snr_db: 10 },
10445 time: now,
10446 },
10447 client::types::TimestampedSignal {
10448 signal: client::types::Signal { rssi_dbm: -80, snr_db: 10 },
10449 time: now,
10450 },
10451 ];
10452
10453 test_helper.telemetry_sender.send(TelemetryEvent::LongDurationSignals { signals });
10454 test_helper.drain_cobalt_events(&mut test_fut);
10455
10456 let logged_metrics =
10457 test_helper.get_logged_metrics(metrics::CONNECTION_SCORE_AVERAGE_METRIC_ID);
10458 assert_eq!(logged_metrics.len(), 1);
10459 assert_eq!(
10460 logged_metrics[0].event_codes,
10461 vec![metrics::ConnectionScoreAverageMetricDimensionDuration::LongDuration as u32]
10462 );
10463 assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(55));
10464
10465 test_helper.telemetry_sender.send(TelemetryEvent::LongDurationSignals { signals: vec![] });
10467 test_helper.drain_cobalt_events(&mut test_fut);
10468 assert_eq!(
10469 test_helper.get_logged_metrics(metrics::CONNECTION_SCORE_AVERAGE_METRIC_ID).len(),
10470 1
10471 );
10472 }
10473
10474 #[fuchsia::test]
10475 fn test_log_connection_rssi_average_long_duration() {
10476 let (mut test_helper, mut test_fut) = setup_test();
10477 let now = fasync::MonotonicInstant::now();
10478 let signals = vec![
10479 client::types::TimestampedSignal {
10480 signal: client::types::Signal { rssi_dbm: -60, snr_db: 30 },
10481 time: now,
10482 },
10483 client::types::TimestampedSignal {
10484 signal: client::types::Signal { rssi_dbm: -60, snr_db: 30 },
10485 time: now,
10486 },
10487 client::types::TimestampedSignal {
10488 signal: client::types::Signal { rssi_dbm: -80, snr_db: 10 },
10489 time: now,
10490 },
10491 client::types::TimestampedSignal {
10492 signal: client::types::Signal { rssi_dbm: -80, snr_db: 10 },
10493 time: now,
10494 },
10495 ];
10496
10497 test_helper.telemetry_sender.send(TelemetryEvent::LongDurationSignals { signals });
10498 test_helper.drain_cobalt_events(&mut test_fut);
10499
10500 let logged_metrics =
10501 test_helper.get_logged_metrics(metrics::CONNECTION_RSSI_AVERAGE_METRIC_ID);
10502 assert_eq!(logged_metrics.len(), 1);
10503 assert_eq!(
10504 logged_metrics[0].event_codes,
10505 vec![metrics::ConnectionScoreAverageMetricDimensionDuration::LongDuration as u32]
10506 );
10507 assert_eq!(logged_metrics[0].payload, MetricEventPayload::IntegerValue(-70));
10508
10509 test_helper.telemetry_sender.send(TelemetryEvent::LongDurationSignals { signals: vec![] });
10511 test_helper.drain_cobalt_events(&mut test_fut);
10512 assert_eq!(
10513 test_helper.get_logged_metrics(metrics::CONNECTION_RSSI_AVERAGE_METRIC_ID).len(),
10514 1
10515 );
10516 }
10517
10518 #[test_case(
10519 TimeoutSource::Scan,
10520 metrics::SmeOperationTimeoutMetricDimensionStalledOperation::Scan_ ;
10521 "log scan timeout"
10522 )]
10523 #[test_case(
10524 TimeoutSource::Connect,
10525 metrics::SmeOperationTimeoutMetricDimensionStalledOperation::Connect_ ;
10526 "log connect"
10527 )]
10528 #[test_case(
10529 TimeoutSource::Disconnect,
10530 metrics::SmeOperationTimeoutMetricDimensionStalledOperation::Disconnect_ ;
10531 "log disconnect timeout"
10532 )]
10533 #[test_case(
10534 TimeoutSource::ClientStatus,
10535 metrics::SmeOperationTimeoutMetricDimensionStalledOperation::ClientStatus_ ;
10536 "log client status timeout"
10537 )]
10538 #[test_case(
10539 TimeoutSource::WmmStatus,
10540 metrics::SmeOperationTimeoutMetricDimensionStalledOperation::WmmStatus_ ;
10541 "log WMM status timeout"
10542 )]
10543 #[test_case(
10544 TimeoutSource::ApStart,
10545 metrics::SmeOperationTimeoutMetricDimensionStalledOperation::ApStart_ ;
10546 "log AP start timeout"
10547 )]
10548 #[test_case(
10549 TimeoutSource::ApStop,
10550 metrics::SmeOperationTimeoutMetricDimensionStalledOperation::ApStop_ ;
10551 "log Ap stop timeout"
10552 )]
10553 #[test_case(
10554 TimeoutSource::ApStatus,
10555 metrics::SmeOperationTimeoutMetricDimensionStalledOperation::ApStatus_ ;
10556 "log AP status timeout"
10557 )]
10558 #[test_case(
10559 TimeoutSource::GetIfaceStats,
10560 metrics::SmeOperationTimeoutMetricDimensionStalledOperation::GetCounterStats_ ;
10561 "log iface stats timeout"
10562 )]
10563 #[test_case(
10564 TimeoutSource::GetHistogramStats,
10565 metrics::SmeOperationTimeoutMetricDimensionStalledOperation::GetHistogramStats_ ;
10566 "log histogram stats timeout"
10567 )]
10568 #[fuchsia::test(add_test_attr = false)]
10569 fn test_log_sme_timeout(
10570 source: TimeoutSource,
10571 expected_dimension: metrics::SmeOperationTimeoutMetricDimensionStalledOperation,
10572 ) {
10573 let (mut test_helper, mut test_fut) = setup_test();
10574
10575 test_helper.telemetry_sender.send(TelemetryEvent::SmeTimeout { source });
10577
10578 assert_matches!(test_helper.advance_test_fut(&mut test_fut), Poll::Pending);
10580
10581 assert_matches!(
10583 test_helper.exec.run_until_stalled(&mut test_helper.cobalt_stream.next()),
10584 Poll::Ready(Some(Ok(fidl_fuchsia_metrics::MetricEventLoggerRequest::LogOccurrence {
10585 metric_id, event_codes, responder, ..
10586 }))) => {
10587 assert_eq!(metric_id, metrics::SME_OPERATION_TIMEOUT_METRIC_ID);
10588 assert_eq!(event_codes, vec![expected_dimension.as_event_code()]);
10589
10590 assert!(responder.send(Ok(())).is_ok());
10591 });
10592 }
10593
10594 struct TestHelper {
10595 telemetry_sender: TelemetrySender,
10596 inspector: Inspector,
10597 monitor_svc_stream: fidl_fuchsia_wlan_device_service::DeviceMonitorRequestStream,
10598 telemetry_svc_stream: Option<fidl_fuchsia_wlan_sme::TelemetryRequestStream>,
10599 cobalt_stream: fidl_fuchsia_metrics::MetricEventLoggerRequestStream,
10600 iface_stats_resp:
10601 Option<Box<dyn Fn() -> fidl_fuchsia_wlan_sme::TelemetryGetIfaceStatsResult>>,
10602 cobalt_events: Vec<MetricEvent>,
10605 _defect_receiver: mpsc::Receiver<Defect>,
10606
10607 exec: fasync::TestExecutor,
10609 }
10610
10611 impl TestHelper {
10612 fn advance_test_fut<T>(
10616 &mut self,
10617 test_fut: &mut (impl Future<Output = T> + Unpin),
10618 ) -> Poll<T> {
10619 let result = self.exec.run_until_stalled(test_fut);
10620 if let Poll::Ready(Some(Ok(req))) =
10621 self.exec.run_until_stalled(&mut self.monitor_svc_stream.next())
10622 {
10623 match req {
10624 fidl_fuchsia_wlan_device_service::DeviceMonitorRequest::GetSmeTelemetry {
10625 iface_id,
10626 telemetry_server,
10627 responder,
10628 } => {
10629 assert_eq!(iface_id, IFACE_ID);
10630 let telemetry_stream = telemetry_server.into_stream();
10631 responder.send(Ok(())).expect("Failed to respond to telemetry request");
10632 self.telemetry_svc_stream = Some(telemetry_stream);
10633 self.exec.run_until_stalled(test_fut)
10634 }
10635 _ => panic!("Unexpected device monitor request: {req:?}"),
10636 }
10637 } else {
10638 result
10639 }
10640 }
10641
10642 fn advance_by(
10647 &mut self,
10648 duration: zx::MonotonicDuration,
10649 mut test_fut: Pin<&mut impl Future<Output = ()>>,
10650 ) {
10651 let target_time = self.exec.now() + duration;
10652
10653 assert_eq!(self.advance_test_fut(&mut test_fut), Poll::Pending);
10661 if let Some(telemetry_svc_stream) = &mut self.telemetry_svc_stream
10662 && !telemetry_svc_stream.is_terminated()
10663 {
10664 respond_iface_counter_stats_req(
10665 &mut self.exec,
10666 telemetry_svc_stream,
10667 &self.iface_stats_resp,
10668 );
10669 }
10670 self.drain_cobalt_events(&mut test_fut);
10671 assert_eq!(self.advance_test_fut(&mut test_fut), Poll::Pending);
10672
10673 while self.exec.now() < target_time {
10674 let next_timer = fasync::TestExecutor::next_timer();
10675 let next_wake = match next_timer {
10676 Some(time) if time <= target_time => time,
10677 _ => target_time,
10678 };
10679
10680 if next_wake > self.exec.now() {
10681 self.exec.set_fake_time(next_wake);
10682 }
10683
10684 let _ = self.exec.wake_expired_timers();
10685 assert_eq!(self.advance_test_fut(&mut test_fut), Poll::Pending);
10686
10687 if let Some(telemetry_svc_stream) = &mut self.telemetry_svc_stream
10688 && !telemetry_svc_stream.is_terminated()
10689 {
10690 respond_iface_counter_stats_req(
10691 &mut self.exec,
10692 telemetry_svc_stream,
10693 &self.iface_stats_resp,
10694 );
10695 }
10696
10697 self.drain_cobalt_events(&mut test_fut);
10700
10701 assert_eq!(self.advance_test_fut(&mut test_fut), Poll::Pending);
10702 }
10703 }
10704
10705 fn set_iface_stats_resp(
10706 &mut self,
10707 iface_stats_resp: Box<dyn Fn() -> fidl_fuchsia_wlan_sme::TelemetryGetIfaceStatsResult>,
10708 ) {
10709 let _ = self.iface_stats_resp.replace(iface_stats_resp);
10710 }
10711
10712 fn advance_to_next_telemetry_checkpoint(
10718 &mut self,
10719 test_fut: Pin<&mut impl Future<Output = ()>>,
10720 ) {
10721 let now = fasync::MonotonicInstant::now();
10722 let remaining_interval = TELEMETRY_QUERY_INTERVAL.into_nanos()
10723 - (now.into_nanos() % TELEMETRY_QUERY_INTERVAL.into_nanos());
10724 self.advance_by(zx::MonotonicDuration::from_nanos(remaining_interval), test_fut)
10725 }
10726
10727 fn drain_cobalt_events(&mut self, test_fut: &mut (impl Future + Unpin)) {
10730 let mut made_progress = true;
10731 while made_progress {
10732 let _result = self.advance_test_fut(test_fut);
10733 made_progress = false;
10734 while let Poll::Ready(Some(Ok(req))) =
10735 self.exec.run_until_stalled(&mut self.cobalt_stream.next())
10736 {
10737 self.cobalt_events.append(&mut req.respond_to_metric_req(Ok(())));
10738 made_progress = true;
10739 }
10740 }
10741 }
10742
10743 fn get_logged_metrics(&self, metric_id: u32) -> Vec<MetricEvent> {
10744 self.cobalt_events.iter().filter(|ev| ev.metric_id == metric_id).cloned().collect()
10745 }
10746
10747 fn send_connected_event(&mut self, ap_state: impl Into<client::types::ApState>) {
10748 let event = TelemetryEvent::ConnectResult {
10749 iface_id: IFACE_ID,
10750 policy_connect_reason: Some(
10751 client::types::ConnectReason::RetryAfterFailedConnectAttempt,
10752 ),
10753 result: fake_connect_result(fidl_ieee80211::StatusCode::Success),
10754 multiple_bss_candidates: true,
10755 ap_state: ap_state.into(),
10756 network_is_likely_hidden: true,
10757 };
10758 self.telemetry_sender.send(event);
10759 }
10760
10761 fn clear_cobalt_events(&mut self) {
10764 self.cobalt_events = Vec::new();
10765 }
10766
10767 fn get_time_series(
10768 &mut self,
10769 test_fut: &mut (impl Future<Output = ()> + Unpin),
10770 ) -> Arc<Mutex<TimeSeriesStats>> {
10771 let (sender, mut receiver) = oneshot::channel();
10772 self.telemetry_sender.send(TelemetryEvent::GetTimeSeries { sender });
10773 assert_matches!(self.advance_test_fut(test_fut), Poll::Pending);
10774 self.drain_cobalt_events(test_fut);
10775 assert_matches!(receiver.try_recv(), Ok(Some(stats)) => stats)
10776 }
10777 }
10778
10779 fn respond_iface_counter_stats_req(
10780 executor: &mut fasync::TestExecutor,
10781 telemetry_svc_stream: &mut fidl_fuchsia_wlan_sme::TelemetryRequestStream,
10782 iface_stats_resp: &Option<
10783 Box<dyn Fn() -> fidl_fuchsia_wlan_sme::TelemetryGetIfaceStatsResult>,
10784 >,
10785 ) {
10786 let telemetry_svc_req_fut = telemetry_svc_stream.try_next();
10787 let mut telemetry_svc_req_fut = pin!(telemetry_svc_req_fut);
10788 if let Poll::Ready(Ok(Some(request))) =
10789 executor.run_until_stalled(&mut telemetry_svc_req_fut)
10790 {
10791 match request {
10792 fidl_fuchsia_wlan_sme::TelemetryRequest::GetIfaceStats { responder } => {
10793 let resp = match &iface_stats_resp {
10794 Some(get_resp) => get_resp(),
10795 None => {
10796 let seed = fasync::MonotonicInstant::now().into_nanos() as u64;
10797 Ok(fidl_fuchsia_wlan_stats::IfaceStats {
10798 connection_stats: Some(fake_connection_stats(seed)),
10799 ..Default::default()
10800 })
10801 }
10802 };
10803 responder
10804 .send(resp.as_ref().map_err(|e| *e))
10805 .expect("expect sending GetIfaceStats response to succeed");
10806 }
10807 _ => {
10808 panic!("unexpected request: {request:?}");
10809 }
10810 }
10811 }
10812 }
10813
10814 fn respond_iface_histogram_stats_req(
10815 executor: &mut fasync::TestExecutor,
10816 telemetry_svc_stream: &mut fidl_fuchsia_wlan_sme::TelemetryRequestStream,
10817 ) {
10818 let telemetry_svc_req_fut = telemetry_svc_stream.try_next();
10819 let mut telemetry_svc_req_fut = pin!(telemetry_svc_req_fut);
10820 if let Poll::Ready(Ok(Some(request))) =
10821 executor.run_until_stalled(&mut telemetry_svc_req_fut)
10822 {
10823 match request {
10824 fidl_fuchsia_wlan_sme::TelemetryRequest::GetHistogramStats { responder } => {
10825 responder
10826 .send(Ok(&fake_iface_histogram_stats()))
10827 .expect("expect sending GetHistogramStats response to succeed");
10828 }
10829 _ => {
10830 panic!("unexpected request: {request:?}");
10831 }
10832 }
10833 }
10834 }
10835
10836 #[track_caller]
10838 fn assert_eq_cobalt_events(
10839 mut left: Vec<fidl_fuchsia_metrics::MetricEvent>,
10840 mut right: Vec<fidl_fuchsia_metrics::MetricEvent>,
10841 ) {
10842 left.sort_by(metric_event_cmp);
10843 right.sort_by(metric_event_cmp);
10844 assert_eq!(left, right);
10845 }
10846
10847 fn metric_event_cmp(
10848 left: &fidl_fuchsia_metrics::MetricEvent,
10849 right: &fidl_fuchsia_metrics::MetricEvent,
10850 ) -> std::cmp::Ordering {
10851 match left.metric_id.cmp(&right.metric_id) {
10852 std::cmp::Ordering::Equal => match left.event_codes.len().cmp(&right.event_codes.len())
10853 {
10854 std::cmp::Ordering::Equal => (),
10855 ordering => return ordering,
10856 },
10857 ordering => return ordering,
10858 }
10859
10860 for i in 0..left.event_codes.len() {
10861 match left.event_codes[i].cmp(&right.event_codes[i]) {
10862 std::cmp::Ordering::Equal => (),
10863 ordering => return ordering,
10864 }
10865 }
10866
10867 match (&left.payload, &right.payload) {
10868 (MetricEventPayload::Count(v1), MetricEventPayload::Count(v2)) => v1.cmp(v2),
10869 (MetricEventPayload::IntegerValue(v1), MetricEventPayload::IntegerValue(v2)) => {
10870 v1.cmp(v2)
10871 }
10872 (MetricEventPayload::StringValue(v1), MetricEventPayload::StringValue(v2)) => {
10873 v1.cmp(v2)
10874 }
10875 (MetricEventPayload::Histogram(_), MetricEventPayload::Histogram(_)) => {
10876 unimplemented!()
10877 }
10878 _ => unimplemented!(),
10879 }
10880 }
10881
10882 trait CobaltExt {
10883 fn respond_to_metric_req(
10885 self,
10886 result: Result<(), fidl_fuchsia_metrics::Error>,
10887 ) -> Vec<fidl_fuchsia_metrics::MetricEvent>;
10888 }
10889
10890 impl CobaltExt for MetricEventLoggerRequest {
10891 fn respond_to_metric_req(
10892 self,
10893 result: Result<(), fidl_fuchsia_metrics::Error>,
10894 ) -> Vec<fidl_fuchsia_metrics::MetricEvent> {
10895 match self {
10896 Self::LogOccurrence { metric_id, count, event_codes, responder } => {
10897 assert!(responder.send(result).is_ok());
10898 vec![MetricEvent {
10899 metric_id,
10900 event_codes,
10901 payload: MetricEventPayload::Count(count),
10902 }]
10903 }
10904 Self::LogInteger { metric_id, value, event_codes, responder } => {
10905 assert!(responder.send(result).is_ok());
10906 vec![MetricEvent {
10907 metric_id,
10908 event_codes,
10909 payload: MetricEventPayload::IntegerValue(value),
10910 }]
10911 }
10912 Self::LogIntegerHistogram { metric_id, histogram, event_codes, responder } => {
10913 assert!(responder.send(result).is_ok());
10914 vec![MetricEvent {
10915 metric_id,
10916 event_codes,
10917 payload: MetricEventPayload::Histogram(histogram),
10918 }]
10919 }
10920 Self::LogString { metric_id, string_value, event_codes, responder } => {
10921 assert!(responder.send(result).is_ok());
10922 vec![MetricEvent {
10923 metric_id,
10924 event_codes,
10925 payload: MetricEventPayload::StringValue(string_value),
10926 }]
10927 }
10928 Self::LogMetricEvents { events, responder } => {
10929 assert!(responder.send(result).is_ok());
10930 events
10931 }
10932 }
10933 }
10934 }
10935
10936 fn setup_test() -> (TestHelper, Pin<Box<impl Future<Output = ()>>>) {
10937 let mut exec = fasync::TestExecutor::new_with_fake_time();
10938 exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
10939
10940 let (monitor_svc_proxy, monitor_svc_stream) =
10941 create_proxy_and_stream::<fidl_fuchsia_wlan_device_service::DeviceMonitorMarker>();
10942
10943 let (cobalt_proxy, cobalt_stream) =
10944 create_proxy_and_stream::<fidl_fuchsia_metrics::MetricEventLoggerMarker>();
10945
10946 let inspector = Inspector::default();
10947 let inspect_node = inspector.root().create_child("stats");
10948 let external_inspect_node = inspector.root().create_child("external");
10949 let (defect_sender, _defect_receiver) = mpsc::channel(100);
10950 let (telemetry_sender, test_fut) = serve_telemetry(
10951 monitor_svc_proxy,
10952 cobalt_proxy.clone(),
10953 inspect_node,
10954 external_inspect_node.create_child("stats"),
10955 defect_sender,
10956 );
10957 inspector.root().record(external_inspect_node);
10958 let mut test_fut = Box::pin(test_fut);
10959
10960 assert_eq!(exec.run_until_stalled(&mut test_fut), Poll::Pending);
10961
10962 let test_helper = TestHelper {
10963 telemetry_sender,
10964 inspector,
10965 monitor_svc_stream,
10966 telemetry_svc_stream: None,
10967 cobalt_stream,
10968 iface_stats_resp: None,
10969 cobalt_events: vec![],
10970 _defect_receiver,
10971 exec,
10972 };
10973 (test_helper, test_fut)
10974 }
10975
10976 fn fake_connection_stats(nth_req: u64) -> fidl_fuchsia_wlan_stats::ConnectionStats {
10977 fidl_fuchsia_wlan_stats::ConnectionStats {
10978 connection_id: Some(1),
10979 rx_unicast_total: Some(nth_req),
10980 rx_unicast_drop: Some(0),
10981 rx_multicast: Some(2 * nth_req),
10982 tx_total: Some(nth_req),
10983 tx_drop: Some(0),
10984 ..Default::default()
10985 }
10986 }
10987
10988 fn fake_iface_histogram_stats() -> fidl_fuchsia_wlan_stats::IfaceHistogramStats {
10989 fidl_fuchsia_wlan_stats::IfaceHistogramStats {
10990 noise_floor_histograms: Some(fake_noise_floor_histograms()),
10991 rssi_histograms: Some(fake_rssi_histograms()),
10992 rx_rate_index_histograms: Some(fake_rx_rate_index_histograms()),
10993 snr_histograms: Some(fake_snr_histograms()),
10994 ..Default::default()
10995 }
10996 }
10997
10998 fn fake_noise_floor_histograms() -> Vec<fidl_fuchsia_wlan_stats::NoiseFloorHistogram> {
10999 vec![fidl_fuchsia_wlan_stats::NoiseFloorHistogram {
11000 hist_scope: fidl_fuchsia_wlan_stats::HistScope::PerAntenna,
11001 antenna_id: Some(Box::new(fidl_fuchsia_wlan_stats::AntennaId {
11002 freq: fidl_fuchsia_wlan_stats::AntennaFreq::Antenna2G,
11003 index: 0,
11004 })),
11005 noise_floor_samples: vec![
11006 fidl_fuchsia_wlan_stats::HistBucket { bucket_index: 199, num_samples: 0 },
11009 fidl_fuchsia_wlan_stats::HistBucket { bucket_index: 200, num_samples: 999 },
11010 ],
11011 invalid_samples: 44,
11012 }]
11013 }
11014
11015 fn fake_rssi_histograms() -> Vec<fidl_fuchsia_wlan_stats::RssiHistogram> {
11016 vec![fidl_fuchsia_wlan_stats::RssiHistogram {
11017 hist_scope: fidl_fuchsia_wlan_stats::HistScope::PerAntenna,
11018 antenna_id: Some(Box::new(fidl_fuchsia_wlan_stats::AntennaId {
11019 freq: fidl_fuchsia_wlan_stats::AntennaFreq::Antenna2G,
11020 index: 0,
11021 })),
11022 rssi_samples: vec![fidl_fuchsia_wlan_stats::HistBucket {
11023 bucket_index: 230,
11024 num_samples: 999,
11025 }],
11026 invalid_samples: 55,
11027 }]
11028 }
11029
11030 fn fake_rx_rate_index_histograms() -> Vec<fidl_fuchsia_wlan_stats::RxRateIndexHistogram> {
11031 vec![
11032 fidl_fuchsia_wlan_stats::RxRateIndexHistogram {
11033 hist_scope: fidl_fuchsia_wlan_stats::HistScope::Station,
11034 antenna_id: None,
11035 rx_rate_index_samples: vec![fidl_fuchsia_wlan_stats::HistBucket {
11036 bucket_index: 99,
11037 num_samples: 1400,
11038 }],
11039 invalid_samples: 22,
11040 },
11041 fidl_fuchsia_wlan_stats::RxRateIndexHistogram {
11042 hist_scope: fidl_fuchsia_wlan_stats::HistScope::PerAntenna,
11043 antenna_id: Some(Box::new(fidl_fuchsia_wlan_stats::AntennaId {
11044 freq: fidl_fuchsia_wlan_stats::AntennaFreq::Antenna5G,
11045 index: 1,
11046 })),
11047 rx_rate_index_samples: vec![fidl_fuchsia_wlan_stats::HistBucket {
11048 bucket_index: 100,
11049 num_samples: 1500,
11050 }],
11051 invalid_samples: 33,
11052 },
11053 ]
11054 }
11055
11056 fn fake_snr_histograms() -> Vec<fidl_fuchsia_wlan_stats::SnrHistogram> {
11057 vec![fidl_fuchsia_wlan_stats::SnrHistogram {
11058 hist_scope: fidl_fuchsia_wlan_stats::HistScope::PerAntenna,
11059 antenna_id: Some(Box::new(fidl_fuchsia_wlan_stats::AntennaId {
11060 freq: fidl_fuchsia_wlan_stats::AntennaFreq::Antenna2G,
11061 index: 0,
11062 })),
11063 snr_samples: vec![fidl_fuchsia_wlan_stats::HistBucket {
11064 bucket_index: 30,
11065 num_samples: 999,
11066 }],
11067 invalid_samples: 11,
11068 }]
11069 }
11070
11071 fn fake_disconnect_info() -> DisconnectInfo {
11072 let is_sme_reconnecting = false;
11073 let fidl_disconnect_info = generate_disconnect_info(is_sme_reconnecting);
11074 DisconnectInfo {
11075 connected_duration: zx::MonotonicDuration::from_hours(6),
11076 is_sme_reconnecting: fidl_disconnect_info.is_sme_reconnecting,
11077 disconnect_source: fidl_disconnect_info.disconnect_source,
11078 previous_connect_reason: client::types::ConnectReason::IdleInterfaceAutoconnect,
11079 ap_state: random_bss_description!(Wpa2).into(),
11080 signals: HistoricalList::new(8),
11081 }
11082 }
11083
11084 fn fake_connect_result(code: fidl_ieee80211::StatusCode) -> fidl_sme::ConnectResult {
11085 fidl_sme::ConnectResult { code, is_credential_rejected: false, is_reconnect: false }
11086 }
11087
11088 #[fuchsia::test]
11089 fn test_error_throttling() {
11090 let exec = fasync::TestExecutor::new_with_fake_time();
11091 exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
11092 let mut error_logger = ThrottledErrorLogger::new(MINUTES_BETWEEN_COBALT_SYSLOG_WARNINGS);
11093
11094 exec.set_fake_time(fasync::MonotonicInstant::after(
11096 fasync::MonotonicDuration::from_minutes(MINUTES_BETWEEN_COBALT_SYSLOG_WARNINGS + 1),
11097 ));
11098
11099 error_logger.throttle_error(Err(format_err!("")));
11102 assert!(!error_logger.suppressed_errors.contains_key(&String::from("")));
11103
11104 error_logger.throttle_error(Err(format_err!("")));
11106 assert_eq!(error_logger.suppressed_errors[&String::from("")], 1);
11107
11108 exec.set_fake_time(fasync::MonotonicInstant::after(
11111 fasync::MonotonicDuration::from_minutes(MINUTES_BETWEEN_COBALT_SYSLOG_WARNINGS + 1),
11112 ));
11113 error_logger.throttle_error(Err(format_err!("")));
11114 assert!(!error_logger.suppressed_errors.contains_key(&String::from("")));
11115
11116 error_logger.throttle_error(Err(format_err!("")));
11118 assert_eq!(error_logger.suppressed_errors[&String::from("")], 1);
11119 }
11120}