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