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