Skip to main content

wlan_telemetry/processors/
pno_scan.rs

1// Copyright 2026 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 fuchsia_async as fasync;
8use std::sync::Arc;
9use wlan_legacy_metrics_registry as metrics;
10
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum PnoScanDisabledReason {
13    ApiRequest,
14    Internal,
15    Firmware,
16}
17
18pub struct PnoScanLogger {
19    cobalt_proxy: Arc<FilteredCobaltLogger>,
20    enabled_at: Option<fasync::BootInstant>,
21    has_scan_results: bool,
22}
23
24impl PnoScanLogger {
25    pub fn new(cobalt_proxy: Arc<FilteredCobaltLogger>) -> Self {
26        Self { cobalt_proxy, enabled_at: None, has_scan_results: false }
27    }
28
29    pub async fn handle_pno_scan_enabled(&mut self, is_connected: bool) {
30        if self.enabled_at.is_none() {
31            self.enabled_at = Some(fasync::BootInstant::now());
32            self.has_scan_results = false;
33
34            if is_connected {
35                let metric_events = vec![MetricEvent {
36                    metric_id: metrics::PNO_SCAN_ENABLED_WHILE_CONNECTED_METRIC_ID,
37                    event_codes: vec![],
38                    payload: MetricEventPayload::Count(1),
39                }];
40                log_cobalt_batch!(
41                    self.cobalt_proxy,
42                    &metric_events,
43                    "pno_scan_enabled_while_connected"
44                );
45            }
46        } else {
47            // It is unexpected that PNO scans are enabled and then enabled again prior to a
48            // cancellation of the first request.  The accounting surrounding PNO scan enablement
49            // should not be updated in this case, since PNO scans have been enabled since the
50            // first enablement.
51            let metric_events = vec![MetricEvent {
52                metric_id: metrics::PNO_SCAN_REQUEST_COLLISION_METRIC_ID,
53                event_codes: vec![],
54                payload: MetricEventPayload::Count(1),
55            }];
56            log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_pno_scan_enabled");
57        }
58    }
59
60    pub async fn handle_pno_scan_results_received(&mut self) {
61        // Log only the first instance of PNO scan results.  This metric is only logged if PNO
62        // scans are currently enabled.  Scan results are not expected to be received if PNO
63        // scans are not enabled.
64        if !self.has_scan_results {
65            self.has_scan_results = true;
66
67            if let Some(enabled_at) = self.enabled_at {
68                let elapsed = fasync::BootInstant::now() - enabled_at;
69                let metric_events = vec![MetricEvent {
70                    metric_id: metrics::PNO_SCAN_FIRST_RESULTS_ELAPSED_TIME_METRIC_ID,
71                    event_codes: vec![],
72                    payload: MetricEventPayload::IntegerValue(elapsed.into_millis()),
73                }];
74                log_cobalt_batch!(
75                    self.cobalt_proxy,
76                    &metric_events,
77                    "handle_pno_scan_results_received"
78                );
79            }
80        }
81    }
82
83    pub async fn handle_pno_scan_disabled(&mut self, reason: PnoScanDisabledReason) {
84        if let Some(enabled_at) = self.enabled_at.take() {
85            let now = fasync::BootInstant::now();
86            let elapsed = now - enabled_at;
87
88            let mut metric_events = vec![];
89
90            // Log elapsed time
91            metric_events.push(MetricEvent {
92                metric_id: metrics::PNO_SCAN_CANCELLED_ELAPSED_TIME_METRIC_ID,
93                event_codes: vec![if self.has_scan_results {
94                    metrics::PnoScanCancelledElapsedTimeMetricDimensionHadAnyScanResults::True
95                        as u32
96                } else {
97                    metrics::PnoScanCancelledElapsedTimeMetricDimensionHadAnyScanResults::False
98                        as u32
99                }],
100                payload: MetricEventPayload::IntegerValue(elapsed.into_millis()),
101            });
102
103            // Log cancellation source and presence of scan results
104            let had_scan_results = if self.has_scan_results {
105                metrics::PnoScanCancellationBreakdownByResultsAndSourceMetricDimensionHasScanResults::True as u32
106            } else {
107                metrics::PnoScanCancellationBreakdownByResultsAndSourceMetricDimensionHasScanResults::False as u32
108            };
109
110            let cancellation_source = match reason {
111                PnoScanDisabledReason::ApiRequest => metrics::PnoScanCancellationBreakdownByResultsAndSourceMetricDimensionCancellationSource::ApiRequest as u32,
112                PnoScanDisabledReason::Internal => metrics::PnoScanCancellationBreakdownByResultsAndSourceMetricDimensionCancellationSource::Internal as u32,
113                PnoScanDisabledReason::Firmware => metrics::PnoScanCancellationBreakdownByResultsAndSourceMetricDimensionCancellationSource::Firmware as u32,
114            };
115
116            metric_events.push(MetricEvent {
117                metric_id: metrics::PNO_SCAN_CANCELLATION_BREAKDOWN_BY_RESULTS_AND_SOURCE_METRIC_ID,
118                event_codes: vec![had_scan_results, cancellation_source],
119                payload: MetricEventPayload::Count(1),
120            });
121
122            log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_pno_scan_disabled");
123        }
124
125        self.has_scan_results = false;
126    }
127
128    pub async fn handle_periodic_telemetry(&mut self) {
129        if let Some(enabled_at) = self.enabled_at {
130            let elapsed = fasync::BootInstant::now() - enabled_at;
131            let hours = elapsed.into_hours();
132            let capped_hours = std::cmp::min(hours, 24);
133
134            let metric_events = vec![MetricEvent {
135                metric_id: metrics::ONGOING_PNO_SCAN_ELAPSED_HOURS_METRIC_ID,
136                event_codes: vec![],
137                payload: MetricEventPayload::IntegerValue(capped_hours),
138            }];
139            log_cobalt_batch!(self.cobalt_proxy, &metric_events, "handle_periodic_telemetry");
140        }
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use crate::testing::setup_test;
148    use fidl_fuchsia_metrics::MetricEventPayload;
149    use futures::task::Poll;
150    use std::pin::pin;
151
152    #[fuchsia::test]
153    fn test_pno_scan_collision() {
154        let mut test_helper = setup_test();
155        let mut logger = PnoScanLogger::new(test_helper.filtered_cobalt_logger());
156
157        {
158            let mut test_fut = pin!(logger.handle_pno_scan_enabled(false));
159            assert_eq!(
160                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
161                Poll::Ready(())
162            );
163        }
164
165        // Call again to trigger collision
166        {
167            let mut test_fut = pin!(logger.handle_pno_scan_enabled(false));
168            assert_eq!(
169                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
170                Poll::Ready(())
171            );
172        }
173
174        let metrics = test_helper.get_logged_metrics(metrics::PNO_SCAN_REQUEST_COLLISION_METRIC_ID);
175        assert_eq!(metrics.len(), 1);
176        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
177    }
178
179    #[fuchsia::test]
180    fn test_pno_scan_enabled_while_connected() {
181        let mut test_helper = setup_test();
182        let mut logger = PnoScanLogger::new(test_helper.filtered_cobalt_logger());
183
184        {
185            let mut test_fut = pin!(logger.handle_pno_scan_enabled(true));
186            assert_eq!(
187                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
188                Poll::Ready(())
189            );
190        }
191
192        let metrics =
193            test_helper.get_logged_metrics(metrics::PNO_SCAN_ENABLED_WHILE_CONNECTED_METRIC_ID);
194        assert_eq!(metrics.len(), 1);
195        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
196    }
197
198    #[fuchsia::test]
199    fn test_pno_scan_results_received_metrics() {
200        let mut test_helper = setup_test();
201        let mut logger = PnoScanLogger::new(test_helper.filtered_cobalt_logger());
202
203        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(10_000_000));
204        {
205            let mut test_fut = pin!(logger.handle_pno_scan_enabled(false));
206            assert_eq!(
207                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
208                Poll::Ready(())
209            );
210        }
211
212        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(25_000_000));
213        {
214            let mut test_fut = pin!(logger.handle_pno_scan_results_received());
215            assert_eq!(
216                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
217                Poll::Ready(())
218            );
219        }
220
221        let metrics =
222            test_helper.get_logged_metrics(metrics::PNO_SCAN_FIRST_RESULTS_ELAPSED_TIME_METRIC_ID);
223        assert_eq!(metrics.len(), 1);
224        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(15)); // 15ms
225    }
226
227    #[fuchsia::test]
228    fn test_pno_scan_disabled_no_results() {
229        let mut test_helper = setup_test();
230        let mut logger = PnoScanLogger::new(test_helper.filtered_cobalt_logger());
231
232        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(10_000_000));
233        {
234            let mut test_fut = pin!(logger.handle_pno_scan_enabled(false));
235            assert_eq!(
236                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
237                Poll::Ready(())
238            );
239        }
240
241        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(30_000_000));
242        {
243            let mut test_fut =
244                pin!(logger.handle_pno_scan_disabled(PnoScanDisabledReason::Internal));
245            assert_eq!(
246                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
247                Poll::Ready(())
248            );
249        }
250
251        let metrics =
252            test_helper.get_logged_metrics(metrics::PNO_SCAN_CANCELLED_ELAPSED_TIME_METRIC_ID);
253        assert_eq!(metrics.len(), 1);
254        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(20)); // 20ms
255        assert_eq!(
256            metrics[0].event_codes,
257            vec![
258                metrics::PnoScanCancelledElapsedTimeMetricDimensionHadAnyScanResults::False as u32
259            ]
260        ); // no results
261
262        let metrics = test_helper.get_logged_metrics(
263            metrics::PNO_SCAN_CANCELLATION_BREAKDOWN_BY_RESULTS_AND_SOURCE_METRIC_ID,
264        );
265        assert_eq!(metrics.len(), 1);
266        assert_eq!(metrics[0].event_codes, vec![
267            metrics::PnoScanCancellationBreakdownByResultsAndSourceMetricDimensionHasScanResults::False as u32,
268            metrics::PnoScanCancellationBreakdownByResultsAndSourceMetricDimensionCancellationSource::Internal as u32
269        ]); // no results, Internal
270    }
271
272    #[fuchsia::test]
273    fn test_pno_scan_disabled_with_results() {
274        let mut test_helper = setup_test();
275        let mut logger = PnoScanLogger::new(test_helper.filtered_cobalt_logger());
276
277        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(10_000_000));
278        {
279            let mut test_fut = pin!(logger.handle_pno_scan_enabled(false));
280            assert_eq!(
281                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
282                Poll::Ready(())
283            );
284        }
285
286        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(20_000_000));
287        {
288            let mut test_fut = pin!(logger.handle_pno_scan_results_received());
289            assert_eq!(
290                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
291                Poll::Ready(())
292            );
293        }
294
295        let metrics =
296            test_helper.get_logged_metrics(metrics::PNO_SCAN_FIRST_RESULTS_ELAPSED_TIME_METRIC_ID);
297        assert_eq!(metrics.len(), 1);
298        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(10)); // 10ms
299
300        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(30_000_000));
301        {
302            let mut test_fut =
303                pin!(logger.handle_pno_scan_disabled(PnoScanDisabledReason::ApiRequest));
304            assert_eq!(
305                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
306                Poll::Ready(())
307            );
308        }
309
310        let metrics =
311            test_helper.get_logged_metrics(metrics::PNO_SCAN_CANCELLED_ELAPSED_TIME_METRIC_ID);
312        assert_eq!(metrics.len(), 1);
313        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(20)); // 20ms
314        assert_eq!(
315            metrics[0].event_codes,
316            vec![metrics::PnoScanCancelledElapsedTimeMetricDimensionHadAnyScanResults::True as u32]
317        ); // had results
318
319        let metrics = test_helper.get_logged_metrics(
320            metrics::PNO_SCAN_CANCELLATION_BREAKDOWN_BY_RESULTS_AND_SOURCE_METRIC_ID,
321        );
322        assert_eq!(metrics.len(), 1);
323        assert_eq!(metrics[0].event_codes, vec![
324            metrics::PnoScanCancellationBreakdownByResultsAndSourceMetricDimensionHasScanResults::True as u32,
325            metrics::PnoScanCancellationBreakdownByResultsAndSourceMetricDimensionCancellationSource::ApiRequest as u32
326        ]); // had results, ApiRequest
327    }
328
329    #[fuchsia::test]
330    fn test_pno_scan_periodic_telemetry() {
331        let mut test_helper = setup_test();
332        let mut logger = PnoScanLogger::new(test_helper.filtered_cobalt_logger());
333
334        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(10_000_000));
335        {
336            let mut test_fut = pin!(logger.handle_pno_scan_enabled(false));
337            assert_eq!(
338                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
339                Poll::Ready(())
340            );
341        }
342
343        // Advance time by 5 hours
344        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(
345            10_000_000 + 5 * 3600 * 1_000_000_000,
346        ));
347        {
348            let mut test_fut = pin!(logger.handle_periodic_telemetry());
349            assert_eq!(
350                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
351                Poll::Ready(())
352            );
353        }
354
355        let metrics =
356            test_helper.get_logged_metrics(metrics::ONGOING_PNO_SCAN_ELAPSED_HOURS_METRIC_ID);
357        assert_eq!(metrics.len(), 1);
358        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(5));
359
360        // Advance time by another 20 hours (total 25)
361        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(
362            10_000_000 + 25 * 3600 * 1_000_000_000,
363        ));
364        {
365            let mut test_fut = pin!(logger.handle_periodic_telemetry());
366            assert_eq!(
367                test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
368                Poll::Ready(())
369            );
370        }
371
372        let metrics =
373            test_helper.get_logged_metrics(metrics::ONGOING_PNO_SCAN_ELAPSED_HOURS_METRIC_ID);
374        assert_eq!(metrics.len(), 2);
375        assert_eq!(metrics[1].payload, MetricEventPayload::IntegerValue(24)); // Capped at 24
376    }
377}