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