Skip to main content

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 fidl_fuchsia_power_battery as fidl_battery;
8use fuchsia_async as fasync;
9use std::ops::BitOr;
10use windowed_stats::experimental::inspect::{InspectSender, InspectedTimeMatrix};
11use windowed_stats::experimental::series::interpolation::ConstantSample;
12use windowed_stats::experimental::series::metadata::BitsetMap;
13use windowed_stats::experimental::series::statistic::Union;
14use windowed_stats::experimental::series::{SamplingProfile, TimeMatrix};
15use wlan_legacy_metrics_registry as metrics;
16
17#[derive(Debug, PartialEq)]
18pub enum ScanResult {
19    Complete { num_results: usize },
20    Failed,
21    Cancelled,
22}
23
24pub struct ScanLogger {
25    cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
26    time_series_stats: ScanTimeSeries,
27    scan_started_at: Option<fasync::BootInstant>,
28    on_battery: bool,
29}
30
31impl ScanLogger {
32    pub fn new<S: InspectSender>(
33        cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
34        time_matrix_client: &S,
35    ) -> Self {
36        Self {
37            cobalt_proxy,
38            time_series_stats: ScanTimeSeries::new(time_matrix_client),
39            scan_started_at: None,
40            on_battery: false,
41        }
42    }
43
44    pub async fn handle_scan_start(&mut self) {
45        self.scan_started_at = Some(fasync::BootInstant::now());
46        self.time_series_stats.scan_events.fold_or_log_error(ScanEvents::START);
47        self.log_scan_start_cobalt().await;
48    }
49
50    pub async fn log_scan_start_cobalt(&mut self) {
51        let mut metric_events = vec![MetricEvent {
52            metric_id: metrics::SCAN_OCCURRENCE_METRIC_ID,
53            event_codes: vec![],
54            payload: MetricEventPayload::Count(1),
55        }];
56        if self.on_battery {
57            metric_events.push(MetricEvent {
58                metric_id: metrics::SCAN_OCCURRENCE_ON_BATTERY_METRIC_ID,
59                event_codes: vec![],
60                payload: MetricEventPayload::Count(1),
61            });
62        }
63        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_scan_start");
64    }
65
66    pub async fn handle_scan_result(&mut self, result: ScanResult) {
67        let mut metric_events = vec![];
68        let now = fasync::BootInstant::now();
69        // Only log scan result metrics if there was a scan
70        if let Some(scan_started_at) = self.scan_started_at.take() {
71            match result {
72                ScanResult::Complete { num_results } => {
73                    let scan_duration = now - scan_started_at;
74                    metric_events.push(MetricEvent {
75                        metric_id: metrics::SCAN_FULFILLMENT_TIME_METRIC_ID,
76                        event_codes: vec![],
77                        payload: MetricEventPayload::IntegerValue(scan_duration.into_millis()),
78                    });
79                    if num_results == 0 {
80                        metric_events.push(MetricEvent {
81                            metric_id: metrics::EMPTY_SCAN_RESULTS_METRIC_ID,
82                            event_codes: vec![],
83                            payload: MetricEventPayload::Count(1),
84                        });
85                    }
86                }
87                ScanResult::Failed => {
88                    metric_events.push(MetricEvent {
89                        metric_id: metrics::CLIENT_SCAN_FAILURE_METRIC_ID,
90                        event_codes: vec![],
91                        payload: MetricEventPayload::Count(1),
92                    });
93                }
94                ScanResult::Cancelled => {
95                    metric_events.push(MetricEvent {
96                        metric_id: metrics::ABORTED_SCAN_METRIC_ID,
97                        event_codes: vec![],
98                        payload: MetricEventPayload::Count(1),
99                    });
100                }
101            }
102        }
103
104        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_scan_result");
105    }
106
107    pub async fn handle_battery_charge_status(
108        &mut self,
109        charge_status: fidl_battery::ChargeStatus,
110    ) {
111        self.on_battery = matches!(charge_status, fidl_battery::ChargeStatus::Discharging);
112    }
113}
114
115#[derive(Default, Copy, Clone, Debug, PartialEq)]
116struct ScanEvents(u64);
117impl ScanEvents {
118    // Note: Keep these bits in sync with ScanEvents::bit_set_map
119    const START: Self = Self(1 << 0);
120}
121
122impl ScanEvents {
123    fn bit_set_map() -> BitsetMap {
124        BitsetMap::from_ordered(["start"])
125    }
126}
127
128impl BitOr for ScanEvents {
129    type Output = Self;
130
131    fn bitor(self, rhs: Self) -> Self::Output {
132        Self(self.0 | rhs.0)
133    }
134}
135
136impl From<ScanEvents> for u64 {
137    fn from(value: ScanEvents) -> u64 {
138        value.0
139    }
140}
141
142#[derive(Debug)]
143struct ScanTimeSeries {
144    scan_events: InspectedTimeMatrix<ScanEvents>,
145}
146
147impl ScanTimeSeries {
148    pub fn new<S: InspectSender>(client: &S) -> Self {
149        let scan_events = client.inspect_time_matrix_with_metadata(
150            "scan_events",
151            TimeMatrix::<Union<ScanEvents>, ConstantSample>::new(
152                SamplingProfile::highly_granular(),
153                ConstantSample::default(),
154            ),
155            ScanEvents::bit_set_map(),
156        );
157        Self { scan_events }
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use crate::testing::{TestHelper, setup_test};
165    use diagnostics_assertions::{AnyBytesProperty, assert_data_tree};
166    use futures::task::Poll;
167    use std::pin::pin;
168    use test_case::test_case;
169    use windowed_stats::experimental::clock::Timed;
170    use windowed_stats::experimental::inspect::TimeMatrixClient;
171    use windowed_stats::experimental::testing::TimeMatrixCall;
172
173    fn run_handle_scan_start(test_helper: &mut TestHelper, scan_logger: &mut ScanLogger) {
174        let mut test_fut = pin!(scan_logger.handle_scan_start());
175        assert_eq!(
176            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
177            Poll::Ready(())
178        );
179    }
180
181    fn run_handle_scan_result(
182        test_helper: &mut TestHelper,
183        scan_logger: &mut ScanLogger,
184        scan_result: ScanResult,
185    ) {
186        let mut test_fut = pin!(scan_logger.handle_scan_result(scan_result));
187        assert_eq!(
188            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
189            Poll::Ready(())
190        );
191    }
192
193    fn run_handle_battery_charge_status(
194        test_helper: &mut TestHelper,
195        scan_logger: &mut ScanLogger,
196        charge_status: fidl_battery::ChargeStatus,
197    ) {
198        let mut test_fut = pin!(scan_logger.handle_battery_charge_status(charge_status));
199        assert_eq!(
200            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
201            Poll::Ready(())
202        );
203    }
204
205    #[fuchsia::test]
206    fn test_handle_scan_start() {
207        let mut test_helper = setup_test();
208        let mut scan_logger =
209            ScanLogger::new(test_helper.cobalt_proxy.clone(), &test_helper.mock_time_matrix_client);
210
211        run_handle_scan_start(&mut test_helper, &mut scan_logger);
212
213        let metrics = test_helper.get_logged_metrics(metrics::SCAN_OCCURRENCE_METRIC_ID);
214        assert_eq!(metrics.len(), 1);
215        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
216
217        let metrics = test_helper.get_logged_metrics(metrics::SCAN_OCCURRENCE_ON_BATTERY_METRIC_ID);
218        assert!(metrics.is_empty());
219    }
220
221    #[fuchsia::test]
222    fn test_handle_scan_start_on_battery() {
223        let mut test_helper = setup_test();
224        let mut scan_logger =
225            ScanLogger::new(test_helper.cobalt_proxy.clone(), &test_helper.mock_time_matrix_client);
226
227        run_handle_battery_charge_status(
228            &mut test_helper,
229            &mut scan_logger,
230            fidl_battery::ChargeStatus::Discharging,
231        );
232        run_handle_scan_start(&mut test_helper, &mut scan_logger);
233
234        let metrics = test_helper.get_logged_metrics(metrics::SCAN_OCCURRENCE_METRIC_ID);
235        assert_eq!(metrics.len(), 1);
236        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
237
238        let metrics = test_helper.get_logged_metrics(metrics::SCAN_OCCURRENCE_ON_BATTERY_METRIC_ID);
239        assert_eq!(metrics.len(), 1);
240        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
241
242        // Set charge status to Charging and verify that scan_onccurrence_on_battery is not
243        // logged. This verifies that we do change back to off battery.
244        test_helper.clear_cobalt_events();
245        run_handle_battery_charge_status(
246            &mut test_helper,
247            &mut scan_logger,
248            fidl_battery::ChargeStatus::Charging,
249        );
250        run_handle_scan_start(&mut test_helper, &mut scan_logger);
251
252        let metrics = test_helper.get_logged_metrics(metrics::SCAN_OCCURRENCE_ON_BATTERY_METRIC_ID);
253        assert!(metrics.is_empty());
254    }
255
256    #[fuchsia::test]
257    fn test_handle_scan_result_complete() {
258        let mut test_helper = setup_test();
259        let mut scan_logger =
260            ScanLogger::new(test_helper.cobalt_proxy.clone(), &test_helper.mock_time_matrix_client);
261
262        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(20_000_000));
263        run_handle_scan_start(&mut test_helper, &mut scan_logger);
264
265        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(100_000_000));
266        let scan_result = ScanResult::Complete { num_results: 10 };
267        run_handle_scan_result(&mut test_helper, &mut scan_logger, scan_result);
268
269        let metrics = test_helper.get_logged_metrics(metrics::SCAN_FULFILLMENT_TIME_METRIC_ID);
270        assert_eq!(metrics.len(), 1);
271        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(80)); // 80ms
272        let metrics = test_helper.get_logged_metrics(metrics::EMPTY_SCAN_RESULTS_METRIC_ID);
273        assert!(metrics.is_empty());
274    }
275
276    #[fuchsia::test]
277    fn test_handle_scan_result_empty() {
278        let mut test_helper = setup_test();
279        let mut scan_logger =
280            ScanLogger::new(test_helper.cobalt_proxy.clone(), &test_helper.mock_time_matrix_client);
281
282        run_handle_scan_start(&mut test_helper, &mut scan_logger);
283
284        let scan_result = ScanResult::Complete { num_results: 0 };
285        run_handle_scan_result(&mut test_helper, &mut scan_logger, scan_result);
286
287        let metrics = test_helper.get_logged_metrics(metrics::SCAN_FULFILLMENT_TIME_METRIC_ID);
288        assert_eq!(metrics.len(), 1);
289        let metrics = test_helper.get_logged_metrics(metrics::EMPTY_SCAN_RESULTS_METRIC_ID);
290        assert_eq!(metrics.len(), 1);
291        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
292    }
293
294    #[fuchsia::test]
295    fn test_handle_scan_result_cancelled() {
296        let mut test_helper = setup_test();
297        let mut scan_logger =
298            ScanLogger::new(test_helper.cobalt_proxy.clone(), &test_helper.mock_time_matrix_client);
299
300        run_handle_scan_start(&mut test_helper, &mut scan_logger);
301
302        let scan_result = ScanResult::Cancelled;
303        run_handle_scan_result(&mut test_helper, &mut scan_logger, scan_result);
304
305        let metrics = test_helper.get_logged_metrics(metrics::ABORTED_SCAN_METRIC_ID);
306        assert_eq!(metrics.len(), 1);
307        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
308    }
309
310    #[fuchsia::test]
311    fn test_handle_scan_result_failure() {
312        let mut test_helper = setup_test();
313        let mut scan_logger =
314            ScanLogger::new(test_helper.cobalt_proxy.clone(), &test_helper.mock_time_matrix_client);
315
316        run_handle_scan_start(&mut test_helper, &mut scan_logger);
317
318        let scan_result = ScanResult::Failed;
319        run_handle_scan_result(&mut test_helper, &mut scan_logger, scan_result);
320
321        let metrics = test_helper.get_logged_metrics(metrics::CLIENT_SCAN_FAILURE_METRIC_ID);
322        assert_eq!(metrics.len(), 1);
323        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
324    }
325
326    #[test_case(
327        ScanResult::Complete { num_results: 10 },
328        metrics::SCAN_FULFILLMENT_TIME_METRIC_ID;
329        "scan complete"
330    )]
331    #[test_case(
332        ScanResult::Failed,
333        metrics::CLIENT_SCAN_FAILURE_METRIC_ID;
334        "scan failed"
335    )]
336    #[test_case(
337        ScanResult::Cancelled,
338        metrics::ABORTED_SCAN_METRIC_ID;
339        "scan cancelled"
340    )]
341    #[fuchsia::test(add_test_attr = false)]
342    fn test_handle_scan_result_no_logging_to_cobalt_if_scan_not_started(
343        scan_result: ScanResult,
344        metric_id: u32,
345    ) {
346        let mut test_helper = setup_test();
347        let mut scan_logger =
348            ScanLogger::new(test_helper.cobalt_proxy.clone(), &test_helper.mock_time_matrix_client);
349
350        run_handle_scan_result(&mut test_helper, &mut scan_logger, scan_result);
351
352        let metrics = test_helper.get_logged_metrics(metric_id);
353        assert!(metrics.is_empty());
354    }
355
356    #[fuchsia::test]
357    fn scan_logger_new_then_inspect_data_tree_contains_time_matrix_metadata() {
358        let mut test_helper = setup_test();
359        let client = TimeMatrixClient::new(test_helper.inspect_node.create_child("wlan_scan"));
360        let _scan_logger = ScanLogger::new(test_helper.cobalt_proxy.clone(), &client);
361
362        let tree = test_helper.get_inspect_data_tree();
363        assert_data_tree!(
364            @executor test_helper.exec,
365            tree,
366            root: contains {
367                test_stats: contains {
368                    wlan_scan: contains {
369                        scan_events: {
370                            "type": "bitset",
371                            "data": AnyBytesProperty,
372                            metadata: {
373                                index: {
374                                    "0": "start",
375                                }
376                            }
377                        }
378                    }
379                }
380            }
381        );
382    }
383
384    #[fuchsia::test]
385    fn log_scan_start_inspect() {
386        let mut test_helper = setup_test();
387        let mut scan_logger =
388            ScanLogger::new(test_helper.cobalt_proxy.clone(), &test_helper.mock_time_matrix_client);
389
390        run_handle_scan_start(&mut test_helper, &mut scan_logger);
391
392        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
393        assert_eq!(
394            &time_matrix_calls.drain::<ScanEvents>("scan_events")[..],
395            &[TimeMatrixCall::Fold(Timed::now(ScanEvents::START))]
396        );
397    }
398}