Skip to main content

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_ieee80211 as fidl_ieee80211;
8use fidl_fuchsia_wlan_stats as fidl_stats;
9use fuchsia_async::{self as fasync, TimeoutExt};
10use fuchsia_inspect_contrib::nodes::LruCacheNode;
11use fuchsia_inspect_derive::{InspectNode, Unit};
12use futures::TryFutureExt;
13use futures::lock::Mutex;
14
15use log::{error, warn};
16use std::collections::HashMap;
17use std::sync::Arc;
18use std::sync::atomic::{AtomicI64, Ordering};
19use windowed_stats::experimental::inspect::{InspectSender, InspectedTimeMatrix};
20use windowed_stats::experimental::series::interpolation::{ConstantSample, LastSample};
21use windowed_stats::experimental::series::metadata::BitsetNode;
22use windowed_stats::experimental::series::statistic::{
23    ArithmeticMean, Last, LatchMax, Max, Min, PostAggregation, Sum, Union,
24};
25use windowed_stats::experimental::series::{SamplingProfile, TimeMatrix};
26use wlan_legacy_metrics_registry as metrics;
27
28// Include a timeout on stats calls so that if the driver deadlocks, telemetry doesn't get stuck.
29const GET_IFACE_STATS_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(5);
30const GET_SIGNAL_REPORT_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(5);
31
32#[derive(Debug)]
33enum IfaceState {
34    NotAvailable,
35    Created { iface_id: u16, telemetry_proxy: Option<fidl_fuchsia_wlan_sme::TelemetryProxy> },
36}
37
38#[derive(Debug)]
39enum GetIfaceStatsError {
40    FutureTimeout,
41    FidlError,
42    ResponseError,
43}
44
45type CountersTimeSeriesMap = HashMap<u16, InspectedTimeMatrix<u64>>;
46type GaugesTimeSeriesMap = HashMap<u16, Vec<InspectedTimeMatrix<i64>>>;
47
48pub struct ClientIfaceCountersLogger<S> {
49    iface_state: Arc<Mutex<IfaceState>>,
50    cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
51    monitor_svc_proxy: fidl_fuchsia_wlan_device_service::DeviceMonitorProxy,
52    inspect_metadata_node: Mutex<InspectMetadataNode>,
53    time_series_stats: IfaceCountersTimeSeries,
54    signal_time_series: SignalTimeSeries,
55    driver_counters_time_matrix_client: S,
56    driver_counters_time_series: Arc<Mutex<CountersTimeSeriesMap>>,
57    driver_gauges_time_matrix_client: S,
58    driver_gauges_time_series: Arc<Mutex<GaugesTimeSeriesMap>>,
59    prev_connection_stats: Arc<Mutex<Option<fidl_stats::ConnectionStats>>>,
60    boot_mono_drift: AtomicI64,
61}
62
63impl<S: InspectSender> ClientIfaceCountersLogger<S> {
64    pub fn new(
65        cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
66        monitor_svc_proxy: fidl_fuchsia_wlan_device_service::DeviceMonitorProxy,
67        inspect_metadata_node: &InspectNode,
68        inspect_metadata_path: &str,
69        time_matrix_client: &S,
70        driver_counters_time_matrix_client: S,
71        driver_gauges_time_matrix_client: S,
72    ) -> Self {
73        Self {
74            iface_state: Arc::new(Mutex::new(IfaceState::NotAvailable)),
75            cobalt_proxy,
76            monitor_svc_proxy,
77            inspect_metadata_node: Mutex::new(InspectMetadataNode::new(inspect_metadata_node)),
78            time_series_stats: IfaceCountersTimeSeries::new(time_matrix_client),
79            signal_time_series: SignalTimeSeries::new(time_matrix_client, inspect_metadata_path),
80            driver_counters_time_matrix_client,
81            driver_counters_time_series: Arc::new(Mutex::new(HashMap::new())),
82            driver_gauges_time_matrix_client,
83            driver_gauges_time_series: Arc::new(Mutex::new(HashMap::new())),
84            prev_connection_stats: Arc::new(Mutex::new(None)),
85            boot_mono_drift: AtomicI64::new(0),
86        }
87    }
88
89    pub async fn handle_iface_created(&self, iface_id: u16) {
90        let (proxy, server) = fidl::endpoints::create_proxy();
91        let telemetry_proxy = match self.monitor_svc_proxy.get_sme_telemetry(iface_id, server).await
92        {
93            Ok(Ok(())) => {
94                let (inspect_counter_configs, inspect_gauge_configs) = match proxy
95                    .query_telemetry_support()
96                    .await
97                {
98                    Ok(Ok(support)) => {
99                        (support.inspect_counter_configs, support.inspect_gauge_configs)
100                    }
101                    Ok(Err(code)) => {
102                        warn!(
103                            "Failed to query telemetry support with status code {}. No driver-specific stats will be captured",
104                            code
105                        );
106                        (None, None)
107                    }
108                    Err(e) => {
109                        error!(
110                            "Failed to query telemetry support with error {}. No driver-specific stats will be captured",
111                            e
112                        );
113                        (None, None)
114                    }
115                };
116                if let Some(inspect_counter_configs) = &inspect_counter_configs {
117                    let mut driver_counters_time_series =
118                        self.driver_counters_time_series.lock().await;
119                    for inspect_counter_config in inspect_counter_configs {
120                        if let fidl_stats::InspectCounterConfig {
121                            counter_id: Some(counter_id),
122                            counter_name: Some(counter_name),
123                            ..
124                        } = inspect_counter_config
125                        {
126                            let _time_matrix_ref = driver_counters_time_series
127                                .entry(*counter_id)
128                                .or_insert_with(|| {
129                                    self.driver_counters_time_matrix_client.inspect_time_matrix(
130                                        counter_name,
131                                        TimeMatrix::<LatchMax<u64>, LastSample>::new(
132                                            SamplingProfile::balanced(),
133                                            LastSample::or(0),
134                                        ),
135                                    )
136                                });
137                        }
138                    }
139                }
140                if let Some(inspect_gauge_configs) = &inspect_gauge_configs {
141                    let mut driver_gauges_time_series = self.driver_gauges_time_series.lock().await;
142                    for inspect_gauge_config in inspect_gauge_configs {
143                        if let fidl_stats::InspectGaugeConfig {
144                            gauge_id: Some(gauge_id),
145                            gauge_name: Some(gauge_name),
146                            statistics: Some(statistics),
147                            ..
148                        } = inspect_gauge_config
149                        {
150                            for statistic in statistics {
151                                if let Some(time_matrix) = create_time_series_for_gauge(
152                                    &self.driver_gauges_time_matrix_client,
153                                    gauge_name,
154                                    statistic,
155                                ) {
156                                    let time_matrices =
157                                        driver_gauges_time_series.entry(*gauge_id).or_default();
158                                    time_matrices.push(time_matrix);
159                                }
160                            }
161                        }
162                    }
163                }
164                Some(proxy)
165            }
166            Ok(Err(e)) => {
167                error!(
168                    "Request for SME telemetry for iface {} completed with error {}. No telemetry will be captured.",
169                    iface_id, e
170                );
171                None
172            }
173            Err(e) => {
174                error!(
175                    "Failed to request SME telemetry for iface {} with error {}. No telemetry will be captured.",
176                    iface_id, e
177                );
178                None
179            }
180        };
181        *self.iface_state.lock().await = IfaceState::Created { iface_id, telemetry_proxy }
182    }
183
184    pub async fn handle_iface_destroyed(&self, iface_id: u16) {
185        let destroyed = matches!(*self.iface_state.lock().await, IfaceState::Created { iface_id: existing_iface_id, .. } if iface_id == existing_iface_id);
186        if destroyed {
187            *self.iface_state.lock().await = IfaceState::NotAvailable;
188        }
189    }
190
191    pub async fn handle_periodic_telemetry(&self) {
192        let boot_now = fasync::BootInstant::now();
193        let mono_now = fasync::MonotonicInstant::now();
194        let boot_mono_drift = boot_now.into_nanos() - mono_now.into_nanos();
195        let prev_boot_mono_drift = self.boot_mono_drift.swap(boot_mono_drift, Ordering::SeqCst);
196        // If the difference between boot time and monotonic time has increased, it means that
197        // there was a suspension since the last time `handle_periodic_telemetry` was called.
198        let suspended_during_last_period = boot_mono_drift > prev_boot_mono_drift;
199        match &*self.iface_state.lock().await {
200            IfaceState::NotAvailable => (),
201            IfaceState::Created { telemetry_proxy, .. } => {
202                if let Some(telemetry_proxy) = &telemetry_proxy {
203                    let result = telemetry_proxy
204                        .get_iface_stats()
205                        .map_err(|_e| GetIfaceStatsError::FidlError)
206                        .map_ok(|response| response.map_err(|_e| GetIfaceStatsError::ResponseError))
207                        .on_timeout(GET_IFACE_STATS_TIMEOUT, || {
208                            Err(GetIfaceStatsError::FutureTimeout)
209                        })
210                        .await;
211
212                    match result {
213                        Ok(Ok(stats)) => {
214                            self.log_iface_stats_inspect(&stats).await;
215                            self.log_iface_stats_cobalt(stats, suspended_during_last_period).await;
216                        }
217                        Ok(Err(e)) | Err(e) => {
218                            warn!("Failed to get interface stats: {:?}", e);
219                            self.log_get_iface_stats_failure_cobalt(&e).await;
220                        }
221                    }
222
223                    match telemetry_proxy
224                        .get_signal_report()
225                        .on_timeout(GET_SIGNAL_REPORT_TIMEOUT, || {
226                            Ok(Err(zx::Status::TIMED_OUT.into_raw()))
227                        })
228                        .await
229                    {
230                        Ok(Ok(report)) => {
231                            self.log_signal_report_inspect(&report).await;
232                        }
233                        error => {
234                            warn!("Failed to get signal report: {:?}", error);
235                        }
236                    }
237                }
238            }
239        }
240    }
241
242    async fn log_iface_stats_inspect(&self, stats: &fidl_stats::IfaceStats) {
243        // Iface-level driver specific counters
244        if let Some(counters) = &stats.driver_specific_counters {
245            let time_series = Arc::clone(&self.driver_counters_time_series);
246            log_driver_specific_counters(&counters[..], time_series).await;
247        }
248        // Iface-level driver specific gauges
249        if let Some(gauges) = &stats.driver_specific_gauges {
250            let time_series = Arc::clone(&self.driver_gauges_time_series);
251            log_driver_specific_gauges(&gauges[..], time_series).await;
252        }
253        log_connection_stats_inspect(
254            stats,
255            &self.time_series_stats,
256            Arc::clone(&self.driver_counters_time_series),
257            Arc::clone(&self.driver_gauges_time_series),
258        )
259        .await;
260    }
261
262    async fn log_iface_stats_cobalt(
263        &self,
264        stats: fidl_stats::IfaceStats,
265        suspended_during_last_period: bool,
266    ) {
267        let mut prev_connection_stats = self.prev_connection_stats.lock().await;
268        // Only log to Cobalt if there was no suspension in-between
269        if !suspended_during_last_period
270            && let (Some(prev_connection_stats), Some(current_connection_stats)) =
271                (prev_connection_stats.as_ref(), stats.connection_stats.as_ref())
272        {
273            match (prev_connection_stats.connection_id, current_connection_stats.connection_id) {
274                (Some(prev_id), Some(current_id)) if prev_id == current_id => {
275                    diff_and_log_connection_stats_cobalt(
276                        &self.cobalt_proxy,
277                        prev_connection_stats,
278                        current_connection_stats,
279                    )
280                    .await;
281                }
282                _ => (),
283            }
284        }
285        *prev_connection_stats = stats.connection_stats;
286    }
287
288    async fn log_get_iface_stats_failure_cobalt(&self, error: &GetIfaceStatsError) {
289        let mut metric_events = vec![MetricEvent {
290            metric_id: metrics::GET_IFACE_STATS_FAILURE_METRIC_ID,
291            event_codes: vec![],
292            payload: MetricEventPayload::Count(1),
293        }];
294        match error {
295            GetIfaceStatsError::FutureTimeout => {
296                metric_events.push(MetricEvent {
297                    metric_id: metrics::GET_IFACE_STATS_TIMEOUT_METRIC_ID,
298                    event_codes: vec![],
299                    payload: MetricEventPayload::Count(1),
300                });
301            }
302            GetIfaceStatsError::FidlError | GetIfaceStatsError::ResponseError => {
303                metric_events.push(MetricEvent {
304                    metric_id: metrics::GET_IFACE_STATS_ERROR_IN_RESPONSE_METRIC_ID,
305                    event_codes: vec![],
306                    payload: MetricEventPayload::Count(1),
307                });
308            }
309        }
310        log_cobalt_batch!(self.cobalt_proxy, &metric_events, "log_get_iface_stats_failure_cobalt");
311    }
312
313    async fn log_signal_report_inspect(&self, report: &fidl_stats::SignalReport) {
314        if let Some(report) = &report.connection_signal_report {
315            if let Some(channel) = report.channel {
316                let channel = InspectWlanChannel::from(channel);
317                let channel_id =
318                    self.inspect_metadata_node.lock().await.wlan_channels.insert(channel) as u64;
319                self.signal_time_series.wlan_channels.fold_or_log_error(1 << channel_id);
320            }
321            if let Some(tx_rate_500kbps) = report.tx_rate_500kbps {
322                self.signal_time_series.tx_rate_500kbps.fold_or_log_error(tx_rate_500kbps as u64);
323            }
324            if let Some(rssi) = report.rssi_dbm {
325                self.signal_time_series.rssi.fold_or_log_error(rssi as i64);
326            }
327            if let Some(snr) = report.snr_db {
328                self.signal_time_series.snr.fold_or_log_error(snr as i64);
329            }
330        }
331    }
332}
333
334fn create_time_series_for_gauge<S: InspectSender>(
335    time_matrix_client: &S,
336    gauge_name: &str,
337    statistic: &fidl_stats::GaugeStatistic,
338) -> Option<InspectedTimeMatrix<i64>> {
339    match statistic {
340        fidl_stats::GaugeStatistic::Min => Some(time_matrix_client.inspect_time_matrix(
341            format!("{gauge_name}.min"),
342            TimeMatrix::<Min<i64>, ConstantSample>::new(
343                SamplingProfile::balanced(),
344                ConstantSample::default(),
345            ),
346        )),
347        fidl_stats::GaugeStatistic::Max => Some(time_matrix_client.inspect_time_matrix(
348            format!("{gauge_name}.max"),
349            TimeMatrix::<Max<i64>, ConstantSample>::new(
350                SamplingProfile::balanced(),
351                ConstantSample::default(),
352            ),
353        )),
354        fidl_stats::GaugeStatistic::Sum => Some(time_matrix_client.inspect_time_matrix(
355            format!("{gauge_name}.sum"),
356            TimeMatrix::<Sum<i64>, ConstantSample>::new(
357                SamplingProfile::balanced(),
358                ConstantSample::default(),
359            ),
360        )),
361        fidl_stats::GaugeStatistic::Last => Some(time_matrix_client.inspect_time_matrix(
362            format!("{gauge_name}.last"),
363            TimeMatrix::<Last<i64>, ConstantSample>::new(
364                SamplingProfile::balanced(),
365                ConstantSample::default(),
366            ),
367        )),
368        fidl_stats::GaugeStatistic::Mean => Some(time_matrix_client.inspect_time_matrix(
369            format!("{gauge_name}.mean"),
370            TimeMatrix::<ArithmeticMean<i64>, ConstantSample>::new(
371                SamplingProfile::balanced(),
372                ConstantSample::default(),
373            ),
374        )),
375        _ => None,
376    }
377}
378
379async fn log_connection_stats_inspect(
380    stats: &fidl_stats::IfaceStats,
381    time_series_stats: &IfaceCountersTimeSeries,
382    driver_counters_time_series: Arc<Mutex<CountersTimeSeriesMap>>,
383    driver_gauges_time_series: Arc<Mutex<GaugesTimeSeriesMap>>,
384) {
385    let connection_stats = match &stats.connection_stats {
386        Some(counters) => counters,
387        None => return,
388    };
389
390    // Enforce that `connection_id` field is there for us to log driver counters.
391    match &connection_stats.connection_id {
392        Some(_connection_id) => (),
393        _ => {
394            warn!("connection_id is not present, no connection counters will be logged");
395            return;
396        }
397    }
398
399    if let fidl_stats::ConnectionStats {
400        rx_unicast_total: Some(rx_unicast_total),
401        rx_unicast_drop: Some(rx_unicast_drop),
402        ..
403    } = connection_stats
404    {
405        time_series_stats.log_rx_unicast_total(*rx_unicast_total);
406        time_series_stats.log_rx_unicast_drop(*rx_unicast_drop);
407    }
408
409    if let fidl_stats::ConnectionStats {
410        tx_total: Some(tx_total), tx_drop: Some(tx_drop), ..
411    } = connection_stats
412    {
413        time_series_stats.log_tx_total(*tx_total);
414        time_series_stats.log_tx_drop(*tx_drop);
415    }
416
417    // Connection-level driver-specific counters
418    if let Some(counters) = &connection_stats.driver_specific_counters {
419        log_driver_specific_counters(&counters[..], driver_counters_time_series).await;
420    }
421    // Connection-level driver-specific gauges
422    if let Some(gauges) = &connection_stats.driver_specific_gauges {
423        log_driver_specific_gauges(&gauges[..], driver_gauges_time_series).await;
424    }
425}
426
427async fn log_driver_specific_counters(
428    driver_specific_counters: &[fidl_stats::UnnamedCounter],
429    driver_counters_time_series: Arc<Mutex<CountersTimeSeriesMap>>,
430) {
431    let time_series_map = driver_counters_time_series.lock().await;
432    for counter in driver_specific_counters {
433        if let Some(ts) = time_series_map.get(&counter.id) {
434            ts.fold_or_log_error(counter.count);
435        }
436    }
437}
438
439async fn log_driver_specific_gauges(
440    driver_specific_gauges: &[fidl_stats::UnnamedGauge],
441    driver_gauges_time_series: Arc<Mutex<GaugesTimeSeriesMap>>,
442) {
443    let time_series_map = driver_gauges_time_series.lock().await;
444    for gauge in driver_specific_gauges {
445        if let Some(time_matrices) = time_series_map.get(&gauge.id) {
446            for ts in time_matrices {
447                ts.fold_or_log_error(gauge.value);
448            }
449        }
450    }
451}
452
453async fn diff_and_log_connection_stats_cobalt(
454    cobalt_proxy: &fidl_fuchsia_metrics::MetricEventLoggerProxy,
455    prev: &fidl_stats::ConnectionStats,
456    current: &fidl_stats::ConnectionStats,
457) {
458    // Early return if the counters have dropped. This indicates that the counters have reset
459    // due to reasons like PHY reset. Counters being reset due to re-connection is already
460    // handled outside this function.
461    match (current.rx_unicast_total, prev.rx_unicast_total) {
462        (Some(current), Some(prev)) if current < prev => return,
463        _ => (),
464    }
465    match (current.rx_unicast_drop, prev.rx_unicast_drop) {
466        (Some(current), Some(prev)) if current < prev => return,
467        _ => (),
468    }
469    match (current.tx_total, prev.tx_total) {
470        (Some(current), Some(prev)) if current < prev => return,
471        _ => (),
472    }
473    match (current.tx_drop, prev.tx_drop) {
474        (Some(current), Some(prev)) if current < prev => return,
475        _ => (),
476    }
477
478    diff_and_log_rx_cobalt(cobalt_proxy, prev, current).await;
479    diff_and_log_tx_cobalt(cobalt_proxy, prev, current).await;
480}
481
482async fn diff_and_log_rx_cobalt(
483    cobalt_proxy: &fidl_fuchsia_metrics::MetricEventLoggerProxy,
484    prev: &fidl_stats::ConnectionStats,
485    current: &fidl_stats::ConnectionStats,
486) {
487    let mut metric_events = vec![];
488
489    let (current_rx_unicast_total, prev_rx_unicast_total) =
490        match (current.rx_unicast_total, prev.rx_unicast_total) {
491            (Some(current), Some(prev)) => (current, prev),
492            _ => return,
493        };
494    let (current_rx_unicast_drop, prev_rx_unicast_drop) =
495        match (current.rx_unicast_drop, prev.rx_unicast_drop) {
496            (Some(current), Some(prev)) => (current, prev),
497            _ => return,
498        };
499
500    let rx_total = match current_rx_unicast_total.checked_sub(prev_rx_unicast_total) {
501        Some(diff) => diff,
502        _ => return,
503    };
504    let rx_drop = match current_rx_unicast_drop.checked_sub(prev_rx_unicast_drop) {
505        Some(diff) => diff,
506        _ => return,
507    };
508    let rx_drop_rate = if rx_total > 0 { rx_drop as f64 / rx_total as f64 } else { 0f64 };
509
510    metric_events.push(MetricEvent {
511        metric_id: metrics::BAD_RX_RATE_METRIC_ID,
512        event_codes: vec![],
513        payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(rx_drop_rate)),
514    });
515    metric_events.push(MetricEvent {
516        metric_id: metrics::RX_UNICAST_PACKETS_METRIC_ID,
517        event_codes: vec![],
518        payload: MetricEventPayload::IntegerValue(rx_total as i64),
519    });
520
521    log_cobalt_batch!(cobalt_proxy, &metric_events, "diff_and_log_rx_cobalt",);
522}
523
524async fn diff_and_log_tx_cobalt(
525    cobalt_proxy: &fidl_fuchsia_metrics::MetricEventLoggerProxy,
526    prev: &fidl_stats::ConnectionStats,
527    current: &fidl_stats::ConnectionStats,
528) {
529    let mut metric_events = vec![];
530
531    let (current_tx_total, prev_tx_total) = match (current.tx_total, prev.tx_total) {
532        (Some(current), Some(prev)) => (current, prev),
533        _ => return,
534    };
535    let (current_tx_drop, prev_tx_drop) = match (current.tx_drop, prev.tx_drop) {
536        (Some(current), Some(prev)) => (current, prev),
537        _ => return,
538    };
539
540    let tx_total = match current_tx_total.checked_sub(prev_tx_total) {
541        Some(diff) => diff,
542        _ => return,
543    };
544    let tx_drop = match current_tx_drop.checked_sub(prev_tx_drop) {
545        Some(diff) => diff,
546        _ => return,
547    };
548    let tx_drop_rate = if tx_total > 0 { tx_drop as f64 / tx_total as f64 } else { 0f64 };
549
550    metric_events.push(MetricEvent {
551        metric_id: metrics::BAD_TX_RATE_METRIC_ID,
552        event_codes: vec![],
553        payload: MetricEventPayload::IntegerValue(float_to_ten_thousandth(tx_drop_rate)),
554    });
555
556    log_cobalt_batch!(cobalt_proxy, &metric_events, "diff_and_log_tx_cobalt",);
557}
558
559// Convert float to an integer in "ten thousandth" unit
560// Example: 0.02f64 (i.e. 2%) -> 200 per ten thousand
561fn float_to_ten_thousandth(value: f64) -> i64 {
562    (value * 10000f64) as i64
563}
564
565#[derive(Debug, Clone)]
566struct IfaceCountersTimeSeries {
567    rx_unicast_total: InspectedTimeMatrix<u64>,
568    rx_unicast_drop: InspectedTimeMatrix<u64>,
569    tx_total: InspectedTimeMatrix<u64>,
570    tx_drop: InspectedTimeMatrix<u64>,
571}
572
573impl IfaceCountersTimeSeries {
574    pub fn new<S: InspectSender>(client: &S) -> Self {
575        let rx_unicast_total = client.inspect_time_matrix(
576            "rx_unicast_total",
577            TimeMatrix::<LatchMax<u64>, LastSample>::new(
578                SamplingProfile::balanced(),
579                LastSample::or(0),
580            ),
581        );
582        let rx_unicast_drop = client.inspect_time_matrix(
583            "rx_unicast_drop",
584            TimeMatrix::<LatchMax<u64>, LastSample>::new(
585                SamplingProfile::balanced(),
586                LastSample::or(0),
587            ),
588        );
589        let tx_total = client.inspect_time_matrix(
590            "tx_total",
591            TimeMatrix::<LatchMax<u64>, LastSample>::new(
592                SamplingProfile::balanced(),
593                LastSample::or(0),
594            ),
595        );
596        let tx_drop = client.inspect_time_matrix(
597            "tx_drop",
598            TimeMatrix::<LatchMax<u64>, LastSample>::new(
599                SamplingProfile::balanced(),
600                LastSample::or(0),
601            ),
602        );
603        Self { rx_unicast_total, rx_unicast_drop, tx_total, tx_drop }
604    }
605
606    fn log_rx_unicast_total(&self, data: u64) {
607        self.rx_unicast_total.fold_or_log_error(data);
608    }
609
610    fn log_rx_unicast_drop(&self, data: u64) {
611        self.rx_unicast_drop.fold_or_log_error(data);
612    }
613
614    fn log_tx_total(&self, data: u64) {
615        self.tx_total.fold_or_log_error(data);
616    }
617
618    fn log_tx_drop(&self, data: u64) {
619        self.tx_drop.fold_or_log_error(data);
620    }
621}
622
623#[derive(Debug, Clone)]
624struct SignalTimeSeries {
625    wlan_channels: InspectedTimeMatrix<u64>,
626    tx_rate_500kbps: InspectedTimeMatrix<u64>,
627    rssi: InspectedTimeMatrix<i64>,
628    snr: InspectedTimeMatrix<i64>,
629}
630
631impl SignalTimeSeries {
632    pub fn new<S: InspectSender>(client: &S, inspect_metadata_path: &str) -> Self {
633        let wlan_channels = client.inspect_time_matrix_with_metadata(
634            "wlan_channels",
635            TimeMatrix::<Union<u64>, ConstantSample>::new(
636                SamplingProfile::highly_granular(),
637                ConstantSample::default(),
638            ),
639            BitsetNode::from_path(format!(
640                "{}/{}",
641                inspect_metadata_path,
642                InspectMetadataNode::WLAN_CHANNELS,
643            )),
644        );
645        let tx_rate_500kbps = client.inspect_time_matrix(
646            "tx_rate_500kbps",
647            TimeMatrix::<_, ConstantSample>::with_statistic(
648                SamplingProfile::default(),
649                ConstantSample::default(),
650                PostAggregation::<ArithmeticMean<u64>, _>::from_transform(|aggregation: f32| {
651                    aggregation.ceil() as u64
652                }),
653            ),
654        );
655        let rssi = client.inspect_time_matrix(
656            "rssi",
657            TimeMatrix::<_, ConstantSample>::with_statistic(
658                SamplingProfile::default(),
659                ConstantSample::default(),
660                PostAggregation::<ArithmeticMean<i64>, _>::from_transform(|aggregation: f32| {
661                    aggregation.ceil() as i64
662                }),
663            ),
664        );
665        let snr = client.inspect_time_matrix(
666            "snr",
667            TimeMatrix::<_, ConstantSample>::with_statistic(
668                SamplingProfile::default(),
669                ConstantSample::default(),
670                PostAggregation::<ArithmeticMean<i64>, _>::from_transform(|aggregation: f32| {
671                    aggregation.ceil() as i64
672                }),
673            ),
674        );
675        Self { wlan_channels, tx_rate_500kbps, rssi, snr }
676    }
677}
678
679#[derive(PartialEq, Eq, Unit, Hash)]
680struct InspectWlanChannel {
681    primary: u8,
682    cbw: String,
683    secondary80: u8,
684}
685
686impl From<fidl_ieee80211::WlanChannel> for InspectWlanChannel {
687    fn from(channel: fidl_ieee80211::WlanChannel) -> Self {
688        Self {
689            primary: channel.primary,
690            cbw: format!("{:?}", channel.cbw),
691            secondary80: channel.secondary80,
692        }
693    }
694}
695
696struct InspectMetadataNode {
697    wlan_channels: LruCacheNode<InspectWlanChannel>,
698}
699
700impl InspectMetadataNode {
701    const WLAN_CHANNELS: &'static str = "wlan_channels";
702    const WLAN_CHANNELS_ID_LIMIT: usize = 32;
703
704    fn new(inspect_node: &InspectNode) -> Self {
705        let wlan_channels = inspect_node.create_child(Self::WLAN_CHANNELS);
706        Self { wlan_channels: LruCacheNode::new(wlan_channels, Self::WLAN_CHANNELS_ID_LIMIT) }
707    }
708}
709
710#[cfg(test)]
711mod tests {
712    use super::*;
713    use crate::testing::*;
714    use assert_matches::assert_matches;
715    use diagnostics_assertions::{AnyNumericProperty, assert_data_tree};
716    use futures::TryStreamExt;
717    use std::pin::pin;
718    use std::task::Poll;
719    use test_case::test_case;
720    use windowed_stats::experimental::clock::Timed;
721    use windowed_stats::experimental::testing::{MockTimeMatrixClient, TimeMatrixCall};
722
723    const IFACE_ID: u16 = 66;
724
725    #[fuchsia::test]
726    fn test_handle_iface_created() {
727        let mut test_helper = setup_test();
728        let driver_counters_mock_matrix_client = MockTimeMatrixClient::new();
729        let driver_gauges_mock_matrix_client = MockTimeMatrixClient::new();
730        let logger = ClientIfaceCountersLogger::new(
731            test_helper.cobalt_proxy.clone(),
732            test_helper.monitor_svc_proxy.clone(),
733            &test_helper.inspect_metadata_node,
734            &test_helper.inspect_metadata_path,
735            &test_helper.mock_time_matrix_client,
736            driver_counters_mock_matrix_client.clone(),
737            driver_gauges_mock_matrix_client.clone(),
738        );
739
740        let mut handle_iface_created_fut = pin!(logger.handle_iface_created(IFACE_ID));
741        assert_eq!(
742            test_helper.run_and_handle_get_sme_telemetry(&mut handle_iface_created_fut),
743            Poll::Pending
744        );
745
746        let mocked_inspect_counter_configs = vec![fidl_stats::InspectCounterConfig {
747            counter_id: Some(1),
748            counter_name: Some("foo_counter".to_string()),
749            ..Default::default()
750        }];
751        let telemetry_support = fidl_stats::TelemetrySupport {
752            inspect_counter_configs: Some(mocked_inspect_counter_configs),
753            ..Default::default()
754        };
755        assert_eq!(
756            test_helper.run_and_respond_query_telemetry_support(
757                &mut handle_iface_created_fut,
758                Ok(&telemetry_support)
759            ),
760            Poll::Ready(())
761        );
762
763        assert_matches!(logger.iface_state.try_lock().as_deref(), Some(IfaceState::Created { .. }));
764        let driver_counters_time_series = logger.driver_counters_time_series.try_lock().unwrap();
765        assert_eq!(driver_counters_time_series.keys().copied().collect::<Vec<u16>>(), vec![1u16],);
766    }
767
768    #[fuchsia::test]
769    fn test_handle_periodic_telemetry_connection_stats() {
770        let mut test_helper = setup_test();
771        let driver_counters_mock_matrix_client = MockTimeMatrixClient::new();
772        let driver_gauges_mock_matrix_client = MockTimeMatrixClient::new();
773        let logger = ClientIfaceCountersLogger::new(
774            test_helper.cobalt_proxy.clone(),
775            test_helper.monitor_svc_proxy.clone(),
776            &test_helper.inspect_metadata_node,
777            &test_helper.inspect_metadata_path,
778            &test_helper.mock_time_matrix_client,
779            driver_counters_mock_matrix_client.clone(),
780            driver_gauges_mock_matrix_client.clone(),
781        );
782
783        // Transition to IfaceCreated state
784        handle_iface_created(&mut test_helper, &logger);
785
786        let mut test_fut = pin!(logger.handle_periodic_telemetry());
787        let iface_stats = fidl_stats::IfaceStats {
788            connection_stats: Some(fidl_stats::ConnectionStats {
789                connection_id: Some(1),
790                rx_unicast_total: Some(100),
791                rx_unicast_drop: Some(5),
792                rx_multicast: Some(30),
793                tx_total: Some(50),
794                tx_drop: Some(2),
795                ..Default::default()
796            }),
797            ..Default::default()
798        };
799        assert_eq!(
800            test_helper.run_and_respond_iface_stats_req(&mut test_fut, Ok(&iface_stats)),
801            Poll::Pending,
802        );
803        let signal_report = Default::default();
804        assert_eq!(
805            test_helper.run_and_respond_get_signal_report(&mut test_fut, Ok(&signal_report)),
806            Poll::Ready(())
807        );
808
809        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
810        assert_eq!(
811            &time_matrix_calls.drain::<u64>("rx_unicast_total")[..],
812            &[TimeMatrixCall::Fold(Timed::now(100u64))]
813        );
814        assert_eq!(
815            &time_matrix_calls.drain::<u64>("rx_unicast_drop")[..],
816            &[TimeMatrixCall::Fold(Timed::now(5u64))]
817        );
818        assert_eq!(
819            &time_matrix_calls.drain::<u64>("tx_total")[..],
820            &[TimeMatrixCall::Fold(Timed::now(50u64))]
821        );
822        assert_eq!(
823            &time_matrix_calls.drain::<u64>("tx_drop")[..],
824            &[TimeMatrixCall::Fold(Timed::now(2u64))]
825        );
826    }
827
828    #[fuchsia::test]
829    fn test_handle_periodic_telemetry_driver_specific_counters() {
830        let mut test_helper = setup_test();
831        let driver_counters_mock_matrix_client = MockTimeMatrixClient::new();
832        let driver_gauges_mock_matrix_client = MockTimeMatrixClient::new();
833        let logger = ClientIfaceCountersLogger::new(
834            test_helper.cobalt_proxy.clone(),
835            test_helper.monitor_svc_proxy.clone(),
836            &test_helper.inspect_metadata_node,
837            &test_helper.inspect_metadata_path,
838            &test_helper.mock_time_matrix_client,
839            driver_counters_mock_matrix_client.clone(),
840            driver_gauges_mock_matrix_client.clone(),
841        );
842
843        let mut handle_iface_created_fut = pin!(logger.handle_iface_created(IFACE_ID));
844        assert_eq!(
845            test_helper.run_and_handle_get_sme_telemetry(&mut handle_iface_created_fut),
846            Poll::Pending
847        );
848
849        let mocked_inspect_configs = vec![
850            fidl_stats::InspectCounterConfig {
851                counter_id: Some(1),
852                counter_name: Some("foo_counter".to_string()),
853                ..Default::default()
854            },
855            fidl_stats::InspectCounterConfig {
856                counter_id: Some(2),
857                counter_name: Some("bar_counter".to_string()),
858                ..Default::default()
859            },
860            fidl_stats::InspectCounterConfig {
861                counter_id: Some(3),
862                counter_name: Some("baz_counter".to_string()),
863                ..Default::default()
864            },
865        ];
866        let telemetry_support = fidl_stats::TelemetrySupport {
867            inspect_counter_configs: Some(mocked_inspect_configs),
868            ..Default::default()
869        };
870        assert_eq!(
871            test_helper.run_and_respond_query_telemetry_support(
872                &mut handle_iface_created_fut,
873                Ok(&telemetry_support)
874            ),
875            Poll::Ready(())
876        );
877
878        let mut test_fut = pin!(logger.handle_periodic_telemetry());
879        let iface_stats = fidl_stats::IfaceStats {
880            driver_specific_counters: Some(vec![fidl_stats::UnnamedCounter { id: 1, count: 50 }]),
881            connection_stats: Some(fidl_stats::ConnectionStats {
882                connection_id: Some(1),
883                driver_specific_counters: Some(vec![
884                    fidl_stats::UnnamedCounter { id: 2, count: 100 },
885                    fidl_stats::UnnamedCounter { id: 3, count: 150 },
886                    // This one is no-op because it's not registered in QueryTelemetrySupport
887                    fidl_stats::UnnamedCounter { id: 4, count: 200 },
888                ]),
889                ..Default::default()
890            }),
891            ..Default::default()
892        };
893        assert_eq!(
894            test_helper.run_and_respond_iface_stats_req(&mut test_fut, Ok(&iface_stats)),
895            Poll::Pending,
896        );
897        let signal_report = Default::default();
898        assert_eq!(
899            test_helper.run_and_respond_get_signal_report(&mut test_fut, Ok(&signal_report)),
900            Poll::Ready(())
901        );
902
903        let time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
904        assert!(time_matrix_calls.is_empty());
905
906        let mut driver_counters_matrix_calls = driver_counters_mock_matrix_client.drain_calls();
907        assert_eq!(
908            &driver_counters_matrix_calls.drain::<u64>("foo_counter")[..],
909            &[TimeMatrixCall::Fold(Timed::now(50))]
910        );
911        assert_eq!(
912            &driver_counters_matrix_calls.drain::<u64>("bar_counter")[..],
913            &[TimeMatrixCall::Fold(Timed::now(100))]
914        );
915        assert_eq!(
916            &driver_counters_matrix_calls.drain::<u64>("baz_counter")[..],
917            &[TimeMatrixCall::Fold(Timed::now(150))]
918        );
919
920        let driver_gauges_matrix_calls = driver_gauges_mock_matrix_client.drain_calls();
921        assert!(driver_gauges_matrix_calls.is_empty());
922    }
923
924    #[fuchsia::test]
925    fn test_handle_periodic_telemetry_driver_specific_gauges() {
926        let mut test_helper = setup_test();
927        let driver_counters_mock_matrix_client = MockTimeMatrixClient::new();
928        let driver_gauges_mock_matrix_client = MockTimeMatrixClient::new();
929        let logger = ClientIfaceCountersLogger::new(
930            test_helper.cobalt_proxy.clone(),
931            test_helper.monitor_svc_proxy.clone(),
932            &test_helper.inspect_metadata_node,
933            &test_helper.inspect_metadata_path,
934            &test_helper.mock_time_matrix_client,
935            driver_counters_mock_matrix_client.clone(),
936            driver_gauges_mock_matrix_client.clone(),
937        );
938
939        let mut handle_iface_created_fut = pin!(logger.handle_iface_created(IFACE_ID));
940        assert_eq!(
941            test_helper.run_and_handle_get_sme_telemetry(&mut handle_iface_created_fut),
942            Poll::Pending
943        );
944
945        let mocked_inspect_configs = vec![
946            fidl_stats::InspectGaugeConfig {
947                gauge_id: Some(1),
948                gauge_name: Some("foo_gauge".to_string()),
949                statistics: Some(vec![
950                    fidl_stats::GaugeStatistic::Mean,
951                    fidl_stats::GaugeStatistic::Last,
952                ]),
953                ..Default::default()
954            },
955            fidl_stats::InspectGaugeConfig {
956                gauge_id: Some(2),
957                gauge_name: Some("bar_gauge".to_string()),
958                statistics: Some(vec![
959                    fidl_stats::GaugeStatistic::Min,
960                    fidl_stats::GaugeStatistic::Sum,
961                ]),
962                ..Default::default()
963            },
964            fidl_stats::InspectGaugeConfig {
965                gauge_id: Some(3),
966                gauge_name: Some("baz_gauge".to_string()),
967                statistics: Some(vec![fidl_stats::GaugeStatistic::Max]),
968                ..Default::default()
969            },
970        ];
971        let telemetry_support = fidl_stats::TelemetrySupport {
972            inspect_gauge_configs: Some(mocked_inspect_configs),
973            ..Default::default()
974        };
975        assert_eq!(
976            test_helper.run_and_respond_query_telemetry_support(
977                &mut handle_iface_created_fut,
978                Ok(&telemetry_support)
979            ),
980            Poll::Ready(())
981        );
982
983        let mut test_fut = pin!(logger.handle_periodic_telemetry());
984        let iface_stats = fidl_stats::IfaceStats {
985            driver_specific_gauges: Some(vec![fidl_stats::UnnamedGauge { id: 1, value: 50 }]),
986            connection_stats: Some(fidl_stats::ConnectionStats {
987                connection_id: Some(1),
988                driver_specific_gauges: Some(vec![
989                    fidl_stats::UnnamedGauge { id: 2, value: 100 },
990                    fidl_stats::UnnamedGauge { id: 3, value: 150 },
991                    // This one is no-op because it's not registered in QueryTelemetrySupport
992                    fidl_stats::UnnamedGauge { id: 4, value: 200 },
993                ]),
994                ..Default::default()
995            }),
996            ..Default::default()
997        };
998        assert_eq!(
999            test_helper.run_and_respond_iface_stats_req(&mut test_fut, Ok(&iface_stats)),
1000            Poll::Pending
1001        );
1002        let signal_report = Default::default();
1003        assert_eq!(
1004            test_helper.run_and_respond_get_signal_report(&mut test_fut, Ok(&signal_report)),
1005            Poll::Ready(())
1006        );
1007
1008        let time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1009        assert!(time_matrix_calls.is_empty());
1010
1011        let driver_counters_matrix_calls = driver_counters_mock_matrix_client.drain_calls();
1012        assert!(driver_counters_matrix_calls.is_empty());
1013
1014        let mut driver_gauges_matrix_calls = driver_gauges_mock_matrix_client.drain_calls();
1015        assert_eq!(
1016            &driver_gauges_matrix_calls.drain::<i64>("foo_gauge.mean")[..],
1017            &[TimeMatrixCall::Fold(Timed::now(50))]
1018        );
1019        assert_eq!(
1020            &driver_gauges_matrix_calls.drain::<i64>("foo_gauge.last")[..],
1021            &[TimeMatrixCall::Fold(Timed::now(50))]
1022        );
1023        assert_eq!(
1024            &driver_gauges_matrix_calls.drain::<i64>("bar_gauge.min")[..],
1025            &[TimeMatrixCall::Fold(Timed::now(100))]
1026        );
1027        assert_eq!(
1028            &driver_gauges_matrix_calls.drain::<i64>("bar_gauge.sum")[..],
1029            &[TimeMatrixCall::Fold(Timed::now(100))]
1030        );
1031        assert_eq!(
1032            &driver_gauges_matrix_calls.drain::<i64>("baz_gauge.max")[..],
1033            &[TimeMatrixCall::Fold(Timed::now(150))]
1034        );
1035    }
1036
1037    #[fuchsia::test]
1038    fn test_handle_periodic_telemetry_signal_report() {
1039        let mut test_helper = setup_test();
1040        let driver_counters_mock_matrix_client = MockTimeMatrixClient::new();
1041        let driver_gauges_mock_matrix_client = MockTimeMatrixClient::new();
1042        let logger = ClientIfaceCountersLogger::new(
1043            test_helper.cobalt_proxy.clone(),
1044            test_helper.monitor_svc_proxy.clone(),
1045            &test_helper.inspect_metadata_node,
1046            &test_helper.inspect_metadata_path,
1047            &test_helper.mock_time_matrix_client,
1048            driver_counters_mock_matrix_client.clone(),
1049            driver_gauges_mock_matrix_client.clone(),
1050        );
1051
1052        let mut handle_iface_created_fut = pin!(logger.handle_iface_created(IFACE_ID));
1053        assert_eq!(
1054            test_helper.run_and_handle_get_sme_telemetry(&mut handle_iface_created_fut),
1055            Poll::Pending
1056        );
1057
1058        let telemetry_support = Default::default();
1059        assert_eq!(
1060            test_helper.run_and_respond_query_telemetry_support(
1061                &mut handle_iface_created_fut,
1062                Ok(&telemetry_support)
1063            ),
1064            Poll::Ready(())
1065        );
1066
1067        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1068        let iface_stats = Default::default();
1069        assert_eq!(
1070            test_helper.run_and_respond_iface_stats_req(&mut test_fut, Ok(&iface_stats)),
1071            Poll::Pending
1072        );
1073        let signal_report = fidl_stats::SignalReport {
1074            connection_signal_report: Some(fidl_stats::ConnectionSignalReport {
1075                channel: Some(fidl_ieee80211::WlanChannel {
1076                    primary: 6,
1077                    cbw: fidl_ieee80211::ChannelBandwidth::Cbw20,
1078                    secondary80: 0,
1079                }),
1080                tx_rate_500kbps: Some(11),
1081                rssi_dbm: Some(-40),
1082                snr_db: Some(25),
1083                ..Default::default()
1084            }),
1085            ..Default::default()
1086        };
1087        assert_eq!(
1088            test_helper.run_and_respond_get_signal_report(&mut test_fut, Ok(&signal_report)),
1089            Poll::Ready(())
1090        );
1091
1092        let mut time_matrix_calls = test_helper.mock_time_matrix_client.drain_calls();
1093        assert_eq!(
1094            &time_matrix_calls.drain::<u64>("wlan_channels")[..],
1095            &[TimeMatrixCall::Fold(Timed::now(1 << 0))] // 0 is the ID of the WLAN channel
1096        );
1097        assert_eq!(
1098            &time_matrix_calls.drain::<u64>("tx_rate_500kbps")[..],
1099            &[TimeMatrixCall::Fold(Timed::now(11))]
1100        );
1101        assert_eq!(
1102            &time_matrix_calls.drain::<i64>("rssi")[..],
1103            &[TimeMatrixCall::Fold(Timed::now(-40))]
1104        );
1105        assert_eq!(
1106            &time_matrix_calls.drain::<i64>("snr")[..],
1107            &[TimeMatrixCall::Fold(Timed::now(25))]
1108        );
1109
1110        let tree = test_helper.get_inspect_data_tree();
1111        assert_data_tree!(@executor test_helper.exec, tree, root: contains {
1112            test_stats: contains {
1113                metadata: contains {
1114                    wlan_channels: contains {
1115                        "0": {
1116                            "@time": AnyNumericProperty,
1117                            "data": contains {
1118                                primary: 6u64,
1119                                cbw: "Cbw20",
1120                                secondary80: 0u64,
1121                            }
1122                        }
1123                    }
1124                }
1125            }
1126        });
1127    }
1128
1129    #[fuchsia::test]
1130    fn test_handle_periodic_telemetry_cobalt() {
1131        let mut test_helper = setup_test();
1132        let driver_counters_mock_matrix_client = MockTimeMatrixClient::new();
1133        let driver_gauges_mock_matrix_client = MockTimeMatrixClient::new();
1134        let logger = ClientIfaceCountersLogger::new(
1135            test_helper.cobalt_proxy.clone(),
1136            test_helper.monitor_svc_proxy.clone(),
1137            &test_helper.inspect_metadata_node,
1138            &test_helper.inspect_metadata_path,
1139            &test_helper.mock_time_matrix_client,
1140            driver_counters_mock_matrix_client.clone(),
1141            driver_gauges_mock_matrix_client.clone(),
1142        );
1143
1144        // Transition to IfaceCreated state
1145        handle_iface_created(&mut test_helper, &logger);
1146
1147        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1148        let iface_stats = fidl_stats::IfaceStats {
1149            connection_stats: Some(fidl_stats::ConnectionStats {
1150                connection_id: Some(1),
1151                rx_unicast_total: Some(100),
1152                rx_unicast_drop: Some(5),
1153                rx_multicast: Some(30),
1154                tx_total: Some(50),
1155                tx_drop: Some(2),
1156                ..Default::default()
1157            }),
1158            ..Default::default()
1159        };
1160        assert_eq!(
1161            test_helper.run_and_respond_iface_stats_req(&mut test_fut, Ok(&iface_stats)),
1162            Poll::Pending
1163        );
1164        let signal_report = Default::default();
1165        assert_eq!(
1166            test_helper.run_and_respond_get_signal_report(&mut test_fut, Ok(&signal_report)),
1167            Poll::Ready(())
1168        );
1169
1170        let metrics = test_helper.get_logged_metrics(metrics::BAD_RX_RATE_METRIC_ID);
1171        assert!(metrics.is_empty());
1172        let metrics = test_helper.get_logged_metrics(metrics::RX_UNICAST_PACKETS_METRIC_ID);
1173        assert!(metrics.is_empty());
1174        let metrics = test_helper.get_logged_metrics(metrics::BAD_TX_RATE_METRIC_ID);
1175        assert!(metrics.is_empty());
1176
1177        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1178        let iface_stats = fidl_stats::IfaceStats {
1179            connection_stats: Some(fidl_stats::ConnectionStats {
1180                connection_id: Some(1),
1181                rx_unicast_total: Some(200),
1182                rx_unicast_drop: Some(15),
1183                rx_multicast: Some(30),
1184                tx_total: Some(150),
1185                tx_drop: Some(3),
1186                ..Default::default()
1187            }),
1188            ..Default::default()
1189        };
1190        assert_eq!(
1191            test_helper.run_and_respond_iface_stats_req(&mut test_fut, Ok(&iface_stats)),
1192            Poll::Pending
1193        );
1194        assert_eq!(test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut), Poll::Pending);
1195        let signal_report: fidl_fuchsia_wlan_stats::SignalReport = Default::default();
1196        assert_eq!(
1197            test_helper.run_and_respond_get_signal_report(&mut test_fut, Ok(&signal_report)),
1198            Poll::Ready(())
1199        );
1200
1201        let metrics = test_helper.get_logged_metrics(metrics::BAD_RX_RATE_METRIC_ID);
1202        assert_eq!(metrics.len(), 1);
1203        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(1000)); // 10%
1204        let metrics = test_helper.get_logged_metrics(metrics::RX_UNICAST_PACKETS_METRIC_ID);
1205        assert_eq!(metrics.len(), 1);
1206        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(100));
1207        let metrics = test_helper.get_logged_metrics(metrics::BAD_TX_RATE_METRIC_ID);
1208        assert_eq!(metrics.len(), 1);
1209        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(100)); // 1%
1210    }
1211
1212    #[fuchsia::test]
1213    fn test_handle_periodic_telemetry_cobalt_changed_connection_id() {
1214        let mut test_helper = setup_test();
1215        let driver_counters_mock_matrix_client = MockTimeMatrixClient::new();
1216        let driver_gauges_mock_matrix_client = MockTimeMatrixClient::new();
1217        let logger = ClientIfaceCountersLogger::new(
1218            test_helper.cobalt_proxy.clone(),
1219            test_helper.monitor_svc_proxy.clone(),
1220            &test_helper.inspect_metadata_node,
1221            &test_helper.inspect_metadata_path,
1222            &test_helper.mock_time_matrix_client,
1223            driver_counters_mock_matrix_client.clone(),
1224            driver_gauges_mock_matrix_client.clone(),
1225        );
1226
1227        // Transition to IfaceCreated state
1228        handle_iface_created(&mut test_helper, &logger);
1229
1230        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1231        let iface_stats = fidl_stats::IfaceStats {
1232            connection_stats: Some(fidl_stats::ConnectionStats {
1233                connection_id: Some(1),
1234                rx_unicast_total: Some(100),
1235                rx_unicast_drop: Some(5),
1236                rx_multicast: Some(30),
1237                tx_total: Some(50),
1238                tx_drop: Some(2),
1239                ..Default::default()
1240            }),
1241            ..Default::default()
1242        };
1243        assert_eq!(
1244            test_helper.run_and_respond_iface_stats_req(&mut test_fut, Ok(&iface_stats)),
1245            Poll::Pending
1246        );
1247        let signal_report = Default::default();
1248        assert_eq!(
1249            test_helper.run_and_respond_get_signal_report(&mut test_fut, Ok(&signal_report)),
1250            Poll::Ready(())
1251        );
1252
1253        let metrics = test_helper.get_logged_metrics(metrics::BAD_RX_RATE_METRIC_ID);
1254        assert!(metrics.is_empty());
1255        let metrics = test_helper.get_logged_metrics(metrics::RX_UNICAST_PACKETS_METRIC_ID);
1256        assert!(metrics.is_empty());
1257        let metrics = test_helper.get_logged_metrics(metrics::BAD_TX_RATE_METRIC_ID);
1258        assert!(metrics.is_empty());
1259
1260        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1261        let iface_stats = fidl_stats::IfaceStats {
1262            connection_stats: Some(fidl_stats::ConnectionStats {
1263                connection_id: Some(2),
1264                rx_unicast_total: Some(200),
1265                rx_unicast_drop: Some(15),
1266                rx_multicast: Some(30),
1267                tx_total: Some(150),
1268                tx_drop: Some(3),
1269                ..Default::default()
1270            }),
1271            ..Default::default()
1272        };
1273        assert_eq!(
1274            test_helper.run_and_respond_iface_stats_req(&mut test_fut, Ok(&iface_stats)),
1275            Poll::Pending
1276        );
1277        let signal_report = Default::default();
1278        assert_eq!(
1279            test_helper.run_and_respond_get_signal_report(&mut test_fut, Ok(&signal_report)),
1280            Poll::Ready(())
1281        );
1282
1283        // No metric is logged because the ID indicates it's a different connection, meaning
1284        // there is nothing to diff with
1285        let metrics = test_helper.get_logged_metrics(metrics::BAD_RX_RATE_METRIC_ID);
1286        assert!(metrics.is_empty());
1287        let metrics = test_helper.get_logged_metrics(metrics::RX_UNICAST_PACKETS_METRIC_ID);
1288        assert!(metrics.is_empty());
1289        let metrics = test_helper.get_logged_metrics(metrics::BAD_TX_RATE_METRIC_ID);
1290        assert!(metrics.is_empty());
1291
1292        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1293        let iface_stats = fidl_stats::IfaceStats {
1294            connection_stats: Some(fidl_stats::ConnectionStats {
1295                connection_id: Some(2),
1296                rx_unicast_total: Some(300),
1297                rx_unicast_drop: Some(18),
1298                rx_multicast: Some(30),
1299                tx_total: Some(250),
1300                tx_drop: Some(5),
1301                ..Default::default()
1302            }),
1303            ..Default::default()
1304        };
1305        assert_eq!(
1306            test_helper.run_and_respond_iface_stats_req(&mut test_fut, Ok(&iface_stats)),
1307            Poll::Pending
1308        );
1309        assert_eq!(test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut), Poll::Pending);
1310        let signal_report = Default::default();
1311        assert_eq!(
1312            test_helper.run_and_respond_get_signal_report(&mut test_fut, Ok(&signal_report)),
1313            Poll::Ready(())
1314        );
1315
1316        let metrics = test_helper.get_logged_metrics(metrics::BAD_RX_RATE_METRIC_ID);
1317        assert_eq!(metrics.len(), 1);
1318        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(300)); // 3%
1319        let metrics = test_helper.get_logged_metrics(metrics::RX_UNICAST_PACKETS_METRIC_ID);
1320        assert_eq!(metrics.len(), 1);
1321        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(100));
1322        let metrics = test_helper.get_logged_metrics(metrics::BAD_TX_RATE_METRIC_ID);
1323        assert_eq!(metrics.len(), 1);
1324        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(200)); // 2%
1325    }
1326
1327    #[fuchsia::test]
1328    fn test_handle_periodic_telemetry_cobalt_suspension_in_between() {
1329        let mut test_helper = setup_test();
1330        let driver_counters_mock_matrix_client = MockTimeMatrixClient::new();
1331        let driver_gauges_mock_matrix_client = MockTimeMatrixClient::new();
1332        let logger = ClientIfaceCountersLogger::new(
1333            test_helper.cobalt_proxy.clone(),
1334            test_helper.monitor_svc_proxy.clone(),
1335            &test_helper.inspect_metadata_node,
1336            &test_helper.inspect_metadata_path,
1337            &test_helper.mock_time_matrix_client,
1338            driver_counters_mock_matrix_client.clone(),
1339            driver_gauges_mock_matrix_client.clone(),
1340        );
1341
1342        // Transition to IfaceCreated state
1343        handle_iface_created(&mut test_helper, &logger);
1344
1345        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1346        let iface_stats = fidl_stats::IfaceStats {
1347            connection_stats: Some(fidl_stats::ConnectionStats {
1348                connection_id: Some(1),
1349                rx_unicast_total: Some(100),
1350                rx_unicast_drop: Some(5),
1351                rx_multicast: Some(30),
1352                tx_total: Some(50),
1353                tx_drop: Some(2),
1354                ..Default::default()
1355            }),
1356            ..Default::default()
1357        };
1358        assert_eq!(
1359            test_helper.run_and_respond_iface_stats_req(&mut test_fut, Ok(&iface_stats)),
1360            Poll::Pending
1361        );
1362        let signal_report = Default::default();
1363        assert_eq!(
1364            test_helper.run_and_respond_get_signal_report(&mut test_fut, Ok(&signal_report)),
1365            Poll::Ready(())
1366        );
1367
1368        let metrics = test_helper.get_logged_metrics(metrics::BAD_RX_RATE_METRIC_ID);
1369        assert!(metrics.is_empty());
1370        let metrics = test_helper.get_logged_metrics(metrics::RX_UNICAST_PACKETS_METRIC_ID);
1371        assert!(metrics.is_empty());
1372        let metrics = test_helper.get_logged_metrics(metrics::BAD_TX_RATE_METRIC_ID);
1373        assert!(metrics.is_empty());
1374
1375        test_helper.exec.set_fake_boot_to_mono_offset(zx::BootDuration::from_millis(1));
1376        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1377        let iface_stats = fidl_stats::IfaceStats {
1378            connection_stats: Some(fidl_stats::ConnectionStats {
1379                connection_id: Some(1),
1380                rx_unicast_total: Some(200),
1381                rx_unicast_drop: Some(15),
1382                rx_multicast: Some(30),
1383                tx_total: Some(150),
1384                tx_drop: Some(3),
1385                ..Default::default()
1386            }),
1387            ..Default::default()
1388        };
1389        assert_eq!(
1390            test_helper.run_and_respond_iface_stats_req(&mut test_fut, Ok(&iface_stats)),
1391            Poll::Pending
1392        );
1393        let signal_report = Default::default();
1394        assert_eq!(
1395            test_helper.run_and_respond_get_signal_report(&mut test_fut, Ok(&signal_report)),
1396            Poll::Ready(())
1397        );
1398
1399        // No metric is logged because the increase in boot-to-mono offset indicates that
1400        // a suspension had happened in between
1401        let metrics = test_helper.get_logged_metrics(metrics::BAD_RX_RATE_METRIC_ID);
1402        assert!(metrics.is_empty());
1403        let metrics = test_helper.get_logged_metrics(metrics::RX_UNICAST_PACKETS_METRIC_ID);
1404        assert!(metrics.is_empty());
1405        let metrics = test_helper.get_logged_metrics(metrics::BAD_TX_RATE_METRIC_ID);
1406        assert!(metrics.is_empty());
1407
1408        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1409        let iface_stats = fidl_stats::IfaceStats {
1410            connection_stats: Some(fidl_stats::ConnectionStats {
1411                connection_id: Some(1),
1412                rx_unicast_total: Some(300),
1413                rx_unicast_drop: Some(18),
1414                rx_multicast: Some(30),
1415                tx_total: Some(250),
1416                tx_drop: Some(5),
1417                ..Default::default()
1418            }),
1419            ..Default::default()
1420        };
1421        assert_eq!(
1422            test_helper.run_and_respond_iface_stats_req(&mut test_fut, Ok(&iface_stats)),
1423            Poll::Pending
1424        );
1425        assert_eq!(test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut), Poll::Pending);
1426        let signal_report = Default::default();
1427        assert_eq!(
1428            test_helper.run_and_respond_get_signal_report(&mut test_fut, Ok(&signal_report)),
1429            Poll::Ready(())
1430        );
1431
1432        let metrics = test_helper.get_logged_metrics(metrics::BAD_RX_RATE_METRIC_ID);
1433        assert_eq!(metrics.len(), 1);
1434        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(300)); // 3%
1435        let metrics = test_helper.get_logged_metrics(metrics::RX_UNICAST_PACKETS_METRIC_ID);
1436        assert_eq!(metrics.len(), 1);
1437        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(100));
1438        let metrics = test_helper.get_logged_metrics(metrics::BAD_TX_RATE_METRIC_ID);
1439        assert_eq!(metrics.len(), 1);
1440        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(200)); // 2%
1441    }
1442
1443    #[fuchsia::test]
1444    fn test_handle_periodic_telemetry_get_iface_stats_failure_non_timeout() {
1445        let mut test_helper = setup_test();
1446        let driver_counters_mock_matrix_client = MockTimeMatrixClient::new();
1447        let driver_gauges_mock_matrix_client = MockTimeMatrixClient::new();
1448        let logger = ClientIfaceCountersLogger::new(
1449            test_helper.cobalt_proxy.clone(),
1450            test_helper.monitor_svc_proxy.clone(),
1451            &test_helper.inspect_metadata_node,
1452            &test_helper.inspect_metadata_path,
1453            &test_helper.mock_time_matrix_client,
1454            driver_counters_mock_matrix_client.clone(),
1455            driver_gauges_mock_matrix_client.clone(),
1456        );
1457
1458        // Transition to IfaceCreated state
1459        handle_iface_created(&mut test_helper, &logger);
1460
1461        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1462        assert_eq!(
1463            test_helper
1464                .run_and_respond_iface_stats_req(&mut test_fut, Err(zx::sys::ZX_ERR_TIMED_OUT)),
1465            Poll::Pending
1466        );
1467        assert_eq!(test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut), Poll::Pending);
1468
1469        let metrics = test_helper.get_logged_metrics(metrics::GET_IFACE_STATS_FAILURE_METRIC_ID);
1470        assert_eq!(metrics.len(), 1);
1471        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1472
1473        // Note that even though the response is ZX_ERR_TIMED_OUT, the `get_iface_stats_timeout`
1474        // metric is not logged because the metric is specifically about timeout waiting for a
1475        // response to `get_iface_stats` call.
1476        let metrics = test_helper.get_logged_metrics(metrics::GET_IFACE_STATS_TIMEOUT_METRIC_ID);
1477        assert!(metrics.is_empty());
1478
1479        let metrics =
1480            test_helper.get_logged_metrics(metrics::GET_IFACE_STATS_ERROR_IN_RESPONSE_METRIC_ID);
1481        assert_eq!(metrics.len(), 1);
1482        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1483    }
1484
1485    #[fuchsia::test]
1486    fn test_handle_periodic_telemetry_get_iface_stats_timeout() {
1487        let mut test_helper = setup_test();
1488        let driver_counters_mock_matrix_client = MockTimeMatrixClient::new();
1489        let driver_gauges_mock_matrix_client = MockTimeMatrixClient::new();
1490        let logger = ClientIfaceCountersLogger::new(
1491            test_helper.cobalt_proxy.clone(),
1492            test_helper.monitor_svc_proxy.clone(),
1493            &test_helper.inspect_metadata_node,
1494            &test_helper.inspect_metadata_path,
1495            &test_helper.mock_time_matrix_client,
1496            driver_counters_mock_matrix_client.clone(),
1497            driver_gauges_mock_matrix_client.clone(),
1498        );
1499
1500        // Transition to IfaceCreated state
1501        handle_iface_created(&mut test_helper, &logger);
1502
1503        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1504        assert_eq!(test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut), Poll::Pending);
1505        test_helper.exec.set_fake_time(fasync::MonotonicInstant::from_nanos(5_000_000_000));
1506        assert_eq!(test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut), Poll::Pending);
1507
1508        let metrics = test_helper.get_logged_metrics(metrics::GET_IFACE_STATS_FAILURE_METRIC_ID);
1509        assert_eq!(metrics.len(), 1);
1510        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1511
1512        let metrics = test_helper.get_logged_metrics(metrics::GET_IFACE_STATS_TIMEOUT_METRIC_ID);
1513        assert_eq!(metrics.len(), 1);
1514        assert_eq!(metrics[0].payload, MetricEventPayload::Count(1));
1515
1516        let metrics =
1517            test_helper.get_logged_metrics(metrics::GET_IFACE_STATS_ERROR_IN_RESPONSE_METRIC_ID);
1518        assert!(metrics.is_empty());
1519    }
1520
1521    #[fuchsia::test]
1522    fn test_diff_and_log_rx_cobalt() {
1523        let mut test_helper = setup_test();
1524        let prev_stats = fidl_stats::ConnectionStats {
1525            rx_unicast_total: Some(100),
1526            rx_unicast_drop: Some(5),
1527            ..Default::default()
1528        };
1529        let current_stats = fidl_stats::ConnectionStats {
1530            rx_unicast_total: Some(300),
1531            rx_unicast_drop: Some(7),
1532            ..Default::default()
1533        };
1534        let cobalt_proxy = test_helper.cobalt_proxy.clone();
1535        let mut test_fut = pin!(diff_and_log_rx_cobalt(&cobalt_proxy, &prev_stats, &current_stats));
1536        assert_eq!(
1537            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1538            Poll::Ready(())
1539        );
1540        let metrics = test_helper.get_logged_metrics(metrics::BAD_RX_RATE_METRIC_ID);
1541        assert_eq!(metrics.len(), 1);
1542        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(100)); // 1%
1543        let metrics = test_helper.get_logged_metrics(metrics::RX_UNICAST_PACKETS_METRIC_ID);
1544        assert_eq!(metrics.len(), 1);
1545        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(200));
1546    }
1547
1548    #[test_case(
1549        fidl_stats::ConnectionStats { ..Default::default() },
1550        fidl_stats::ConnectionStats { ..Default::default() };
1551        "both empty"
1552    )]
1553    #[test_case(
1554        fidl_stats::ConnectionStats {
1555            rx_unicast_total: Some(100),
1556            rx_unicast_drop: Some(5),
1557            ..Default::default()
1558        },
1559        fidl_stats::ConnectionStats { ..Default::default() };
1560        "current empty"
1561    )]
1562    #[test_case(
1563        fidl_stats::ConnectionStats { ..Default::default() },
1564        fidl_stats::ConnectionStats {
1565            rx_unicast_total: Some(300),
1566            rx_unicast_drop: Some(7),
1567            ..Default::default()
1568        };
1569        "prev empty"
1570    )]
1571    #[fuchsia::test(add_test_attr = false)]
1572    fn test_diff_and_log_rx_cobalt_empty(
1573        prev_stats: fidl_stats::ConnectionStats,
1574        current_stats: fidl_stats::ConnectionStats,
1575    ) {
1576        let mut test_helper = setup_test();
1577        let cobalt_proxy = test_helper.cobalt_proxy.clone();
1578        let mut test_fut = pin!(diff_and_log_rx_cobalt(&cobalt_proxy, &prev_stats, &current_stats));
1579        assert_eq!(
1580            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1581            Poll::Ready(())
1582        );
1583        let metrics = test_helper.get_logged_metrics(metrics::BAD_RX_RATE_METRIC_ID);
1584        assert!(metrics.is_empty())
1585    }
1586
1587    #[fuchsia::test]
1588    fn test_diff_and_log_tx_cobalt() {
1589        let mut test_helper = setup_test();
1590        let prev_stats = fidl_stats::ConnectionStats {
1591            tx_total: Some(100),
1592            tx_drop: Some(5),
1593            ..Default::default()
1594        };
1595        let current_stats = fidl_stats::ConnectionStats {
1596            tx_total: Some(300),
1597            tx_drop: Some(7),
1598            ..Default::default()
1599        };
1600        let cobalt_proxy = test_helper.cobalt_proxy.clone();
1601        let mut test_fut = pin!(diff_and_log_tx_cobalt(&cobalt_proxy, &prev_stats, &current_stats));
1602        assert_eq!(
1603            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1604            Poll::Ready(())
1605        );
1606        let metrics = test_helper.get_logged_metrics(metrics::BAD_TX_RATE_METRIC_ID);
1607        assert_eq!(metrics.len(), 1);
1608        assert_eq!(metrics[0].payload, MetricEventPayload::IntegerValue(100)); // 1%
1609    }
1610
1611    #[test_case(
1612        fidl_stats::ConnectionStats { ..Default::default() },
1613        fidl_stats::ConnectionStats { ..Default::default() };
1614        "both empty"
1615    )]
1616    #[test_case(
1617        fidl_stats::ConnectionStats {
1618            tx_total: Some(100),
1619            tx_drop: Some(5),
1620            ..Default::default()
1621        },
1622        fidl_stats::ConnectionStats { ..Default::default() };
1623        "current empty"
1624    )]
1625    #[test_case(
1626        fidl_stats::ConnectionStats { ..Default::default() },
1627        fidl_stats::ConnectionStats {
1628            tx_total: Some(300),
1629            tx_drop: Some(7),
1630            ..Default::default()
1631        };
1632        "prev empty"
1633    )]
1634    #[fuchsia::test(add_test_attr = false)]
1635    fn test_diff_and_log_tx_cobalt_empty(
1636        prev_stats: fidl_stats::ConnectionStats,
1637        current_stats: fidl_stats::ConnectionStats,
1638    ) {
1639        let mut test_helper = setup_test();
1640        let cobalt_proxy = test_helper.cobalt_proxy.clone();
1641        let mut test_fut = pin!(diff_and_log_tx_cobalt(&cobalt_proxy, &prev_stats, &current_stats));
1642        assert_eq!(
1643            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1644            Poll::Ready(())
1645        );
1646        let metrics = test_helper.get_logged_metrics(metrics::BAD_TX_RATE_METRIC_ID);
1647        assert!(metrics.is_empty())
1648    }
1649
1650    #[test_case(
1651        fidl_stats::ConnectionStats {
1652            rx_unicast_total: Some(100),
1653            rx_unicast_drop: Some(5),
1654            tx_total: Some(100),
1655            tx_drop: Some(5),
1656            ..Default::default()
1657        },
1658        fidl_stats::ConnectionStats {
1659            rx_unicast_total: Some(10),
1660            rx_unicast_drop: Some(1),
1661            tx_total: Some(100),
1662            tx_drop: Some(5),
1663            ..Default::default()
1664        };
1665        "rx regressed"
1666    )]
1667    #[test_case(
1668        fidl_stats::ConnectionStats {
1669            tx_total: Some(100),
1670            tx_drop: Some(5),
1671            rx_unicast_total: Some(100),
1672            rx_unicast_drop: Some(5),
1673            ..Default::default()
1674        },
1675        fidl_stats::ConnectionStats {
1676            tx_total: Some(10),
1677            tx_drop: Some(1),
1678            rx_unicast_total: Some(100),
1679            rx_unicast_drop: Some(5),
1680            ..Default::default()
1681        };
1682        "tx regressed"
1683    )]
1684    #[fuchsia::test(add_test_attr = false)]
1685    fn test_diff_and_log_connection_stats_cobalt_counters_reset(
1686        prev_stats: fidl_stats::ConnectionStats,
1687        current_stats: fidl_stats::ConnectionStats,
1688    ) {
1689        let mut test_helper = setup_test();
1690        let cobalt_proxy = test_helper.cobalt_proxy.clone();
1691        let mut test_fut =
1692            pin!(diff_and_log_connection_stats_cobalt(&cobalt_proxy, &prev_stats, &current_stats));
1693        // Verify no crash
1694        assert_eq!(
1695            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
1696            Poll::Ready(())
1697        );
1698        // Verify neither RX nor TX metrics are logged
1699        let metrics = test_helper.get_logged_metrics(metrics::BAD_RX_RATE_METRIC_ID);
1700        assert!(metrics.is_empty());
1701        let metrics = test_helper.get_logged_metrics(metrics::BAD_TX_RATE_METRIC_ID);
1702        assert!(metrics.is_empty());
1703    }
1704
1705    #[fuchsia::test]
1706    fn test_handle_iface_destroyed() {
1707        let mut test_helper = setup_test();
1708        let driver_counters_mock_matrix_client = MockTimeMatrixClient::new();
1709        let driver_gauges_mock_matrix_client = MockTimeMatrixClient::new();
1710        let logger = ClientIfaceCountersLogger::new(
1711            test_helper.cobalt_proxy.clone(),
1712            test_helper.monitor_svc_proxy.clone(),
1713            &test_helper.inspect_metadata_node,
1714            &test_helper.inspect_metadata_path,
1715            &test_helper.mock_time_matrix_client,
1716            driver_counters_mock_matrix_client.clone(),
1717            driver_gauges_mock_matrix_client.clone(),
1718        );
1719
1720        // Transition to IfaceCreated state
1721        handle_iface_created(&mut test_helper, &logger);
1722
1723        let mut handle_iface_destroyed_fut = pin!(logger.handle_iface_destroyed(IFACE_ID));
1724        assert_eq!(
1725            test_helper.exec.run_until_stalled(&mut handle_iface_destroyed_fut),
1726            Poll::Ready(())
1727        );
1728
1729        let mut test_fut = pin!(logger.handle_periodic_telemetry());
1730        assert_eq!(test_helper.exec.run_until_stalled(&mut test_fut), Poll::Ready(()));
1731        let telemetry_svc_stream = test_helper.telemetry_svc_stream.as_mut().unwrap();
1732        let mut telemetry_svc_req_fut = pin!(telemetry_svc_stream.try_next());
1733        // Verify that no telemetry request is made now that the iface is destroyed
1734        match test_helper.exec.run_until_stalled(&mut telemetry_svc_req_fut) {
1735            Poll::Ready(Ok(None)) => (),
1736            other => panic!("unexpected variant: {other:?}"),
1737        }
1738    }
1739
1740    fn handle_iface_created<S: InspectSender>(
1741        test_helper: &mut TestHelper,
1742        logger: &ClientIfaceCountersLogger<S>,
1743    ) {
1744        let mut handle_iface_created_fut = pin!(logger.handle_iface_created(IFACE_ID));
1745        assert_eq!(
1746            test_helper.run_and_handle_get_sme_telemetry(&mut handle_iface_created_fut),
1747            Poll::Pending
1748        );
1749        let telemetry_support = fidl_stats::TelemetrySupport::default();
1750        assert_eq!(
1751            test_helper.run_and_respond_query_telemetry_support(
1752                &mut handle_iface_created_fut,
1753                Ok(&telemetry_support)
1754            ),
1755            Poll::Ready(())
1756        );
1757    }
1758}