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 {fuchsia_async as fasync, wlan_legacy_metrics_registry as metrics};
8
9#[derive(Debug, PartialEq)]
10pub enum ScanResult {
11    Complete { num_results: usize },
12    Failed,
13    Cancelled,
14}
15
16pub struct ScanLogger {
17    cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
18    scan_started_at: Option<fasync::BootInstant>,
19}
20
21impl ScanLogger {
22    pub fn new(cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy) -> Self {
23        Self { cobalt_proxy, scan_started_at: None }
24    }
25
26    pub async fn handle_scan_start(&mut self) {
27        self.scan_started_at = Some(fasync::BootInstant::now());
28        let metric_events = vec![MetricEvent {
29            metric_id: metrics::SCAN_OCCURRENCE_METRIC_ID,
30            event_codes: vec![],
31            payload: MetricEventPayload::Count(1),
32        }];
33        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_scan_start");
34    }
35
36    pub async fn handle_scan_result(&mut self, result: ScanResult) {
37        let mut metric_events = vec![];
38        let now = fasync::BootInstant::now();
39        // Only log scan result metrics if there was a scan
40        if let Some(scan_started_at) = self.scan_started_at.take() {
41            match result {
42                ScanResult::Complete { num_results } => {
43                    let scan_duration = now - scan_started_at;
44                    metric_events.push(MetricEvent {
45                        metric_id: metrics::SCAN_FULFILLMENT_TIME_METRIC_ID,
46                        event_codes: vec![],
47                        payload: MetricEventPayload::IntegerValue(scan_duration.into_millis()),
48                    });
49                    if num_results == 0 {
50                        metric_events.push(MetricEvent {
51                            metric_id: metrics::EMPTY_SCAN_RESULTS_METRIC_ID,
52                            event_codes: vec![],
53                            payload: MetricEventPayload::Count(1),
54                        });
55                    }
56                }
57                ScanResult::Failed => {
58                    metric_events.push(MetricEvent {
59                        metric_id: metrics::CLIENT_SCAN_FAILURE_METRIC_ID,
60                        event_codes: vec![],
61                        payload: MetricEventPayload::Count(1),
62                    });
63                }
64                ScanResult::Cancelled => {
65                    metric_events.push(MetricEvent {
66                        metric_id: metrics::ABORTED_SCAN_METRIC_ID,
67                        event_codes: vec![],
68                        payload: MetricEventPayload::Count(1),
69                    });
70                }
71            }
72        }
73
74        if !metric_events.is_empty() {
75            log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_scan_result");
76        }
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use crate::testing::{setup_test, TestHelper};
84    use futures::task::Poll;
85    use std::pin::pin;
86    use test_case::test_case;
87
88    fn run_handle_scan_start(test_helper: &mut TestHelper, scan_logger: &mut ScanLogger) {
89        let mut test_fut = pin!(scan_logger.handle_scan_start());
90        assert_eq!(
91            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
92            Poll::Ready(())
93        );
94    }
95
96    fn run_handle_scan_result(
97        test_helper: &mut TestHelper,
98        scan_logger: &mut ScanLogger,
99        scan_result: ScanResult,
100    ) {
101        let mut test_fut = pin!(scan_logger.handle_scan_result(scan_result));
102        assert_eq!(
103            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
104            Poll::Ready(())
105        );
106    }
107
108    #[fuchsia::test]
109    fn test_handle_scan_start() {
110        let mut test_helper = setup_test();
111        let mut scan_logger = ScanLogger::new(test_helper.cobalt_proxy.clone());
112
113        run_handle_scan_start(&mut test_helper, &mut scan_logger);
114
115        let metrics = test_helper.get_logged_metrics(metrics::SCAN_OCCURRENCE_METRIC_ID);
116        assert_eq!(metrics.len(), 1);
117        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
118    }
119
120    #[fuchsia::test]
121    fn test_handle_scan_result_complete() {
122        let mut test_helper = setup_test();
123        let mut scan_logger = ScanLogger::new(test_helper.cobalt_proxy.clone());
124
125        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(20_000_000));
126        run_handle_scan_start(&mut test_helper, &mut scan_logger);
127
128        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(100_000_000));
129        let scan_result = ScanResult::Complete { num_results: 10 };
130        run_handle_scan_result(&mut test_helper, &mut scan_logger, scan_result);
131
132        let metrics = test_helper.get_logged_metrics(metrics::SCAN_FULFILLMENT_TIME_METRIC_ID);
133        assert_eq!(metrics.len(), 1);
134        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(80)); // 80ms
135        let metrics = test_helper.get_logged_metrics(metrics::EMPTY_SCAN_RESULTS_METRIC_ID);
136        assert!(metrics.is_empty());
137    }
138
139    #[fuchsia::test]
140    fn test_handle_scan_result_empty() {
141        let mut test_helper = setup_test();
142        let mut scan_logger = ScanLogger::new(test_helper.cobalt_proxy.clone());
143
144        run_handle_scan_start(&mut test_helper, &mut scan_logger);
145
146        let scan_result = ScanResult::Complete { num_results: 0 };
147        run_handle_scan_result(&mut test_helper, &mut scan_logger, scan_result);
148
149        let metrics = test_helper.get_logged_metrics(metrics::SCAN_FULFILLMENT_TIME_METRIC_ID);
150        assert_eq!(metrics.len(), 1);
151        let metrics = test_helper.get_logged_metrics(metrics::EMPTY_SCAN_RESULTS_METRIC_ID);
152        assert_eq!(metrics.len(), 1);
153        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
154    }
155
156    #[fuchsia::test]
157    fn test_handle_scan_result_cancelled() {
158        let mut test_helper = setup_test();
159        let mut scan_logger = ScanLogger::new(test_helper.cobalt_proxy.clone());
160
161        run_handle_scan_start(&mut test_helper, &mut scan_logger);
162
163        let scan_result = ScanResult::Cancelled;
164        run_handle_scan_result(&mut test_helper, &mut scan_logger, scan_result);
165
166        let metrics = test_helper.get_logged_metrics(metrics::ABORTED_SCAN_METRIC_ID);
167        assert_eq!(metrics.len(), 1);
168        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
169    }
170
171    #[fuchsia::test]
172    fn test_handle_scan_result_failure() {
173        let mut test_helper = setup_test();
174        let mut scan_logger = ScanLogger::new(test_helper.cobalt_proxy.clone());
175
176        run_handle_scan_start(&mut test_helper, &mut scan_logger);
177
178        let scan_result = ScanResult::Failed;
179        run_handle_scan_result(&mut test_helper, &mut scan_logger, scan_result);
180
181        let metrics = test_helper.get_logged_metrics(metrics::CLIENT_SCAN_FAILURE_METRIC_ID);
182        assert_eq!(metrics.len(), 1);
183        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
184    }
185
186    #[test_case(
187        ScanResult::Complete { num_results: 10 },
188        metrics::SCAN_FULFILLMENT_TIME_METRIC_ID;
189        "scan complete"
190    )]
191    #[test_case(
192        ScanResult::Failed,
193        metrics::CLIENT_SCAN_FAILURE_METRIC_ID;
194        "scan failed"
195    )]
196    #[test_case(
197        ScanResult::Cancelled,
198        metrics::ABORTED_SCAN_METRIC_ID;
199        "scan cancelled"
200    )]
201    #[fuchsia::test(add_test_attr = false)]
202    fn test_handle_scan_result_no_logging_to_cobalt_if_scan_not_started(
203        scan_result: ScanResult,
204        metric_id: u32,
205    ) {
206        let mut test_helper = setup_test();
207        let mut scan_logger = ScanLogger::new(test_helper.cobalt_proxy.clone());
208
209        run_handle_scan_result(&mut test_helper, &mut scan_logger, scan_result);
210
211        let metrics = test_helper.get_logged_metrics(metric_id);
212        assert!(metrics.is_empty());
213    }
214}