wlan_telemetry/processors/
scan.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::util::cobalt_logger::log_cobalt_batch;
6use fidl_fuchsia_metrics::{MetricEvent, MetricEventPayload};
7use {
8    fidl_fuchsia_power_battery as fidl_battery, fuchsia_async as fasync,
9    wlan_legacy_metrics_registry as metrics,
10};
11
12#[derive(Debug, PartialEq)]
13pub enum ScanResult {
14    Complete { num_results: usize },
15    Failed,
16    Cancelled,
17}
18
19pub struct ScanLogger {
20    cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
21    scan_started_at: Option<fasync::BootInstant>,
22    on_battery: bool,
23}
24
25impl ScanLogger {
26    pub fn new(cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy) -> Self {
27        Self { cobalt_proxy, scan_started_at: None, on_battery: false }
28    }
29
30    pub async fn handle_scan_start(&mut self) {
31        self.scan_started_at = Some(fasync::BootInstant::now());
32        let mut metric_events = vec![MetricEvent {
33            metric_id: metrics::SCAN_OCCURRENCE_METRIC_ID,
34            event_codes: vec![],
35            payload: MetricEventPayload::Count(1),
36        }];
37        if self.on_battery {
38            metric_events.push(MetricEvent {
39                metric_id: metrics::SCAN_OCCURRENCE_ON_BATTERY_METRIC_ID,
40                event_codes: vec![],
41                payload: MetricEventPayload::Count(1),
42            });
43        }
44        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_scan_start");
45    }
46
47    pub async fn handle_scan_result(&mut self, result: ScanResult) {
48        let mut metric_events = vec![];
49        let now = fasync::BootInstant::now();
50        // Only log scan result metrics if there was a scan
51        if let Some(scan_started_at) = self.scan_started_at.take() {
52            match result {
53                ScanResult::Complete { num_results } => {
54                    let scan_duration = now - scan_started_at;
55                    metric_events.push(MetricEvent {
56                        metric_id: metrics::SCAN_FULFILLMENT_TIME_METRIC_ID,
57                        event_codes: vec![],
58                        payload: MetricEventPayload::IntegerValue(scan_duration.into_millis()),
59                    });
60                    if num_results == 0 {
61                        metric_events.push(MetricEvent {
62                            metric_id: metrics::EMPTY_SCAN_RESULTS_METRIC_ID,
63                            event_codes: vec![],
64                            payload: MetricEventPayload::Count(1),
65                        });
66                    }
67                }
68                ScanResult::Failed => {
69                    metric_events.push(MetricEvent {
70                        metric_id: metrics::CLIENT_SCAN_FAILURE_METRIC_ID,
71                        event_codes: vec![],
72                        payload: MetricEventPayload::Count(1),
73                    });
74                }
75                ScanResult::Cancelled => {
76                    metric_events.push(MetricEvent {
77                        metric_id: metrics::ABORTED_SCAN_METRIC_ID,
78                        event_codes: vec![],
79                        payload: MetricEventPayload::Count(1),
80                    });
81                }
82            }
83        }
84
85        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_scan_result");
86    }
87
88    pub async fn handle_battery_charge_status(
89        &mut self,
90        charge_status: fidl_battery::ChargeStatus,
91    ) {
92        self.on_battery = matches!(charge_status, fidl_battery::ChargeStatus::Discharging);
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use crate::testing::{setup_test, TestHelper};
100    use futures::task::Poll;
101    use std::pin::pin;
102    use test_case::test_case;
103
104    fn run_handle_scan_start(test_helper: &mut TestHelper, scan_logger: &mut ScanLogger) {
105        let mut test_fut = pin!(scan_logger.handle_scan_start());
106        assert_eq!(
107            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
108            Poll::Ready(())
109        );
110    }
111
112    fn run_handle_scan_result(
113        test_helper: &mut TestHelper,
114        scan_logger: &mut ScanLogger,
115        scan_result: ScanResult,
116    ) {
117        let mut test_fut = pin!(scan_logger.handle_scan_result(scan_result));
118        assert_eq!(
119            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
120            Poll::Ready(())
121        );
122    }
123
124    fn run_handle_battery_charge_status(
125        test_helper: &mut TestHelper,
126        scan_logger: &mut ScanLogger,
127        charge_status: fidl_battery::ChargeStatus,
128    ) {
129        let mut test_fut = pin!(scan_logger.handle_battery_charge_status(charge_status));
130        assert_eq!(
131            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
132            Poll::Ready(())
133        );
134    }
135
136    #[fuchsia::test]
137    fn test_handle_scan_start() {
138        let mut test_helper = setup_test();
139        let mut scan_logger = ScanLogger::new(test_helper.cobalt_proxy.clone());
140
141        run_handle_scan_start(&mut test_helper, &mut scan_logger);
142
143        let metrics = test_helper.get_logged_metrics(metrics::SCAN_OCCURRENCE_METRIC_ID);
144        assert_eq!(metrics.len(), 1);
145        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
146
147        let metrics = test_helper.get_logged_metrics(metrics::SCAN_OCCURRENCE_ON_BATTERY_METRIC_ID);
148        assert!(metrics.is_empty());
149    }
150
151    #[fuchsia::test]
152    fn test_handle_scan_start_on_battery() {
153        let mut test_helper = setup_test();
154        let mut scan_logger = ScanLogger::new(test_helper.cobalt_proxy.clone());
155
156        run_handle_battery_charge_status(
157            &mut test_helper,
158            &mut scan_logger,
159            fidl_battery::ChargeStatus::Discharging,
160        );
161        run_handle_scan_start(&mut test_helper, &mut scan_logger);
162
163        let metrics = test_helper.get_logged_metrics(metrics::SCAN_OCCURRENCE_METRIC_ID);
164        assert_eq!(metrics.len(), 1);
165        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
166
167        let metrics = test_helper.get_logged_metrics(metrics::SCAN_OCCURRENCE_ON_BATTERY_METRIC_ID);
168        assert_eq!(metrics.len(), 1);
169        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
170
171        // Set charge status to Charging and verify that scan_onccurrence_on_battery is not
172        // logged. This verifies that we do change back to off battery.
173        test_helper.clear_cobalt_events();
174        run_handle_battery_charge_status(
175            &mut test_helper,
176            &mut scan_logger,
177            fidl_battery::ChargeStatus::Charging,
178        );
179        run_handle_scan_start(&mut test_helper, &mut scan_logger);
180
181        let metrics = test_helper.get_logged_metrics(metrics::SCAN_OCCURRENCE_ON_BATTERY_METRIC_ID);
182        assert!(metrics.is_empty());
183    }
184
185    #[fuchsia::test]
186    fn test_handle_scan_result_complete() {
187        let mut test_helper = setup_test();
188        let mut scan_logger = ScanLogger::new(test_helper.cobalt_proxy.clone());
189
190        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(20_000_000));
191        run_handle_scan_start(&mut test_helper, &mut scan_logger);
192
193        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(100_000_000));
194        let scan_result = ScanResult::Complete { num_results: 10 };
195        run_handle_scan_result(&mut test_helper, &mut scan_logger, scan_result);
196
197        let metrics = test_helper.get_logged_metrics(metrics::SCAN_FULFILLMENT_TIME_METRIC_ID);
198        assert_eq!(metrics.len(), 1);
199        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(80)); // 80ms
200        let metrics = test_helper.get_logged_metrics(metrics::EMPTY_SCAN_RESULTS_METRIC_ID);
201        assert!(metrics.is_empty());
202    }
203
204    #[fuchsia::test]
205    fn test_handle_scan_result_empty() {
206        let mut test_helper = setup_test();
207        let mut scan_logger = ScanLogger::new(test_helper.cobalt_proxy.clone());
208
209        run_handle_scan_start(&mut test_helper, &mut scan_logger);
210
211        let scan_result = ScanResult::Complete { num_results: 0 };
212        run_handle_scan_result(&mut test_helper, &mut scan_logger, scan_result);
213
214        let metrics = test_helper.get_logged_metrics(metrics::SCAN_FULFILLMENT_TIME_METRIC_ID);
215        assert_eq!(metrics.len(), 1);
216        let metrics = test_helper.get_logged_metrics(metrics::EMPTY_SCAN_RESULTS_METRIC_ID);
217        assert_eq!(metrics.len(), 1);
218        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
219    }
220
221    #[fuchsia::test]
222    fn test_handle_scan_result_cancelled() {
223        let mut test_helper = setup_test();
224        let mut scan_logger = ScanLogger::new(test_helper.cobalt_proxy.clone());
225
226        run_handle_scan_start(&mut test_helper, &mut scan_logger);
227
228        let scan_result = ScanResult::Cancelled;
229        run_handle_scan_result(&mut test_helper, &mut scan_logger, scan_result);
230
231        let metrics = test_helper.get_logged_metrics(metrics::ABORTED_SCAN_METRIC_ID);
232        assert_eq!(metrics.len(), 1);
233        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
234    }
235
236    #[fuchsia::test]
237    fn test_handle_scan_result_failure() {
238        let mut test_helper = setup_test();
239        let mut scan_logger = ScanLogger::new(test_helper.cobalt_proxy.clone());
240
241        run_handle_scan_start(&mut test_helper, &mut scan_logger);
242
243        let scan_result = ScanResult::Failed;
244        run_handle_scan_result(&mut test_helper, &mut scan_logger, scan_result);
245
246        let metrics = test_helper.get_logged_metrics(metrics::CLIENT_SCAN_FAILURE_METRIC_ID);
247        assert_eq!(metrics.len(), 1);
248        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
249    }
250
251    #[test_case(
252        ScanResult::Complete { num_results: 10 },
253        metrics::SCAN_FULFILLMENT_TIME_METRIC_ID;
254        "scan complete"
255    )]
256    #[test_case(
257        ScanResult::Failed,
258        metrics::CLIENT_SCAN_FAILURE_METRIC_ID;
259        "scan failed"
260    )]
261    #[test_case(
262        ScanResult::Cancelled,
263        metrics::ABORTED_SCAN_METRIC_ID;
264        "scan cancelled"
265    )]
266    #[fuchsia::test(add_test_attr = false)]
267    fn test_handle_scan_result_no_logging_to_cobalt_if_scan_not_started(
268        scan_result: ScanResult,
269        metric_id: u32,
270    ) {
271        let mut test_helper = setup_test();
272        let mut scan_logger = ScanLogger::new(test_helper.cobalt_proxy.clone());
273
274        run_handle_scan_result(&mut test_helper, &mut scan_logger, scan_result);
275
276        let metrics = test_helper.get_logged_metrics(metric_id);
277        assert!(metrics.is_empty());
278    }
279}