wlan_telemetry/processors/
scan.rs1use crate::util::cobalt_logger::log_cobalt_batch;
6use fidl_fuchsia_metrics::{MetricEvent, MetricEventPayload};
7use std::ops::BitOr;
8use windowed_stats::experimental::inspect::{InspectSender, InspectedTimeMatrix};
9use windowed_stats::experimental::series::interpolation::ConstantSample;
10use windowed_stats::experimental::series::metadata::BitSetMap;
11use windowed_stats::experimental::series::statistic::Union;
12use windowed_stats::experimental::series::{SamplingProfile, TimeMatrix};
13use {
14 fidl_fuchsia_power_battery as fidl_battery, fuchsia_async as fasync,
15 wlan_legacy_metrics_registry as metrics,
16};
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: fidl_fuchsia_metrics::MetricEventLoggerProxy,
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: fidl_fuchsia_metrics::MetricEventLoggerProxy,
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 =
210 ScanLogger::new(test_helper.cobalt_proxy.clone(), &test_helper.mock_time_matrix_client);
211
212 run_handle_scan_start(&mut test_helper, &mut scan_logger);
213
214 let metrics = test_helper.get_logged_metrics(metrics::SCAN_OCCURRENCE_METRIC_ID);
215 assert_eq!(metrics.len(), 1);
216 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
217
218 let metrics = test_helper.get_logged_metrics(metrics::SCAN_OCCURRENCE_ON_BATTERY_METRIC_ID);
219 assert!(metrics.is_empty());
220 }
221
222 #[fuchsia::test]
223 fn test_handle_scan_start_on_battery() {
224 let mut test_helper = setup_test();
225 let mut scan_logger =
226 ScanLogger::new(test_helper.cobalt_proxy.clone(), &test_helper.mock_time_matrix_client);
227
228 run_handle_battery_charge_status(
229 &mut test_helper,
230 &mut scan_logger,
231 fidl_battery::ChargeStatus::Discharging,
232 );
233 run_handle_scan_start(&mut test_helper, &mut scan_logger);
234
235 let metrics = test_helper.get_logged_metrics(metrics::SCAN_OCCURRENCE_METRIC_ID);
236 assert_eq!(metrics.len(), 1);
237 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
238
239 let metrics = test_helper.get_logged_metrics(metrics::SCAN_OCCURRENCE_ON_BATTERY_METRIC_ID);
240 assert_eq!(metrics.len(), 1);
241 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
242
243 test_helper.clear_cobalt_events();
246 run_handle_battery_charge_status(
247 &mut test_helper,
248 &mut scan_logger,
249 fidl_battery::ChargeStatus::Charging,
250 );
251 run_handle_scan_start(&mut test_helper, &mut scan_logger);
252
253 let metrics = test_helper.get_logged_metrics(metrics::SCAN_OCCURRENCE_ON_BATTERY_METRIC_ID);
254 assert!(metrics.is_empty());
255 }
256
257 #[fuchsia::test]
258 fn test_handle_scan_result_complete() {
259 let mut test_helper = setup_test();
260 let mut scan_logger =
261 ScanLogger::new(test_helper.cobalt_proxy.clone(), &test_helper.mock_time_matrix_client);
262
263 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(20_000_000));
264 run_handle_scan_start(&mut test_helper, &mut scan_logger);
265
266 test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(100_000_000));
267 let scan_result = ScanResult::Complete { num_results: 10 };
268 run_handle_scan_result(&mut test_helper, &mut scan_logger, scan_result);
269
270 let metrics = test_helper.get_logged_metrics(metrics::SCAN_FULFILLMENT_TIME_METRIC_ID);
271 assert_eq!(metrics.len(), 1);
272 assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(80)); let metrics = test_helper.get_logged_metrics(metrics::EMPTY_SCAN_RESULTS_METRIC_ID);
274 assert!(metrics.is_empty());
275 }
276
277 #[fuchsia::test]
278 fn test_handle_scan_result_empty() {
279 let mut test_helper = setup_test();
280 let mut scan_logger =
281 ScanLogger::new(test_helper.cobalt_proxy.clone(), &test_helper.mock_time_matrix_client);
282
283 run_handle_scan_start(&mut test_helper, &mut scan_logger);
284
285 let scan_result = ScanResult::Complete { num_results: 0 };
286 run_handle_scan_result(&mut test_helper, &mut scan_logger, scan_result);
287
288 let metrics = test_helper.get_logged_metrics(metrics::SCAN_FULFILLMENT_TIME_METRIC_ID);
289 assert_eq!(metrics.len(), 1);
290 let metrics = test_helper.get_logged_metrics(metrics::EMPTY_SCAN_RESULTS_METRIC_ID);
291 assert_eq!(metrics.len(), 1);
292 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
293 }
294
295 #[fuchsia::test]
296 fn test_handle_scan_result_cancelled() {
297 let mut test_helper = setup_test();
298 let mut scan_logger =
299 ScanLogger::new(test_helper.cobalt_proxy.clone(), &test_helper.mock_time_matrix_client);
300
301 run_handle_scan_start(&mut test_helper, &mut scan_logger);
302
303 let scan_result = ScanResult::Cancelled;
304 run_handle_scan_result(&mut test_helper, &mut scan_logger, scan_result);
305
306 let metrics = test_helper.get_logged_metrics(metrics::ABORTED_SCAN_METRIC_ID);
307 assert_eq!(metrics.len(), 1);
308 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
309 }
310
311 #[fuchsia::test]
312 fn test_handle_scan_result_failure() {
313 let mut test_helper = setup_test();
314 let mut scan_logger =
315 ScanLogger::new(test_helper.cobalt_proxy.clone(), &test_helper.mock_time_matrix_client);
316
317 run_handle_scan_start(&mut test_helper, &mut scan_logger);
318
319 let scan_result = ScanResult::Failed;
320 run_handle_scan_result(&mut test_helper, &mut scan_logger, scan_result);
321
322 let metrics = test_helper.get_logged_metrics(metrics::CLIENT_SCAN_FAILURE_METRIC_ID);
323 assert_eq!(metrics.len(), 1);
324 assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
325 }
326
327 #[test_case(
328 ScanResult::Complete { num_results: 10 },
329 metrics::SCAN_FULFILLMENT_TIME_METRIC_ID;
330 "scan complete"
331 )]
332 #[test_case(
333 ScanResult::Failed,
334 metrics::CLIENT_SCAN_FAILURE_METRIC_ID;
335 "scan failed"
336 )]
337 #[test_case(
338 ScanResult::Cancelled,
339 metrics::ABORTED_SCAN_METRIC_ID;
340 "scan cancelled"
341 )]
342 #[fuchsia::test(add_test_attr = false)]
343 fn test_handle_scan_result_no_logging_to_cobalt_if_scan_not_started(
344 scan_result: ScanResult,
345 metric_id: u32,
346 ) {
347 let mut test_helper = setup_test();
348 let mut scan_logger =
349 ScanLogger::new(test_helper.cobalt_proxy.clone(), &test_helper.mock_time_matrix_client);
350
351 run_handle_scan_result(&mut test_helper, &mut scan_logger, scan_result);
352
353 let metrics = test_helper.get_logged_metrics(metric_id);
354 assert!(metrics.is_empty());
355 }
356
357 #[fuchsia::test]
358 fn scan_logger_new_then_inspect_data_tree_contains_time_matrix_metadata() {
359 let mut test_helper = setup_test();
360 let client = TimeMatrixClient::new(test_helper.inspect_node.create_child("wlan_scan"));
361 let _scan_logger = ScanLogger::new(test_helper.cobalt_proxy.clone(), &client);
362
363 let tree = test_helper.get_inspect_data_tree();
364 assert_data_tree!(
365 @executor test_helper.exec,
366 tree,
367 root: contains {
368 test_stats: contains {
369 wlan_scan: contains {
370 scan_events: {
371 "type": "bitset",
372 "data": AnyBytesProperty,
373 metadata: {
374 index: {
375 "0": "start",
376 }
377 }
378 }
379 }
380 }
381 }
382 );
383 }
384
385 #[fuchsia::test]
386 fn log_scan_start_inspect() {
387 let mut test_helper = setup_test();
388 let mut scan_logger =
389 ScanLogger::new(test_helper.cobalt_proxy.clone(), &test_helper.mock_time_matrix_client);
390
391 run_handle_scan_start(&mut test_helper, &mut scan_logger);
392
393 let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
394 assert_eq!(
395 &time_matrix_calls.drain::<ScanEvents>("scan_events")[..],
396 &[TimeMatrixCall::Fold(Timed::now(ScanEvents::START))]
397 );
398 }
399}