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