wlan_telemetry/processors/
client_iface_counters.rs

1// Copyright 2024 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::log_cobalt_batch;
6use fidl_fuchsia_metrics::{MetricEvent, MetricEventPayload};
7use fidl_fuchsia_wlan_stats as fidl_stats;
8use fuchsia_async::{self as fasync, TimeoutExt};
9use futures::lock::Mutex;
10
11use log::{error, warn};
12use std::collections::HashMap;
13use std::sync::atomic::{AtomicI64, Ordering};
14use std::sync::Arc;
15use windowed_stats::experimental::clock::Timed;
16use windowed_stats::experimental::series::interpolation::{Constant, LastSample};
17use windowed_stats::experimental::series::statistic::{
18    ArithmeticMean, Last, LatchMax, Max, Min, Sum,
19};
20use windowed_stats::experimental::series::{SamplingProfile, TimeMatrix};
21use windowed_stats::experimental::serve::{InspectSender, InspectedTimeMatrix};
22use wlan_legacy_metrics_registry as metrics;
23
24// Include a timeout on stats calls so that if the driver deadlocks, telemtry doesn't get stuck.
25const GET_IFACE_STATS_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(5);
26
27#[derive(Debug)]
28enum IfaceState {
29    NotAvailable,
30    Created { iface_id: u16, telemetry_proxy: Option<fidl_fuchsia_wlan_sme::TelemetryProxy> },
31}
32
33type CountersTimeSeriesMap = HashMap<u16, InspectedTimeMatrix<u64>>;
34type GaugesTimeSeriesMap = HashMap<u16, Vec<InspectedTimeMatrix<i64>>>;
35
36pub struct ClientIfaceCountersLogger<S> {
37    iface_state: Arc<Mutex<IfaceState>>,
38    cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
39    monitor_svc_proxy: fidl_fuchsia_wlan_device_service::DeviceMonitorProxy,
40    time_series_stats: IfaceCountersTimeSeries,
41    driver_counters_time_matrix_client: S,
42    driver_counters_time_series: Arc<Mutex<CountersTimeSeriesMap>>,
43    driver_gauges_time_matrix_client: S,
44    driver_gauges_time_series: Arc<Mutex<GaugesTimeSeriesMap>>,
45    prev_connection_stats: Arc<Mutex<Option<fidl_stats::ConnectionStats>>>,
46    boot_mono_drift: AtomicI64,
47}
48
49impl<S: InspectSender> ClientIfaceCountersLogger<S> {
50    pub fn new(
51        cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
52        monitor_svc_proxy: fidl_fuchsia_wlan_device_service::DeviceMonitorProxy,
53        time_matrix_client: &S,
54        driver_counters_time_matrix_client: S,
55        driver_gauges_time_matrix_client: S,
56    ) -> Self {
57        Self {
58            iface_state: Arc::new(Mutex::new(IfaceState::NotAvailable)),
59            cobalt_proxy,
60            monitor_svc_proxy,
61            time_series_stats: IfaceCountersTimeSeries::new(time_matrix_client),
62            driver_counters_time_matrix_client,
63            driver_counters_time_series: Arc::new(Mutex::new(HashMap::new())),
64            driver_gauges_time_matrix_client,
65            driver_gauges_time_series: Arc::new(Mutex::new(HashMap::new())),
66            prev_connection_stats: Arc::new(Mutex::new(None)),
67            boot_mono_drift: AtomicI64::new(0),
68        }
69    }
70
71    pub async fn handle_iface_created(&self, iface_id: u16) {
72        let (proxy, server) = fidl::endpoints::create_proxy();
73        let telemetry_proxy = match self.monitor_svc_proxy.get_sme_telemetry(iface_id, server).await
74        {
75            Ok(Ok(())) => {
76                let (inspect_counter_configs, inspect_gauge_configs) = match proxy
77                    .query_telemetry_support()
78                    .await
79                {
80                    Ok(Ok(support)) => {
81                        (support.inspect_counter_configs, support.inspect_gauge_configs)
82                    }
83                    Ok(Err(code)) => {
84                        warn!("Failed to query telemetry support with status code {}. No driver-specific stats will be captured", code);
85                        (None, None)
86                    }
87                    Err(e) => {
88                        error!("Failed to query telemetry support with error {}. No driver-specific stats will be captured", e);
89                        (None, None)
90                    }
91                };
92                if let Some(inspect_counter_configs) = &inspect_counter_configs {
93                    let mut driver_counters_time_series =
94                        self.driver_counters_time_series.lock().await;
95                    for inspect_counter_config in inspect_counter_configs {
96                        if let fidl_stats::InspectCounterConfig {
97                            counter_id: Some(counter_id),
98                            counter_name: Some(counter_name),
99                            ..
100                        } = inspect_counter_config
101                        {
102                            let _time_matrix_ref = driver_counters_time_series
103                                .entry(*counter_id)
104                                .or_insert_with(|| {
105                                    self.driver_counters_time_matrix_client.inspect_time_matrix(
106                                        counter_name,
107                                        TimeMatrix::<LatchMax<u64>, LastSample>::new(
108                                            SamplingProfile::balanced(),
109                                            LastSample::or(0),
110                                        ),
111                                    )
112                                });
113                        }
114                    }
115                }
116                if let Some(inspect_gauge_configs) = &inspect_gauge_configs {
117                    let mut driver_gauges_time_series = self.driver_gauges_time_series.lock().await;
118                    for inspect_gauge_config in inspect_gauge_configs {
119                        if let fidl_stats::InspectGaugeConfig {
120                            gauge_id: Some(gauge_id),
121                            gauge_name: Some(gauge_name),
122                            statistics: Some(statistics),
123                            ..
124                        } = inspect_gauge_config
125                        {
126                            for statistic in statistics {
127                                if let Some(time_matrix) = create_time_series_for_gauge(
128                                    &self.driver_gauges_time_matrix_client,
129                                    gauge_name,
130                                    statistic,
131                                ) {
132                                    let time_matrices =
133                                        driver_gauges_time_series.entry(*gauge_id).or_default();
134                                    time_matrices.push(time_matrix);
135                                }
136                            }
137                        }
138                    }
139                }
140                Some(proxy)
141            }
142            Ok(Err(e)) => {
143                error!("Request for SME telemetry for iface {} completed with error {}. No telemetry will be captured.", iface_id, e);
144                None
145            }
146            Err(e) => {
147                error!("Failed to request SME telemetry for iface {} with error {}. No telemetry will be captured.", iface_id, e);
148                None
149            }
150        };
151        *self.iface_state.lock().await = IfaceState::Created { iface_id, telemetry_proxy }
152    }
153
154    pub async fn handle_iface_destroyed(&self, iface_id: u16) {
155        let destroyed = matches!(*self.iface_state.lock().await, IfaceState::Created { iface_id: existing_iface_id, .. } if iface_id == existing_iface_id);
156        if destroyed {
157            *self.iface_state.lock().await = IfaceState::NotAvailable;
158        }
159    }
160
161    pub async fn handle_periodic_telemetry(&self) {
162        let boot_now = fasync::BootInstant::now();
163        let mono_now = fasync::MonotonicInstant::now();
164        let boot_mono_drift = boot_now.into_nanos() - mono_now.into_nanos();
165        let prev_boot_mono_drift = self.boot_mono_drift.swap(boot_mono_drift, Ordering::SeqCst);
166        // If the difference between boot time and monotonic time has increased, it means that
167        // there was a suspension since the last time `handle_periodic_telemetry` was called.
168        let suspended_during_last_period = boot_mono_drift > prev_boot_mono_drift;
169        match &*self.iface_state.lock().await {
170            IfaceState::NotAvailable => (),
171            IfaceState::Created { telemetry_proxy, .. } => {
172                if let Some(telemetry_proxy) = &telemetry_proxy {
173                    match telemetry_proxy
174                        .get_iface_stats()
175                        .on_timeout(GET_IFACE_STATS_TIMEOUT, || {
176                            Ok(Err(zx::Status::TIMED_OUT.into_raw()))
177                        })
178                        .await
179                    {
180                        Ok(Ok(stats)) => {
181                            self.log_iface_stats_inspect(&stats).await;
182                            self.log_iface_stats_cobalt(stats, suspended_during_last_period).await;
183                        }
184                        error => {
185                            warn!("Failed to get interface stats: {:?}", error);
186                        }
187                    }
188                }
189            }
190        }
191    }
192
193    async fn log_iface_stats_inspect(&self, stats: &fidl_stats::IfaceStats) {
194        // Iface-level driver specific counters
195        if let Some(counters) = &stats.driver_specific_counters {
196            let time_series = Arc::clone(&self.driver_counters_time_series);
197            log_driver_specific_counters(&counters[..], time_series).await;
198        }
199        // Iface-level driver specific gauges
200        if let Some(gauges) = &stats.driver_specific_gauges {
201            let time_series = Arc::clone(&self.driver_gauges_time_series);
202            log_driver_specific_gauges(&gauges[..], time_series).await;
203        }
204        log_connection_stats_inspect(
205            stats,
206            &self.time_series_stats,
207            Arc::clone(&self.driver_counters_time_series),
208            Arc::clone(&self.driver_gauges_time_series),
209        )
210        .await;
211    }
212
213    async fn log_iface_stats_cobalt(
214        &self,
215        stats: fidl_stats::IfaceStats,
216        suspended_during_last_period: bool,
217    ) {
218        let mut prev_connection_stats = self.prev_connection_stats.lock().await;
219        // Only log to Cobalt if there was no suspension in-between
220        if !suspended_during_last_period {
221            if let (Some(prev_connection_stats), Some(current_connection_stats)) =
222                (prev_connection_stats.as_ref(), stats.connection_stats.as_ref())
223            {
224                match (prev_connection_stats.connection_id, current_connection_stats.connection_id)
225                {
226                    (Some(prev_id), Some(current_id)) if prev_id == current_id => {
227                        diff_and_log_connection_stats_cobalt(
228                            &self.cobalt_proxy,
229                            prev_connection_stats,
230                            current_connection_stats,
231                        )
232                        .await;
233                    }
234                    _ => (),
235                }
236            }
237        }
238        *prev_connection_stats = stats.connection_stats;
239    }
240}
241
242fn create_time_series_for_gauge<S: InspectSender>(
243    time_matrix_client: &S,
244    gauge_name: &str,
245    statistic: &fidl_stats::GaugeStatistic,
246) -> Option<InspectedTimeMatrix<i64>> {
247    match statistic {
248        fidl_stats::GaugeStatistic::Min => Some(time_matrix_client.inspect_time_matrix(
249            format!("{gauge_name}.min"),
250            TimeMatrix::<Min<i64>, Constant>::new(SamplingProfile::balanced(), Constant::default()),
251        )),
252        fidl_stats::GaugeStatistic::Max => Some(time_matrix_client.inspect_time_matrix(
253            format!("{gauge_name}.max"),
254            TimeMatrix::<Max<i64>, Constant>::new(SamplingProfile::balanced(), Constant::default()),
255        )),
256        fidl_stats::GaugeStatistic::Sum => Some(time_matrix_client.inspect_time_matrix(
257            format!("{gauge_name}.sum"),
258            TimeMatrix::<Sum<i64>, Constant>::new(SamplingProfile::balanced(), Constant::default()),
259        )),
260        fidl_stats::GaugeStatistic::Last => Some(time_matrix_client.inspect_time_matrix(
261            format!("{gauge_name}.last"),
262            TimeMatrix::<Last<i64>, Constant>::new(
263                SamplingProfile::balanced(),
264                Constant::default(),
265            ),
266        )),
267        fidl_stats::GaugeStatistic::Mean => Some(time_matrix_client.inspect_time_matrix(
268            format!("{gauge_name}.mean"),
269            TimeMatrix::<ArithmeticMean<i64>, Constant>::new(
270                SamplingProfile::balanced(),
271                Constant::default(),
272            ),
273        )),
274        _ => None,
275    }
276}
277
278async fn log_connection_stats_inspect(
279    stats: &fidl_stats::IfaceStats,
280    time_series_stats: &IfaceCountersTimeSeries,
281    driver_counters_time_series: Arc<Mutex<CountersTimeSeriesMap>>,
282    driver_gauges_time_series: Arc<Mutex<GaugesTimeSeriesMap>>,
283) {
284    let connection_stats = match &stats.connection_stats {
285        Some(counters) => counters,
286        None => return,
287    };
288
289    // Enforce that `connection_id` field is there for us to log driver counters.
290    match &connection_stats.connection_id {
291        Some(_connection_id) => (),
292        _ => {
293            warn!("connection_id is not present, no connection counters will be logged");
294            return;
295        }
296    }
297
298    if let fidl_stats::ConnectionStats {
299        rx_unicast_total: Some(rx_unicast_total),
300        rx_unicast_drop: Some(rx_unicast_drop),
301        ..
302    } = connection_stats
303    {
304        time_series_stats.log_rx_unicast_total(*rx_unicast_total);
305        time_series_stats.log_rx_unicast_drop(*rx_unicast_drop);
306    }
307
308    if let fidl_stats::ConnectionStats {
309        tx_total: Some(tx_total), tx_drop: Some(tx_drop), ..
310    } = connection_stats
311    {
312        time_series_stats.log_tx_total(*tx_total);
313        time_series_stats.log_tx_drop(*tx_drop);
314    }
315
316    // Connection-level driver-specific counters
317    if let Some(counters) = &connection_stats.driver_specific_counters {
318        log_driver_specific_counters(&counters[..], driver_counters_time_series).await;
319    }
320    // Connection-level driver-specific gauges
321    if let Some(gauges) = &connection_stats.driver_specific_gauges {
322        log_driver_specific_gauges(&gauges[..], driver_gauges_time_series).await;
323    }
324}
325
326async fn log_driver_specific_counters(
327    driver_specific_counters: &[fidl_stats::UnnamedCounter],
328    driver_counters_time_series: Arc<Mutex<CountersTimeSeriesMap>>,
329) {
330    let time_series_map = driver_counters_time_series.lock().await;
331    for counter in driver_specific_counters {
332        if let Some(ts) = time_series_map.get(&counter.id) {
333            ts.fold_or_log_error(Timed::now(counter.count));
334        }
335    }
336}
337
338async fn log_driver_specific_gauges(
339    driver_specific_gauges: &[fidl_stats::UnnamedGauge],
340    driver_gauges_time_series: Arc<Mutex<GaugesTimeSeriesMap>>,
341) {
342    let time_series_map = driver_gauges_time_series.lock().await;
343    for gauge in driver_specific_gauges {
344        if let Some(time_matrices) = time_series_map.get(&gauge.id) {
345            for ts in time_matrices {
346                ts.fold_or_log_error(Timed::now(gauge.value));
347            }
348        }
349    }
350}
351
352async fn diff_and_log_connection_stats_cobalt(
353    cobalt_proxy: &fidl_fuchsia_metrics::MetricEventLoggerProxy,
354    prev: &fidl_stats::ConnectionStats,
355    current: &fidl_stats::ConnectionStats,
356) {
357    diff_and_log_rx_cobalt(cobalt_proxy, prev, current).await;
358    diff_and_log_tx_cobalt(cobalt_proxy, prev, current).await;
359}
360
361async fn diff_and_log_rx_cobalt(
362    cobalt_proxy: &fidl_fuchsia_metrics::MetricEventLoggerProxy,
363    prev: &fidl_stats::ConnectionStats,
364    current: &fidl_stats::ConnectionStats,
365) {
366    let mut metric_events = vec![];
367
368    let (current_rx_unicast_total, prev_rx_unicast_total) =
369        match (current.rx_unicast_total, prev.rx_unicast_total) {
370            (Some(current), Some(prev)) => (current, prev),
371            _ => return,
372        };
373    let (current_rx_unicast_drop, prev_rx_unicast_drop) =
374        match (current.rx_unicast_drop, prev.rx_unicast_drop) {
375            (Some(current), Some(prev)) => (current, prev),
376            _ => return,
377        };
378
379    let rx_total = current_rx_unicast_total - prev_rx_unicast_total;
380    let rx_drop = current_rx_unicast_drop - prev_rx_unicast_drop;
381    let rx_drop_rate = if rx_total > 0 { rx_drop as f64 / rx_total as f64 } else { 0f64 };
382
383    metric_events.push(MetricEvent {
384        metric_id: metrics::BAD_RX_RATE_METRIC_ID,
385        event_codes: vec![],
386        payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(rx_drop_rate)),
387    });
388    metric_events.push(MetricEvent {
389        metric_id: metrics::RX_UNICAST_PACKETS_METRIC_ID,
390        event_codes: vec![],
391        payload: MetricEventPayload::IntegerValue(rx_total as i64),
392    });
393
394    log_cobalt_batch!(cobalt_proxy, &metric_events, "diff_and_log_rx_cobalt",);
395}
396
397async fn diff_and_log_tx_cobalt(
398    cobalt_proxy: &fidl_fuchsia_metrics::MetricEventLoggerProxy,
399    prev: &fidl_stats::ConnectionStats,
400    current: &fidl_stats::ConnectionStats,
401) {
402    let mut metric_events = vec![];
403
404    let (current_tx_total, prev_tx_total) = match (current.tx_total, prev.tx_total) {
405        (Some(current), Some(prev)) => (current, prev),
406        _ => return,
407    };
408    let (current_tx_drop, prev_tx_drop) = match (current.tx_drop, prev.tx_drop) {
409        (Some(current), Some(prev)) => (current, prev),
410        _ => return,
411    };
412
413    let tx_total = current_tx_total - prev_tx_total;
414    let tx_drop = current_tx_drop - prev_tx_drop;
415    let tx_drop_rate = if tx_total > 0 { tx_drop as f64 / tx_total as f64 } else { 0f64 };
416
417    metric_events.push(MetricEvent {
418        metric_id: metrics::BAD_TX_RATE_METRIC_ID,
419        event_codes: vec![],
420        payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(tx_drop_rate)),
421    });
422
423    log_cobalt_batch!(cobalt_proxy, &metric_events, "diff_and_log_tx_cobalt",);
424}
425
426// Convert float to an integer in "ten thousandth" unit
427// Example: 0.02f64 (i.e. 2%) -> 200 per ten thousand
428fn float_to_ten_thousandth(value: f64) -> i64 {
429    (value * 10000f64) as i64
430}
431
432#[derive(Debug, Clone)]
433struct IfaceCountersTimeSeries {
434    rx_unicast_total: InspectedTimeMatrix<u64>,
435    rx_unicast_drop: InspectedTimeMatrix<u64>,
436    tx_total: InspectedTimeMatrix<u64>,
437    tx_drop: InspectedTimeMatrix<u64>,
438}
439
440impl IfaceCountersTimeSeries {
441    pub fn new<S: InspectSender>(client: &S) -> Self {
442        let rx_unicast_total = client.inspect_time_matrix(
443            "rx_unicast_total",
444            TimeMatrix::<LatchMax<u64>, LastSample>::new(
445                SamplingProfile::balanced(),
446                LastSample::or(0),
447            ),
448        );
449        let rx_unicast_drop = client.inspect_time_matrix(
450            "rx_unicast_drop",
451            TimeMatrix::<LatchMax<u64>, LastSample>::new(
452                SamplingProfile::balanced(),
453                LastSample::or(0),
454            ),
455        );
456        let tx_total = client.inspect_time_matrix(
457            "tx_total",
458            TimeMatrix::<LatchMax<u64>, LastSample>::new(
459                SamplingProfile::balanced(),
460                LastSample::or(0),
461            ),
462        );
463        let tx_drop = client.inspect_time_matrix(
464            "tx_drop",
465            TimeMatrix::<LatchMax<u64>, LastSample>::new(
466                SamplingProfile::balanced(),
467                LastSample::or(0),
468            ),
469        );
470        Self { rx_unicast_total, rx_unicast_drop, tx_total, tx_drop }
471    }
472
473    fn log_rx_unicast_total(&self, data: u64) {
474        self.rx_unicast_total.fold_or_log_error(Timed::now(data));
475    }
476
477    fn log_rx_unicast_drop(&self, data: u64) {
478        self.rx_unicast_drop.fold_or_log_error(Timed::now(data));
479    }
480
481    fn log_tx_total(&self, data: u64) {
482        self.tx_total.fold_or_log_error(Timed::now(data));
483    }
484
485    fn log_tx_drop(&self, data: u64) {
486        self.tx_drop.fold_or_log_error(Timed::now(data));
487    }
488}
489
490#[cfg(test)]
491mod tests {
492    use super::*;
493    use crate::testing::*;
494    use futures::TryStreamExt;
495    use std::pin::pin;
496    use std::task::Poll;
497    use test_case::test_case;
498    use windowed_stats::experimental::testing::{MockTimeMatrixClient, TimeMatrixCall};
499    use wlan_common::assert_variant;
500
501    const IFACE_ID: u16 = 66;
502
503    #[fuchsia::test]
504    fn test_handle_iface_created() {
505        let mut test_helper = setup_test();
506        let driver_counters_mock_matrix_client = MockTimeMatrixClient::new();
507        let driver_gauges_mock_matrix_client = MockTimeMatrixClient::new();
508        let logger = ClientIfaceCountersLogger::new(
509            test_helper.cobalt_proxy.clone(),
510            test_helper.monitor_svc_proxy.clone(),
511            &test_helper.mock_time_matrix_client,
512            driver_counters_mock_matrix_client.clone(),
513            driver_gauges_mock_matrix_client.clone(),
514        );
515
516        let mut handle_iface_created_fut = pin!(logger.handle_iface_created(IFACE_ID));
517        assert_eq!(
518            test_helper.run_and_handle_get_sme_telemetry(&mut handle_iface_created_fut),
519            Poll::Pending
520        );
521
522        let mocked_inspect_counter_configs = vec![fidl_stats::InspectCounterConfig {
523            counter_id: Some(1),
524            counter_name: Some("foo_counter".to_string()),
525            ..Default::default()
526        }];
527        let telemetry_support = fidl_stats::TelemetrySupport {
528            inspect_counter_configs: Some(mocked_inspect_counter_configs),
529            ..Default::default()
530        };
531        assert_eq!(
532            test_helper.run_and_respond_query_telemetry_support(
533                &mut handle_iface_created_fut,
534                Ok(&telemetry_support)
535            ),
536            Poll::Ready(())
537        );
538
539        assert_variant!(logger.iface_state.try_lock().as_deref(), Some(IfaceState::Created { .. }));
540        let driver_counters_time_series = logger.driver_counters_time_series.try_lock().unwrap();
541        assert_eq!(driver_counters_time_series.keys().copied().collect::<Vec<u16>>(), vec![1u16],);
542    }
543
544    #[fuchsia::test]
545    fn test_handle_periodic_telemetry_connection_stats() {
546        let mut test_helper = setup_test();
547        let driver_counters_mock_matrix_client = MockTimeMatrixClient::new();
548        let driver_gauges_mock_matrix_client = MockTimeMatrixClient::new();
549        let logger = ClientIfaceCountersLogger::new(
550            test_helper.cobalt_proxy.clone(),
551            test_helper.monitor_svc_proxy.clone(),
552            &test_helper.mock_time_matrix_client,
553            driver_counters_mock_matrix_client.clone(),
554            driver_gauges_mock_matrix_client.clone(),
555        );
556
557        // Transition to IfaceCreated state
558        handle_iface_created(&mut test_helper, &logger);
559
560        let mut test_fut = pin!(logger.handle_periodic_telemetry());
561        let iface_stats = fidl_stats::IfaceStats {
562            connection_stats: Some(fidl_stats::ConnectionStats {
563                connection_id: Some(1),
564                rx_unicast_total: Some(100),
565                rx_unicast_drop: Some(5),
566                rx_multicast: Some(30),
567                tx_total: Some(50),
568                tx_drop: Some(2),
569                ..Default::default()
570            }),
571            ..Default::default()
572        };
573        assert_eq!(
574            test_helper.run_and_respond_iface_stats_req(&mut test_fut, Ok(&iface_stats)),
575            Poll::Ready(())
576        );
577
578        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
579        assert_eq!(
580            &time_matrix_calls.drain::<u64>("rx_unicast_total")[..],
581            &[TimeMatrixCall::Fold(Timed::now(100u64))]
582        );
583        assert_eq!(
584            &time_matrix_calls.drain::<u64>("rx_unicast_drop")[..],
585            &[TimeMatrixCall::Fold(Timed::now(5u64))]
586        );
587        assert_eq!(
588            &time_matrix_calls.drain::<u64>("tx_total")[..],
589            &[TimeMatrixCall::Fold(Timed::now(50u64))]
590        );
591        assert_eq!(
592            &time_matrix_calls.drain::<u64>("tx_drop")[..],
593            &[TimeMatrixCall::Fold(Timed::now(2u64))]
594        );
595    }
596
597    #[fuchsia::test]
598    fn test_handle_periodic_telemetry_driver_specific_counters() {
599        let mut test_helper = setup_test();
600        let driver_counters_mock_matrix_client = MockTimeMatrixClient::new();
601        let driver_gauges_mock_matrix_client = MockTimeMatrixClient::new();
602        let logger = ClientIfaceCountersLogger::new(
603            test_helper.cobalt_proxy.clone(),
604            test_helper.monitor_svc_proxy.clone(),
605            &test_helper.mock_time_matrix_client,
606            driver_counters_mock_matrix_client.clone(),
607            driver_gauges_mock_matrix_client.clone(),
608        );
609
610        let mut handle_iface_created_fut = pin!(logger.handle_iface_created(IFACE_ID));
611        assert_eq!(
612            test_helper.run_and_handle_get_sme_telemetry(&mut handle_iface_created_fut),
613            Poll::Pending
614        );
615
616        let mocked_inspect_configs = vec![
617            fidl_stats::InspectCounterConfig {
618                counter_id: Some(1),
619                counter_name: Some("foo_counter".to_string()),
620                ..Default::default()
621            },
622            fidl_stats::InspectCounterConfig {
623                counter_id: Some(2),
624                counter_name: Some("bar_counter".to_string()),
625                ..Default::default()
626            },
627            fidl_stats::InspectCounterConfig {
628                counter_id: Some(3),
629                counter_name: Some("baz_counter".to_string()),
630                ..Default::default()
631            },
632        ];
633        let telemetry_support = fidl_stats::TelemetrySupport {
634            inspect_counter_configs: Some(mocked_inspect_configs),
635            ..Default::default()
636        };
637        assert_eq!(
638            test_helper.run_and_respond_query_telemetry_support(
639                &mut handle_iface_created_fut,
640                Ok(&telemetry_support)
641            ),
642            Poll::Ready(())
643        );
644
645        let mut test_fut = pin!(logger.handle_periodic_telemetry());
646        let iface_stats = fidl_stats::IfaceStats {
647            driver_specific_counters: Some(vec![fidl_stats::UnnamedCounter { id: 1, count: 50 }]),
648            connection_stats: Some(fidl_stats::ConnectionStats {
649                connection_id: Some(1),
650                driver_specific_counters: Some(vec![
651                    fidl_stats::UnnamedCounter { id: 2, count: 100 },
652                    fidl_stats::UnnamedCounter { id: 3, count: 150 },
653                    // This one is no-op because it's not registered in QueryTelemetrySupport
654                    fidl_stats::UnnamedCounter { id: 4, count: 200 },
655                ]),
656                ..Default::default()
657            }),
658            ..Default::default()
659        };
660        assert_eq!(
661            test_helper.run_and_respond_iface_stats_req(&mut test_fut, Ok(&iface_stats)),
662            Poll::Ready(())
663        );
664
665        let time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
666        assert!(time_matrix_calls.is_empty());
667
668        let mut driver_counters_matrix_calls = driver_counters_mock_matrix_client.drain_calls();
669        assert_eq!(
670            &driver_counters_matrix_calls.drain::<u64>("foo_counter")[..],
671            &[TimeMatrixCall::Fold(Timed::now(50))]
672        );
673        assert_eq!(
674            &driver_counters_matrix_calls.drain::<u64>("bar_counter")[..],
675            &[TimeMatrixCall::Fold(Timed::now(100))]
676        );
677        assert_eq!(
678            &driver_counters_matrix_calls.drain::<u64>("baz_counter")[..],
679            &[TimeMatrixCall::Fold(Timed::now(150))]
680        );
681
682        let driver_gauges_matrix_calls = driver_gauges_mock_matrix_client.drain_calls();
683        assert!(driver_gauges_matrix_calls.is_empty());
684    }
685
686    #[fuchsia::test]
687    fn test_handle_periodic_telemetry_driver_specific_gauges() {
688        let mut test_helper = setup_test();
689        let driver_counters_mock_matrix_client = MockTimeMatrixClient::new();
690        let driver_gauges_mock_matrix_client = MockTimeMatrixClient::new();
691        let logger = ClientIfaceCountersLogger::new(
692            test_helper.cobalt_proxy.clone(),
693            test_helper.monitor_svc_proxy.clone(),
694            &test_helper.mock_time_matrix_client,
695            driver_counters_mock_matrix_client.clone(),
696            driver_gauges_mock_matrix_client.clone(),
697        );
698
699        let mut handle_iface_created_fut = pin!(logger.handle_iface_created(IFACE_ID));
700        assert_eq!(
701            test_helper.run_and_handle_get_sme_telemetry(&mut handle_iface_created_fut),
702            Poll::Pending
703        );
704
705        let mocked_inspect_configs = vec![
706            fidl_stats::InspectGaugeConfig {
707                gauge_id: Some(1),
708                gauge_name: Some("foo_gauge".to_string()),
709                statistics: Some(vec![
710                    fidl_stats::GaugeStatistic::Mean,
711                    fidl_stats::GaugeStatistic::Last,
712                ]),
713                ..Default::default()
714            },
715            fidl_stats::InspectGaugeConfig {
716                gauge_id: Some(2),
717                gauge_name: Some("bar_gauge".to_string()),
718                statistics: Some(vec![
719                    fidl_stats::GaugeStatistic::Min,
720                    fidl_stats::GaugeStatistic::Sum,
721                ]),
722                ..Default::default()
723            },
724            fidl_stats::InspectGaugeConfig {
725                gauge_id: Some(3),
726                gauge_name: Some("baz_gauge".to_string()),
727                statistics: Some(vec![fidl_stats::GaugeStatistic::Max]),
728                ..Default::default()
729            },
730        ];
731        let telemetry_support = fidl_stats::TelemetrySupport {
732            inspect_gauge_configs: Some(mocked_inspect_configs),
733            ..Default::default()
734        };
735        assert_eq!(
736            test_helper.run_and_respond_query_telemetry_support(
737                &mut handle_iface_created_fut,
738                Ok(&telemetry_support)
739            ),
740            Poll::Ready(())
741        );
742
743        let mut test_fut = pin!(logger.handle_periodic_telemetry());
744        let iface_stats = fidl_stats::IfaceStats {
745            driver_specific_gauges: Some(vec![fidl_stats::UnnamedGauge { id: 1, value: 50 }]),
746            connection_stats: Some(fidl_stats::ConnectionStats {
747                connection_id: Some(1),
748                driver_specific_gauges: Some(vec![
749                    fidl_stats::UnnamedGauge { id: 2, value: 100 },
750                    fidl_stats::UnnamedGauge { id: 3, value: 150 },
751                    // This one is no-op because it's not registered in QueryTelemetrySupport
752                    fidl_stats::UnnamedGauge { id: 4, value: 200 },
753                ]),
754                ..Default::default()
755            }),
756            ..Default::default()
757        };
758        assert_eq!(
759            test_helper.run_and_respond_iface_stats_req(&mut test_fut, Ok(&iface_stats)),
760            Poll::Ready(())
761        );
762
763        let time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
764        assert!(time_matrix_calls.is_empty());
765
766        let driver_counters_matrix_calls = driver_counters_mock_matrix_client.drain_calls();
767        assert!(driver_counters_matrix_calls.is_empty());
768
769        let mut driver_gauges_matrix_calls = driver_gauges_mock_matrix_client.drain_calls();
770        assert_eq!(
771            &driver_gauges_matrix_calls.drain::<i64>("foo_gauge.mean")[..],
772            &[TimeMatrixCall::Fold(Timed::now(50))]
773        );
774        assert_eq!(
775            &driver_gauges_matrix_calls.drain::<i64>("foo_gauge.last")[..],
776            &[TimeMatrixCall::Fold(Timed::now(50))]
777        );
778        assert_eq!(
779            &driver_gauges_matrix_calls.drain::<i64>("bar_gauge.min")[..],
780            &[TimeMatrixCall::Fold(Timed::now(100))]
781        );
782        assert_eq!(
783            &driver_gauges_matrix_calls.drain::<i64>("bar_gauge.sum")[..],
784            &[TimeMatrixCall::Fold(Timed::now(100))]
785        );
786        assert_eq!(
787            &driver_gauges_matrix_calls.drain::<i64>("baz_gauge.max")[..],
788            &[TimeMatrixCall::Fold(Timed::now(150))]
789        );
790    }
791
792    #[fuchsia::test]
793    fn test_handle_periodic_telemetry_cobalt() {
794        let mut test_helper = setup_test();
795        let driver_counters_mock_matrix_client = MockTimeMatrixClient::new();
796        let driver_gauges_mock_matrix_client = MockTimeMatrixClient::new();
797        let logger = ClientIfaceCountersLogger::new(
798            test_helper.cobalt_proxy.clone(),
799            test_helper.monitor_svc_proxy.clone(),
800            &test_helper.mock_time_matrix_client,
801            driver_counters_mock_matrix_client.clone(),
802            driver_gauges_mock_matrix_client.clone(),
803        );
804
805        // Transition to IfaceCreated state
806        handle_iface_created(&mut test_helper, &logger);
807
808        let mut test_fut = pin!(logger.handle_periodic_telemetry());
809        let iface_stats = fidl_stats::IfaceStats {
810            connection_stats: Some(fidl_stats::ConnectionStats {
811                connection_id: Some(1),
812                rx_unicast_total: Some(100),
813                rx_unicast_drop: Some(5),
814                rx_multicast: Some(30),
815                tx_total: Some(50),
816                tx_drop: Some(2),
817                ..Default::default()
818            }),
819            ..Default::default()
820        };
821        assert_eq!(
822            test_helper.run_and_respond_iface_stats_req(&mut test_fut, Ok(&iface_stats)),
823            Poll::Ready(())
824        );
825
826        let metrics = test_helper.get_logged_metrics(metrics::BAD_RX_RATE_METRIC_ID);
827        assert!(metrics.is_empty());
828        let metrics = test_helper.get_logged_metrics(metrics::RX_UNICAST_PACKETS_METRIC_ID);
829        assert!(metrics.is_empty());
830        let metrics = test_helper.get_logged_metrics(metrics::BAD_TX_RATE_METRIC_ID);
831        assert!(metrics.is_empty());
832
833        let mut test_fut = pin!(logger.handle_periodic_telemetry());
834        let iface_stats = fidl_stats::IfaceStats {
835            connection_stats: Some(fidl_stats::ConnectionStats {
836                connection_id: Some(1),
837                rx_unicast_total: Some(200),
838                rx_unicast_drop: Some(15),
839                rx_multicast: Some(30),
840                tx_total: Some(150),
841                tx_drop: Some(3),
842                ..Default::default()
843            }),
844            ..Default::default()
845        };
846        assert_eq!(
847            test_helper.run_and_respond_iface_stats_req(&mut test_fut, Ok(&iface_stats)),
848            Poll::Pending
849        );
850        assert_eq!(
851            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
852            Poll::Ready(())
853        );
854
855        let metrics = test_helper.get_logged_metrics(metrics::BAD_RX_RATE_METRIC_ID);
856        assert_eq!(metrics.len(), 1);
857        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(1000)); // 10%
858        let metrics = test_helper.get_logged_metrics(metrics::RX_UNICAST_PACKETS_METRIC_ID);
859        assert_eq!(metrics.len(), 1);
860        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(100));
861        let metrics = test_helper.get_logged_metrics(metrics::BAD_TX_RATE_METRIC_ID);
862        assert_eq!(metrics.len(), 1);
863        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(100)); // 1%
864    }
865
866    #[fuchsia::test]
867    fn test_handle_periodic_telemetry_cobalt_changed_connection_id() {
868        let mut test_helper = setup_test();
869        let driver_counters_mock_matrix_client = MockTimeMatrixClient::new();
870        let driver_gauges_mock_matrix_client = MockTimeMatrixClient::new();
871        let logger = ClientIfaceCountersLogger::new(
872            test_helper.cobalt_proxy.clone(),
873            test_helper.monitor_svc_proxy.clone(),
874            &test_helper.mock_time_matrix_client,
875            driver_counters_mock_matrix_client.clone(),
876            driver_gauges_mock_matrix_client.clone(),
877        );
878
879        // Transition to IfaceCreated state
880        handle_iface_created(&mut test_helper, &logger);
881
882        let mut test_fut = pin!(logger.handle_periodic_telemetry());
883        let iface_stats = fidl_stats::IfaceStats {
884            connection_stats: Some(fidl_stats::ConnectionStats {
885                connection_id: Some(1),
886                rx_unicast_total: Some(100),
887                rx_unicast_drop: Some(5),
888                rx_multicast: Some(30),
889                tx_total: Some(50),
890                tx_drop: Some(2),
891                ..Default::default()
892            }),
893            ..Default::default()
894        };
895        assert_eq!(
896            test_helper.run_and_respond_iface_stats_req(&mut test_fut, Ok(&iface_stats)),
897            Poll::Ready(())
898        );
899
900        let metrics = test_helper.get_logged_metrics(metrics::BAD_RX_RATE_METRIC_ID);
901        assert!(metrics.is_empty());
902        let metrics = test_helper.get_logged_metrics(metrics::RX_UNICAST_PACKETS_METRIC_ID);
903        assert!(metrics.is_empty());
904        let metrics = test_helper.get_logged_metrics(metrics::BAD_TX_RATE_METRIC_ID);
905        assert!(metrics.is_empty());
906
907        let mut test_fut = pin!(logger.handle_periodic_telemetry());
908        let iface_stats = fidl_stats::IfaceStats {
909            connection_stats: Some(fidl_stats::ConnectionStats {
910                connection_id: Some(2),
911                rx_unicast_total: Some(200),
912                rx_unicast_drop: Some(15),
913                rx_multicast: Some(30),
914                tx_total: Some(150),
915                tx_drop: Some(3),
916                ..Default::default()
917            }),
918            ..Default::default()
919        };
920        assert_eq!(
921            test_helper.run_and_respond_iface_stats_req(&mut test_fut, Ok(&iface_stats)),
922            Poll::Ready(())
923        );
924
925        // No metric is logged because the ID indicates it's a different connection, meaning
926        // there is nothing to diff with
927        let metrics = test_helper.get_logged_metrics(metrics::BAD_RX_RATE_METRIC_ID);
928        assert!(metrics.is_empty());
929        let metrics = test_helper.get_logged_metrics(metrics::RX_UNICAST_PACKETS_METRIC_ID);
930        assert!(metrics.is_empty());
931        let metrics = test_helper.get_logged_metrics(metrics::BAD_TX_RATE_METRIC_ID);
932        assert!(metrics.is_empty());
933
934        let mut test_fut = pin!(logger.handle_periodic_telemetry());
935        let iface_stats = fidl_stats::IfaceStats {
936            connection_stats: Some(fidl_stats::ConnectionStats {
937                connection_id: Some(2),
938                rx_unicast_total: Some(300),
939                rx_unicast_drop: Some(18),
940                rx_multicast: Some(30),
941                tx_total: Some(250),
942                tx_drop: Some(5),
943                ..Default::default()
944            }),
945            ..Default::default()
946        };
947        assert_eq!(
948            test_helper.run_and_respond_iface_stats_req(&mut test_fut, Ok(&iface_stats)),
949            Poll::Pending
950        );
951        assert_eq!(
952            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
953            Poll::Ready(())
954        );
955
956        let metrics = test_helper.get_logged_metrics(metrics::BAD_RX_RATE_METRIC_ID);
957        assert_eq!(metrics.len(), 1);
958        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(300)); // 3%
959        let metrics = test_helper.get_logged_metrics(metrics::RX_UNICAST_PACKETS_METRIC_ID);
960        assert_eq!(metrics.len(), 1);
961        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(100));
962        let metrics = test_helper.get_logged_metrics(metrics::BAD_TX_RATE_METRIC_ID);
963        assert_eq!(metrics.len(), 1);
964        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(200)); // 2%
965    }
966
967    #[fuchsia::test]
968    fn test_handle_periodic_telemetry_cobalt_suspension_in_between() {
969        let mut test_helper = setup_test();
970        let driver_counters_mock_matrix_client = MockTimeMatrixClient::new();
971        let driver_gauges_mock_matrix_client = MockTimeMatrixClient::new();
972        let logger = ClientIfaceCountersLogger::new(
973            test_helper.cobalt_proxy.clone(),
974            test_helper.monitor_svc_proxy.clone(),
975            &test_helper.mock_time_matrix_client,
976            driver_counters_mock_matrix_client.clone(),
977            driver_gauges_mock_matrix_client.clone(),
978        );
979
980        // Transition to IfaceCreated state
981        handle_iface_created(&mut test_helper, &logger);
982
983        let mut test_fut = pin!(logger.handle_periodic_telemetry());
984        let iface_stats = fidl_stats::IfaceStats {
985            connection_stats: Some(fidl_stats::ConnectionStats {
986                connection_id: Some(1),
987                rx_unicast_total: Some(100),
988                rx_unicast_drop: Some(5),
989                rx_multicast: Some(30),
990                tx_total: Some(50),
991                tx_drop: Some(2),
992                ..Default::default()
993            }),
994            ..Default::default()
995        };
996        assert_eq!(
997            test_helper.run_and_respond_iface_stats_req(&mut test_fut, Ok(&iface_stats)),
998            Poll::Ready(())
999        );
1000
1001        let metrics = test_helper.get_logged_metrics(metrics::BAD_RX_RATE_METRIC_ID);
1002        assert!(metrics.is_empty());
1003        let metrics = test_helper.get_logged_metrics(metrics::RX_UNICAST_PACKETS_METRIC_ID);
1004        assert!(metrics.is_empty());
1005        let metrics = test_helper.get_logged_metrics(metrics::BAD_TX_RATE_METRIC_ID);
1006        assert!(metrics.is_empty());
1007
1008        test_helper.exec.set_fake_boot_to_mono_offset(zx::BootDuration::from_millis(1));
1009        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1010        let iface_stats = fidl_stats::IfaceStats {
1011            connection_stats: Some(fidl_stats::ConnectionStats {
1012                connection_id: Some(1),
1013                rx_unicast_total: Some(200),
1014                rx_unicast_drop: Some(15),
1015                rx_multicast: Some(30),
1016                tx_total: Some(150),
1017                tx_drop: Some(3),
1018                ..Default::default()
1019            }),
1020            ..Default::default()
1021        };
1022        assert_eq!(
1023            test_helper.run_and_respond_iface_stats_req(&mut test_fut, Ok(&iface_stats)),
1024            Poll::Ready(())
1025        );
1026
1027        // No metric is logged because the increase in boot-to-mono offset indicates that
1028        // a suspension had happened in between
1029        let metrics = test_helper.get_logged_metrics(metrics::BAD_RX_RATE_METRIC_ID);
1030        assert!(metrics.is_empty());
1031        let metrics = test_helper.get_logged_metrics(metrics::RX_UNICAST_PACKETS_METRIC_ID);
1032        assert!(metrics.is_empty());
1033        let metrics = test_helper.get_logged_metrics(metrics::BAD_TX_RATE_METRIC_ID);
1034        assert!(metrics.is_empty());
1035
1036        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1037        let iface_stats = fidl_stats::IfaceStats {
1038            connection_stats: Some(fidl_stats::ConnectionStats {
1039                connection_id: Some(1),
1040                rx_unicast_total: Some(300),
1041                rx_unicast_drop: Some(18),
1042                rx_multicast: Some(30),
1043                tx_total: Some(250),
1044                tx_drop: Some(5),
1045                ..Default::default()
1046            }),
1047            ..Default::default()
1048        };
1049        assert_eq!(
1050            test_helper.run_and_respond_iface_stats_req(&mut test_fut, Ok(&iface_stats)),
1051            Poll::Pending
1052        );
1053        assert_eq!(
1054            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1055            Poll::Ready(())
1056        );
1057
1058        let metrics = test_helper.get_logged_metrics(metrics::BAD_RX_RATE_METRIC_ID);
1059        assert_eq!(metrics.len(), 1);
1060        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(300)); // 3%
1061        let metrics = test_helper.get_logged_metrics(metrics::RX_UNICAST_PACKETS_METRIC_ID);
1062        assert_eq!(metrics.len(), 1);
1063        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(100));
1064        let metrics = test_helper.get_logged_metrics(metrics::BAD_TX_RATE_METRIC_ID);
1065        assert_eq!(metrics.len(), 1);
1066        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(200)); // 2%
1067    }
1068
1069    #[fuchsia::test]
1070    fn test_diff_and_log_rx_cobalt() {
1071        let mut test_helper = setup_test();
1072        let prev_stats = fidl_stats::ConnectionStats {
1073            rx_unicast_total: Some(100),
1074            rx_unicast_drop: Some(5),
1075            ..Default::default()
1076        };
1077        let current_stats = fidl_stats::ConnectionStats {
1078            rx_unicast_total: Some(300),
1079            rx_unicast_drop: Some(7),
1080            ..Default::default()
1081        };
1082        let cobalt_proxy = test_helper.cobalt_proxy.clone();
1083        let mut test_fut = pin!(diff_and_log_rx_cobalt(&cobalt_proxy, &prev_stats, &current_stats));
1084        assert_eq!(
1085            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1086            Poll::Ready(())
1087        );
1088        let metrics = test_helper.get_logged_metrics(metrics::BAD_RX_RATE_METRIC_ID);
1089        assert_eq!(metrics.len(), 1);
1090        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(100)); // 1%
1091        let metrics = test_helper.get_logged_metrics(metrics::RX_UNICAST_PACKETS_METRIC_ID);
1092        assert_eq!(metrics.len(), 1);
1093        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(200));
1094    }
1095
1096    #[test_case(
1097        fidl_stats::ConnectionStats { ..Default::default() },
1098        fidl_stats::ConnectionStats { ..Default::default() };
1099        "both empty"
1100    )]
1101    #[test_case(
1102        fidl_stats::ConnectionStats {
1103            rx_unicast_total: Some(100),
1104            rx_unicast_drop: Some(5),
1105            ..Default::default()
1106        },
1107        fidl_stats::ConnectionStats { ..Default::default() };
1108        "current empty"
1109    )]
1110    #[test_case(
1111        fidl_stats::ConnectionStats { ..Default::default() },
1112        fidl_stats::ConnectionStats {
1113            rx_unicast_total: Some(300),
1114            rx_unicast_drop: Some(7),
1115            ..Default::default()
1116        };
1117        "prev empty"
1118    )]
1119    #[fuchsia::test(add_test_attr = false)]
1120    fn test_diff_and_log_rx_cobalt_empty(
1121        prev_stats: fidl_stats::ConnectionStats,
1122        current_stats: fidl_stats::ConnectionStats,
1123    ) {
1124        let mut test_helper = setup_test();
1125        let cobalt_proxy = test_helper.cobalt_proxy.clone();
1126        let mut test_fut = pin!(diff_and_log_rx_cobalt(&cobalt_proxy, &prev_stats, &current_stats));
1127        assert_eq!(
1128            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1129            Poll::Ready(())
1130        );
1131        let metrics = test_helper.get_logged_metrics(metrics::BAD_RX_RATE_METRIC_ID);
1132        assert!(metrics.is_empty())
1133    }
1134
1135    #[fuchsia::test]
1136    fn test_diff_and_log_tx_cobalt() {
1137        let mut test_helper = setup_test();
1138        let prev_stats = fidl_stats::ConnectionStats {
1139            tx_total: Some(100),
1140            tx_drop: Some(5),
1141            ..Default::default()
1142        };
1143        let current_stats = fidl_stats::ConnectionStats {
1144            tx_total: Some(300),
1145            tx_drop: Some(7),
1146            ..Default::default()
1147        };
1148        let cobalt_proxy = test_helper.cobalt_proxy.clone();
1149        let mut test_fut = pin!(diff_and_log_tx_cobalt(&cobalt_proxy, &prev_stats, &current_stats));
1150        assert_eq!(
1151            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1152            Poll::Ready(())
1153        );
1154        let metrics = test_helper.get_logged_metrics(metrics::BAD_TX_RATE_METRIC_ID);
1155        assert_eq!(metrics.len(), 1);
1156        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(100)); // 1%
1157    }
1158
1159    #[test_case(
1160        fidl_stats::ConnectionStats { ..Default::default() },
1161        fidl_stats::ConnectionStats { ..Default::default() };
1162        "both empty"
1163    )]
1164    #[test_case(
1165        fidl_stats::ConnectionStats {
1166            tx_total: Some(100),
1167            tx_drop: Some(5),
1168            ..Default::default()
1169        },
1170        fidl_stats::ConnectionStats { ..Default::default() };
1171        "current empty"
1172    )]
1173    #[test_case(
1174        fidl_stats::ConnectionStats { ..Default::default() },
1175        fidl_stats::ConnectionStats {
1176            tx_total: Some(300),
1177            tx_drop: Some(7),
1178            ..Default::default()
1179        };
1180        "prev empty"
1181    )]
1182    #[fuchsia::test(add_test_attr = false)]
1183    fn test_diff_and_log_tx_cobalt_empty(
1184        prev_stats: fidl_stats::ConnectionStats,
1185        current_stats: fidl_stats::ConnectionStats,
1186    ) {
1187        let mut test_helper = setup_test();
1188        let cobalt_proxy = test_helper.cobalt_proxy.clone();
1189        let mut test_fut = pin!(diff_and_log_tx_cobalt(&cobalt_proxy, &prev_stats, &current_stats));
1190        assert_eq!(
1191            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1192            Poll::Ready(())
1193        );
1194        let metrics = test_helper.get_logged_metrics(metrics::BAD_TX_RATE_METRIC_ID);
1195        assert!(metrics.is_empty())
1196    }
1197
1198    #[fuchsia::test]
1199    fn test_handle_iface_destroyed() {
1200        let mut test_helper = setup_test();
1201        let driver_counters_mock_matrix_client = MockTimeMatrixClient::new();
1202        let driver_gauges_mock_matrix_client = MockTimeMatrixClient::new();
1203        let logger = ClientIfaceCountersLogger::new(
1204            test_helper.cobalt_proxy.clone(),
1205            test_helper.monitor_svc_proxy.clone(),
1206            &test_helper.mock_time_matrix_client,
1207            driver_counters_mock_matrix_client.clone(),
1208            driver_gauges_mock_matrix_client.clone(),
1209        );
1210
1211        // Transition to IfaceCreated state
1212        handle_iface_created(&mut test_helper, &logger);
1213
1214        let mut handle_iface_destroyed_fut = pin!(logger.handle_iface_destroyed(IFACE_ID));
1215        assert_eq!(
1216            test_helper.exec.run_until_stalled(&mut handle_iface_destroyed_fut),
1217            Poll::Ready(())
1218        );
1219
1220        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1221        assert_eq!(test_helper.exec.run_until_stalled(&mut test_fut), Poll::Ready(()));
1222        let telemetry_svc_stream = test_helper.telemetry_svc_stream.as_mut().unwrap();
1223        let mut telemetry_svc_req_fut = pin!(telemetry_svc_stream.try_next());
1224        // Verify that no telemetry request is made now that the iface is destroyed
1225        match test_helper.exec.run_until_stalled(&mut telemetry_svc_req_fut) {
1226            Poll::Ready(Ok(None)) => (),
1227            other => panic!("unexpected variant: {other:?}"),
1228        }
1229    }
1230
1231    fn handle_iface_created<S: InspectSender>(
1232        test_helper: &mut TestHelper,
1233        logger: &ClientIfaceCountersLogger<S>,
1234    ) {
1235        let mut handle_iface_created_fut = pin!(logger.handle_iface_created(IFACE_ID));
1236        assert_eq!(
1237            test_helper.run_and_handle_get_sme_telemetry(&mut handle_iface_created_fut),
1238            Poll::Pending
1239        );
1240        let telemetry_support = fidl_stats::TelemetrySupport::default();
1241        assert_eq!(
1242            test_helper.run_and_respond_query_telemetry_support(
1243                &mut handle_iface_created_fut,
1244                Ok(&telemetry_support)
1245            ),
1246            Poll::Ready(())
1247        );
1248    }
1249}