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