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