1use crate::convert::{
6 convert_channel_band, convert_is_owe_transition, convert_rssi_bucket, convert_security_type,
7 convert_snr_bucket,
8};
9use crate::processors::toggle_events::ClientConnectionsToggleEvent;
10use crate::util::cobalt_logger::log_cobalt_batch;
11use derivative::Derivative;
12use fidl_fuchsia_metrics::{MetricEvent, MetricEventPayload};
13use fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211;
14use fidl_fuchsia_wlan_sme as fidl_sme;
15use fuchsia_async as fasync;
16use fuchsia_inspect::Node as InspectNode;
17use fuchsia_inspect_contrib::id_enum::IdEnum;
18use fuchsia_inspect_contrib::inspect_log;
19use fuchsia_inspect_contrib::nodes::{BoundedListNode, LruCacheNode};
20use fuchsia_inspect_derive::Unit;
21use fuchsia_sync::Mutex;
22use ieee80211::OuiFmt;
23use std::collections::HashMap;
24use std::sync::Arc;
25use std::sync::atomic::{AtomicUsize, Ordering};
26use strum_macros::{Display, EnumIter};
27use windowed_stats::experimental::inspect::{InspectSender, InspectedTimeMatrix};
28use windowed_stats::experimental::series::interpolation::{ConstantSample, LastSample};
29use windowed_stats::experimental::series::metadata::{BitSetMap, BitSetNode};
30use windowed_stats::experimental::series::statistic::Union;
31use windowed_stats::experimental::series::{SamplingProfile, TimeMatrix};
32use wlan_common::bss::BssDescription;
33use wlan_common::channel::Channel;
34use wlan_legacy_metrics_registry as metrics;
35use zx;
36
37const INSPECT_CONNECT_EVENTS_LIMIT: usize = 10;
38const INSPECT_DISCONNECT_EVENTS_LIMIT: usize = 20;
39const INSPECT_CONNECT_ATTEMPT_RESULTS_LIMIT: usize = 50;
40const INSPECT_CONNECTED_NETWORKS_ID_LIMIT: usize = 16;
41const INSPECT_DISCONNECT_SOURCES_ID_LIMIT: usize = 32;
42const INSPECT_CONNECT_ATTEMPT_RESULTS_ID_LIMIT: usize = 32;
43const SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_TIMEOUT: zx::BootDuration =
44 zx::BootDuration::from_minutes(2);
45const DAILY_METRICS_LOG_INTERVAL: zx::BootDuration = zx::BootDuration::from_hours(24);
46
47#[derive(Clone, Debug, Display, EnumIter)]
48enum ConnectionState {
49 Idle(IdleState),
50 Connected(ConnectedState),
51 Disconnected(DisconnectedState),
52 ConnectFailed(ConnectFailedState),
53 FailedToStart(FailedToStartState),
54 FailedToStop(FailedToStopState),
55 PnoScanFailedIdle(PnoScanFailedIdleState),
56}
57
58impl IdEnum for ConnectionState {
60 type Id = u8;
61 fn to_id(&self) -> Self::Id {
62 match self {
63 Self::Idle(_) => 0,
64 Self::Disconnected(_) => 1,
65 Self::ConnectFailed(_) => 2,
66 Self::Connected(_) => 3,
67 Self::FailedToStart(_) => 4,
68 Self::FailedToStop(_) => 5,
69 Self::PnoScanFailedIdle(_) => 6,
70 }
71 }
72}
73
74#[derive(Clone, Debug, Default)]
75struct IdleState {}
76
77#[derive(Clone, Debug, Default)]
78struct ConnectedState {}
79
80#[derive(Clone, Debug, Default)]
81struct DisconnectedState {}
82
83#[derive(Clone, Debug, Default)]
84struct ConnectFailedState {}
85
86#[derive(Clone, Debug, Default)]
87struct FailedToStartState {}
88
89#[derive(Clone, Debug, Default)]
90struct FailedToStopState {}
91
92#[derive(Clone, Debug, Default)]
93struct PnoScanFailedIdleState {}
94
95#[derive(Derivative, Unit)]
96#[derivative(PartialEq, Eq, Hash)]
97struct InspectConnectedNetwork {
98 bssid: String,
99 ssid: String,
100 protection: String,
101 ht_cap: Option<Vec<u8>>,
102 vht_cap: Option<Vec<u8>>,
103 #[derivative(PartialEq = "ignore")]
104 #[derivative(Hash = "ignore")]
105 wsc: Option<InspectNetworkWsc>,
106 is_wmm_assoc: bool,
107 wmm_param: Option<Vec<u8>>,
108}
109
110impl From<&BssDescription> for InspectConnectedNetwork {
111 fn from(bss_description: &BssDescription) -> Self {
112 Self {
113 bssid: bss_description.bssid.to_string(),
114 ssid: bss_description.ssid.to_string(),
115 protection: format!("{:?}", bss_description.protection()),
116 ht_cap: bss_description.raw_ht_cap().map(|cap| cap.bytes.into()),
117 vht_cap: bss_description.raw_vht_cap().map(|cap| cap.bytes.into()),
118 wsc: bss_description.probe_resp_wsc().as_ref().map(InspectNetworkWsc::from),
119 is_wmm_assoc: bss_description.find_wmm_param().is_some(),
120 wmm_param: bss_description.find_wmm_param().map(|bytes| bytes.into()),
121 }
122 }
123}
124
125#[derive(PartialEq, Unit, Hash)]
126struct InspectNetworkWsc {
127 device_name: String,
128 manufacturer: String,
129 model_name: String,
130 model_number: String,
131}
132
133impl From<&wlan_common::ie::wsc::ProbeRespWsc> for InspectNetworkWsc {
134 fn from(wsc: &wlan_common::ie::wsc::ProbeRespWsc) -> Self {
135 Self {
136 device_name: String::from_utf8_lossy(&wsc.device_name[..]).to_string(),
137 manufacturer: String::from_utf8_lossy(&wsc.manufacturer[..]).to_string(),
138 model_name: String::from_utf8_lossy(&wsc.model_name[..]).to_string(),
139 model_number: String::from_utf8_lossy(&wsc.model_number[..]).to_string(),
140 }
141 }
142}
143
144#[derive(PartialEq, Eq, Unit, Hash)]
145struct InspectConnectAttemptResult {
146 status_code: u16,
147 result: String,
148}
149
150#[derive(PartialEq, Eq, Unit, Hash)]
151struct InspectDisconnectSource {
152 source: String,
153 reason: String,
154 mlme_event_name: Option<String>,
155}
156
157impl From<&fidl_sme::DisconnectSource> for InspectDisconnectSource {
158 fn from(disconnect_source: &fidl_sme::DisconnectSource) -> Self {
159 match disconnect_source {
160 fidl_sme::DisconnectSource::User(reason) => Self {
161 source: "user".to_string(),
162 reason: format!("{reason:?}"),
163 mlme_event_name: None,
164 },
165 fidl_sme::DisconnectSource::Ap(cause) => Self {
166 source: "ap".to_string(),
167 reason: format!("{:?}", cause.reason_code),
168 mlme_event_name: Some(format!("{:?}", cause.mlme_event_name)),
169 },
170 fidl_sme::DisconnectSource::Mlme(cause) => Self {
171 source: "mlme".to_string(),
172 reason: format!("{:?}", cause.reason_code),
173 mlme_event_name: Some(format!("{:?}", cause.mlme_event_name)),
174 },
175 }
176 }
177}
178
179#[derive(Clone, Debug, PartialEq)]
180pub struct DisconnectInfo {
181 pub iface_id: u16,
182 pub connected_duration: zx::BootDuration,
183 pub is_sme_reconnecting: bool,
184 pub disconnect_source: fidl_sme::DisconnectSource,
185 pub original_bss_desc: Box<BssDescription>,
186 pub current_rssi_dbm: i8,
187 pub current_snr_db: i8,
188 pub current_channel: Channel,
189}
190
191pub struct ConnectDisconnectLogger {
192 connection_state: Arc<Mutex<ConnectionState>>,
193 cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
194 connect_events_node: Mutex<BoundedListNode>,
195 disconnect_events_node: Mutex<BoundedListNode>,
196 connect_attempt_results_node: Mutex<BoundedListNode>,
197 inspect_metadata_node: Mutex<InspectMetadataNode>,
198 time_series_stats: ConnectDisconnectTimeSeries,
199 successive_connect_attempt_failures: AtomicUsize,
200 last_connect_failure_at: Arc<Mutex<Option<fasync::BootInstant>>>,
201 last_disconnect_at: Arc<Mutex<Option<fasync::MonotonicInstant>>>,
202 daily_connect_stats: Mutex<DailyConnectStats>,
203}
204
205impl ConnectDisconnectLogger {
206 pub fn new<S: InspectSender>(
207 cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
208 inspect_node: &InspectNode,
209 inspect_metadata_node: &InspectNode,
210 inspect_metadata_path: &str,
211 time_matrix_client: &S,
212 ) -> Self {
213 let connect_events = inspect_node.create_child("connect_events");
214 let disconnect_events = inspect_node.create_child("disconnect_events");
215 let connect_attempt_results = inspect_node.create_child("connect_attempt_results");
216 let this = Self {
217 cobalt_proxy,
218 connection_state: Arc::new(Mutex::new(ConnectionState::Idle(IdleState {}))),
219 connect_events_node: Mutex::new(BoundedListNode::new(
220 connect_events,
221 INSPECT_CONNECT_EVENTS_LIMIT,
222 )),
223 disconnect_events_node: Mutex::new(BoundedListNode::new(
224 disconnect_events,
225 INSPECT_DISCONNECT_EVENTS_LIMIT,
226 )),
227 connect_attempt_results_node: Mutex::new(BoundedListNode::new(
228 connect_attempt_results,
229 INSPECT_CONNECT_ATTEMPT_RESULTS_LIMIT,
230 )),
231 inspect_metadata_node: Mutex::new(InspectMetadataNode::new(inspect_metadata_node)),
232 time_series_stats: ConnectDisconnectTimeSeries::new(
233 time_matrix_client,
234 inspect_metadata_path,
235 ),
236 successive_connect_attempt_failures: AtomicUsize::new(0),
237 last_connect_failure_at: Arc::new(Mutex::new(None)),
238 last_disconnect_at: Arc::new(Mutex::new(None)),
239 daily_connect_stats: Mutex::new(DailyConnectStats::new(fasync::BootInstant::now())),
240 };
241 this.log_connection_state();
242 this
243 }
244
245 fn update_connection_state(&self, state: ConnectionState) {
246 *self.connection_state.lock() = state;
247 self.log_connection_state();
248 }
249
250 fn log_connection_state(&self) {
251 let wlan_connectivity_state_id = self.connection_state.lock().to_id() as u64;
252 self.time_series_stats.log_wlan_connectivity_state(1 << wlan_connectivity_state_id);
253 }
254
255 pub fn is_connected(&self) -> bool {
256 matches!(*self.connection_state.lock(), ConnectionState::Connected(_))
257 }
258
259 pub async fn handle_connect_attempt(
260 &self,
261 result: fidl_ieee80211::StatusCode,
262 bss: &BssDescription,
263 is_credential_rejected: bool,
264 is_owe_transition: bool,
265 ) {
266 let mut flushed_successive_failures = None;
267 let mut downtime_duration = None;
268 if result == fidl_ieee80211::StatusCode::Success {
269 self.update_connection_state(ConnectionState::Connected(ConnectedState {}));
270 flushed_successive_failures =
271 Some(self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst));
272 downtime_duration =
273 self.last_disconnect_at.lock().map(|t| fasync::MonotonicInstant::now() - t);
274 } else if is_credential_rejected {
275 self.update_connection_state(ConnectionState::Idle(IdleState {}));
276 let _prev = self.successive_connect_attempt_failures.fetch_add(1, Ordering::SeqCst);
277 let _prev = self.last_connect_failure_at.lock().replace(fasync::BootInstant::now());
278 } else {
279 self.update_connection_state(ConnectionState::ConnectFailed(ConnectFailedState {}));
280 let _prev = self.successive_connect_attempt_failures.fetch_add(1, Ordering::SeqCst);
281 let _prev = self.last_connect_failure_at.lock().replace(fasync::BootInstant::now());
282 }
283
284 self.log_connect_attempt_inspect(result, bss);
285 self.log_connect_attempt_cobalt(result, flushed_successive_failures, downtime_duration)
286 .await;
287 if result == fidl_ieee80211::StatusCode::Success {
288 self.log_device_connected_cobalt_metrics(bss, is_owe_transition).await;
289 }
290
291 let security_type = convert_security_type(&bss.protection());
292 let primary_channel = bss.channel.primary;
293 let channel_band = convert_channel_band(primary_channel);
294 let rssi_bucket = convert_rssi_bucket(bss.rssi_dbm);
295 let snr_bucket = convert_snr_bucket(bss.snr_db);
296 let is_owe_transition_dim = convert_is_owe_transition(is_owe_transition);
297
298 let mut daily_stats = self.daily_connect_stats.lock();
299 daily_stats.connect_per_security_type.entry(security_type).or_default().increment(result);
300 daily_stats
301 .connect_per_primary_channel
302 .entry(primary_channel)
303 .or_default()
304 .increment(result);
305 daily_stats.connect_per_channel_band.entry(channel_band).or_default().increment(result);
306 daily_stats.connect_per_rssi_bucket.entry(rssi_bucket).or_default().increment(result);
307 daily_stats.connect_per_snr_bucket.entry(snr_bucket).or_default().increment(result);
308 daily_stats
309 .connect_per_is_owe_transition
310 .entry(is_owe_transition_dim)
311 .or_default()
312 .increment(result);
313 }
314
315 fn log_connect_attempt_inspect(
316 &self,
317 result: fidl_ieee80211::StatusCode,
318 bss: &BssDescription,
319 ) {
320 let mut inspect_metadata_node = self.inspect_metadata_node.lock();
321 let connect_result_id =
322 inspect_metadata_node.connect_attempt_results.insert(InspectConnectAttemptResult {
323 status_code: result.into_primitive(),
324 result: format!("{:?}", result),
325 }) as u64;
326 self.time_series_stats.log_connect_attempt_results(1 << connect_result_id);
327
328 inspect_log!(self.connect_attempt_results_node.lock(), {
329 result: format!("{:?}", result),
330 ssid: bss.ssid.to_string(),
331 bssid: bss.bssid.to_string(),
332 protection: format!("{:?}", bss.protection()),
333 });
334
335 if result == fidl_ieee80211::StatusCode::Success {
336 let connected_network = InspectConnectedNetwork::from(bss);
337 let connected_network_id =
338 inspect_metadata_node.connected_networks.insert(connected_network) as u64;
339
340 self.time_series_stats.log_connected_networks(1 << connected_network_id);
341
342 inspect_log!(self.connect_events_node.lock(), {
343 network_id: connected_network_id,
344 });
345 }
346 }
347
348 #[allow(clippy::vec_init_then_push, reason = "mass allow for https://fxbug.dev/381896734")]
349 async fn log_connect_attempt_cobalt(
350 &self,
351 result: fidl_ieee80211::StatusCode,
352 flushed_successive_failures: Option<usize>,
353 downtime_duration: Option<zx::MonotonicDuration>,
354 ) {
355 let mut metric_events = vec![];
356 metric_events.push(MetricEvent {
357 metric_id: metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID,
358 event_codes: vec![result.into_primitive() as u32],
359 payload: MetricEventPayload::Count(1),
360 });
361
362 if let Some(failures) = flushed_successive_failures {
363 metric_events.push(MetricEvent {
364 metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
365 event_codes: vec![],
366 payload: MetricEventPayload::IntegerValue(failures as i64),
367 });
368 }
369
370 if let Some(duration) = downtime_duration {
371 metric_events.push(MetricEvent {
372 metric_id: metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID,
373 event_codes: vec![],
374 payload: MetricEventPayload::IntegerValue(duration.into_millis()),
375 });
376 }
377
378 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_connect_attempt_cobalt");
379 }
380
381 async fn log_device_connected_cobalt_metrics(
382 &self,
383 bss: &BssDescription,
384 is_owe_transition: bool,
385 ) {
386 let mut metric_events = vec![];
387 metric_events.push(MetricEvent {
388 metric_id: metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID,
389 event_codes: vec![],
390 payload: MetricEventPayload::Count(1),
391 });
392
393 let security_type_dim = convert_security_type(&bss.protection());
394 metric_events.push(MetricEvent {
395 metric_id: metrics::CONNECTED_NETWORK_SECURITY_TYPE_METRIC_ID,
396 event_codes: vec![security_type_dim as u32],
397 payload: MetricEventPayload::Count(1),
398 });
399
400 if bss.supports_uapsd() {
401 metric_events.push(MetricEvent {
402 metric_id: metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_APSD_METRIC_ID,
403 event_codes: vec![],
404 payload: MetricEventPayload::Count(1),
405 });
406 }
407
408 if let Some(rm_enabled_cap) = bss.rm_enabled_cap() {
409 if rm_enabled_cap.link_measurement_enabled() {
410 metric_events.push(MetricEvent {
411 metric_id:
412 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_LINK_MEASUREMENT_METRIC_ID,
413 event_codes: vec![],
414 payload: MetricEventPayload::Count(1),
415 });
416 }
417 if rm_enabled_cap.neighbor_report_enabled() {
418 metric_events.push(MetricEvent {
419 metric_id:
420 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_NEIGHBOR_REPORT_METRIC_ID,
421 event_codes: vec![],
422 payload: MetricEventPayload::Count(1),
423 });
424 }
425 }
426
427 if bss.supports_ft() {
428 metric_events.push(MetricEvent {
429 metric_id: metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_FT_METRIC_ID,
430 event_codes: vec![],
431 payload: MetricEventPayload::Count(1),
432 });
433 }
434
435 if let Some(cap) = bss.ext_cap().and_then(|cap| cap.ext_caps_octet_3)
436 && cap.bss_transition()
437 {
438 metric_events.push(MetricEvent {
439 metric_id: metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_BSS_TRANSITION_MANAGEMENT_METRIC_ID,
440 event_codes: vec![],
441 payload: MetricEventPayload::Count(1),
442 });
443 }
444
445 metric_events.push(MetricEvent {
446 metric_id: metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
447 event_codes: vec![bss.channel.primary as u32],
448 payload: MetricEventPayload::Count(1),
449 });
450
451 let channel_band_dim = convert_channel_band(bss.channel.primary);
452 metric_events.push(MetricEvent {
453 metric_id: metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
454 event_codes: vec![channel_band_dim as u32],
455 payload: MetricEventPayload::Count(1),
456 });
457
458 let oui_string = bss.bssid.to_oui_uppercase("");
459 metric_events.push(MetricEvent {
460 metric_id: metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID,
461 event_codes: vec![],
462 payload: MetricEventPayload::StringValue(oui_string),
463 });
464
465 let is_owe_transition_dim = convert_is_owe_transition(is_owe_transition);
466 metric_events.push(MetricEvent {
467 metric_id: metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_IS_OWE_TRANSITION_METRIC_ID,
468 event_codes: vec![is_owe_transition_dim as u32],
469 payload: MetricEventPayload::Count(1),
470 });
471
472 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_device_connected_cobalt_metrics");
473 }
474
475 pub async fn log_disconnect(&self, info: &DisconnectInfo) {
476 if !info.disconnect_source.should_log_for_mobile_device() {
482 self.update_connection_state(ConnectionState::Idle(IdleState {}));
483 } else {
484 self.update_connection_state(ConnectionState::Disconnected(DisconnectedState {}));
485 }
486 let _prev = self.last_disconnect_at.lock().replace(fasync::MonotonicInstant::now());
487 self.log_disconnect_inspect(info);
488 self.log_disconnect_cobalt(info).await;
489 }
490
491 fn log_disconnect_inspect(&self, info: &DisconnectInfo) {
492 let mut inspect_metadata_node = self.inspect_metadata_node.lock();
493 let connected_network = InspectConnectedNetwork::from(&*info.original_bss_desc);
494 let connected_network_id =
495 inspect_metadata_node.connected_networks.insert(connected_network) as u64;
496 let disconnect_source = InspectDisconnectSource::from(&info.disconnect_source);
497 let disconnect_source_id =
498 inspect_metadata_node.disconnect_sources.insert(disconnect_source) as u64;
499 inspect_log!(self.disconnect_events_node.lock(), {
500 connected_duration: info.connected_duration.into_nanos(),
501 disconnect_source_id: disconnect_source_id,
502 network_id: connected_network_id,
503 rssi_dbm: info.current_rssi_dbm,
504 snr_db: info.current_snr_db,
505 channel: format!("{}", info.current_channel),
506 });
507
508 self.time_series_stats.log_disconnected_networks(1 << connected_network_id);
509 self.time_series_stats.log_disconnect_sources(1 << disconnect_source_id);
510 }
511
512 async fn log_disconnect_cobalt(&self, info: &DisconnectInfo) {
513 let mut metric_events = vec![];
514 metric_events.push(MetricEvent {
515 metric_id: metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID,
516 event_codes: vec![],
517 payload: MetricEventPayload::Count(1),
518 });
519
520 if info.disconnect_source.should_log_for_mobile_device() {
521 metric_events.push(MetricEvent {
522 metric_id: metrics::DISCONNECT_OCCURRENCE_FOR_MOBILE_DEVICE_METRIC_ID,
523 event_codes: vec![],
524 payload: MetricEventPayload::Count(1),
525 });
526 }
527
528 metric_events.push(MetricEvent {
529 metric_id: metrics::CONNECTED_DURATION_ON_DISCONNECT_METRIC_ID,
530 event_codes: vec![],
531 payload: MetricEventPayload::IntegerValue(info.connected_duration.into_millis()),
532 });
533
534 metric_events.push(MetricEvent {
535 metric_id: metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID,
536 event_codes: vec![
537 u32::from(info.disconnect_source.cobalt_reason_code()),
538 info.disconnect_source.as_cobalt_disconnect_source() as u32,
539 ],
540 payload: MetricEventPayload::Count(1),
541 });
542
543 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_disconnect_cobalt");
544 }
545
546 pub async fn handle_periodic_telemetry(&self) {
547 let mut metric_events = vec![];
548 let now = fasync::BootInstant::now();
549 if let Some(failed_at) = *self.last_connect_failure_at.lock()
550 && now - failed_at >= SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_TIMEOUT
551 {
552 let failures = self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst);
553 if failures > 0 {
554 metric_events.push(MetricEvent {
555 metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
556 event_codes: vec![],
557 payload: MetricEventPayload::IntegerValue(failures as i64),
558 });
559 }
560 }
561
562 {
563 let mut daily_stats = self.daily_connect_stats.lock();
564 if now - daily_stats.last_log_time >= DAILY_METRICS_LOG_INTERVAL {
565 for (security_type, counter) in daily_stats.connect_per_security_type.drain() {
566 if counter.total > 0 {
567 let success_rate = counter.success as f64 / counter.total as f64;
568 metric_events.push(MetricEvent {
569 metric_id:
570 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID,
571 event_codes: vec![security_type as u32],
572 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
573 success_rate,
574 )),
575 });
576 }
577 }
578 for (primary_channel, counter) in daily_stats.connect_per_primary_channel.drain() {
579 if counter.total > 0 {
580 let success_rate = counter.success as f64 / counter.total as f64;
581 metric_events.push(MetricEvent {
582 metric_id:
583 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
584 event_codes: vec![primary_channel as u32],
585 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
586 success_rate,
587 )),
588 });
589 }
590 }
591 for (channel_band, counter) in daily_stats.connect_per_channel_band.drain() {
592 if counter.total > 0 {
593 let success_rate = counter.success as f64 / counter.total as f64;
594 metric_events.push(MetricEvent {
595 metric_id:
596 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
597 event_codes: vec![channel_band as u32],
598 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
599 success_rate,
600 )),
601 });
602 }
603 }
604 for (rssi_bucket, counter) in daily_stats.connect_per_rssi_bucket.drain() {
605 if counter.total > 0 {
606 let success_rate = counter.success as f64 / counter.total as f64;
607 metric_events.push(MetricEvent {
608 metric_id:
609 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_RSSI_BUCKET_METRIC_ID,
610 event_codes: vec![rssi_bucket as u32],
611 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
612 success_rate,
613 )),
614 });
615 }
616 }
617 for (snr_bucket, counter) in daily_stats.connect_per_snr_bucket.drain() {
618 if counter.total > 0 {
619 let success_rate = counter.success as f64 / counter.total as f64;
620 metric_events.push(MetricEvent {
621 metric_id:
622 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SNR_BUCKET_METRIC_ID,
623 event_codes: vec![snr_bucket as u32],
624 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
625 success_rate,
626 )),
627 });
628 }
629 }
630 for (is_owe_transition, counter) in
631 daily_stats.connect_per_is_owe_transition.drain()
632 {
633 if counter.total > 0 {
634 let success_rate = counter.success as f64 / counter.total as f64;
635 metric_events.push(MetricEvent {
636 metric_id:
637 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_IS_OWE_TRANSITION_METRIC_ID,
638 event_codes: vec![is_owe_transition as u32],
639 payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(
640 success_rate,
641 )),
642 });
643 }
644 }
645 daily_stats.last_log_time = now;
646 }
647 }
648
649 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_periodic_telemetry");
650 }
651
652 pub async fn handle_suspend_imminent(&self) {
653 let mut metric_events = vec![];
654
655 let flushed_successive_failures =
656 self.successive_connect_attempt_failures.swap(0, Ordering::SeqCst);
657 if flushed_successive_failures > 0 {
658 metric_events.push(MetricEvent {
659 metric_id: metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID,
660 event_codes: vec![],
661 payload: MetricEventPayload::IntegerValue(flushed_successive_failures as i64),
662 });
663 }
664
665 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_suspend_imminent");
666 }
667
668 pub async fn handle_iface_destroyed(&self) {
669 self.update_connection_state(ConnectionState::Idle(IdleState {}));
670 }
671
672 pub async fn handle_client_connections_toggle(&self, event: &ClientConnectionsToggleEvent) {
673 if event == &ClientConnectionsToggleEvent::Disabled {
674 self.update_connection_state(ConnectionState::Idle(IdleState {}));
675 }
676 }
677
678 pub async fn handle_pno_scan_failure(&self) {
679 let mut metric_events = vec![MetricEvent {
680 metric_id: metrics::PNO_SCAN_FAILURE_OCCURRENCE_METRIC_ID,
681 event_codes: vec![],
682 payload: MetricEventPayload::Count(1),
683 }];
684
685 let state = self.connection_state.lock().clone();
686 match state {
687 ConnectionState::Idle(_)
688 | ConnectionState::Disconnected(_)
689 | ConnectionState::ConnectFailed(_)
690 | ConnectionState::PnoScanFailedIdle(_) => {
691 metric_events.push(MetricEvent {
692 metric_id: metrics::PNO_SCAN_FAILURE_WHILE_NOT_CONNECTED_OCCURRENCE_METRIC_ID,
693 event_codes: vec![],
694 payload: MetricEventPayload::Count(1),
695 });
696
697 self.update_connection_state(ConnectionState::PnoScanFailedIdle(
701 PnoScanFailedIdleState {},
702 ));
703 }
704 ConnectionState::Connected(_)
705 | ConnectionState::FailedToStart(_)
706 | ConnectionState::FailedToStop(_) => {
707 }
711 }
712
713 log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_pno_scan_failure");
714 }
715 pub async fn handle_client_connections_failed_to_start(&self) {
716 self.update_connection_state(ConnectionState::FailedToStart(FailedToStartState {}));
717 }
718
719 pub async fn handle_client_connections_failed_to_stop(&self) {
720 self.update_connection_state(ConnectionState::FailedToStop(FailedToStopState {}));
721 }
722}
723
724struct InspectMetadataNode {
725 connected_networks: LruCacheNode<InspectConnectedNetwork>,
726 disconnect_sources: LruCacheNode<InspectDisconnectSource>,
727 connect_attempt_results: LruCacheNode<InspectConnectAttemptResult>,
728}
729
730impl InspectMetadataNode {
731 const CONNECTED_NETWORKS: &'static str = "connected_networks";
732 const DISCONNECT_SOURCES: &'static str = "disconnect_sources";
733 const CONNECT_ATTEMPT_RESULTS: &'static str = "connect_attempt_results";
734
735 fn new(inspect_node: &InspectNode) -> Self {
736 let connected_networks = inspect_node.create_child(Self::CONNECTED_NETWORKS);
737 let disconnect_sources = inspect_node.create_child(Self::DISCONNECT_SOURCES);
738 let connect_attempt_results = inspect_node.create_child(Self::CONNECT_ATTEMPT_RESULTS);
739 Self {
740 connected_networks: LruCacheNode::new(
741 connected_networks,
742 INSPECT_CONNECTED_NETWORKS_ID_LIMIT,
743 ),
744 disconnect_sources: LruCacheNode::new(
745 disconnect_sources,
746 INSPECT_DISCONNECT_SOURCES_ID_LIMIT,
747 ),
748 connect_attempt_results: LruCacheNode::new(
749 connect_attempt_results,
750 INSPECT_CONNECT_ATTEMPT_RESULTS_ID_LIMIT,
751 ),
752 }
753 }
754}
755
756#[derive(Debug, Clone)]
757struct ConnectDisconnectTimeSeries {
758 wlan_connectivity_states: InspectedTimeMatrix<u64>,
759 connected_networks: InspectedTimeMatrix<u64>,
760 disconnected_networks: InspectedTimeMatrix<u64>,
761 disconnect_sources: InspectedTimeMatrix<u64>,
762 connect_attempt_results: InspectedTimeMatrix<u64>,
763}
764
765impl ConnectDisconnectTimeSeries {
766 pub fn new<S: InspectSender>(client: &S, inspect_metadata_path: &str) -> Self {
767 let wlan_connectivity_states = client.inspect_time_matrix_with_metadata(
768 "wlan_connectivity_states",
769 TimeMatrix::<Union<u64>, LastSample>::new(
770 SamplingProfile::highly_granular(),
771 LastSample::or(0),
772 ),
773 BitSetMap::from_ordered(Self::wlan_connectivity_states_bitset_map().iter().copied()),
775 );
776 let connected_networks = client.inspect_time_matrix_with_metadata(
777 "connected_networks",
778 TimeMatrix::<Union<u64>, ConstantSample>::new(
779 SamplingProfile::granular(),
780 ConstantSample::default(),
781 ),
782 BitSetNode::from_path(format!(
783 "{}/{}",
784 inspect_metadata_path,
785 InspectMetadataNode::CONNECTED_NETWORKS
786 )),
787 );
788 let disconnected_networks = client.inspect_time_matrix_with_metadata(
789 "disconnected_networks",
790 TimeMatrix::<Union<u64>, ConstantSample>::new(
791 SamplingProfile::granular(),
792 ConstantSample::default(),
793 ),
794 BitSetNode::from_path(format!(
796 "{}/{}",
797 inspect_metadata_path,
798 InspectMetadataNode::CONNECTED_NETWORKS
799 )),
800 );
801 let disconnect_sources = client.inspect_time_matrix_with_metadata(
802 "disconnect_sources",
803 TimeMatrix::<Union<u64>, ConstantSample>::new(
804 SamplingProfile::granular(),
805 ConstantSample::default(),
806 ),
807 BitSetNode::from_path(format!(
808 "{}/{}",
809 inspect_metadata_path,
810 InspectMetadataNode::DISCONNECT_SOURCES,
811 )),
812 );
813 let connect_attempt_results = client.inspect_time_matrix_with_metadata(
814 "connect_attempt_results",
815 TimeMatrix::<Union<u64>, ConstantSample>::new(
816 SamplingProfile::granular(),
817 ConstantSample::default(),
818 ),
819 BitSetNode::from_path(format!(
820 "{}/{}",
821 inspect_metadata_path,
822 InspectMetadataNode::CONNECT_ATTEMPT_RESULTS,
823 )),
824 );
825 Self {
826 wlan_connectivity_states,
827 connected_networks,
828 disconnected_networks,
829 disconnect_sources,
830 connect_attempt_results,
831 }
832 }
833
834 fn wlan_connectivity_states_bitset_map() -> &'static [&'static str] {
837 &[
838 "idle",
839 "disconnected",
840 "connect_failed",
841 "connected",
842 "start_failure",
843 "stop_failure",
844 "pno_scan_failed",
845 ]
846 }
847
848 fn log_wlan_connectivity_state(&self, data: u64) {
849 self.wlan_connectivity_states.fold_or_log_error(data);
850 }
851 fn log_connected_networks(&self, data: u64) {
852 self.connected_networks.fold_or_log_error(data);
853 }
854 fn log_disconnected_networks(&self, data: u64) {
855 self.disconnected_networks.fold_or_log_error(data);
856 }
857 fn log_disconnect_sources(&self, data: u64) {
858 self.disconnect_sources.fold_or_log_error(data);
859 }
860 fn log_connect_attempt_results(&self, data: u64) {
861 self.connect_attempt_results.fold_or_log_error(data);
862 }
863}
864
865pub trait DisconnectSourceExt {
866 fn should_log_for_mobile_device(&self) -> bool;
867 fn cobalt_reason_code(&self) -> u16;
868 fn as_cobalt_disconnect_source(
869 &self,
870 ) -> metrics::ConnectivityWlanMetricDimensionDisconnectSource;
871}
872
873impl DisconnectSourceExt for fidl_sme::DisconnectSource {
874 fn should_log_for_mobile_device(&self) -> bool {
875 match self {
876 fidl_sme::DisconnectSource::Ap(_) => true,
877 fidl_sme::DisconnectSource::Mlme(cause)
878 if cause.reason_code != fidl_ieee80211::ReasonCode::MlmeLinkFailed =>
879 {
880 true
881 }
882 _ => false,
883 }
884 }
885
886 fn cobalt_reason_code(&self) -> u16 {
887 let cobalt_disconnect_reason_code = match self {
888 fidl_sme::DisconnectSource::Ap(cause) | fidl_sme::DisconnectSource::Mlme(cause) => {
889 cause.reason_code.into_primitive()
890 }
891 fidl_sme::DisconnectSource::User(reason) => *reason as u16,
892 };
893 const REASON_CODE_MAX: u16 = 1000;
896 std::cmp::min(cobalt_disconnect_reason_code, REASON_CODE_MAX)
897 }
898
899 fn as_cobalt_disconnect_source(
900 &self,
901 ) -> metrics::ConnectivityWlanMetricDimensionDisconnectSource {
902 use metrics::ConnectivityWlanMetricDimensionDisconnectSource as DS;
903 match self {
904 fidl_sme::DisconnectSource::Ap(..) => DS::Ap,
905 fidl_sme::DisconnectSource::User(..) => DS::User,
906 fidl_sme::DisconnectSource::Mlme(..) => DS::Mlme,
907 }
908 }
909}
910
911#[derive(Debug, Default, Copy, Clone, PartialEq)]
912struct ConnectAttemptsCounter {
913 success: u64,
914 total: u64,
915}
916
917impl ConnectAttemptsCounter {
918 fn increment(&mut self, code: fidl_ieee80211::StatusCode) {
919 self.total += 1;
920 if code == fidl_ieee80211::StatusCode::Success {
921 self.success += 1;
922 }
923 }
924}
925
926struct DailyConnectStats {
927 last_log_time: fasync::BootInstant,
928 connect_per_security_type: HashMap<
929 metrics::SuccessfulConnectBreakdownBySecurityTypeMetricDimensionSecurityType,
930 ConnectAttemptsCounter,
931 >,
932 connect_per_primary_channel: HashMap<u8, ConnectAttemptsCounter>,
933 connect_per_channel_band: HashMap<
934 metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand,
935 ConnectAttemptsCounter,
936 >,
937 connect_per_rssi_bucket:
938 HashMap<metrics::ConnectivityWlanMetricDimensionRssiBucket, ConnectAttemptsCounter>,
939 connect_per_snr_bucket:
940 HashMap<metrics::ConnectivityWlanMetricDimensionSnrBucket, ConnectAttemptsCounter>,
941 connect_per_is_owe_transition: HashMap<
942 metrics::DailyConnectSuccessRateBreakdownByIsOweTransitionMetricDimensionIsOweTransition,
943 ConnectAttemptsCounter,
944 >,
945}
946
947impl DailyConnectStats {
948 fn new(now: fasync::BootInstant) -> Self {
949 Self {
950 last_log_time: now,
951 connect_per_security_type: HashMap::new(),
952 connect_per_primary_channel: HashMap::new(),
953 connect_per_channel_band: HashMap::new(),
954 connect_per_rssi_bucket: HashMap::new(),
955 connect_per_snr_bucket: HashMap::new(),
956 connect_per_is_owe_transition: HashMap::new(),
957 }
958 }
959}
960
961fn float_to_ten_thousandth(value: f64) -> i64 {
964 (value * 10000f64) as i64
965}
966
967#[cfg(test)]
968mod tests {
969 use super::*;
970 use crate::testing::*;
971 use assert_matches::assert_matches;
972 use diagnostics_assertions::{
973 AnyBoolProperty, AnyBytesProperty, AnyNumericProperty, AnyStringProperty, assert_data_tree,
974 };
975 use futures::task::Poll;
976 use ieee80211_testutils::{BSSID_REGEX, SSID_REGEX};
977 use rand::Rng;
978 use std::pin::pin;
979 use strum::IntoEnumIterator;
980 use test_case::test_case;
981 use windowed_stats::experimental::clock::Timed;
982 use windowed_stats::experimental::inspect::TimeMatrixClient;
983 use windowed_stats::experimental::testing::TimeMatrixCall;
984 use wlan_common::channel::{Cbw, Channel};
985 use wlan_common::ie::IeType;
986 use wlan_common::test_utils::fake_stas::IesOverrides;
987 use wlan_common::{fake_bss_description, random_bss_description};
988
989 #[fuchsia::test]
990 fn log_connect_attempt_then_inspect_data_tree_contains_time_matrix_metadata() {
991 let mut harness = setup_test();
992
993 let client =
994 TimeMatrixClient::new(harness.inspect_node.create_child("wlan_connect_disconnect"));
995 let logger = ConnectDisconnectLogger::new(
996 harness.cobalt_proxy.clone(),
997 &harness.inspect_node,
998 &harness.inspect_metadata_node,
999 &harness.inspect_metadata_path,
1000 &client,
1001 );
1002 let bss = random_bss_description!();
1003 let mut log_connect_attempt = pin!(logger.handle_connect_attempt(
1004 fidl_ieee80211::StatusCode::Success,
1005 &bss,
1006 false,
1007 false
1008 ));
1009 assert!(
1010 harness.run_until_stalled_drain_cobalt_events(&mut log_connect_attempt).is_ready(),
1011 "`log_connect_attempt` did not complete",
1012 );
1013
1014 let tree = harness.get_inspect_data_tree();
1015 assert_data_tree!(
1016 @executor harness.exec,
1017 tree,
1018 root: contains {
1019 test_stats: contains {
1020 wlan_connect_disconnect: contains {
1021 wlan_connectivity_states: {
1022 "type": "bitset",
1023 "data": AnyBytesProperty,
1024 metadata: {
1025 index: {
1026 "0": "idle",
1027 "1": "disconnected",
1028 "2": "connect_failed",
1029 "3": "connected",
1030 "4": "start_failure",
1031 "5": "stop_failure",
1032 "6": "pno_scan_failed",
1033 },
1034 },
1035 },
1036 connected_networks: {
1037 "type": "bitset",
1038 "data": AnyBytesProperty,
1039 metadata: {
1040 "index_node_path": "root/test_stats/metadata/connected_networks",
1041 },
1042 },
1043 disconnected_networks: {
1044 "type": "bitset",
1045 "data": AnyBytesProperty,
1046 metadata: {
1047 "index_node_path": "root/test_stats/metadata/connected_networks",
1048 },
1049 },
1050 disconnect_sources: {
1051 "type": "bitset",
1052 "data": AnyBytesProperty,
1053 metadata: {
1054 "index_node_path": "root/test_stats/metadata/disconnect_sources",
1055 },
1056 },
1057 connect_attempt_results: {
1058 "type": "bitset",
1059 "data": AnyBytesProperty,
1060 metadata: {
1061 "index_node_path": "root/test_stats/metadata/connect_attempt_results",
1062 },
1063 },
1064 },
1065 },
1066 }
1067 );
1068 }
1069
1070 #[fuchsia::test]
1071 fn test_log_connect_attempt_inspect() {
1072 let mut test_helper = setup_test();
1073 let logger = ConnectDisconnectLogger::new(
1074 test_helper.cobalt_proxy.clone(),
1075 &test_helper.inspect_node,
1076 &test_helper.inspect_metadata_node,
1077 &test_helper.inspect_metadata_path,
1078 &test_helper.mock_time_matrix_client,
1079 );
1080
1081 let bss_description = random_bss_description!();
1083 let mut test_fut = pin!(logger.handle_connect_attempt(
1084 fidl_ieee80211::StatusCode::Success,
1085 &bss_description,
1086 false,
1087 false
1088 ));
1089 assert_eq!(
1090 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1091 Poll::Ready(())
1092 );
1093
1094 let data = test_helper.get_inspect_data_tree();
1096 assert_data_tree!(@executor test_helper.exec, data, root: contains {
1097 test_stats: contains {
1098 metadata: contains {
1099 connected_networks: contains {
1100 "0": {
1101 "@time": AnyNumericProperty,
1102 "data": contains {
1103 bssid: &*BSSID_REGEX,
1104 ssid: &*SSID_REGEX,
1105 }
1106 }
1107 },
1108 connect_attempt_results: contains {
1109 "0": {
1110 "@time": AnyNumericProperty,
1111 "data": contains {
1112 status_code: 0u64,
1113 result: "Success",
1114 }
1115 }
1116 },
1117 },
1118 connect_events: {
1119 "0": {
1120 "@time": AnyNumericProperty,
1121 network_id: 0u64,
1122 }
1123 },
1124 connect_attempt_results: {
1125 "0": {
1126 "@time": AnyNumericProperty,
1127 result: "Success",
1128 ssid: &*SSID_REGEX,
1129 bssid: &*BSSID_REGEX,
1130 protection: AnyStringProperty,
1131 }
1132 }
1133 }
1134 });
1135
1136 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1137 assert_eq!(
1138 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1139 &[TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 3)),]
1140 );
1141 assert_eq!(
1142 &time_matrix_calls.drain::<u64>("connected_networks")[..],
1143 &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
1144 );
1145 assert_eq!(
1146 &time_matrix_calls.drain::<u64>("connect_attempt_results")[..],
1147 &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
1148 );
1149 }
1150
1151 #[fuchsia::test]
1152 fn test_log_connect_attempt_cobalt() {
1153 let mut test_helper = setup_test();
1154 let logger = ConnectDisconnectLogger::new(
1155 test_helper.cobalt_proxy.clone(),
1156 &test_helper.inspect_node,
1157 &test_helper.inspect_metadata_node,
1158 &test_helper.inspect_metadata_path,
1159 &test_helper.mock_time_matrix_client,
1160 );
1161
1162 let bss_description = random_bss_description!(Wpa2,
1164 channel: Channel::new(157, Cbw::Cbw40),
1165 bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
1166 );
1167
1168 let mut test_fut = pin!(logger.handle_connect_attempt(
1170 fidl_ieee80211::StatusCode::Success,
1171 &bss_description,
1172 false,
1173 false
1174 ));
1175 assert_eq!(
1176 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1177 Poll::Ready(())
1178 );
1179
1180 let breakdowns_by_status_code = test_helper
1182 .get_logged_metrics(metrics::CONNECT_ATTEMPT_BREAKDOWN_BY_STATUS_CODE_METRIC_ID);
1183 assert_eq!(breakdowns_by_status_code.len(), 1);
1184 assert_eq!(
1185 breakdowns_by_status_code[0].event_codes,
1186 vec![fidl_ieee80211::StatusCode::Success.into_primitive() as u32]
1187 );
1188 assert_eq!(breakdowns_by_status_code[0].payload, MetricEventPayload::Count(1));
1189
1190 let metrics_devices =
1191 test_helper.get_logged_metrics(metrics::NUMBER_OF_CONNECTED_DEVICES_METRIC_ID);
1192 assert_eq!(metrics_devices.len(), 1);
1193 assert_eq!(metrics_devices[0].payload, MetricEventPayload::Count(1));
1194
1195 let metrics_security =
1196 test_helper.get_logged_metrics(metrics::CONNECTED_NETWORK_SECURITY_TYPE_METRIC_ID);
1197 assert_eq!(metrics_security.len(), 1);
1198 assert_eq!(metrics_security[0].event_codes, vec![5]); let metrics_channel = test_helper.get_logged_metrics(
1201 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
1202 );
1203 assert_eq!(metrics_channel.len(), 1);
1204 assert_eq!(metrics_channel[0].event_codes, vec![157]);
1205
1206 let metrics_band = test_helper.get_logged_metrics(
1207 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
1208 );
1209 assert_eq!(metrics_band.len(), 1);
1210 assert_eq!(metrics_band[0].event_codes, vec![2]); let metrics_oui =
1213 test_helper.get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_OUI_2_METRIC_ID);
1214 assert_eq!(metrics_oui.len(), 1);
1215 assert_eq!(metrics_oui[0].payload, MetricEventPayload::StringValue("00F620".to_string()));
1216
1217 let metrics_owe_transition = test_helper.get_logged_metrics(
1218 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_IS_OWE_TRANSITION_METRIC_ID,
1219 );
1220 assert_eq!(metrics_owe_transition.len(), 1);
1221 assert_eq!(
1222 metrics_owe_transition[0].event_codes,
1223 vec![
1224 metrics::DailyConnectSuccessRateBreakdownByIsOweTransitionMetricDimensionIsOweTransition::No
1225 as u32
1226 ]
1227 );
1228 }
1229
1230 #[fuchsia::test]
1231 fn test_successive_connect_attempt_failures_cobalt_zero_failures() {
1232 let mut test_helper = setup_test();
1233 let logger = ConnectDisconnectLogger::new(
1234 test_helper.cobalt_proxy.clone(),
1235 &test_helper.inspect_node,
1236 &test_helper.inspect_metadata_node,
1237 &test_helper.inspect_metadata_path,
1238 &test_helper.mock_time_matrix_client,
1239 );
1240
1241 let bss_description = random_bss_description!(Wpa2);
1242 let mut test_fut = pin!(logger.handle_connect_attempt(
1243 fidl_ieee80211::StatusCode::Success,
1244 &bss_description,
1245 false,
1246 false
1247 ));
1248 assert_eq!(
1249 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1250 Poll::Ready(())
1251 );
1252
1253 let metrics =
1254 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1255 assert_eq!(metrics.len(), 1);
1256 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(0));
1257 }
1258
1259 #[fuchsia::test]
1260 fn test_log_device_connected_metrics_capabilities() {
1261 let mut test_helper = setup_test();
1262 let logger = ConnectDisconnectLogger::new(
1263 test_helper.cobalt_proxy.clone(),
1264 &test_helper.inspect_node,
1265 &test_helper.inspect_metadata_node,
1266 &test_helper.inspect_metadata_path,
1267 &test_helper.mock_time_matrix_client,
1268 );
1269
1270 let wmm_info = vec![0x80]; #[rustfmt::skip]
1272 let rm_enabled_capabilities = vec![
1273 0x03, 0x00, 0x00, 0x00, 0x00,
1275 ];
1276 #[rustfmt::skip]
1277 let ext_capabilities = vec![
1278 0x04, 0x00,
1279 0x08, 0x00, 0x00, 0x00, 0x00, 0x40
1281 ];
1282
1283 let bss_description = fake_bss_description!(Wpa2,
1284 ies_overrides: IesOverrides::new()
1285 .remove(IeType::WMM_PARAM)
1286 .set(IeType::WMM_INFO, wmm_info)
1287 .set(IeType::RM_ENABLED_CAPABILITIES, rm_enabled_capabilities)
1288 .set(IeType::MOBILITY_DOMAIN, vec![0x00; 3])
1289 .set(IeType::EXT_CAPABILITIES, ext_capabilities),
1290 );
1291
1292 let mut test_fut = pin!(logger.handle_connect_attempt(
1293 fidl_ieee80211::StatusCode::Success,
1294 &bss_description,
1295 false,
1296 false
1297 ));
1298 assert_eq!(
1299 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1300 Poll::Ready(())
1301 );
1302
1303 let metrics = test_helper
1304 .get_logged_metrics(metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_APSD_METRIC_ID);
1305 assert_eq!(metrics.len(), 1);
1306 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1307
1308 let metrics = test_helper.get_logged_metrics(
1309 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_BSS_TRANSITION_MANAGEMENT_METRIC_ID,
1310 );
1311 assert_eq!(metrics.len(), 1);
1312 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1313
1314 let metrics = test_helper.get_logged_metrics(
1315 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_LINK_MEASUREMENT_METRIC_ID,
1316 );
1317 assert_eq!(metrics.len(), 1);
1318 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1319
1320 let metrics = test_helper.get_logged_metrics(
1321 metrics::DEVICE_CONNECTED_TO_AP_THAT_SUPPORTS_NEIGHBOR_REPORT_METRIC_ID,
1322 );
1323 assert_eq!(metrics.len(), 1);
1324 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1325 }
1326
1327 #[test_case(1; "one_failure")]
1328 #[test_case(2; "two_failures")]
1329 #[fuchsia::test(add_test_attr = false)]
1330 fn test_successive_connect_attempt_failures_cobalt_one_failure_then_success(n_failures: usize) {
1331 let mut test_helper = setup_test();
1332 let logger = ConnectDisconnectLogger::new(
1333 test_helper.cobalt_proxy.clone(),
1334 &test_helper.inspect_node,
1335 &test_helper.inspect_metadata_node,
1336 &test_helper.inspect_metadata_path,
1337 &test_helper.mock_time_matrix_client,
1338 );
1339
1340 let bss_description = random_bss_description!(Wpa2);
1341 for _i in 0..n_failures {
1342 let mut test_fut = pin!(logger.handle_connect_attempt(
1343 fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1344 &bss_description,
1345 false,
1346 false
1347 ));
1348 assert_eq!(
1349 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1350 Poll::Ready(())
1351 );
1352 }
1353
1354 let metrics =
1355 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1356 assert!(metrics.is_empty());
1357
1358 let mut test_fut = pin!(logger.handle_connect_attempt(
1359 fidl_ieee80211::StatusCode::Success,
1360 &bss_description,
1361 false,
1362 false
1363 ));
1364 assert_eq!(
1365 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1366 Poll::Ready(())
1367 );
1368
1369 let metrics =
1370 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1371 assert_eq!(metrics.len(), 1);
1372 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
1373
1374 test_helper.clear_cobalt_events();
1376 let mut test_fut = pin!(logger.handle_connect_attempt(
1377 fidl_ieee80211::StatusCode::Success,
1378 &bss_description,
1379 false,
1380 false
1381 ));
1382 assert_eq!(
1383 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1384 Poll::Ready(())
1385 );
1386 let metrics =
1387 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1388 assert_eq!(metrics.len(), 1);
1389 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(0));
1390 }
1391
1392 #[test_case(1; "one_failure")]
1393 #[test_case(2; "two_failures")]
1394 #[fuchsia::test(add_test_attr = false)]
1395 fn test_successive_connect_attempt_failures_cobalt_one_failure_then_timeout(n_failures: usize) {
1396 let mut test_helper = setup_test();
1397 let logger = ConnectDisconnectLogger::new(
1398 test_helper.cobalt_proxy.clone(),
1399 &test_helper.inspect_node,
1400 &test_helper.inspect_metadata_node,
1401 &test_helper.inspect_metadata_path,
1402 &test_helper.mock_time_matrix_client,
1403 );
1404
1405 let bss_description = random_bss_description!(Wpa2);
1406 for _i in 0..n_failures {
1407 let mut test_fut = pin!(logger.handle_connect_attempt(
1408 fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1409 &bss_description,
1410 false,
1411 false
1412 ));
1413 assert_eq!(
1414 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1415 Poll::Ready(())
1416 );
1417 }
1418
1419 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(60_000_000_000));
1420 let mut test_fut = pin!(logger.handle_periodic_telemetry());
1421 assert_eq!(
1422 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1423 Poll::Ready(())
1424 );
1425
1426 let metrics =
1428 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1429 assert!(metrics.is_empty());
1430
1431 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(120_000_000_000));
1432 let mut test_fut = pin!(logger.handle_periodic_telemetry());
1433 assert_eq!(
1434 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1435 Poll::Ready(())
1436 );
1437
1438 let metrics =
1439 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1440 assert_eq!(metrics.len(), 1);
1441 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
1442
1443 test_helper.clear_cobalt_events();
1445 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(240_000_000_000));
1446 let mut test_fut = pin!(logger.handle_periodic_telemetry());
1447 assert_eq!(
1448 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1449 Poll::Ready(())
1450 );
1451 let metrics =
1452 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1453 assert!(metrics.is_empty());
1454 }
1455
1456 #[fuchsia::test]
1457 fn test_daily_connect_success_rate_breakdowns() {
1458 let mut test_helper = setup_test();
1459 let logger = ConnectDisconnectLogger::new(
1460 test_helper.cobalt_proxy.clone(),
1461 &test_helper.inspect_node,
1462 &test_helper.inspect_metadata_node,
1463 &test_helper.inspect_metadata_path,
1464 &test_helper.mock_time_matrix_client,
1465 );
1466
1467 let mut bss = random_bss_description!(Wpa2);
1468 bss.channel = Channel::new(6, Cbw::Cbw20); bss.rssi_dbm = -50; bss.snr_db = 15; let mut test_fut = pin!(logger.handle_connect_attempt(
1474 fidl_ieee80211::StatusCode::Success,
1475 &bss,
1476 false,
1477 true
1478 ));
1479 assert_eq!(
1480 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1481 Poll::Ready(())
1482 );
1483
1484 let mut test_fut = pin!(logger.handle_connect_attempt(
1485 fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1486 &bss,
1487 false,
1488 true
1489 ));
1490 assert_eq!(
1491 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1492 Poll::Ready(())
1493 );
1494
1495 test_helper.clear_cobalt_events();
1497 test_helper
1498 .exec
1499 .set_fake_time(fasync::MonotonicInstant::from_nanos(24 * 3600 * 1_000_000_000 - 1));
1500 let mut test_fut = pin!(logger.handle_periodic_telemetry());
1501 assert_eq!(
1502 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1503 Poll::Ready(())
1504 );
1505 assert!(
1506 test_helper
1507 .get_logged_metrics(
1508 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID
1509 )
1510 .is_empty()
1511 );
1512
1513 test_helper
1515 .exec
1516 .set_fake_time(fasync::MonotonicInstant::from_nanos(24 * 3600 * 1_000_000_000));
1517 let mut test_fut = pin!(logger.handle_periodic_telemetry());
1518 assert_eq!(
1519 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1520 Poll::Ready(())
1521 );
1522
1523 let daily_security_metrics = test_helper.get_logged_metrics(
1525 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SECURITY_TYPE_METRIC_ID,
1526 );
1527 assert_eq!(daily_security_metrics.len(), 1);
1528 assert_eq!(
1529 daily_security_metrics[0].event_codes,
1530 vec![
1531 metrics::SuccessfulConnectBreakdownBySecurityTypeMetricDimensionSecurityType::Wpa2Personal
1532 as u32
1533 ]
1534 );
1535 assert_eq!(daily_security_metrics[0].payload, MetricEventPayload::IntegerValue(5000));
1536
1537 let daily_channel_metrics = test_helper.get_logged_metrics(
1539 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_PRIMARY_CHANNEL_METRIC_ID,
1540 );
1541 assert_eq!(daily_channel_metrics.len(), 1);
1542 assert_eq!(daily_channel_metrics[0].event_codes, vec![6]);
1543 assert_eq!(daily_channel_metrics[0].payload, MetricEventPayload::IntegerValue(5000));
1544
1545 let daily_band_metrics = test_helper.get_logged_metrics(
1547 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_CHANNEL_BAND_METRIC_ID,
1548 );
1549 assert_eq!(daily_band_metrics.len(), 1);
1550 assert_eq!(
1551 daily_band_metrics[0].event_codes,
1552 vec![
1553 metrics::SuccessfulConnectBreakdownByChannelBandMetricDimensionChannelBand::Band2Dot4Ghz
1554 as u32
1555 ]
1556 );
1557 assert_eq!(daily_band_metrics[0].payload, MetricEventPayload::IntegerValue(5000));
1558
1559 let daily_rssi_metrics = test_helper.get_logged_metrics(
1561 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_RSSI_BUCKET_METRIC_ID,
1562 );
1563 assert_eq!(daily_rssi_metrics.len(), 1);
1564 assert_eq!(
1565 daily_rssi_metrics[0].event_codes,
1566 vec![metrics::ConnectivityWlanMetricDimensionRssiBucket::From50To35 as u32]
1567 );
1568 assert_eq!(daily_rssi_metrics[0].payload, MetricEventPayload::IntegerValue(5000));
1569
1570 let daily_snr_metrics = test_helper.get_logged_metrics(
1572 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_SNR_BUCKET_METRIC_ID,
1573 );
1574 assert_eq!(daily_snr_metrics.len(), 1);
1575 assert_eq!(
1576 daily_snr_metrics[0].event_codes,
1577 vec![metrics::ConnectivityWlanMetricDimensionSnrBucket::From11To15 as u32]
1578 );
1579 assert_eq!(daily_snr_metrics[0].payload, MetricEventPayload::IntegerValue(5000));
1580
1581 let daily_owe_metrics = test_helper.get_logged_metrics(
1583 metrics::DAILY_CONNECT_SUCCESS_RATE_BREAKDOWN_BY_IS_OWE_TRANSITION_METRIC_ID,
1584 );
1585 assert_eq!(daily_owe_metrics.len(), 1);
1586 assert_eq!(
1587 daily_owe_metrics[0].event_codes,
1588 vec![
1589 metrics::DailyConnectSuccessRateBreakdownByIsOweTransitionMetricDimensionIsOweTransition::Yes
1590 as u32
1591 ]
1592 );
1593 assert_eq!(daily_owe_metrics[0].payload, MetricEventPayload::IntegerValue(5000));
1594 }
1595
1596 #[fuchsia::test]
1597 fn test_log_connect_attempt_cobalt_owe_transition() {
1598 let mut test_helper = setup_test();
1599 let logger = ConnectDisconnectLogger::new(
1600 test_helper.cobalt_proxy.clone(),
1601 &test_helper.inspect_node,
1602 &test_helper.inspect_metadata_node,
1603 &test_helper.inspect_metadata_path,
1604 &test_helper.mock_time_matrix_client,
1605 );
1606
1607 let bss_description = random_bss_description!(Wpa2,
1609 channel: Channel::new(157, Cbw::Cbw40),
1610 bssid: [0x00, 0xf6, 0x20, 0x03, 0x04, 0x05],
1611 );
1612
1613 let mut test_fut = pin!(logger.handle_connect_attempt(
1615 fidl_ieee80211::StatusCode::Success,
1616 &bss_description,
1617 false,
1618 true
1619 ));
1620 assert_eq!(
1621 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1622 Poll::Ready(())
1623 );
1624
1625 let metrics_owe_transition = test_helper.get_logged_metrics(
1626 metrics::DEVICE_CONNECTED_TO_AP_BREAKDOWN_BY_IS_OWE_TRANSITION_METRIC_ID,
1627 );
1628 assert_eq!(metrics_owe_transition.len(), 1);
1629 assert_eq!(
1630 metrics_owe_transition[0].event_codes,
1631 vec![
1632 metrics::DailyConnectSuccessRateBreakdownByIsOweTransitionMetricDimensionIsOweTransition::Yes
1633 as u32
1634 ]
1635 );
1636 }
1637
1638 #[fuchsia::test]
1639 fn test_zero_successive_connect_attempt_failures_on_suspend() {
1640 let mut test_helper = setup_test();
1641 let logger = ConnectDisconnectLogger::new(
1642 test_helper.cobalt_proxy.clone(),
1643 &test_helper.inspect_node,
1644 &test_helper.inspect_metadata_node,
1645 &test_helper.inspect_metadata_path,
1646 &test_helper.mock_time_matrix_client,
1647 );
1648
1649 let mut test_fut = pin!(logger.handle_suspend_imminent());
1650 assert_eq!(
1651 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1652 Poll::Ready(())
1653 );
1654
1655 let metrics =
1656 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1657 assert!(metrics.is_empty());
1658 }
1659
1660 #[test_case(1; "one_failure")]
1661 #[test_case(2; "two_failures")]
1662 #[fuchsia::test(add_test_attr = false)]
1663 fn test_one_or_more_successive_connect_attempt_failures_on_suspend(n_failures: usize) {
1664 let mut test_helper = setup_test();
1665 let logger = ConnectDisconnectLogger::new(
1666 test_helper.cobalt_proxy.clone(),
1667 &test_helper.inspect_node,
1668 &test_helper.inspect_metadata_node,
1669 &test_helper.inspect_metadata_path,
1670 &test_helper.mock_time_matrix_client,
1671 );
1672
1673 let bss_description = random_bss_description!(Wpa2);
1674 for _i in 0..n_failures {
1675 let mut test_fut = pin!(logger.handle_connect_attempt(
1676 fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
1677 &bss_description,
1678 false,
1679 false
1680 ));
1681 assert_eq!(
1682 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1683 Poll::Ready(())
1684 );
1685 }
1686
1687 let mut test_fut = pin!(logger.handle_suspend_imminent());
1688 assert_eq!(
1689 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1690 Poll::Ready(())
1691 );
1692
1693 let metrics =
1694 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1695 assert_eq!(metrics.len(), 1);
1696 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(n_failures as i64));
1697
1698 test_helper.clear_cobalt_events();
1699 let mut test_fut = pin!(logger.handle_suspend_imminent());
1700 assert_eq!(
1701 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1702 Poll::Ready(())
1703 );
1704
1705 let metrics =
1707 test_helper.get_logged_metrics(metrics::SUCCESSIVE_CONNECT_ATTEMPT_FAILURES_METRIC_ID);
1708 assert!(metrics.is_empty());
1709
1710 assert_matches!(*logger.connection_state.lock(), ConnectionState::ConnectFailed(_));
1712 }
1713
1714 #[fuchsia::test]
1715 fn test_log_disconnect_inspect() {
1716 let mut test_helper = setup_test();
1717 let logger = ConnectDisconnectLogger::new(
1718 test_helper.cobalt_proxy.clone(),
1719 &test_helper.inspect_node,
1720 &test_helper.inspect_metadata_node,
1721 &test_helper.inspect_metadata_path,
1722 &test_helper.mock_time_matrix_client,
1723 );
1724
1725 let bss_description = fake_bss_description!(Open);
1727 let channel = bss_description.channel;
1728 let disconnect_info = DisconnectInfo {
1729 iface_id: 32,
1730 connected_duration: zx::BootDuration::from_seconds(30),
1731 is_sme_reconnecting: false,
1732 disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1733 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1734 reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1735 }),
1736 original_bss_desc: Box::new(bss_description),
1737 current_rssi_dbm: -30,
1738 current_snr_db: 25,
1739 current_channel: channel,
1740 };
1741 let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1742 assert_eq!(
1743 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1744 Poll::Ready(())
1745 );
1746
1747 let data = test_helper.get_inspect_data_tree();
1749 assert_data_tree!(@executor test_helper.exec, data, root: contains {
1750 test_stats: contains {
1751 metadata: contains {
1752 connected_networks: {
1753 "0": {
1754 "@time": AnyNumericProperty,
1755 "data": {
1756 bssid: &*BSSID_REGEX,
1757 ssid: &*SSID_REGEX,
1758 ht_cap: AnyBytesProperty,
1759 vht_cap: AnyBytesProperty,
1760 protection: "Open",
1761 is_wmm_assoc: AnyBoolProperty,
1762 wmm_param: AnyBytesProperty,
1763 }
1764 }
1765 },
1766 disconnect_sources: {
1767 "0": {
1768 "@time": AnyNumericProperty,
1769 "data": {
1770 source: "ap",
1771 reason: "UnspecifiedReason",
1772 mlme_event_name: "DeauthenticateIndication",
1773 }
1774 }
1775 },
1776 },
1777 disconnect_events: {
1778 "0": {
1779 "@time": AnyNumericProperty,
1780 connected_duration: zx::BootDuration::from_seconds(30).into_nanos(),
1781 disconnect_source_id: 0u64,
1782 network_id: 0u64,
1783 rssi_dbm: -30i64,
1784 snr_db: 25i64,
1785 channel: AnyStringProperty,
1786 }
1787 }
1788 }
1789 });
1790
1791 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1792 assert_eq!(
1793 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
1794 &[TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 1)),]
1795 );
1796 assert_eq!(
1797 &time_matrix_calls.drain::<u64>("disconnected_networks")[..],
1798 &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
1799 );
1800 assert_eq!(
1801 &time_matrix_calls.drain::<u64>("disconnect_sources")[..],
1802 &[TimeMatrixCall::Fold(Timed::now(1 << 0))]
1803 );
1804 }
1805
1806 #[fuchsia::test]
1807 fn test_log_disconnect_cobalt() {
1808 let mut test_helper = setup_test();
1809 let logger = ConnectDisconnectLogger::new(
1810 test_helper.cobalt_proxy.clone(),
1811 &test_helper.inspect_node,
1812 &test_helper.inspect_metadata_node,
1813 &test_helper.inspect_metadata_path,
1814 &test_helper.mock_time_matrix_client,
1815 );
1816
1817 let disconnect_info = DisconnectInfo {
1819 connected_duration: zx::BootDuration::from_millis(300_000),
1820 disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1821 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1822 reason_code: fidl_ieee80211::ReasonCode::ApInitiated,
1823 }),
1824 ..fake_disconnect_info()
1825 };
1826 let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1827 assert_eq!(
1828 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1829 Poll::Ready(())
1830 );
1831
1832 let disconnect_count_metrics =
1833 test_helper.get_logged_metrics(metrics::TOTAL_DISCONNECT_COUNT_METRIC_ID);
1834 assert_eq!(disconnect_count_metrics.len(), 1);
1835 assert_eq!(disconnect_count_metrics[0].payload, MetricEventPayload::Count(1));
1836
1837 let connected_duration_metrics =
1838 test_helper.get_logged_metrics(metrics::CONNECTED_DURATION_ON_DISCONNECT_METRIC_ID);
1839 assert_eq!(connected_duration_metrics.len(), 1);
1840 assert_eq!(
1841 connected_duration_metrics[0].payload,
1842 MetricEventPayload::IntegerValue(300_000)
1843 );
1844
1845 let disconnect_by_reason_metrics =
1846 test_helper.get_logged_metrics(metrics::DISCONNECT_BREAKDOWN_BY_REASON_CODE_METRIC_ID);
1847 assert_eq!(disconnect_by_reason_metrics.len(), 1);
1848 assert_eq!(disconnect_by_reason_metrics[0].payload, MetricEventPayload::Count(1));
1849 assert_eq!(disconnect_by_reason_metrics[0].event_codes.len(), 2);
1850 assert_eq!(
1851 disconnect_by_reason_metrics[0].event_codes[0],
1852 fidl_ieee80211::ReasonCode::ApInitiated.into_primitive() as u32
1853 );
1854 assert_eq!(
1855 disconnect_by_reason_metrics[0].event_codes[1],
1856 metrics::ConnectivityWlanMetricDimensionDisconnectSource::Ap as u32
1857 );
1858 }
1859
1860 #[test_case(
1861 fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1862 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1863 reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1864 }),
1865 true;
1866 "ap_disconnect_source"
1867 )]
1868 #[test_case(
1869 fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1870 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1871 reason_code: fidl_ieee80211::ReasonCode::UnspecifiedReason,
1872 }),
1873 true;
1874 "mlme_disconnect_source_not_link_failed"
1875 )]
1876 #[test_case(
1877 fidl_sme::DisconnectSource::Mlme(fidl_sme::DisconnectCause {
1878 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1879 reason_code: fidl_ieee80211::ReasonCode::MlmeLinkFailed,
1880 }),
1881 false;
1882 "mlme_link_failed"
1883 )]
1884 #[test_case(
1885 fidl_sme::DisconnectSource::User(fidl_sme::UserDisconnectReason::Unknown),
1886 false;
1887 "user_disconnect_source"
1888 )]
1889 #[fuchsia::test(add_test_attr = false)]
1890 fn test_log_disconnect_for_mobile_device_cobalt(
1891 disconnect_source: fidl_sme::DisconnectSource,
1892 should_log: bool,
1893 ) {
1894 let mut test_helper = setup_test();
1895 let logger = ConnectDisconnectLogger::new(
1896 test_helper.cobalt_proxy.clone(),
1897 &test_helper.inspect_node,
1898 &test_helper.inspect_metadata_node,
1899 &test_helper.inspect_metadata_path,
1900 &test_helper.mock_time_matrix_client,
1901 );
1902
1903 let disconnect_info = DisconnectInfo { disconnect_source, ..fake_disconnect_info() };
1905 let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1906 assert_eq!(
1907 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1908 Poll::Ready(())
1909 );
1910
1911 let metrics = test_helper
1912 .get_logged_metrics(metrics::DISCONNECT_OCCURRENCE_FOR_MOBILE_DEVICE_METRIC_ID);
1913 if should_log {
1914 assert_eq!(metrics.len(), 1);
1915 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1916 assert_matches!(*logger.connection_state.lock(), ConnectionState::Disconnected(_));
1917 } else {
1918 assert!(metrics.is_empty());
1919 assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
1920 }
1921 }
1922
1923 #[fuchsia::test]
1924 fn test_log_downtime_post_disconnect_on_reconnect() {
1925 let mut test_helper = setup_test();
1926 let logger = ConnectDisconnectLogger::new(
1927 test_helper.cobalt_proxy.clone(),
1928 &test_helper.inspect_node,
1929 &test_helper.inspect_metadata_node,
1930 &test_helper.inspect_metadata_path,
1931 &test_helper.mock_time_matrix_client,
1932 );
1933
1934 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(15_000_000_000));
1936 let bss_description = random_bss_description!(Wpa2);
1937 let mut test_fut = pin!(logger.handle_connect_attempt(
1938 fidl_ieee80211::StatusCode::Success,
1939 &bss_description,
1940 false,
1941 false
1942 ));
1943 assert_eq!(
1944 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1945 Poll::Ready(())
1946 );
1947
1948 let metrics = test_helper.get_logged_metrics(metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID);
1950 assert!(metrics.is_empty());
1951
1952 assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
1954
1955 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(25_000_000_000));
1957 let disconnect_info = DisconnectInfo {
1958 connected_duration: zx::BootDuration::from_millis(300_000),
1959 disconnect_source: fidl_sme::DisconnectSource::Ap(fidl_sme::DisconnectCause {
1960 mlme_event_name: fidl_sme::DisconnectMlmeEventName::DeauthenticateIndication,
1961 reason_code: fidl_ieee80211::ReasonCode::ApInitiated,
1962 }),
1963 ..fake_disconnect_info()
1964 };
1965 let mut test_fut = pin!(logger.log_disconnect(&disconnect_info));
1966 assert_eq!(
1967 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1968 Poll::Ready(())
1969 );
1970
1971 assert_matches!(*logger.connection_state.lock(), ConnectionState::Disconnected(_));
1973
1974 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(60_000_000_000));
1976 let mut test_fut = pin!(logger.handle_connect_attempt(
1977 fidl_ieee80211::StatusCode::Success,
1978 &bss_description,
1979 false,
1980 false
1981 ));
1982 assert_eq!(
1983 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1984 Poll::Ready(())
1985 );
1986
1987 let metrics = test_helper.get_logged_metrics(metrics::DOWNTIME_POST_DISCONNECT_METRIC_ID);
1989 assert_eq!(metrics.len(), 1);
1990 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(35_000));
1991
1992 assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
1994 }
1995
1996 #[fuchsia::test]
1997 fn test_log_iface_destroyed() {
1998 let mut test_helper = setup_test();
1999 let logger = ConnectDisconnectLogger::new(
2000 test_helper.cobalt_proxy.clone(),
2001 &test_helper.inspect_node,
2002 &test_helper.inspect_metadata_node,
2003 &test_helper.inspect_metadata_path,
2004 &test_helper.mock_time_matrix_client,
2005 );
2006
2007 let bss_description = random_bss_description!();
2009 let mut test_fut = pin!(logger.handle_connect_attempt(
2010 fidl_ieee80211::StatusCode::Success,
2011 &bss_description,
2012 false,
2013 false
2014 ));
2015 assert_eq!(
2016 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2017 Poll::Ready(())
2018 );
2019
2020 assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
2022
2023 let mut test_fut = pin!(logger.handle_iface_destroyed());
2025 assert_eq!(
2026 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2027 Poll::Ready(())
2028 );
2029
2030 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
2031 assert_eq!(
2032 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
2033 &[
2034 TimeMatrixCall::Fold(Timed::now(1 << 0)),
2035 TimeMatrixCall::Fold(Timed::now(1 << 3)),
2036 TimeMatrixCall::Fold(Timed::now(1 << 0))
2037 ]
2038 );
2039
2040 assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
2042 }
2043
2044 #[fuchsia::test]
2045 fn test_log_disable_client_connections() {
2046 let mut test_helper = setup_test();
2047 let logger = ConnectDisconnectLogger::new(
2048 test_helper.cobalt_proxy.clone(),
2049 &test_helper.inspect_node,
2050 &test_helper.inspect_metadata_node,
2051 &test_helper.inspect_metadata_path,
2052 &test_helper.mock_time_matrix_client,
2053 );
2054
2055 let bss_description = random_bss_description!();
2057 let mut test_fut = pin!(logger.handle_connect_attempt(
2058 fidl_ieee80211::StatusCode::Success,
2059 &bss_description,
2060 false,
2061 false
2062 ));
2063 assert_eq!(
2064 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2065 Poll::Ready(())
2066 );
2067
2068 assert_matches!(*logger.connection_state.lock(), ConnectionState::Connected(_));
2070
2071 let mut test_fut =
2073 pin!(logger.handle_client_connections_toggle(&ClientConnectionsToggleEvent::Disabled));
2074 assert_eq!(
2075 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2076 Poll::Ready(())
2077 );
2078
2079 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
2080 assert_eq!(
2081 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
2082 &[
2083 TimeMatrixCall::Fold(Timed::now(1 << 0)),
2084 TimeMatrixCall::Fold(Timed::now(1 << 3)),
2085 TimeMatrixCall::Fold(Timed::now(1 << 0))
2086 ]
2087 );
2088
2089 assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
2091 }
2092
2093 #[fuchsia::test]
2094 fn test_wlan_connectivity_states_credential_rejected() {
2095 let mut test_helper = setup_test();
2096 let logger = ConnectDisconnectLogger::new(
2097 test_helper.cobalt_proxy.clone(),
2098 &test_helper.inspect_node,
2099 &test_helper.inspect_metadata_node,
2100 &test_helper.inspect_metadata_path,
2101 &test_helper.mock_time_matrix_client,
2102 );
2103
2104 let bss_description = random_bss_description!();
2106 let mut test_fut = pin!(logger.handle_connect_attempt(
2107 fidl_ieee80211::StatusCode::RefusedReasonUnspecified,
2108 &bss_description,
2109 true,
2110 false
2111 ));
2112 assert_eq!(
2113 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2114 Poll::Ready(())
2115 );
2116
2117 assert_matches!(*logger.connection_state.lock(), ConnectionState::Idle(_));
2118 }
2119
2120 #[fuchsia::test]
2121 fn test_wlan_connectivity_states_failed_to_start() {
2122 let mut test_helper = setup_test();
2123 let logger = ConnectDisconnectLogger::new(
2124 test_helper.cobalt_proxy.clone(),
2125 &test_helper.inspect_node,
2126 &test_helper.inspect_metadata_node,
2127 &test_helper.inspect_metadata_path,
2128 &test_helper.mock_time_matrix_client,
2129 );
2130
2131 let mut test_fut = pin!(logger.handle_client_connections_failed_to_start());
2132 assert_eq!(
2133 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2134 Poll::Ready(())
2135 );
2136
2137 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
2138 assert_eq!(
2139 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
2140 &[
2141 TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 4)), ]
2144 );
2145 assert_matches!(*logger.connection_state.lock(), ConnectionState::FailedToStart(_));
2146 }
2147
2148 #[fuchsia::test]
2149 fn test_wlan_connectivity_states_failed_to_stop() {
2150 let mut test_helper = setup_test();
2151 let logger = ConnectDisconnectLogger::new(
2152 test_helper.cobalt_proxy.clone(),
2153 &test_helper.inspect_node,
2154 &test_helper.inspect_metadata_node,
2155 &test_helper.inspect_metadata_path,
2156 &test_helper.mock_time_matrix_client,
2157 );
2158
2159 let mut test_fut = pin!(logger.handle_client_connections_failed_to_stop());
2160 assert_eq!(
2161 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2162 Poll::Ready(())
2163 );
2164
2165 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
2166 assert_eq!(
2167 &time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..],
2168 &[
2169 TimeMatrixCall::Fold(Timed::now(1 << 0)), TimeMatrixCall::Fold(Timed::now(1 << 5)), ]
2172 );
2173
2174 assert_matches!(*logger.connection_state.lock(), ConnectionState::FailedToStop(_));
2175 }
2176
2177 #[test_case(ConnectionState::Idle(IdleState {}))]
2178 #[test_case(ConnectionState::Disconnected(DisconnectedState {}))]
2179 #[test_case(ConnectionState::ConnectFailed(ConnectFailedState {}))]
2180 #[test_case(ConnectionState::PnoScanFailedIdle(PnoScanFailedIdleState {}))]
2181 fn test_connectivity_state_transition_on_pno_scan_failure(initial_state: ConnectionState) {
2182 let mut test_helper = setup_test();
2183 let logger = ConnectDisconnectLogger::new(
2184 test_helper.cobalt_proxy.clone(),
2185 &test_helper.inspect_node,
2186 &test_helper.inspect_metadata_node,
2187 &test_helper.inspect_metadata_path,
2188 &test_helper.mock_time_matrix_client,
2189 );
2190
2191 *logger.connection_state.lock() = initial_state.clone();
2193
2194 let mut test_fut = pin!(logger.handle_pno_scan_failure());
2196 assert_matches!(
2197 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2198 Poll::Ready(())
2199 );
2200
2201 let metric_events = test_helper
2203 .get_logged_metrics(metrics::PNO_SCAN_FAILURE_WHILE_NOT_CONNECTED_OCCURRENCE_METRIC_ID);
2204 assert_eq!(metric_events.len(), 1);
2205 assert_eq!(metric_events[0].payload, MetricEventPayload::Count(1));
2206
2207 let metric_events =
2208 test_helper.get_logged_metrics(metrics::PNO_SCAN_FAILURE_OCCURRENCE_METRIC_ID);
2209 assert_eq!(metric_events.len(), 1);
2210 assert_eq!(metric_events[0].payload, MetricEventPayload::Count(1));
2211
2212 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
2214 assert_eq!(
2215 *time_matrix_calls.drain::<u64>("wlan_connectivity_states")[..].last().unwrap(),
2216 TimeMatrixCall::Fold(Timed::now(1 << 6)), );
2218
2219 assert_matches!(*logger.connection_state.lock(), ConnectionState::PnoScanFailedIdle(_));
2221 }
2222
2223 #[test_case(ConnectionState::Connected(ConnectedState {}))]
2224 #[test_case(ConnectionState::FailedToStart(FailedToStartState {}))]
2225 #[test_case(ConnectionState::FailedToStop(FailedToStopState {}))]
2226 fn test_no_connectivity_state_transition_on_pno_scan_failure(initial_state: ConnectionState) {
2227 let mut test_helper = setup_test();
2228 let logger = ConnectDisconnectLogger::new(
2229 test_helper.cobalt_proxy.clone(),
2230 &test_helper.inspect_node,
2231 &test_helper.inspect_metadata_node,
2232 &test_helper.inspect_metadata_path,
2233 &test_helper.mock_time_matrix_client,
2234 );
2235
2236 *logger.connection_state.lock() = initial_state.clone();
2238
2239 let mut test_fut = pin!(logger.handle_pno_scan_failure());
2241 assert_matches!(
2242 test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
2243 Poll::Ready(())
2244 );
2245
2246 let metric_events =
2248 test_helper.get_logged_metrics(metrics::PNO_SCAN_FAILURE_OCCURRENCE_METRIC_ID);
2249 assert_eq!(metric_events.len(), 1);
2250 assert_eq!(metric_events[0].payload, MetricEventPayload::Count(1));
2251
2252 assert_eq!(logger.connection_state.lock().to_id(), initial_state.to_id());
2254 }
2255
2256 #[fuchsia::test]
2257 fn test_wlan_connectivity_states_bitset_map_size() {
2258 let enum_variant_count = ConnectionState::iter().count();
2259 let bitset_map_size =
2260 ConnectDisconnectTimeSeries::wlan_connectivity_states_bitset_map().len();
2261 assert_eq!(enum_variant_count, bitset_map_size);
2262 }
2263
2264 fn fake_disconnect_info() -> DisconnectInfo {
2265 let bss_description = random_bss_description!(Wpa2);
2266 let channel = bss_description.channel;
2267 DisconnectInfo {
2268 iface_id: 1,
2269 connected_duration: zx::BootDuration::from_hours(6),
2270 is_sme_reconnecting: false,
2271 disconnect_source: fidl_sme::DisconnectSource::User(
2272 fidl_sme::UserDisconnectReason::Unknown,
2273 ),
2274 original_bss_desc: bss_description.into(),
2275 current_rssi_dbm: -30,
2276 current_snr_db: 25,
2277 current_channel: channel,
2278 }
2279 }
2280}