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