1use crate::processors::toggle_events::ClientConnectionsToggleEvent;
6use crate::util::cobalt_logger::log_cobalt_batch;
7use derivative::Derivative;
8use fidl_fuchsia_metrics::{MetricEvent, MetricEventPayload};
9use fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211;
10use fidl_fuchsia_wlan_sme as fidl_sme;
11use fuchsia_async as fasync;
12use fuchsia_inspect::Node as InspectNode;
13use fuchsia_inspect_contrib::id_enum::IdEnum;
14use fuchsia_inspect_contrib::inspect_log;
15use fuchsia_inspect_contrib::nodes::{BoundedListNode, LruCacheNode};
16use fuchsia_inspect_derive::Unit;
17use fuchsia_sync::Mutex;
18use std::sync::Arc;
19use std::sync::atomic::{AtomicUsize, Ordering};
20use strum_macros::{Display, EnumIter};
21use windowed_stats::experimental::inspect::{InspectSender, InspectedTimeMatrix};
22use windowed_stats::experimental::series::interpolation::{ConstantSample, LastSample};
23use windowed_stats::experimental::series::metadata::{BitSetMap, BitSetNode};
24use windowed_stats::experimental::series::statistic::Union;
25use windowed_stats::experimental::series::{SamplingProfile, TimeMatrix};
26use wlan_common::bss::BssDescription;
27use wlan_common::channel::Channel;
28use wlan_legacy_metrics_registry as metrics;
29use zx;
30
31const INSPECT_CONNECT_EVENTS_LIMIT: usize = 10;
32const INSPECT_DISCONNECT_EVENTS_LIMIT: usize = 20;
33const INSPECT_CONNECT_ATTEMPT_RESULTS_LIMIT: usize = 50;
34const INSPECT_CONNECTED_NETWORKS_ID_LIMIT: usize = 16;
35const INSPECT_DISCONNECT_SOURCES_ID_LIMIT: usize = 32;
36const INSPECT_CONNECT_ATTEMPT_RESULTS_ID_LIMIT: usize = 32;
37const SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_TIMEOUT: zx::BootDuration =
38 zx::BootDuration::from_minutes(2);
39
40#[derive(Clone, Debug, Display, EnumIter)]
41enum ConnectionState {
42 Idle(IdleState),
43 Connected(ConnectedState),
44 Disconnected(DisconnectedState),
45 ConnectFailed(ConnectFailedState),
46 FailedToStart(FailedToStartState),
47 FailedToStop(FailedToStopState),
48 PnoScanFailedIdle(PnoScanFailedIdleState),
49}
50
51impl IdEnum for ConnectionState {
53 type Id = u8;
54 fn to_id(&self) -> Self::Id {
55 match self {
56 Self::Idle(_) => 0,
57 Self::Disconnected(_) => 1,
58 Self::ConnectFailed(_) => 2,
59 Self::Connected(_) => 3,
60 Self::FailedToStart(_) => 4,
61 Self::FailedToStop(_) => 5,
62 Self::PnoScanFailedIdle(_) => 6,
63 }
64 }
65}
66
67#[derive(Clone, Debug, Default)]
68struct IdleState {}
69
70#[derive(Clone, Debug, Default)]
71struct ConnectedState {}
72
73#[derive(Clone, Debug, Default)]
74struct DisconnectedState {}
75
76#[derive(Clone, Debug, Default)]
77struct ConnectFailedState {}
78
79#[derive(Clone, Debug, Default)]
80struct FailedToStartState {}
81
82#[derive(Clone, Debug, Default)]
83struct FailedToStopState {}
84
85#[derive(Clone, Debug, Default)]
86struct PnoScanFailedIdleState {}
87
88#[derive(Derivative, Unit)]
89#[derivative(PartialEq, Eq, Hash)]
90struct InspectConnectedNetwork {
91 bssid: String,
92 ssid: String,
93 protection: String,
94 ht_cap: Option<Vec<u8>>,
95 vht_cap: Option<Vec<u8>>,
96 #[derivative(PartialEq = "ignore")]
97 #[derivative(Hash = "ignore")]
98 wsc: Option<InspectNetworkWsc>,
99 is_wmm_assoc: bool,
100 wmm_param: Option<Vec<u8>>,
101}
102
103impl From<&BssDescription> for InspectConnectedNetwork {
104 fn from(bss_description: &BssDescription) -> Self {
105 Self {
106 bssid: bss_description.bssid.to_string(),
107 ssid: bss_description.ssid.to_string(),
108 protection: format!("{:?}", bss_description.protection()),
109 ht_cap: bss_description.raw_ht_cap().map(|cap| cap.bytes.into()),
110 vht_cap: bss_description.raw_vht_cap().map(|cap| cap.bytes.into()),
111 wsc: bss_description.probe_resp_wsc().as_ref().map(InspectNetworkWsc::from),
112 is_wmm_assoc: bss_description.find_wmm_param().is_some(),
113 wmm_param: bss_description.find_wmm_param().map(|bytes| bytes.into()),
114 }
115 }
116}
117
118#[derive(PartialEq, Unit, Hash)]
119struct InspectNetworkWsc {
120 device_name: String,
121 manufacturer: String,
122 model_name: String,
123 model_number: String,
124}
125
126impl From<&wlan_common::ie::wsc::ProbeRespWsc> for InspectNetworkWsc {
127 fn from(wsc: &wlan_common::ie::wsc::ProbeRespWsc) -> Self {
128 Self {
129 device_name: String::from_utf8_lossy(&wsc.device_name[..]).to_string(),
130 manufacturer: String::from_utf8_lossy(&wsc.manufacturer[..]).to_string(),
131 model_name: String::from_utf8_lossy(&wsc.model_name[..]).to_string(),
132 model_number: String::from_utf8_lossy(&wsc.model_number[..]).to_string(),
133 }
134 }
135}
136
137#[derive(PartialEq, Eq, Unit, Hash)]
138struct InspectConnectAttemptResult {
139 status_code: u16,
140 result: String,
141}
142
143#[derive(PartialEq, Eq, Unit, Hash)]
144struct InspectDisconnectSource {
145 source: String,
146 reason: String,
147 mlme_event_name: Option<String>,
148}
149
150impl From<&fidl_sme::DisconnectSource> for InspectDisconnectSource {
151 fn from(disconnect_source: &fidl_sme::DisconnectSource) -> Self {
152 match disconnect_source {
153 fidl_sme::DisconnectSource::User(reason) => Self {
154 source: "user".to_string(),
155 reason: format!("{reason:?}"),
156 mlme_event_name: None,
157 },
158 fidl_sme::DisconnectSource::Ap(cause) => Self {
159 source: "ap".to_string(),
160 reason: format!("{:?}", cause.reason_code),
161 mlme_event_name: Some(format!("{:?}", cause.mlme_event_name)),
162 },
163 fidl_sme::DisconnectSource::Mlme(cause) => Self {
164 source: "mlme".to_string(),
165 reason: format!("{:?}", cause.reason_code),
166 mlme_event_name: Some(format!("{:?}", cause.mlme_event_name)),
167 },
168 }
169 }
170}
171
172#[derive(Clone, Debug, PartialEq)]
173pub struct DisconnectInfo {
174 pub iface_id: u16,
175 pub connected_duration: zx::BootDuration,
176 pub is_sme_reconnecting: bool,
177 pub disconnect_source: fidl_sme::DisconnectSource,
178 pub original_bss_desc: Box<BssDescription>,
179 pub current_rssi_dbm: i8,
180 pub current_snr_db: i8,
181 pub current_channel: Channel,
182}
183
184pub struct ConnectDisconnectLogger {
185 connection_state: Arc<Mutex<ConnectionState>>,
186 cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
187 connect_events_node: Mutex<BoundedListNode>,
188 disconnect_events_node: Mutex<BoundedListNode>,
189 connect_attempt_results_node: Mutex<BoundedListNode>,
190 inspect_metadata_node: Mutex<InspectMetadataNode>,
191 time_series_stats: ConnectDisconnectTimeSeries,
192 successive_connect_attempt_failures: AtomicUsize,
193 last_connect_failure_at: Arc<Mutex<Option<fasync::BootInstant>>>,
194 last_disconnect_at: Arc<Mutex<Option<fasync::MonotonicInstant>>>,
195}
196
197impl ConnectDisconnectLogger {
198 pub fn new<S: InspectSender>(
199 cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
200 inspect_node: &InspectNode,
201 inspect_metadata_node: &InspectNode,
202 inspect_metadata_path: &str,
203 time_matrix_client: &S,
204 ) -> Self {
205 let connect_events = inspect_node.create_child("connect_events");
206 let disconnect_events = inspect_node.create_child("disconnect_events");
207 let connect_attempt_results = inspect_node.create_child("connect_attempt_results");
208 let this = Self {
209 cobalt_proxy,
210 connection_state: Arc::new(Mutex::new(ConnectionState::Idle(IdleState {}))),
211 connect_events_node: Mutex::new(BoundedListNode::new(
212 connect_events,
213 INSPECT_CONNECT_EVENTS_LIMIT,
214 )),
215 disconnect_events_node: Mutex::new(BoundedListNode::new(
216 disconnect_events,
217 INSPECT_DISCONNECT_EVENTS_LIMIT,
218 )),
219 connect_attempt_results_node: Mutex::new(BoundedListNode::new(
220 connect_attempt_results,
221 INSPECT_CONNECT_ATTEMPT_RESULTS_LIMIT,
222 )),
223 inspect_metadata_node: Mutex::new(InspectMetadataNode::new(inspect_metadata_node)),
224 time_series_stats: ConnectDisconnectTimeSeries::new(
225 time_matrix_client,
226 inspect_metadata_path,
227 ),
228 successive_connect_attempt_failures: AtomicUsize::new(0),
229 last_connect_failure_at: Arc::new(Mutex::new(None)),
230 last_disconnect_at: Arc::new(Mutex::new(None)),
231 };
232 this.log_connection_state();
233 this
234 }
235
236 fn update_connection_state(&self, state: ConnectionState) {
237 *self.connection_state.lock() = state;
238 self.log_connection_state();
239 }
240
241 fn log_connection_state(&self) {
242 let wlan_connectivity_state_id = self.connection_state.lock().to_id() as u64;
243 self.time_series_stats.log_wlan_connectivity_state(1 << wlan_connectivity_state_id);
244 }
245
246 pub fn is_connected(&self) -> bool {
247 matches!(*self.connection_state.lock(), ConnectionState::Connected(_))
248 }
249
250 pub async fn handle_connect_attempt(
251 &self,
252 result: fidl_ieee80211::StatusCode,
253 bss: &BssDescription,
254 is_credential_rejected: bool,
255 ) {
256 let mut flushed_successive_failures = None;
257 let mut downtime_duration = None;
258 if result == fidl_ieee80211::StatusCode::Success {
259 self.update_connection_state(ConnectionState::Connected(ConnectedState {}));
260 flushed_successive_failures =
261 Some(self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst));
262 downtime_duration =
263 self.last_disconnect_at.lock().map(|t| fasync::MonotonicInstant::now() - t);
264 } else if is_credential_rejected {
265 self.update_connection_state(ConnectionState::Idle(IdleState {}));
266 let _prev = self.successive_connect_attempt_failures.fetch_add(1, Ordering::SeqCst);
267 let _prev = self.last_connect_failure_at.lock().replace(fasync::BootInstant::now());
268 } else {
269 self.update_connection_state(ConnectionState::ConnectFailed(ConnectFailedState {}));
270 let _prev = self.successive_connect_attempt_failures.fetch_add(1, Ordering::SeqCst);
271 let _prev = self.last_connect_failure_at.lock().replace(fasync::BootInstant::now());
272 }
273
274 self.log_connect_attempt_inspect(result, bss);
275 self.log_connect_attempt_cobalt(result, flushed_successive_failures, downtime_duration)
276 .await;
277 }
278
279 fn log_connect_attempt_inspect(
280 &self,
281 result: fidl_ieee80211::StatusCode,
282 bss: &BssDescription,
283 ) {
284 let mut inspect_metadata_node = self.inspect_metadata_node.lock();
285 let connect_result_id =
286 inspect_metadata_node.connect_attempt_results.insert(InspectConnectAttemptResult {
287 status_code: result.into_primitive(),
288 result: format!("{:?}", result),
289 }) as u64;
290 self.time_series_stats.log_connect_attempt_results(1 << connect_result_id);
291
292 inspect_log!(self.connect_attempt_results_node.lock(), {
293 result: format!("{:?}", result),
294 ssid: bss.ssid.to_string(),
295 bssid: bss.bssid.to_string(),
296 protection: format!("{:?}", bss.protection()),
297 });
298
299 if result == fidl_ieee80211::StatusCode::Success {
300 let connected_network = InspectConnectedNetwork::from(bss);
301 let connected_network_id =
302 inspect_metadata_node.connected_networks.insert(connected_network) as u64;
303
304 self.time_series_stats.log_connected_networks(1 << connected_network_id);
305
306 inspect_log!(self.connect_events_node.lock(), {
307 network_id: connected_network_id,
308 });
309 }
310 }
311
312 #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
313 async fn log_connect_attempt_cobalt(
314 &self,
315 result: fidl_ieee80211::StatusCode,
316 flushed_successive_failures: Option<usize>,
317 downtime_duration: Option<zx::MonotonicDuration>,
318 ) {
319 let mut metric_events = vec![];
320 metric_events.push(MetricEvent {
321 metric_id: metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
322 event_codes: vec![result.into_primitive() as u32],
323 payload: MetricEventPayload::Count(1),
324 });
325
326 if let Some(failures) = flushed_successive_failures {
327 metric_events.push(MetricEvent {
328 metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
329 event_codes: vec![],
330 payload: MetricEventPayload::IntegerValue(failures as i64),
331 });
332 }
333
334 if let Some(duration) = downtime_duration {
335 metric_events.push(MetricEvent {
336 metric_id: metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID,
337 event_codes: vec![],
338 payload: MetricEventPayload::IntegerValue(duration.into_millis()),
339 });
340 }
341
342 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_connect_attempt_cobalt");
343 }
344
345 pub async fn log_disconnect(&self, info: &DisconnectInfo) {
346 if !info.disconnect_source.should_log_for_mobile_device() {
352 self.update_connection_state(ConnectionState::Idle(IdleState {}));
353 } else {
354 self.update_connection_state(ConnectionState::Disconnected(DisconnectedState {}));
355 }
356 let _prev = self.last_disconnect_at.lock().replace(fasync::MonotonicInstant::now());
357 self.log_disconnect_inspect(info);
358 self.log_disconnect_cobalt(info).await;
359 }
360
361 fn log_disconnect_inspect(&self, info: &DisconnectInfo) {
362 let mut inspect_metadata_node = self.inspect_metadata_node.lock();
363 let connected_network = InspectConnectedNetwork::from(&*info.original_bss_desc);
364 let connected_network_id =
365 inspect_metadata_node.connected_networks.insert(connected_network) as u64;
366 let disconnect_source = InspectDisconnectSource::from(&info.disconnect_source);
367 let disconnect_source_id =
368 inspect_metadata_node.disconnect_sources.insert(disconnect_source) as u64;
369 inspect_log!(self.disconnect_events_node.lock(), {
370 connected_duration: info.connected_duration.into_nanos(),
371 disconnect_source_id: disconnect_source_id,
372 network_id: connected_network_id,
373 rssi_dbm: info.current_rssi_dbm,
374 snr_db: info.current_snr_db,
375 channel: format!("{}", info.current_channel),
376 });
377
378 self.time_series_stats.log_disconnected_networks(1 << connected_network_id);
379 self.time_series_stats.log_disconnect_sources(1 << disconnect_source_id);
380 }
381
382 async fn log_disconnect_cobalt(&self, info: &DisconnectInfo) {
383 let mut metric_events = vec![];
384 metric_events.push(MetricEvent {
385 metric_id: metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID,
386 event_codes: vec![],
387 payload: MetricEventPayload::Count(1),
388 });
389
390 if info.disconnect_source.should_log_for_mobile_device() {
391 metric_events.push(MetricEvent {
392 metric_id: metrics::DISCONNECT_OCCURRENCE_FOR_MOBILE_DEVICE_METRIC_ID,
393 event_codes: vec![],
394 payload: MetricEventPayload::Count(1),
395 });
396 }
397
398 metric_events.push(MetricEvent {
399 metric_id: metrics::CONNECTED_DURATION_ON_DISCONNECT_METRIC_ID,
400 event_codes: vec![],
401 payload: MetricEventPayload::IntegerValue(info.connected_duration.into_millis()),
402 });
403
404 metric_events.push(MetricEvent {
405 metric_id: metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID,
406 event_codes: vec![
407 u32::from(info.disconnect_source.cobalt_reason_code()),
408 info.disconnect_source.as_cobalt_disconnect_source() as u32,
409 ],
410 payload: MetricEventPayload::Count(1),
411 });
412
413 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_disconnect_cobalt");
414 }
415
416 pub async fn handle_periodic_telemetry(&self) {
417 let mut metric_events = vec![];
418 let now = fasync::BootInstant::now();
419 if let Some(failed_at) = *self.last_connect_failure_at.lock()
420 && now - failed_at >= SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_TIMEOUT
421 {
422 let failures = self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst);
423 if failures > 0 {
424 metric_events.push(MetricEvent {
425 metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
426 event_codes: vec![],
427 payload: MetricEventPayload::IntegerValue(failures as i64),
428 });
429 }
430 }
431
432 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_periodic_telemetry");
433 }
434
435 pub async fn handle_suspend_imminent(&self) {
436 let mut metric_events = vec![];
437
438 let flushed_successive_failures =
439 self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst);
440 if flushed_successive_failures > 0 {
441 metric_events.push(MetricEvent {
442 metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
443 event_codes: vec![],
444 payload: MetricEventPayload::IntegerValue(flushed_successive_failures as i64),
445 });
446 }
447
448 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_suspend_imminent");
449 }
450
451 pub async fn handle_iface_destroyed(&self) {
452 self.update_connection_state(ConnectionState::Idle(IdleState {}));
453 }
454
455 pub async fn handle_client_connections_toggle(&self, event: &ClientConnectionsToggleEvent) {
456 if event == &ClientConnectionsToggleEvent::Disabled {
457 self.update_connection_state(ConnectionState::Idle(IdleState {}));
458 }
459 }
460
461 pub async fn handle_pno_scan_failure(&self) {
462 let mut metric_events = vec![MetricEvent {
463 metric_id: metrics::PNO_SCAN_FAILURE_OCCURRENCE_METRIC_ID,
464 event_codes: vec![],
465 payload: MetricEventPayload::Count(1),
466 }];
467
468 let state = self.connection_state.lock().clone();
469 match state {
470 ConnectionState::Idle(_)
471 | ConnectionState::Disconnected(_)
472 | ConnectionState::ConnectFailed(_)
473 | ConnectionState::PnoScanFailedIdle(_) => {
474 metric_events.push(MetricEvent {
475 metric_id: metrics::PNO_SCAN_FAILURE_WHILE_NOT_CONNECTED_OCCURRENCE_METRIC_ID,
476 event_codes: vec![],
477 payload: MetricEventPayload::Count(1),
478 });
479
480 self.update_connection_state(ConnectionState::PnoScanFailedIdle(
484 PnoScanFailedIdleState {},
485 ));
486 }
487 ConnectionState::Connected(_)
488 | ConnectionState::FailedToStart(_)
489 | ConnectionState::FailedToStop(_) => {
490 }
494 }
495
496 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_pno_scan_failure");
497 }
498 pub async fn handle_client_connections_failed_to_start(&self) {
499 self.update_connection_state(ConnectionState::FailedToStart(FailedToStartState {}));
500 }
501
502 pub async fn handle_client_connections_failed_to_stop(&self) {
503 self.update_connection_state(ConnectionState::FailedToStop(FailedToStopState {}));
504 }
505}
506
507struct InspectMetadataNode {
508 connected_networks: LruCacheNode<InspectConnectedNetwork>,
509 disconnect_sources: LruCacheNode<InspectDisconnectSource>,
510 connect_attempt_results: LruCacheNode<InspectConnectAttemptResult>,
511}
512
513impl InspectMetadataNode {
514 const CONNECTED_NETWORKS: &'static str = "connected_networks";
515 const DISCONNECT_SOURCES: &'static str = "disconnect_sources";
516 const CONNECT_ATTEMPT_RESULTS: &'static str = "connect_attempt_results";
517
518 fn new(inspect_node: &InspectNode) -> Self {
519 let connected_networks = inspect_node.create_child(Self::CONNECTED_NETWORKS);
520 let disconnect_sources = inspect_node.create_child(Self::DISCONNECT_SOURCES);
521 let connect_attempt_results = inspect_node.create_child(Self::CONNECT_ATTEMPT_RESULTS);
522 Self {
523 connected_networks: LruCacheNode::new(
524 connected_networks,
525 INSPECT_CONNECTED_NETWORKS_ID_LIMIT,
526 ),
527 disconnect_sources: LruCacheNode::new(
528 disconnect_sources,
529 INSPECT_DISCONNECT_SOURCES_ID_LIMIT,
530 ),
531 connect_attempt_results: LruCacheNode::new(
532 connect_attempt_results,
533 INSPECT_CONNECT_ATTEMPT_RESULTS_ID_LIMIT,
534 ),
535 }
536 }
537}
538
539#[derive(Debug, Clone)]
540struct ConnectDisconnectTimeSeries {
541 wlan_connectivity_states: InspectedTimeMatrix<u64>,
542 connected_networks: InspectedTimeMatrix<u64>,
543 disconnected_networks: InspectedTimeMatrix<u64>,
544 disconnect_sources: InspectedTimeMatrix<u64>,
545 connect_attempt_results: InspectedTimeMatrix<u64>,
546}
547
548impl ConnectDisconnectTimeSeries {
549 pub fn new<S: InspectSender>(client: &S, inspect_metadata_path: &str) -> Self {
550 let wlan_connectivity_states = client.inspect_time_matrix_with_metadata(
551 "wlan_connectivity_states",
552 TimeMatrix::<Union<u64>, LastSample>::new(
553 SamplingProfile::highly_granular(),
554 LastSample::or(0),
555 ),
556 BitSetMap::from_ordered(Self::wlan_connectivity_states_bitset_map().iter().copied()),
558 );
559 let connected_networks = client.inspect_time_matrix_with_metadata(
560 "connected_networks",
561 TimeMatrix::<Union<u64>, ConstantSample>::new(
562 SamplingProfile::granular(),
563 ConstantSample::default(),
564 ),
565 BitSetNode::from_path(format!(
566 "{}/{}",
567 inspect_metadata_path,
568 InspectMetadataNode::CONNECTED_NETWORKS
569 )),
570 );
571 let disconnected_networks = client.inspect_time_matrix_with_metadata(
572 "disconnected_networks",
573 TimeMatrix::<Union<u64>, ConstantSample>::new(
574 SamplingProfile::granular(),
575 ConstantSample::default(),
576 ),
577 BitSetNode::from_path(format!(
579 "{}/{}",
580 inspect_metadata_path,
581 InspectMetadataNode::CONNECTED_NETWORKS
582 )),
583 );
584 let disconnect_sources = client.inspect_time_matrix_with_metadata(
585 "disconnect_sources",
586 TimeMatrix::<Union<u64>, ConstantSample>::new(
587 SamplingProfile::granular(),
588 ConstantSample::default(),
589 ),
590 BitSetNode::from_path(format!(
591 "{}/{}",
592 inspect_metadata_path,
593 InspectMetadataNode::DISCONNECT_SOURCES,
594 )),
595 );
596 let connect_attempt_results = client.inspect_time_matrix_with_metadata(
597 "connect_attempt_results",
598 TimeMatrix::<Union<u64>, ConstantSample>::new(
599 SamplingProfile::granular(),
600 ConstantSample::default(),
601 ),
602 BitSetNode::from_path(format!(
603 "{}/{}",
604 inspect_metadata_path,
605 InspectMetadataNode::CONNECT_ATTEMPT_RESULTS,
606 )),
607 );
608 Self {
609 wlan_connectivity_states,
610 connected_networks,
611 disconnected_networks,
612 disconnect_sources,
613 connect_attempt_results,
614 }
615 }
616
617 fn wlan_connectivity_states_bitset_map() -> &'static [&'static str] {
620 &[
621 "idle",
622 "disconnected",
623 "connect_failed",
624 "connected",
625 "start_failure",
626 "stop_failure",
627 "pno_scan_failed",
628 ]
629 }
630
631 fn log_wlan_connectivity_state(&self, data: u64) {
632 self.wlan_connectivity_states.fold_or_log_error(data);
633 }
634 fn log_connected_networks(&self, data: u64) {
635 self.connected_networks.fold_or_log_error(data);
636 }
637 fn log_disconnected_networks(&self, data: u64) {
638 self.disconnected_networks.fold_or_log_error(data);
639 }
640 fn log_disconnect_sources(&self, data: u64) {
641 self.disconnect_sources.fold_or_log_error(data);
642 }
643 fn log_connect_attempt_results(&self, data: u64) {
644 self.connect_attempt_results.fold_or_log_error(data);
645 }
646}
647
648pub trait DisconnectSourceExt {
649 fn should_log_for_mobile_device(&self) -> bool;
650 fn cobalt_reason_code(&self) -> u16;
651 fn as_cobalt_disconnect_source(
652 &self,
653 ) -> metrics::ConnectivityWlanMetricDimensionDisconnectSource;
654}
655
656impl DisconnectSourceExt for fidl_sme::DisconnectSource {
657 fn should_log_for_mobile_device(&self) -> bool {
658 match self {
659 fidl_sme::DisconnectSource::Ap(_) => true,
660 fidl_sme::DisconnectSource::Mlme(cause)
661 if cause.reason_code != fidl_ieee80211::ReasonCode::MlmeLinkFailed =>
662 {
663 true
664 }
665 _ => false,
666 }
667 }
668
669 fn cobalt_reason_code(&self) -> u16 {
670 let cobalt_disconnect_reason_code = match self {
671 fidl_sme::DisconnectSource::Ap(cause) | fidl_sme::DisconnectSource::Mlme(cause) => {
672 cause.reason_code.into_primitive()
673 }
674 fidl_sme::DisconnectSource::User(reason) => *reason as u16,
675 };
676 const REASON_CODE_MAX: u16 = 1000;
679 std::cmp::min(cobalt_disconnect_reason_code, REASON_CODE_MAX)
680 }
681
682 fn as_cobalt_disconnect_source(
683 &self,
684 ) -> metrics::ConnectivityWlanMetricDimensionDisconnectSource {
685 use metrics::ConnectivityWlanMetricDimensionDisconnectSource as DS;
686 match self {
687 fidl_sme::DisconnectSource::Ap(..) => DS::Ap,
688 fidl_sme::DisconnectSource::User(..) => DS::User,
689 fidl_sme::DisconnectSource::Mlme(..) => DS::Mlme,
690 }
691 }
692}
693
694#[cfg(test)]
695mod tests {
696 use super::*;
697 use crate::testing::*;
698 use assert_matches::assert_matches;
699 use diagnostics_assertions::{
700 AnyBoolProperty, AnyBytesProperty, AnyNumericProperty, AnyStringProperty, assert_data_tree,
701 };
702 use futures::task::Poll;
703 use ieee80211_testutils::{BSSID_REGEX, SSID_REGEX};
704 use rand::Rng;
705 use std::pin::pin;
706 use strum::IntoEnumIterator;
707 use test_case::test_case;
708 use windowed_stats::experimental::clock::Timed;
709 use windowed_stats::experimental::inspect::TimeMatrixClient;
710 use windowed_stats::experimental::testing::TimeMatrixCall;
711 use wlan_common::channel::{Cbw, Channel};
712 use wlan_common::{fake_bss_description, random_bss_description};
713
714 #[fuchsia::test]
715 fn log_connect_attempt_then_inspect_data_tree_contains_time_matrix_metadata() {
716 let mut harness = setup_test();
717
718 let client =
719 TimeMatrixClient::new(harness.inspect_node.create_child("wlan_connect_disconnect"));
720 let logger = ConnectDisconnectLogger::new(
721 harness.cobalt_proxy.clone(),
722 &harness.inspect_node,
723 &harness.inspect_metadata_node,
724 &harness.inspect_metadata_path,
725 &client,
726 );
727 let bss = random_bss_description!();
728 let mut log_connect_attempt =
729 pin!(logger.handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss, false));
730 assert!(
731 harness.run_until_stalled_drain_cobalt_events(&mut log_connect_attempt).is_ready(),
732 "`log_connect_attempt` did not complete",
733 );
734
735 let tree = harness.get_inspect_data_tree();
736 assert_data_tree!(
737 @executor harness.exec,
738 tree,
739 root: contains {
740 test_stats: contains {
741 wlan_connect_disconnect: contains {
742 wlan_connectivity_states: {
743 "type": "bitset",
744 "data": AnyBytesProperty,
745 metadata: {
746 index: {
747 "0": "idle",
748 "1": "disconnected",
749 "2": "connect_failed",
750 "3": "connected",
751 "4": "start_failure",
752 "5": "stop_failure",
753 "6": "pno_scan_failed",
754 },
755 },
756 },
757 connected_networks: {
758 "type": "bitset",
759 "data": AnyBytesProperty,
760 metadata: {
761 "index_node_path": "root/test_stats/metadata/connected_networks",
762 },
763 },
764 disconnected_networks: {
765 "type": "bitset",
766 "data": AnyBytesProperty,
767 metadata: {
768 "index_node_path": "root/test_stats/metadata/connected_networks",
769 },
770 },
771 disconnect_sources: {
772 "type": "bitset",
773 "data": AnyBytesProperty,
774 metadata: {
775 "index_node_path": "root/test_stats/metadata/disconnect_sources",
776 },
777 },
778 connect_attempt_results: {
779 "type": "bitset",
780 "data": AnyBytesProperty,
781 metadata: {
782 "index_node_path": "root/test_stats/metadata/connect_attempt_results",
783 },
784 },
785 },
786 },
787 }
788 );
789 }
790
791 #[fuchsia::test]
792 fn test_log_connect_attempt_inspect() {
793 let mut test_helper = setup_test();
794 let logger = ConnectDisconnectLogger::new(
795 test_helper.cobalt_proxy.clone(),
796 &test_helper.inspect_node,
797 &test_helper.inspect_metadata_node,
798 &test_helper.inspect_metadata_path,
799 &test_helper.mock_time_matrix_client,
800 );
801
802 let bss_description = random_bss_description!();
804 let mut test_fut = pin!(logger.handle_connect_attempt(
805 fidl_ieee80211::StatusCode::Success,
806 &bss_description,
807 false
808 ));
809 assert_eq!(
810 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
811 Poll::Ready(())
812 );
813
814 let data = test_helper.get_inspect_data_tree();
816 assert_data_tree!(@executor test_helper.exec, data, root: contains {
817 test_stats: contains {
818 metadata: contains {
819 connected_networks: contains {
820 "0": {
821 "@time": AnyNumericProperty,
822 "data": contains {
823 bssid: &*BSSID_REGEX,
824 ssid: &*SSID_REGEX,
825 }
826 }
827 },
828 connect_attempt_results: contains {
829 "0": {
830 "@time": AnyNumericProperty,
831 "data": contains {
832 status_code: 0u64,
833 result: "Success",
834 }
835 }
836 },
837 },
838 connect_events: {
839 "0": {
840 "@time": AnyNumericProperty,
841 network_id: 0u64,
842 }
843 },
844 connect_attempt_results: {
845 "0": {
846 "@time": AnyNumericProperty,
847 result: "Success",
848 ssid: &*SSID_REGEX,
849 bssid: &*BSSID_REGEX,
850 protection: AnyStringProperty,
851 }
852 }
853 }
854 });
855
856 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
857 assert_eq!(
858 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
859 &[TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 3)),]
860 );
861 assert_eq!(
862 &time_matrix_calls.drain::<u64>("connected_networks")[..],
863 &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
864 );
865 assert_eq!(
866 &time_matrix_calls.drain::<u64>("connect_attempt_results")[..],
867 &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
868 );
869 }
870
871 #[fuchsia::test]
872 fn test_log_connect_attempt_cobalt() {
873 let mut test_helper = setup_test();
874 let logger = ConnectDisconnectLogger::new(
875 test_helper.cobalt_proxy.clone(),
876 &test_helper.inspect_node,
877 &test_helper.inspect_metadata_node,
878 &test_helper.inspect_metadata_path,
879 &test_helper.mock_time_matrix_client,
880 );
881
882 let bss_description = random_bss_description!(Wpa2,
884 channel: Channel::new(157, Cbw::Cbw40),
885 bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
886 );
887
888 let mut test_fut = pin!(logger.handle_connect_attempt(
890 fidl_ieee80211::StatusCode::Success,
891 &bss_description,
892 false
893 ));
894 assert_eq!(
895 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
896 Poll::Ready(())
897 );
898
899 let breakdowns_by_status_code = test_helper
901 .get_logged_metrics(metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID);
902 assert_eq!(breakdowns_by_status_code.len(), 1);
903 assert_eq!(
904 breakdowns_by_status_code[0].event_codes,
905 vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32]
906 );
907 assert_eq!(breakdowns_by_status_code[0].payload, MetricEventPayload::Count(1));
908 }
909
910 #[fuchsia::test]
911 fn test_successive_connect_attempt_failures_cobalt_zero_failures() {
912 let mut test_helper = setup_test();
913 let logger = ConnectDisconnectLogger::new(
914 test_helper.cobalt_proxy.clone(),
915 &test_helper.inspect_node,
916 &test_helper.inspect_metadata_node,
917 &test_helper.inspect_metadata_path,
918 &test_helper.mock_time_matrix_client,
919 );
920
921 let bss_description = random_bss_description!(Wpa2);
922 let mut test_fut = pin!(logger.handle_connect_attempt(
923 fidl_ieee80211::StatusCode::Success,
924 &bss_description,
925 false
926 ));
927 assert_eq!(
928 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
929 Poll::Ready(())
930 );
931
932 let metrics =
933 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
934 assert_eq!(metrics.len(), 1);
935 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(0));
936 }
937
938 #[test_case(1; "one_failure")]
939 #[test_case(2; "two_failures")]
940 #[fuchsia::test(add_test_attr = false)]
941 fn test_successive_connect_attempt_failures_cobalt_one_failure_then_success(n_failures: usize) {
942 let mut test_helper = setup_test();
943 let logger = ConnectDisconnectLogger::new(
944 test_helper.cobalt_proxy.clone(),
945 &test_helper.inspect_node,
946 &test_helper.inspect_metadata_node,
947 &test_helper.inspect_metadata_path,
948 &test_helper.mock_time_matrix_client,
949 );
950
951 let bss_description = random_bss_description!(Wpa2);
952 for _i in 0..n_failures {
953 let mut test_fut = pin!(logger.handle_connect_attempt(
954 fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
955 &bss_description,
956 false
957 ));
958 assert_eq!(
959 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
960 Poll::Ready(())
961 );
962 }
963
964 let metrics =
965 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
966 assert!(metrics.is_empty());
967
968 let mut test_fut = pin!(logger.handle_connect_attempt(
969 fidl_ieee80211::StatusCode::Success,
970 &bss_description,
971 false
972 ));
973 assert_eq!(
974 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
975 Poll::Ready(())
976 );
977
978 let metrics =
979 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
980 assert_eq!(metrics.len(), 1);
981 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
982
983 test_helper.clear_cobalt_events();
985 let mut test_fut = pin!(logger.handle_connect_attempt(
986 fidl_ieee80211::StatusCode::Success,
987 &bss_description,
988 false
989 ));
990 assert_eq!(
991 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
992 Poll::Ready(())
993 );
994 let metrics =
995 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
996 assert_eq!(metrics.len(), 1);
997 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(0));
998 }
999
1000 #[test_case(1; "one_failure")]
1001 #[test_case(2; "two_failures")]
1002 #[fuchsia::test(add_test_attr = false)]
1003 fn test_successive_connect_attempt_failures_cobalt_one_failure_then_timeout(n_failures: usize) {
1004 let mut test_helper = setup_test();
1005 let logger = ConnectDisconnectLogger::new(
1006 test_helper.cobalt_proxy.clone(),
1007 &test_helper.inspect_node,
1008 &test_helper.inspect_metadata_node,
1009 &test_helper.inspect_metadata_path,
1010 &test_helper.mock_time_matrix_client,
1011 );
1012
1013 let bss_description = random_bss_description!(Wpa2);
1014 for _i in 0..n_failures {
1015 let mut test_fut = pin!(logger.handle_connect_attempt(
1016 fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1017 &bss_description,
1018 false
1019 ));
1020 assert_eq!(
1021 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1022 Poll::Ready(())
1023 );
1024 }
1025
1026 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(60_000_000_000));
1027 let mut test_fut = pin!(logger.handle_periodic_telemetry());
1028 assert_eq!(
1029 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1030 Poll::Ready(())
1031 );
1032
1033 let metrics =
1035 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1036 assert!(metrics.is_empty());
1037
1038 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(120_000_000_000));
1039 let mut test_fut = pin!(logger.handle_periodic_telemetry());
1040 assert_eq!(
1041 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1042 Poll::Ready(())
1043 );
1044
1045 let metrics =
1046 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1047 assert_eq!(metrics.len(), 1);
1048 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
1049
1050 test_helper.clear_cobalt_events();
1052 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(240_000_000_000));
1053 let mut test_fut = pin!(logger.handle_periodic_telemetry());
1054 assert_eq!(
1055 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1056 Poll::Ready(())
1057 );
1058 let metrics =
1059 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1060 assert!(metrics.is_empty());
1061 }
1062
1063 #[fuchsia::test]
1064 fn test_zero_successive_connect_attempt_failures_on_suspend() {
1065 let mut test_helper = setup_test();
1066 let logger = ConnectDisconnectLogger::new(
1067 test_helper.cobalt_proxy.clone(),
1068 &test_helper.inspect_node,
1069 &test_helper.inspect_metadata_node,
1070 &test_helper.inspect_metadata_path,
1071 &test_helper.mock_time_matrix_client,
1072 );
1073
1074 let mut test_fut = pin!(logger.handle_suspend_imminent());
1075 assert_eq!(
1076 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1077 Poll::Ready(())
1078 );
1079
1080 let metrics =
1081 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1082 assert!(metrics.is_empty());
1083 }
1084
1085 #[test_case(1; "one_failure")]
1086 #[test_case(2; "two_failures")]
1087 #[fuchsia::test(add_test_attr = false)]
1088 fn test_one_or_more_successive_connect_attempt_failures_on_suspend(n_failures: usize) {
1089 let mut test_helper = setup_test();
1090 let logger = ConnectDisconnectLogger::new(
1091 test_helper.cobalt_proxy.clone(),
1092 &test_helper.inspect_node,
1093 &test_helper.inspect_metadata_node,
1094 &test_helper.inspect_metadata_path,
1095 &test_helper.mock_time_matrix_client,
1096 );
1097
1098 let bss_description = random_bss_description!(Wpa2);
1099 for _i in 0..n_failures {
1100 let mut test_fut = pin!(logger.handle_connect_attempt(
1101 fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1102 &bss_description,
1103 false
1104 ));
1105 assert_eq!(
1106 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1107 Poll::Ready(())
1108 );
1109 }
1110
1111 let mut test_fut = pin!(logger.handle_suspend_imminent());
1112 assert_eq!(
1113 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1114 Poll::Ready(())
1115 );
1116
1117 let metrics =
1118 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1119 assert_eq!(metrics.len(), 1);
1120 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
1121
1122 test_helper.clear_cobalt_events();
1123 let mut test_fut = pin!(logger.handle_suspend_imminent());
1124 assert_eq!(
1125 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1126 Poll::Ready(())
1127 );
1128
1129 let metrics =
1131 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1132 assert!(metrics.is_empty());
1133
1134 assert_matches!(*logger.connection_state.lock(), ConnectionState::ConnectFailed(_));
1136 }
1137
1138 #[fuchsia::test]
1139 fn test_log_disconnect_inspect() {
1140 let mut test_helper = setup_test();
1141 let logger = ConnectDisconnectLogger::new(
1142 test_helper.cobalt_proxy.clone(),
1143 &test_helper.inspect_node,
1144 &test_helper.inspect_metadata_node,
1145 &test_helper.inspect_metadata_path,
1146 &test_helper.mock_time_matrix_client,
1147 );
1148
1149 let bss_description = fake_bss_description!(Open);
1151 let channel = bss_description.channel;
1152 let disconnect_info = DisconnectInfo {
1153 iface_id: 32,
1154 connected_duration: zx::BootDuration::from_seconds(30),
1155 is_sme_reconnecting: false,
1156 disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1157 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1158 reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1159 }),
1160 original_bss_desc: Box::new(bss_description),
1161 current_rssi_dbm: -30,
1162 current_snr_db: 25,
1163 current_channel: channel,
1164 };
1165 let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1166 assert_eq!(
1167 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1168 Poll::Ready(())
1169 );
1170
1171 let data = test_helper.get_inspect_data_tree();
1173 assert_data_tree!(@executor test_helper.exec, data, root: contains {
1174 test_stats: contains {
1175 metadata: contains {
1176 connected_networks: {
1177 "0": {
1178 "@time": AnyNumericProperty,
1179 "data": {
1180 bssid: &*BSSID_REGEX,
1181 ssid: &*SSID_REGEX,
1182 ht_cap: AnyBytesProperty,
1183 vht_cap: AnyBytesProperty,
1184 protection: "Open",
1185 is_wmm_assoc: AnyBoolProperty,
1186 wmm_param: AnyBytesProperty,
1187 }
1188 }
1189 },
1190 disconnect_sources: {
1191 "0": {
1192 "@time": AnyNumericProperty,
1193 "data": {
1194 source: "ap",
1195 reason: "UnspecifiedReason",
1196 mlme_event_name: "DeauthenticateIndication",
1197 }
1198 }
1199 },
1200 },
1201 disconnect_events: {
1202 "0": {
1203 "@time": AnyNumericProperty,
1204 connected_duration: zx::BootDuration::from_seconds(30).into_nanos(),
1205 disconnect_source_id: 0u64,
1206 network_id: 0u64,
1207 rssi_dbm: -30i64,
1208 snr_db: 25i64,
1209 channel: AnyStringProperty,
1210 }
1211 }
1212 }
1213 });
1214
1215 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1216 assert_eq!(
1217 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1218 &[TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 1)),]
1219 );
1220 assert_eq!(
1221 &time_matrix_calls.drain::<u64>("disconnected_networks")[..],
1222 &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
1223 );
1224 assert_eq!(
1225 &time_matrix_calls.drain::<u64>("disconnect_sources")[..],
1226 &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
1227 );
1228 }
1229
1230 #[fuchsia::test]
1231 fn test_log_disconnect_cobalt() {
1232 let mut test_helper = setup_test();
1233 let logger = ConnectDisconnectLogger::new(
1234 test_helper.cobalt_proxy.clone(),
1235 &test_helper.inspect_node,
1236 &test_helper.inspect_metadata_node,
1237 &test_helper.inspect_metadata_path,
1238 &test_helper.mock_time_matrix_client,
1239 );
1240
1241 let disconnect_info = DisconnectInfo {
1243 connected_duration: zx::BootDuration::from_millis(300_000),
1244 disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1245 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1246 reason_code: fidl_ieee80211::ReasonCode::ApInitiated,
1247 }),
1248 ..fake_disconnect_info()
1249 };
1250 let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1251 assert_eq!(
1252 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1253 Poll::Ready(())
1254 );
1255
1256 let disconnect_count_metrics =
1257 test_helper.get_logged_metrics(metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID);
1258 assert_eq!(disconnect_count_metrics.len(), 1);
1259 assert_eq!(disconnect_count_metrics[0].payload, MetricEventPayload::Count(1));
1260
1261 let connected_duration_metrics =
1262 test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_ON_DISCONNECT_METRIC_ID);
1263 assert_eq!(connected_duration_metrics.len(), 1);
1264 assert_eq!(
1265 connected_duration_metrics[0].payload,
1266 MetricEventPayload::IntegerValue(300_000)
1267 );
1268
1269 let disconnect_by_reason_metrics =
1270 test_helper.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID);
1271 assert_eq!(disconnect_by_reason_metrics.len(), 1);
1272 assert_eq!(disconnect_by_reason_metrics[0].payload, MetricEventPayload::Count(1));
1273 assert_eq!(disconnect_by_reason_metrics[0].event_codes.len(), 2);
1274 assert_eq!(
1275 disconnect_by_reason_metrics[0].event_codes[0],
1276 fidl_ieee80211::ReasonCode::ApInitiated.into_primitive() as u32
1277 );
1278 assert_eq!(
1279 disconnect_by_reason_metrics[0].event_codes[1],
1280 metrics::ConnectivityWlanMetricDimensionDisconnectSource::Ap as u32
1281 );
1282 }
1283
1284 #[test_case(
1285 fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1286 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1287 reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1288 }),
1289 true;
1290 "ap_disconnect_source"
1291 )]
1292 #[test_case(
1293 fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1294 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1295 reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1296 }),
1297 true;
1298 "mlme_disconnect_source_not_link_failed"
1299 )]
1300 #[test_case(
1301 fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1302 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1303 reason_code: fidl_ieee80211::ReasonCode::MlmeLinkFailed,
1304 }),
1305 false;
1306 "mlme_link_failed"
1307 )]
1308 #[test_case(
1309 fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::Unknown),
1310 false;
1311 "user_disconnect_source"
1312 )]
1313 #[fuchsia::test(add_test_attr = false)]
1314 fn test_log_disconnect_for_mobile_device_cobalt(
1315 disconnect_source: fidl_sme::DisconnectSource,
1316 should_log: bool,
1317 ) {
1318 let mut test_helper = setup_test();
1319 let logger = ConnectDisconnectLogger::new(
1320 test_helper.cobalt_proxy.clone(),
1321 &test_helper.inspect_node,
1322 &test_helper.inspect_metadata_node,
1323 &test_helper.inspect_metadata_path,
1324 &test_helper.mock_time_matrix_client,
1325 );
1326
1327 let disconnect_info = DisconnectInfo { disconnect_source, ..fake_disconnect_info() };
1329 let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1330 assert_eq!(
1331 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1332 Poll::Ready(())
1333 );
1334
1335 let metrics = test_helper
1336 .get_logged_metrics(metrics::DISCONNECT_OCCURRENCE_FOR_MOBILE_DEVICE_METRIC_ID);
1337 if should_log {
1338 assert_eq!(metrics.len(), 1);
1339 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1340 assert_matches!(*logger.connection_state.lock(), ConnectionState::Disconnected(_));
1341 } else {
1342 assert!(metrics.is_empty());
1343 assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
1344 }
1345 }
1346
1347 #[fuchsia::test]
1348 fn test_log_downtime_post_disconnect_on_reconnect() {
1349 let mut test_helper = setup_test();
1350 let logger = ConnectDisconnectLogger::new(
1351 test_helper.cobalt_proxy.clone(),
1352 &test_helper.inspect_node,
1353 &test_helper.inspect_metadata_node,
1354 &test_helper.inspect_metadata_path,
1355 &test_helper.mock_time_matrix_client,
1356 );
1357
1358 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(15_000_000_000));
1360 let bss_description = random_bss_description!(Wpa2);
1361 let mut test_fut = pin!(logger.handle_connect_attempt(
1362 fidl_ieee80211::StatusCode::Success,
1363 &bss_description,
1364 false
1365 ));
1366 assert_eq!(
1367 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1368 Poll::Ready(())
1369 );
1370
1371 let metrics = test_helper.get_logged_metrics(metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID);
1373 assert!(metrics.is_empty());
1374
1375 assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
1377
1378 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(25_000_000_000));
1380 let disconnect_info = DisconnectInfo {
1381 connected_duration: zx::BootDuration::from_millis(300_000),
1382 disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1383 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1384 reason_code: fidl_ieee80211::ReasonCode::ApInitiated,
1385 }),
1386 ..fake_disconnect_info()
1387 };
1388 let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1389 assert_eq!(
1390 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1391 Poll::Ready(())
1392 );
1393
1394 assert_matches!(*logger.connection_state.lock(), ConnectionState::Disconnected(_));
1396
1397 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(60_000_000_000));
1399 let mut test_fut = pin!(logger.handle_connect_attempt(
1400 fidl_ieee80211::StatusCode::Success,
1401 &bss_description,
1402 false
1403 ));
1404 assert_eq!(
1405 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1406 Poll::Ready(())
1407 );
1408
1409 let metrics = test_helper.get_logged_metrics(metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID);
1411 assert_eq!(metrics.len(), 1);
1412 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(35_000));
1413
1414 assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
1416 }
1417
1418 #[fuchsia::test]
1419 fn test_log_iface_destroyed() {
1420 let mut test_helper = setup_test();
1421 let logger = ConnectDisconnectLogger::new(
1422 test_helper.cobalt_proxy.clone(),
1423 &test_helper.inspect_node,
1424 &test_helper.inspect_metadata_node,
1425 &test_helper.inspect_metadata_path,
1426 &test_helper.mock_time_matrix_client,
1427 );
1428
1429 let bss_description = random_bss_description!();
1431 let mut test_fut = pin!(logger.handle_connect_attempt(
1432 fidl_ieee80211::StatusCode::Success,
1433 &bss_description,
1434 false
1435 ));
1436 assert_eq!(
1437 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1438 Poll::Ready(())
1439 );
1440
1441 assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
1443
1444 let mut test_fut = pin!(logger.handle_iface_destroyed());
1446 assert_eq!(
1447 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1448 Poll::Ready(())
1449 );
1450
1451 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1452 assert_eq!(
1453 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1454 &[
1455 TimeMatrixCall::Fold(Timed::now(1 << 0)),
1456 TimeMatrixCall::Fold(Timed::now(1 << 3)),
1457 TimeMatrixCall::Fold(Timed::now(1 << 0))
1458 ]
1459 );
1460
1461 assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
1463 }
1464
1465 #[fuchsia::test]
1466 fn test_log_disable_client_connections() {
1467 let mut test_helper = setup_test();
1468 let logger = ConnectDisconnectLogger::new(
1469 test_helper.cobalt_proxy.clone(),
1470 &test_helper.inspect_node,
1471 &test_helper.inspect_metadata_node,
1472 &test_helper.inspect_metadata_path,
1473 &test_helper.mock_time_matrix_client,
1474 );
1475
1476 let bss_description = random_bss_description!();
1478 let mut test_fut = pin!(logger.handle_connect_attempt(
1479 fidl_ieee80211::StatusCode::Success,
1480 &bss_description,
1481 false
1482 ));
1483 assert_eq!(
1484 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1485 Poll::Ready(())
1486 );
1487
1488 assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
1490
1491 let mut test_fut =
1493 pin!(logger.handle_client_connections_toggle(&ClientConnectionsToggleEvent::Disabled));
1494 assert_eq!(
1495 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1496 Poll::Ready(())
1497 );
1498
1499 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1500 assert_eq!(
1501 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1502 &[
1503 TimeMatrixCall::Fold(Timed::now(1 << 0)),
1504 TimeMatrixCall::Fold(Timed::now(1 << 3)),
1505 TimeMatrixCall::Fold(Timed::now(1 << 0))
1506 ]
1507 );
1508
1509 assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
1511 }
1512
1513 #[fuchsia::test]
1514 fn test_wlan_connectivity_states_credential_rejected() {
1515 let mut test_helper = setup_test();
1516 let logger = ConnectDisconnectLogger::new(
1517 test_helper.cobalt_proxy.clone(),
1518 &test_helper.inspect_node,
1519 &test_helper.inspect_metadata_node,
1520 &test_helper.inspect_metadata_path,
1521 &test_helper.mock_time_matrix_client,
1522 );
1523
1524 let bss_description = random_bss_description!();
1526 let mut test_fut = pin!(logger.handle_connect_attempt(
1527 fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1528 &bss_description,
1529 true
1530 ));
1531 assert_eq!(
1532 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1533 Poll::Ready(())
1534 );
1535
1536 assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
1537 }
1538
1539 #[fuchsia::test]
1540 fn test_wlan_connectivity_states_failed_to_start() {
1541 let mut test_helper = setup_test();
1542 let logger = ConnectDisconnectLogger::new(
1543 test_helper.cobalt_proxy.clone(),
1544 &test_helper.inspect_node,
1545 &test_helper.inspect_metadata_node,
1546 &test_helper.inspect_metadata_path,
1547 &test_helper.mock_time_matrix_client,
1548 );
1549
1550 let mut test_fut = pin!(logger.handle_client_connections_failed_to_start());
1551 assert_eq!(
1552 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1553 Poll::Ready(())
1554 );
1555
1556 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1557 assert_eq!(
1558 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1559 &[
1560 TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 4)), ]
1563 );
1564 assert_matches!(*logger.connection_state.lock(), ConnectionState::FailedToStart(_));
1565 }
1566
1567 #[fuchsia::test]
1568 fn test_wlan_connectivity_states_failed_to_stop() {
1569 let mut test_helper = setup_test();
1570 let logger = ConnectDisconnectLogger::new(
1571 test_helper.cobalt_proxy.clone(),
1572 &test_helper.inspect_node,
1573 &test_helper.inspect_metadata_node,
1574 &test_helper.inspect_metadata_path,
1575 &test_helper.mock_time_matrix_client,
1576 );
1577
1578 let mut test_fut = pin!(logger.handle_client_connections_failed_to_stop());
1579 assert_eq!(
1580 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1581 Poll::Ready(())
1582 );
1583
1584 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1585 assert_eq!(
1586 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1587 &[
1588 TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 5)), ]
1591 );
1592
1593 assert_matches!(*logger.connection_state.lock(), ConnectionState::FailedToStop(_));
1594 }
1595
1596 #[test_case(ConnectionState::Idle(IdleState {}))]
1597 #[test_case(ConnectionState::Disconnected(DisconnectedState {}))]
1598 #[test_case(ConnectionState::ConnectFailed(ConnectFailedState {}))]
1599 #[test_case(ConnectionState::PnoScanFailedIdle(PnoScanFailedIdleState {}))]
1600 fn test_connectivity_state_transition_on_pno_scan_failure(initial_state: ConnectionState) {
1601 let mut test_helper = setup_test();
1602 let logger = ConnectDisconnectLogger::new(
1603 test_helper.cobalt_proxy.clone(),
1604 &test_helper.inspect_node,
1605 &test_helper.inspect_metadata_node,
1606 &test_helper.inspect_metadata_path,
1607 &test_helper.mock_time_matrix_client,
1608 );
1609
1610 *logger.connection_state.lock() = initial_state.clone();
1612
1613 let mut test_fut = pin!(logger.handle_pno_scan_failure());
1615 assert_matches!(
1616 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1617 Poll::Ready(())
1618 );
1619
1620 let metric_events = test_helper
1622 .get_logged_metrics(metrics::PNO_SCAN_FAILURE_WHILE_NOT_CONNECTED_OCCURRENCE_METRIC_ID);
1623 assert_eq!(metric_events.len(), 1);
1624 assert_eq!(metric_events[0].payload, MetricEventPayload::Count(1));
1625
1626 let metric_events =
1627 test_helper.get_logged_metrics(metrics::PNO_SCAN_FAILURE_OCCURRENCE_METRIC_ID);
1628 assert_eq!(metric_events.len(), 1);
1629 assert_eq!(metric_events[0].payload, MetricEventPayload::Count(1));
1630
1631 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1633 assert_eq!(
1634 *time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..].last().unwrap(),
1635 TimeMatrixCall::Fold(Timed::now(1 << 6)), );
1637
1638 assert_matches!(*logger.connection_state.lock(), ConnectionState::PnoScanFailedIdle(_));
1640 }
1641
1642 #[test_case(ConnectionState::Connected(ConnectedState {}))]
1643 #[test_case(ConnectionState::FailedToStart(FailedToStartState {}))]
1644 #[test_case(ConnectionState::FailedToStop(FailedToStopState {}))]
1645 fn test_no_connectivity_state_transition_on_pno_scan_failure(initial_state: ConnectionState) {
1646 let mut test_helper = setup_test();
1647 let logger = ConnectDisconnectLogger::new(
1648 test_helper.cobalt_proxy.clone(),
1649 &test_helper.inspect_node,
1650 &test_helper.inspect_metadata_node,
1651 &test_helper.inspect_metadata_path,
1652 &test_helper.mock_time_matrix_client,
1653 );
1654
1655 *logger.connection_state.lock() = initial_state.clone();
1657
1658 let mut test_fut = pin!(logger.handle_pno_scan_failure());
1660 assert_matches!(
1661 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1662 Poll::Ready(())
1663 );
1664
1665 let metric_events =
1667 test_helper.get_logged_metrics(metrics::PNO_SCAN_FAILURE_OCCURRENCE_METRIC_ID);
1668 assert_eq!(metric_events.len(), 1);
1669 assert_eq!(metric_events[0].payload, MetricEventPayload::Count(1));
1670
1671 assert_eq!(logger.connection_state.lock().to_id(), initial_state.to_id());
1673 }
1674
1675 #[fuchsia::test]
1676 fn test_wlan_connectivity_states_bitset_map_size() {
1677 let enum_variant_count = ConnectionState::iter().count();
1678 let bitset_map_size =
1679 ConnectDisconnectTimeSeries::wlan_connectivity_states_bitset_map().len();
1680 assert_eq!(enum_variant_count, bitset_map_size);
1681 }
1682
1683 fn fake_disconnect_info() -> DisconnectInfo {
1684 let bss_description = random_bss_description!(Wpa2);
1685 let channel = bss_description.channel;
1686 DisconnectInfo {
1687 iface_id: 1,
1688 connected_duration: zx::BootDuration::from_hours(6),
1689 is_sme_reconnecting: false,
1690 disconnect_source: fidl_sme::DisconnectSource::User(
1691 fidl_sme::UserDisconnectReason::Unknown,
1692 ),
1693 original_bss_desc: bss_description.into(),
1694 current_rssi_dbm: -30,
1695 current_snr_db: 25,
1696 current_channel: channel,
1697 }
1698 }
1699}