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