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