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