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