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