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