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