wlan_telemetry/processors/
scan.rs1use 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 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 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 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)); 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}