1use crate::util::cobalt_logger::log_cobalt_batch;
6use derivative::Derivative;
7use fidl_fuchsia_metrics::{MetricEvent, MetricEventPayload};
8use fuchsia_inspect::Node as InspectNode;
9use fuchsia_inspect_auto_persist::{self as auto_persist, AutoPersist};
10use fuchsia_inspect_contrib::id_enum::IdEnum;
11use fuchsia_inspect_contrib::nodes::{BoundedListNode, LruCacheNode};
12use fuchsia_inspect_contrib::{inspect_insert, inspect_log};
13use fuchsia_inspect_derive::Unit;
14use fuchsia_sync::Mutex;
15use std::sync::atomic::{AtomicUsize, Ordering};
16use std::sync::Arc;
17use strum_macros::{Display, EnumIter};
18use windowed_stats::experimental::clock::Timed;
19use windowed_stats::experimental::series::interpolation::{Constant, LastSample};
20use windowed_stats::experimental::series::metadata::{BitSetMap, BitSetNode};
21use windowed_stats::experimental::series::statistic::Union;
22use windowed_stats::experimental::series::{SamplingProfile, TimeMatrix};
23use windowed_stats::experimental::serve::{InspectSender, InspectedTimeMatrix};
24use wlan_common::bss::BssDescription;
25use wlan_common::channel::Channel;
26use {
27 fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211, fidl_fuchsia_wlan_sme as fidl_sme,
28 fuchsia_async as fasync, wlan_legacy_metrics_registry as metrics, zx,
29};
30
31const INSPECT_CONNECT_EVENTS_LIMIT: usize = 10;
32const INSPECT_DISCONNECT_EVENTS_LIMIT: usize = 10;
33const INSPECT_CONNECTED_NETWORKS_ID_LIMIT: usize = 16;
34const INSPECT_DISCONNECT_SOURCES_ID_LIMIT: usize = 32;
35const SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_TIMEOUT: zx::BootDuration =
36 zx::BootDuration::from_minutes(2);
37
38#[derive(Debug, Display, EnumIter)]
39enum ConnectionState {
40 Idle(IdleState),
41 Connected(ConnectedState),
42 Disconnected(DisconnectedState),
43}
44
45impl IdEnum for ConnectionState {
46 type Id = u8;
47 fn to_id(&self) -> Self::Id {
48 match self {
49 Self::Idle(_) => 0,
50 Self::Disconnected(_) => 1,
51 Self::Connected(_) => 2,
52 }
53 }
54}
55
56#[derive(Debug, Default)]
57struct IdleState {}
58
59#[derive(Debug, Default)]
60struct ConnectedState {}
61
62#[derive(Debug, Default)]
63struct DisconnectedState {}
64
65#[derive(Derivative, Unit)]
66#[derivative(PartialEq, Eq, Hash)]
67struct InspectConnectedNetwork {
68 bssid: String,
69 ssid: String,
70 protection: String,
71 ht_cap: Option<Vec<u8>>,
72 vht_cap: Option<Vec<u8>>,
73 #[derivative(PartialEq = "ignore")]
74 #[derivative(Hash = "ignore")]
75 wsc: Option<InspectNetworkWsc>,
76 is_wmm_assoc: bool,
77 wmm_param: Option<Vec<u8>>,
78}
79
80impl From<&BssDescription> for InspectConnectedNetwork {
81 fn from(bss_description: &BssDescription) -> Self {
82 Self {
83 bssid: bss_description.bssid.to_string(),
84 ssid: bss_description.ssid.to_string(),
85 protection: format!("{:?}", bss_description.protection()),
86 ht_cap: bss_description.raw_ht_cap().map(|cap| cap.bytes.into()),
87 vht_cap: bss_description.raw_vht_cap().map(|cap| cap.bytes.into()),
88 wsc: bss_description.probe_resp_wsc().as_ref().map(InspectNetworkWsc::from),
89 is_wmm_assoc: bss_description.find_wmm_param().is_some(),
90 wmm_param: bss_description.find_wmm_param().map(|bytes| bytes.into()),
91 }
92 }
93}
94
95#[derive(PartialEq, Unit, Hash)]
96struct InspectNetworkWsc {
97 device_name: String,
98 manufacturer: String,
99 model_name: String,
100 model_number: String,
101}
102
103impl From<&wlan_common::ie::wsc::ProbeRespWsc> for InspectNetworkWsc {
104 fn from(wsc: &wlan_common::ie::wsc::ProbeRespWsc) -> Self {
105 Self {
106 device_name: String::from_utf8_lossy(&wsc.device_name[..]).to_string(),
107 manufacturer: String::from_utf8_lossy(&wsc.manufacturer[..]).to_string(),
108 model_name: String::from_utf8_lossy(&wsc.model_name[..]).to_string(),
109 model_number: String::from_utf8_lossy(&wsc.model_number[..]).to_string(),
110 }
111 }
112}
113
114#[derive(PartialEq, Eq, Unit, Hash)]
115struct InspectDisconnectSource {
116 source: String,
117 reason: String,
118 mlme_event_name: Option<String>,
119}
120
121impl From<&fidl_sme::DisconnectSource> for InspectDisconnectSource {
122 fn from(disconnect_source: &fidl_sme::DisconnectSource) -> Self {
123 match disconnect_source {
124 fidl_sme::DisconnectSource::User(reason) => Self {
125 source: "user".to_string(),
126 reason: format!("{reason:?}"),
127 mlme_event_name: None,
128 },
129 fidl_sme::DisconnectSource::Ap(cause) => Self {
130 source: "ap".to_string(),
131 reason: format!("{:?}", cause.reason_code),
132 mlme_event_name: Some(format!("{:?}", cause.mlme_event_name)),
133 },
134 fidl_sme::DisconnectSource::Mlme(cause) => Self {
135 source: "mlme".to_string(),
136 reason: format!("{:?}", cause.reason_code),
137 mlme_event_name: Some(format!("{:?}", cause.mlme_event_name)),
138 },
139 }
140 }
141}
142
143#[derive(Clone, Debug, PartialEq)]
144pub struct DisconnectInfo {
145 pub iface_id: u16,
146 pub connected_duration: zx::BootDuration,
147 pub is_sme_reconnecting: bool,
148 pub disconnect_source: fidl_sme::DisconnectSource,
149 pub original_bss_desc: Box<BssDescription>,
150 pub current_rssi_dbm: i8,
151 pub current_snr_db: i8,
152 pub current_channel: Channel,
153}
154
155pub struct ConnectDisconnectLogger {
156 connection_state: Arc<Mutex<ConnectionState>>,
157 cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
158 connect_events_node: Mutex<AutoPersist<BoundedListNode>>,
159 disconnect_events_node: Mutex<AutoPersist<BoundedListNode>>,
160 inspect_metadata_node: Mutex<InspectMetadataNode>,
161 time_series_stats: ConnectDisconnectTimeSeries,
162 successive_connect_attempt_failures: AtomicUsize,
163 last_connect_failure_at: Arc<Mutex<Option<fasync::BootInstant>>>,
164 last_disconnect_at: Arc<Mutex<Option<fasync::MonotonicInstant>>>,
165}
166
167impl ConnectDisconnectLogger {
168 pub fn new<S: InspectSender>(
169 cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
170 inspect_node: &InspectNode,
171 inspect_metadata_node: &InspectNode,
172 inspect_metadata_path: &str,
173 persistence_req_sender: auto_persist::PersistenceReqSender,
174 time_matrix_client: &S,
175 ) -> Self {
176 let connect_events = inspect_node.create_child("connect_events");
177 let disconnect_events = inspect_node.create_child("disconnect_events");
178 let this = Self {
179 cobalt_proxy,
180 connection_state: Arc::new(Mutex::new(ConnectionState::Idle(IdleState {}))),
181 connect_events_node: Mutex::new(AutoPersist::new(
182 BoundedListNode::new(connect_events, INSPECT_CONNECT_EVENTS_LIMIT),
183 "wlan-connect-events",
184 persistence_req_sender.clone(),
185 )),
186 disconnect_events_node: Mutex::new(AutoPersist::new(
187 BoundedListNode::new(disconnect_events, INSPECT_DISCONNECT_EVENTS_LIMIT),
188 "wlan-disconnect-events",
189 persistence_req_sender,
190 )),
191 inspect_metadata_node: Mutex::new(InspectMetadataNode::new(inspect_metadata_node)),
192 time_series_stats: ConnectDisconnectTimeSeries::new(
193 time_matrix_client,
194 inspect_metadata_path,
195 ),
196 successive_connect_attempt_failures: AtomicUsize::new(0),
197 last_connect_failure_at: Arc::new(Mutex::new(None)),
198 last_disconnect_at: Arc::new(Mutex::new(None)),
199 };
200 this.log_connection_state();
201 this
202 }
203
204 fn update_connection_state(&self, state: ConnectionState) {
205 *self.connection_state.lock() = state;
206 self.log_connection_state();
207 }
208
209 fn log_connection_state(&self) {
210 let wlan_connectivity_state_id = self.connection_state.lock().to_id() as u64;
211 self.time_series_stats.log_wlan_connectivity_state(1 << wlan_connectivity_state_id);
212 }
213
214 pub async fn handle_connect_attempt(
215 &self,
216 result: fidl_ieee80211::StatusCode,
217 bss: &BssDescription,
218 ) {
219 let mut flushed_successive_failures = None;
220 let mut downtime_duration = None;
221 if result == fidl_ieee80211::StatusCode::Success {
222 self.update_connection_state(ConnectionState::Connected(ConnectedState {}));
223 flushed_successive_failures =
226 Some(self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst));
227 downtime_duration =
228 self.last_disconnect_at.lock().map(|t| fasync::MonotonicInstant::now() - t);
229 } else {
230 self.update_connection_state(ConnectionState::Idle(IdleState {}));
231 let _prev = self.successive_connect_attempt_failures.fetch_add(1, Ordering::SeqCst);
232 let _prev = self.last_connect_failure_at.lock().replace(fasync::BootInstant::now());
233 }
234
235 self.log_connect_attempt_inspect(result, bss);
236 self.log_connect_attempt_cobalt(result, flushed_successive_failures, downtime_duration)
237 .await;
238 }
239
240 pub fn log_connect_attempt_inspect(
241 &self,
242 result: fidl_ieee80211::StatusCode,
243 bss: &BssDescription,
244 ) {
245 if result == fidl_ieee80211::StatusCode::Success {
246 let mut inspect_metadata_node = self.inspect_metadata_node.lock();
247 let connected_network = InspectConnectedNetwork::from(bss);
248 let connected_network_id =
249 inspect_metadata_node.connected_networks.insert(connected_network) as u64;
250
251 self.time_series_stats.log_connected_networks(1 << connected_network_id);
252
253 inspect_log!(self.connect_events_node.lock().get_mut(), {
254 network_id: connected_network_id,
255 });
256 }
257 }
258
259 #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
260 pub async fn log_connect_attempt_cobalt(
261 &self,
262 result: fidl_ieee80211::StatusCode,
263 flushed_successive_failures: Option<usize>,
264 downtime_duration: Option<zx::MonotonicDuration>,
265 ) {
266 let mut metric_events = vec![];
267 metric_events.push(MetricEvent {
268 metric_id: metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
269 event_codes: vec![result as u32],
270 payload: MetricEventPayload::Count(1),
271 });
272
273 if let Some(failures) = flushed_successive_failures {
274 metric_events.push(MetricEvent {
275 metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
276 event_codes: vec![],
277 payload: MetricEventPayload::IntegerValue(failures as i64),
278 });
279 }
280
281 if let Some(duration) = downtime_duration {
282 metric_events.push(MetricEvent {
283 metric_id: metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID,
284 event_codes: vec![],
285 payload: MetricEventPayload::IntegerValue(duration.into_millis()),
286 });
287 }
288
289 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_connect_attempt_cobalt_metrics");
290 }
291
292 pub async fn log_disconnect(&self, info: &DisconnectInfo) {
293 self.update_connection_state(ConnectionState::Disconnected(DisconnectedState {}));
294 let _prev = self.last_disconnect_at.lock().replace(fasync::MonotonicInstant::now());
295 self.log_disconnect_inspect(info);
296 self.log_disconnect_cobalt(info).await;
297 }
298
299 fn log_disconnect_inspect(&self, info: &DisconnectInfo) {
300 let mut inspect_metadata_node = self.inspect_metadata_node.lock();
301 let connected_network = InspectConnectedNetwork::from(&*info.original_bss_desc);
302 let connected_network_id =
303 inspect_metadata_node.connected_networks.insert(connected_network) as u64;
304 let disconnect_source = InspectDisconnectSource::from(&info.disconnect_source);
305 let disconnect_source_id =
306 inspect_metadata_node.disconnect_sources.insert(disconnect_source) as u64;
307 inspect_log!(self.disconnect_events_node.lock().get_mut(), {
308 connected_duration: info.connected_duration.into_nanos(),
309 disconnect_source_id: disconnect_source_id,
310 network_id: connected_network_id,
311 rssi_dbm: info.current_rssi_dbm,
312 snr_db: info.current_snr_db,
313 channel: format!("{}", info.current_channel),
314 });
315
316 self.time_series_stats.log_disconnected_networks(1 << connected_network_id);
317 self.time_series_stats.log_disconnect_sources(1 << disconnect_source_id);
318 }
319
320 async fn log_disconnect_cobalt(&self, info: &DisconnectInfo) {
321 let mut metric_events = vec![];
322 metric_events.push(MetricEvent {
323 metric_id: metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID,
324 event_codes: vec![],
325 payload: MetricEventPayload::Count(1),
326 });
327
328 if should_log_disconnect_for_mobile_device(info) {
329 metric_events.push(MetricEvent {
330 metric_id: metrics::DISCONNECT_OCCURRENCE_FOR_MOBILE_DEVICE_METRIC_ID,
331 event_codes: vec![],
332 payload: MetricEventPayload::Count(1),
333 });
334 }
335
336 metric_events.push(MetricEvent {
337 metric_id: metrics::CONNECTED_DURATION_ON_DISCONNECT_METRIC_ID,
338 event_codes: vec![],
339 payload: MetricEventPayload::IntegerValue(info.connected_duration.into_millis()),
340 });
341
342 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_disconnect_cobalt_metrics");
343 }
344
345 pub async fn handle_periodic_telemetry(&self) {
346 let mut metric_events = vec![];
347 let now = fasync::BootInstant::now();
348 if let Some(failed_at) = *self.last_connect_failure_at.lock() {
349 if now - failed_at >= SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_TIMEOUT {
350 let failures = self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst);
351 if failures > 0 {
352 metric_events.push(MetricEvent {
353 metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
354 event_codes: vec![],
355 payload: MetricEventPayload::IntegerValue(failures as i64),
356 });
357 }
358 }
359 }
360
361 if !metric_events.is_empty() {
362 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_periodic_telemetry");
363 }
364 }
365}
366
367fn should_log_disconnect_for_mobile_device(info: &DisconnectInfo) -> bool {
368 match info.disconnect_source {
369 fidl_sme::DisconnectSource::Ap(_) => true,
370 fidl_sme::DisconnectSource::Mlme(cause)
371 if cause.reason_code != fidl_ieee80211::ReasonCode::MlmeLinkFailed =>
372 {
373 true
374 }
375 _ => false,
376 }
377}
378
379struct InspectMetadataNode {
380 connected_networks: LruCacheNode<InspectConnectedNetwork>,
381 disconnect_sources: LruCacheNode<InspectDisconnectSource>,
382}
383
384impl InspectMetadataNode {
385 const CONNECTED_NETWORKS: &'static str = "connected_networks";
386 const DISCONNECT_SOURCES: &'static str = "disconnect_sources";
387
388 fn new(inspect_node: &InspectNode) -> Self {
389 let connected_networks = inspect_node.create_child(Self::CONNECTED_NETWORKS);
390 let disconnect_sources = inspect_node.create_child(Self::DISCONNECT_SOURCES);
391 Self {
392 connected_networks: LruCacheNode::new(
393 connected_networks,
394 INSPECT_CONNECTED_NETWORKS_ID_LIMIT,
395 ),
396 disconnect_sources: LruCacheNode::new(
397 disconnect_sources,
398 INSPECT_DISCONNECT_SOURCES_ID_LIMIT,
399 ),
400 }
401 }
402}
403
404#[derive(Debug, Clone)]
405struct ConnectDisconnectTimeSeries {
406 wlan_connectivity_states: InspectedTimeMatrix<u64>,
407 connected_networks: InspectedTimeMatrix<u64>,
408 disconnected_networks: InspectedTimeMatrix<u64>,
409 disconnect_sources: InspectedTimeMatrix<u64>,
410}
411
412impl ConnectDisconnectTimeSeries {
413 pub fn new<S: InspectSender>(client: &S, inspect_metadata_path: &str) -> Self {
414 let wlan_connectivity_states = client.inspect_time_matrix_with_metadata(
415 "wlan_connectivity_states",
416 TimeMatrix::<Union<u64>, LastSample>::new(
417 SamplingProfile::highly_granular(),
418 LastSample::or(0),
419 ),
420 BitSetMap::from_ordered(["idle", "disconnected", "connected"]),
421 );
422 let connected_networks = client.inspect_time_matrix_with_metadata(
423 "connected_networks",
424 TimeMatrix::<Union<u64>, Constant>::new(
425 SamplingProfile::granular(),
426 Constant::default(),
427 ),
428 BitSetNode::from_path(format!(
429 "{}/{}",
430 inspect_metadata_path,
431 InspectMetadataNode::CONNECTED_NETWORKS
432 )),
433 );
434 let disconnected_networks = client.inspect_time_matrix_with_metadata(
435 "disconnected_networks",
436 TimeMatrix::<Union<u64>, Constant>::new(
437 SamplingProfile::granular(),
438 Constant::default(),
439 ),
440 BitSetNode::from_path(format!(
442 "{}/{}",
443 inspect_metadata_path,
444 InspectMetadataNode::CONNECTED_NETWORKS
445 )),
446 );
447 let disconnect_sources = client.inspect_time_matrix_with_metadata(
448 "disconnect_sources",
449 TimeMatrix::<Union<u64>, Constant>::new(
450 SamplingProfile::granular(),
451 Constant::default(),
452 ),
453 BitSetNode::from_path(format!(
454 "{}/{}",
455 inspect_metadata_path,
456 InspectMetadataNode::DISCONNECT_SOURCES,
457 )),
458 );
459 Self {
460 wlan_connectivity_states,
461 connected_networks,
462 disconnected_networks,
463 disconnect_sources,
464 }
465 }
466
467 fn log_wlan_connectivity_state(&self, data: u64) {
468 self.wlan_connectivity_states.fold_or_log_error(Timed::now(data));
469 }
470 fn log_connected_networks(&self, data: u64) {
471 self.connected_networks.fold_or_log_error(Timed::now(data));
472 }
473 fn log_disconnected_networks(&self, data: u64) {
474 self.disconnected_networks.fold_or_log_error(Timed::now(data));
475 }
476 fn log_disconnect_sources(&self, data: u64) {
477 self.disconnect_sources.fold_or_log_error(Timed::now(data));
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484 use crate::testing::*;
485 use diagnostics_assertions::{
486 assert_data_tree, AnyBoolProperty, AnyBytesProperty, AnyNumericProperty, AnyStringProperty,
487 };
488
489 use futures::task::Poll;
490 use ieee80211_testutils::{BSSID_REGEX, SSID_REGEX};
491 use rand::Rng;
492 use std::pin::pin;
493 use test_case::test_case;
494 use windowed_stats::experimental::serve;
495 use windowed_stats::experimental::testing::TimeMatrixCall;
496 use wlan_common::channel::{Cbw, Channel};
497 use wlan_common::{fake_bss_description, random_bss_description};
498
499 #[fuchsia::test]
500 fn log_connect_attempt_then_inspect_data_tree_contains_time_matrix_metadata() {
501 let mut harness = setup_test();
502
503 let (client, _server) = serve::serve_time_matrix_inspection(
504 harness.inspect_node.create_child("wlan_connect_disconnect"),
505 );
506 let logger = ConnectDisconnectLogger::new(
507 harness.cobalt_proxy.clone(),
508 &harness.inspect_node,
509 &harness.inspect_metadata_node,
510 &harness.inspect_metadata_path,
511 harness.persistence_sender.clone(),
512 &client,
513 );
514 let bss = random_bss_description!();
515 let mut log_connect_attempt =
516 pin!(logger.handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss));
517 assert!(
518 harness.run_until_stalled_drain_cobalt_events(&mut log_connect_attempt).is_ready(),
519 "`log_connect_attempt` did not complete",
520 );
521
522 let tree = harness.get_inspect_data_tree();
523 assert_data_tree!(
524 tree,
525 root: contains {
526 test_stats: contains {
527 wlan_connect_disconnect: contains {
528 wlan_connectivity_states: {
529 "type": "bitset",
530 "data": AnyBytesProperty,
531 metadata: {
532 index: {
533 "0": "idle",
534 "1": "disconnected",
535 "2": "connected",
536 },
537 },
538 },
539 connected_networks: {
540 "type": "bitset",
541 "data": AnyBytesProperty,
542 metadata: {
543 "index_node_path": "root/test_stats/metadata/connected_networks",
544 },
545 },
546 disconnected_networks: {
547 "type": "bitset",
548 "data": AnyBytesProperty,
549 metadata: {
550 "index_node_path": "root/test_stats/metadata/connected_networks",
551 },
552 },
553 disconnect_sources: {
554 "type": "bitset",
555 "data": AnyBytesProperty,
556 metadata: {
557 "index_node_path": "root/test_stats/metadata/disconnect_sources",
558 },
559 },
560 },
561 },
562 }
563 );
564 }
565
566 #[fuchsia::test]
567 fn test_log_connect_attempt_inspect() {
568 let mut test_helper = setup_test();
569 let logger = ConnectDisconnectLogger::new(
570 test_helper.cobalt_proxy.clone(),
571 &test_helper.inspect_node,
572 &test_helper.inspect_metadata_node,
573 &test_helper.inspect_metadata_path,
574 test_helper.persistence_sender.clone(),
575 &test_helper.mock_time_matrix_client,
576 );
577
578 let bss_description = random_bss_description!();
580 let mut test_fut =
581 pin!(logger
582 .handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss_description));
583 assert_eq!(
584 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
585 Poll::Ready(())
586 );
587
588 let data = test_helper.get_inspect_data_tree();
590 assert_data_tree!(data, root: contains {
591 test_stats: contains {
592 metadata: contains {
593 connected_networks: contains {
594 "0": {
595 "@time": AnyNumericProperty,
596 "data": contains {
597 bssid: &*BSSID_REGEX,
598 ssid: &*SSID_REGEX,
599 }
600 }
601 },
602 },
603 connect_events: {
604 "0": {
605 "@time": AnyNumericProperty,
606 network_id: 0u64,
607 }
608 }
609 }
610 });
611
612 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
613 assert_eq!(
614 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
615 &[TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 2)),]
616 );
617 assert_eq!(
618 &time_matrix_calls.drain::<u64>("connected_networks")[..],
619 &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
620 );
621 }
622
623 #[fuchsia::test]
624 fn test_log_connect_attempt_cobalt() {
625 let mut test_helper = setup_test();
626 let logger = ConnectDisconnectLogger::new(
627 test_helper.cobalt_proxy.clone(),
628 &test_helper.inspect_node,
629 &test_helper.inspect_metadata_node,
630 &test_helper.inspect_metadata_path,
631 test_helper.persistence_sender.clone(),
632 &test_helper.mock_time_matrix_client,
633 );
634
635 let bss_description = random_bss_description!(Wpa2,
637 channel: Channel::new(157, Cbw::Cbw40),
638 bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
639 );
640
641 let mut test_fut =
643 pin!(logger
644 .handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss_description));
645 assert_eq!(
646 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
647 Poll::Ready(())
648 );
649
650 let breakdowns_by_status_code = test_helper
652 .get_logged_metrics(metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID);
653 assert_eq!(breakdowns_by_status_code.len(), 1);
654 assert_eq!(
655 breakdowns_by_status_code[0].event_codes,
656 vec![fidl_ieee80211::StatusCode::Success as u32]
657 );
658 assert_eq!(breakdowns_by_status_code[0].payload, MetricEventPayload::Count(1));
659 }
660
661 #[fuchsia::test]
662 fn test_successive_connect_attempt_failures_cobalt_zero_failures() {
663 let mut test_helper = setup_test();
664 let logger = ConnectDisconnectLogger::new(
665 test_helper.cobalt_proxy.clone(),
666 &test_helper.inspect_node,
667 &test_helper.inspect_metadata_node,
668 &test_helper.inspect_metadata_path,
669 test_helper.persistence_sender.clone(),
670 &test_helper.mock_time_matrix_client,
671 );
672
673 let bss_description = random_bss_description!(Wpa2);
674 let mut test_fut =
675 pin!(logger
676 .handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss_description));
677 assert_eq!(
678 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
679 Poll::Ready(())
680 );
681
682 let metrics =
683 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
684 assert_eq!(metrics.len(), 1);
685 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(0));
686 }
687
688 #[test_case(1; "one_failure")]
689 #[test_case(2; "two_failures")]
690 #[fuchsia::test(add_test_attr = false)]
691 fn test_successive_connect_attempt_failures_cobalt_one_failure_then_success(n_failures: usize) {
692 let mut test_helper = setup_test();
693 let logger = ConnectDisconnectLogger::new(
694 test_helper.cobalt_proxy.clone(),
695 &test_helper.inspect_node,
696 &test_helper.inspect_metadata_node,
697 &test_helper.inspect_metadata_path,
698 test_helper.persistence_sender.clone(),
699 &test_helper.mock_time_matrix_client,
700 );
701
702 let bss_description = random_bss_description!(Wpa2);
703 for _i in 0..n_failures {
704 let mut test_fut = pin!(logger.handle_connect_attempt(
705 fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
706 &bss_description
707 ));
708 assert_eq!(
709 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
710 Poll::Ready(())
711 );
712 }
713
714 let metrics =
715 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
716 assert!(metrics.is_empty());
717
718 let mut test_fut =
719 pin!(logger
720 .handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss_description));
721 assert_eq!(
722 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
723 Poll::Ready(())
724 );
725
726 let metrics =
727 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
728 assert_eq!(metrics.len(), 1);
729 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
730
731 test_helper.clear_cobalt_events();
733 let mut test_fut =
734 pin!(logger
735 .handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss_description));
736 assert_eq!(
737 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
738 Poll::Ready(())
739 );
740 let metrics =
741 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
742 assert_eq!(metrics.len(), 1);
743 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(0));
744 }
745
746 #[test_case(1; "one_failure")]
747 #[test_case(2; "two_failures")]
748 #[fuchsia::test(add_test_attr = false)]
749 fn test_successive_connect_attempt_failures_cobalt_one_failure_then_timeout(n_failures: usize) {
750 let mut test_helper = setup_test();
751 let logger = ConnectDisconnectLogger::new(
752 test_helper.cobalt_proxy.clone(),
753 &test_helper.inspect_node,
754 &test_helper.inspect_metadata_node,
755 &test_helper.inspect_metadata_path,
756 test_helper.persistence_sender.clone(),
757 &test_helper.mock_time_matrix_client,
758 );
759
760 let bss_description = random_bss_description!(Wpa2);
761 for _i in 0..n_failures {
762 let mut test_fut = pin!(logger.handle_connect_attempt(
763 fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
764 &bss_description
765 ));
766 assert_eq!(
767 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
768 Poll::Ready(())
769 );
770 }
771
772 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(60_000_000_000));
773 let mut test_fut = pin!(logger.handle_periodic_telemetry());
774 assert_eq!(
775 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
776 Poll::Ready(())
777 );
778
779 let metrics =
781 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
782 assert!(metrics.is_empty());
783
784 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(120_000_000_000));
785 let mut test_fut = pin!(logger.handle_periodic_telemetry());
786 assert_eq!(
787 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
788 Poll::Ready(())
789 );
790
791 let metrics =
792 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
793 assert_eq!(metrics.len(), 1);
794 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
795
796 test_helper.clear_cobalt_events();
798 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(240_000_000_000));
799 let mut test_fut = pin!(logger.handle_periodic_telemetry());
800 assert_eq!(
801 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
802 Poll::Ready(())
803 );
804 let metrics =
805 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
806 assert!(metrics.is_empty());
807 }
808
809 #[fuchsia::test]
810 fn test_log_disconnect_inspect() {
811 let mut test_helper = setup_test();
812 let logger = ConnectDisconnectLogger::new(
813 test_helper.cobalt_proxy.clone(),
814 &test_helper.inspect_node,
815 &test_helper.inspect_metadata_node,
816 &test_helper.inspect_metadata_path,
817 test_helper.persistence_sender.clone(),
818 &test_helper.mock_time_matrix_client,
819 );
820
821 let bss_description = fake_bss_description!(Open);
823 let channel = bss_description.channel;
824 let disconnect_info = DisconnectInfo {
825 iface_id: 32,
826 connected_duration: zx::BootDuration::from_seconds(30),
827 is_sme_reconnecting: false,
828 disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
829 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
830 reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
831 }),
832 original_bss_desc: Box::new(bss_description),
833 current_rssi_dbm: -30,
834 current_snr_db: 25,
835 current_channel: channel,
836 };
837 let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
838 assert_eq!(
839 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
840 Poll::Ready(())
841 );
842
843 let data = test_helper.get_inspect_data_tree();
845 assert_data_tree!(data, root: contains {
846 test_stats: contains {
847 metadata: {
848 connected_networks: {
849 "0": {
850 "@time": AnyNumericProperty,
851 "data": {
852 bssid: &*BSSID_REGEX,
853 ssid: &*SSID_REGEX,
854 ht_cap: AnyBytesProperty,
855 vht_cap: AnyBytesProperty,
856 protection: "Open",
857 is_wmm_assoc: AnyBoolProperty,
858 wmm_param: AnyBytesProperty,
859 }
860 }
861 },
862 disconnect_sources: {
863 "0": {
864 "@time": AnyNumericProperty,
865 "data": {
866 source: "ap",
867 reason: "UnspecifiedReason",
868 mlme_event_name: "DeauthenticateIndication",
869 }
870 }
871 },
872 },
873 disconnect_events: {
874 "0": {
875 "@time": AnyNumericProperty,
876 connected_duration: zx::BootDuration::from_seconds(30).into_nanos(),
877 disconnect_source_id: 0u64,
878 network_id: 0u64,
879 rssi_dbm: -30i64,
880 snr_db: 25i64,
881 channel: AnyStringProperty,
882 }
883 }
884 }
885 });
886
887 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
888 assert_eq!(
889 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
890 &[TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 1)),]
891 );
892 assert_eq!(
893 &time_matrix_calls.drain::<u64>("disconnected_networks")[..],
894 &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
895 );
896 assert_eq!(
897 &time_matrix_calls.drain::<u64>("disconnect_sources")[..],
898 &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
899 );
900 }
901
902 #[fuchsia::test]
903 fn test_log_disconnect_cobalt() {
904 let mut test_helper = setup_test();
905 let logger = ConnectDisconnectLogger::new(
906 test_helper.cobalt_proxy.clone(),
907 &test_helper.inspect_node,
908 &test_helper.inspect_metadata_node,
909 &test_helper.inspect_metadata_path,
910 test_helper.persistence_sender.clone(),
911 &test_helper.mock_time_matrix_client,
912 );
913
914 let disconnect_info = DisconnectInfo {
916 connected_duration: zx::BootDuration::from_millis(300_000),
917 ..fake_disconnect_info()
918 };
919 let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
920 assert_eq!(
921 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
922 Poll::Ready(())
923 );
924
925 let disconnect_count_metrics =
926 test_helper.get_logged_metrics(metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID);
927 assert_eq!(disconnect_count_metrics.len(), 1);
928 assert_eq!(disconnect_count_metrics[0].payload, MetricEventPayload::Count(1));
929
930 let connected_duration_metrics =
931 test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_ON_DISCONNECT_METRIC_ID);
932 assert_eq!(connected_duration_metrics.len(), 1);
933 assert_eq!(
934 connected_duration_metrics[0].payload,
935 MetricEventPayload::IntegerValue(300_000)
936 );
937 }
938
939 #[test_case(
940 fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
941 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
942 reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
943 }),
944 true;
945 "ap_disconnect_source"
946 )]
947 #[test_case(
948 fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
949 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
950 reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
951 }),
952 true;
953 "mlme_disconnect_source_not_link_failed"
954 )]
955 #[test_case(
956 fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
957 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
958 reason_code: fidl_ieee80211::ReasonCode::MlmeLinkFailed,
959 }),
960 false;
961 "mlme_link_failed"
962 )]
963 #[test_case(
964 fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::Unknown),
965 false;
966 "user_disconnect_source"
967 )]
968 #[fuchsia::test(add_test_attr = false)]
969 fn test_log_disconnect_for_mobile_device_cobalt(
970 disconnect_source: fidl_sme::DisconnectSource,
971 should_log: bool,
972 ) {
973 let mut test_helper = setup_test();
974 let logger = ConnectDisconnectLogger::new(
975 test_helper.cobalt_proxy.clone(),
976 &test_helper.inspect_node,
977 &test_helper.inspect_metadata_node,
978 &test_helper.inspect_metadata_path,
979 test_helper.persistence_sender.clone(),
980 &test_helper.mock_time_matrix_client,
981 );
982
983 let disconnect_info = DisconnectInfo { disconnect_source, ..fake_disconnect_info() };
985 let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
986 assert_eq!(
987 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
988 Poll::Ready(())
989 );
990
991 let metrics = test_helper
992 .get_logged_metrics(metrics::DISCONNECT_OCCURRENCE_FOR_MOBILE_DEVICE_METRIC_ID);
993 if should_log {
994 assert_eq!(metrics.len(), 1);
995 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
996 } else {
997 assert!(metrics.is_empty());
998 }
999 }
1000
1001 #[fuchsia::test]
1002 fn test_log_downtime_post_disconnect_on_reconnect() {
1003 let mut test_helper = setup_test();
1004 let logger = ConnectDisconnectLogger::new(
1005 test_helper.cobalt_proxy.clone(),
1006 &test_helper.inspect_node,
1007 &test_helper.inspect_metadata_node,
1008 &test_helper.inspect_metadata_path,
1009 test_helper.persistence_sender.clone(),
1010 &test_helper.mock_time_matrix_client,
1011 );
1012
1013 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(15_000_000_000));
1015 let bss_description = random_bss_description!(Wpa2);
1016 let mut test_fut =
1017 pin!(logger
1018 .handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss_description));
1019 assert_eq!(
1020 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1021 Poll::Ready(())
1022 );
1023
1024 let metrics = test_helper.get_logged_metrics(metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID);
1026 assert!(metrics.is_empty());
1027
1028 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(25_000_000_000));
1030 let disconnect_info = fake_disconnect_info();
1031 let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1032 assert_eq!(
1033 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1034 Poll::Ready(())
1035 );
1036
1037 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(60_000_000_000));
1039 let mut test_fut =
1040 pin!(logger
1041 .handle_connect_attempt(fidl_ieee80211::StatusCode::Success, &bss_description));
1042 assert_eq!(
1043 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1044 Poll::Ready(())
1045 );
1046
1047 let metrics = test_helper.get_logged_metrics(metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID);
1049 assert_eq!(metrics.len(), 1);
1050 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(35_000));
1051 }
1052
1053 fn fake_disconnect_info() -> DisconnectInfo {
1054 let bss_description = random_bss_description!(Wpa2);
1055 let channel = bss_description.channel;
1056 DisconnectInfo {
1057 iface_id: 1,
1058 connected_duration: zx::BootDuration::from_hours(6),
1059 is_sme_reconnecting: false,
1060 disconnect_source: fidl_sme::DisconnectSource::User(
1061 fidl_sme::UserDisconnectReason::Unknown,
1062 ),
1063 original_bss_desc: bss_description.into(),
1064 current_rssi_dbm: -30,
1065 current_snr_db: 25,
1066 current_channel: channel,
1067 }
1068 }
1069}