Skip to main content

wlan_telemetry/processors/
power.rs

1// Copyright 2025 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.
4use crate::util::cobalt_logger::log_cobalt;
5use cobalt_client::traits::AsEventCode;
6use fuchsia_inspect::Node as InspectNode;
7use fuchsia_inspect_contrib::inspect_log;
8use fuchsia_inspect_contrib::nodes::BoundedListNode;
9use futures::lock::Mutex;
10use std::collections::HashMap;
11use std::sync::Arc;
12use wlan_legacy_metrics_registry as metrics;
13
14pub const INSPECT_POWER_EVENTS_LIMIT: usize = 20;
15
16#[derive(Debug, PartialEq)]
17pub enum IfacePowerLevel {
18    Disconnected,
19    SuspendMode,
20    Normal,
21    NoPowerSavings,
22}
23
24#[derive(Debug)]
25pub enum UnclearPowerDemand {
26    PowerSaveRequestedWhileSuspendModeEnabled,
27}
28
29pub struct PowerLogger {
30    power_inspect_node: Mutex<BoundedListNode>,
31    cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
32    iface_power_states: Arc<Mutex<HashMap<u16, IfacePowerLevel>>>,
33}
34
35impl PowerLogger {
36    pub fn new(
37        cobalt_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
38        inspect_node: &InspectNode,
39    ) -> Self {
40        // Initialize inspect children
41        let iface_power_events = inspect_node.create_child("iface_power_events");
42        let power_inspect_node: BoundedListNode =
43            BoundedListNode::new(iface_power_events, INSPECT_POWER_EVENTS_LIMIT);
44
45        Self {
46            power_inspect_node: Mutex::new(power_inspect_node),
47            cobalt_proxy,
48            iface_power_states: Arc::new(Mutex::new(HashMap::new())),
49        }
50    }
51
52    pub async fn log_iface_power_event(&self, iface_power_level: IfacePowerLevel, iface_id: u16) {
53        inspect_log!(self.power_inspect_node.lock().await, {
54            power_level: std::format!("{:?}", iface_power_level),
55            iface_id: iface_id,
56        });
57        let _ = self.iface_power_states.lock().await.insert(iface_id, iface_power_level);
58    }
59
60    pub async fn handle_iface_disconnect(&self, iface_id: u16) {
61        let _ =
62            self.iface_power_states.lock().await.insert(iface_id, IfacePowerLevel::Disconnected);
63    }
64
65    pub async fn handle_iface_destroyed(&self, iface_id: u16) {
66        let _ = self.iface_power_states.lock().await.remove(&iface_id);
67    }
68
69    pub async fn handle_suspend_imminent(&self) {
70        for (_iface_id, iface_power_level) in self.iface_power_states.lock().await.iter() {
71            use metrics::PowerLevelAtSuspendMetricDimensionPowerLevel as dim;
72            log_cobalt!(
73                self.cobalt_proxy,
74                log_occurrence,
75                metrics::POWER_LEVEL_AT_SUSPEND_METRIC_ID,
76                1,
77                &[match iface_power_level {
78                    IfacePowerLevel::Disconnected => dim::Disconnected,
79                    IfacePowerLevel::SuspendMode => dim::SuspendMode,
80                    IfacePowerLevel::Normal => dim::PowerSaveMode,
81                    IfacePowerLevel::NoPowerSavings => dim::HighPerformanceMode,
82                }
83                .as_event_code()]
84            )
85        }
86    }
87
88    pub async fn handle_unclear_power_demand(&self, demand: UnclearPowerDemand) {
89        use metrics::UnclearPowerLevelDemandMetricDimensionReason as dim;
90        log_cobalt!(
91            self.cobalt_proxy,
92            log_occurrence,
93            metrics::UNCLEAR_POWER_LEVEL_DEMAND_METRIC_ID,
94            1,
95            &[match demand {
96                UnclearPowerDemand::PowerSaveRequestedWhileSuspendModeEnabled =>
97                    dim::PowerSaveRequestedWhileSuspendModeEnabled,
98            }
99            .as_event_code()]
100        )
101    }
102
103    pub async fn handle_chip_power_up_failure(&self) {
104        log_cobalt!(
105            self.cobalt_proxy,
106            log_occurrence,
107            metrics::CHIP_POWER_UP_FAILURE_METRIC_ID,
108            1,
109            &[]
110        )
111    }
112
113    pub async fn chip_power_down_failure(&self) {
114        log_cobalt!(
115            self.cobalt_proxy,
116            log_occurrence,
117            metrics::STOP_FAILURE_OCCURRENCE_METRIC_ID,
118            1,
119            &[]
120        )
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use crate::testing::setup_test;
128    use assert_matches::assert_matches;
129    use diagnostics_assertions::{AnyNumericProperty, assert_data_tree};
130    use futures::pin_mut;
131    use futures::task::Poll;
132    use std::pin::pin;
133
134    #[fuchsia::test]
135    fn test_iface_power_event_in_inspect() {
136        let mut test_helper = setup_test();
137        let node = test_helper.create_inspect_node("wlan_mock_node");
138        let power_logger = PowerLogger::new(test_helper.cobalt_proxy.clone(), &node);
139
140        let test_fut = power_logger.log_iface_power_event(IfacePowerLevel::NoPowerSavings, 11);
141        pin_mut!(test_fut);
142        assert_eq!(
143            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
144            Poll::Ready(())
145        );
146        let test_fut = power_logger.log_iface_power_event(IfacePowerLevel::NoPowerSavings, 22);
147        pin_mut!(test_fut);
148        assert_eq!(
149            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
150            Poll::Ready(())
151        );
152        let mut test_fut = pin!(power_logger.log_iface_power_event(IfacePowerLevel::Normal, 33));
153        assert_eq!(
154            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
155            Poll::Ready(())
156        );
157
158        // Update the first one
159        let test_fut = power_logger.log_iface_power_event(IfacePowerLevel::SuspendMode, 11);
160        pin_mut!(test_fut);
161        assert_eq!(
162            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
163            Poll::Ready(())
164        );
165
166        assert_data_tree!(@executor test_helper.exec, test_helper.inspector, root: contains {
167            wlan_mock_node: {
168                iface_power_events: {
169                    "0": {
170                        "power_level": "NoPowerSavings",
171                        "iface_id": 11_u64,
172                        "@time": AnyNumericProperty
173                    },
174                    "1": {
175                        "power_level": "NoPowerSavings",
176                        "iface_id": 22_u64,
177                        "@time": AnyNumericProperty
178                    },
179                    "2": {
180                        "power_level": "Normal",
181                        "iface_id": 33_u64,
182                        "@time": AnyNumericProperty
183                    },
184                    "3": {
185                        "power_level": "SuspendMode",
186                        "iface_id": 11_u64,
187                        "@time": AnyNumericProperty
188                    },
189                }
190            }
191        });
192    }
193
194    #[fuchsia::test]
195    fn test_iface_power_event_adds_to_internal_hashmap() {
196        let mut test_helper = setup_test();
197        let node = test_helper.create_inspect_node("wlan_mock_node");
198        let power_logger = PowerLogger::new(test_helper.cobalt_proxy.clone(), &node);
199
200        let test_fut = power_logger.log_iface_power_event(IfacePowerLevel::NoPowerSavings, 11);
201        pin_mut!(test_fut);
202        assert_eq!(
203            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
204            Poll::Ready(())
205        );
206        let test_fut = power_logger.log_iface_power_event(IfacePowerLevel::NoPowerSavings, 22);
207        pin_mut!(test_fut);
208        assert_eq!(
209            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
210            Poll::Ready(())
211        );
212        let mut test_fut = pin!(power_logger.log_iface_power_event(IfacePowerLevel::Normal, 33));
213        assert_eq!(
214            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
215            Poll::Ready(())
216        );
217
218        // Send a new value for the first iface_id, ensuring it gets updated
219        let test_fut = power_logger.log_iface_power_event(IfacePowerLevel::SuspendMode, 11);
220        pin_mut!(test_fut);
221        assert_eq!(
222            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
223            Poll::Ready(())
224        );
225
226        assert_eq!(power_logger.iface_power_states.try_lock().unwrap().len(), 3);
227        assert_eq!(
228            power_logger.iface_power_states.try_lock().unwrap().get(&11),
229            Some(&IfacePowerLevel::SuspendMode)
230        );
231        assert_eq!(
232            power_logger.iface_power_states.try_lock().unwrap().get(&22),
233            Some(&IfacePowerLevel::NoPowerSavings)
234        );
235        assert_eq!(
236            power_logger.iface_power_states.try_lock().unwrap().get(&33),
237            Some(&IfacePowerLevel::Normal)
238        );
239    }
240
241    #[fuchsia::test]
242    fn test_disconnect_updates_internal_hashmap() {
243        let mut test_helper = setup_test();
244        let node = test_helper.create_inspect_node("wlan_mock_node");
245        let power_logger = PowerLogger::new(test_helper.cobalt_proxy.clone(), &node);
246
247        let _ =
248            power_logger.iface_power_states.try_lock().unwrap().insert(33, IfacePowerLevel::Normal);
249        assert_eq!(power_logger.iface_power_states.try_lock().unwrap().len(), 1);
250
251        let mut test_fut = pin!(power_logger.handle_iface_disconnect(33));
252        assert_eq!(
253            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
254            Poll::Ready(())
255        );
256        assert_eq!(power_logger.iface_power_states.try_lock().unwrap().len(), 1);
257        assert_eq!(
258            power_logger.iface_power_states.try_lock().unwrap().get(&33),
259            Some(&IfacePowerLevel::Disconnected)
260        );
261    }
262
263    #[fuchsia::test]
264    fn test_destroy_removes_from_internal_hashmap() {
265        let mut test_helper = setup_test();
266        let node = test_helper.create_inspect_node("wlan_mock_node");
267        let power_logger = PowerLogger::new(test_helper.cobalt_proxy.clone(), &node);
268
269        let _ =
270            power_logger.iface_power_states.try_lock().unwrap().insert(33, IfacePowerLevel::Normal);
271        assert_eq!(power_logger.iface_power_states.try_lock().unwrap().len(), 1);
272
273        let mut test_fut = pin!(power_logger.handle_iface_destroyed(33));
274        assert_eq!(
275            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
276            Poll::Ready(())
277        );
278        assert_eq!(power_logger.iface_power_states.try_lock().unwrap().len(), 0);
279    }
280
281    #[fuchsia::test]
282    fn test_imminent_suspension_logs_to_cobalt() {
283        let mut test_helper = setup_test();
284        let node = test_helper.create_inspect_node("wlan_mock_node");
285        let power_logger = PowerLogger::new(test_helper.cobalt_proxy.clone(), &node);
286
287        let _ =
288            power_logger.iface_power_states.try_lock().unwrap().insert(33, IfacePowerLevel::Normal);
289        let mut test_fut = pin!(power_logger.handle_suspend_imminent());
290        assert_eq!(
291            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
292            Poll::Ready(())
293        );
294
295        let logged_metrics =
296            test_helper.get_logged_metrics(metrics::POWER_LEVEL_AT_SUSPEND_METRIC_ID);
297        assert_matches!(&logged_metrics[..], [metric] => {
298            let expected_metric = fidl_fuchsia_metrics::MetricEvent {
299                metric_id: metrics::POWER_LEVEL_AT_SUSPEND_METRIC_ID,
300                event_codes: vec![metrics::PowerLevelAtSuspendMetricDimensionPowerLevel::PowerSaveMode
301                    .as_event_code()],
302                payload: fidl_fuchsia_metrics::MetricEventPayload::Count(1),
303            };
304            assert_eq!(metric, &expected_metric);
305        });
306    }
307
308    #[fuchsia::test]
309    fn test_unclear_power_demand_logs_to_cobalt() {
310        let mut test_helper = setup_test();
311        let node = test_helper.create_inspect_node("wlan_mock_node");
312        let power_logger = PowerLogger::new(test_helper.cobalt_proxy.clone(), &node);
313
314        let mut test_fut = pin!(power_logger.handle_unclear_power_demand(
315            UnclearPowerDemand::PowerSaveRequestedWhileSuspendModeEnabled
316        ));
317        assert_eq!(
318            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
319            Poll::Ready(())
320        );
321
322        let logged_metrics =
323            test_helper.get_logged_metrics(metrics::UNCLEAR_POWER_LEVEL_DEMAND_METRIC_ID);
324        assert_matches!(&logged_metrics[..], [metric] => {
325            let expected_metric = fidl_fuchsia_metrics::MetricEvent {
326                metric_id: metrics::UNCLEAR_POWER_LEVEL_DEMAND_METRIC_ID,
327                event_codes: vec![metrics::UnclearPowerLevelDemandMetricDimensionReason::PowerSaveRequestedWhileSuspendModeEnabled
328                    .as_event_code()],
329                payload: fidl_fuchsia_metrics::MetricEventPayload::Count(1),
330            };
331            assert_eq!(metric, &expected_metric);
332        });
333    }
334
335    #[fuchsia::test]
336    fn test_chip_power_up_failure_logs_to_cobalt() {
337        let mut test_helper = setup_test();
338        let node = test_helper.create_inspect_node("wlan_mock_node");
339        let power_logger = PowerLogger::new(test_helper.cobalt_proxy.clone(), &node);
340
341        let mut test_fut = pin!(power_logger.handle_chip_power_up_failure());
342        assert_eq!(
343            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
344            Poll::Ready(())
345        );
346
347        let logged_metrics =
348            test_helper.get_logged_metrics(metrics::CHIP_POWER_UP_FAILURE_METRIC_ID);
349        assert_matches!(&logged_metrics[..], [metric] => {
350            let expected_metric = fidl_fuchsia_metrics::MetricEvent {
351                metric_id: metrics::CHIP_POWER_UP_FAILURE_METRIC_ID,
352                event_codes: vec![],
353                payload: fidl_fuchsia_metrics::MetricEventPayload::Count(1),
354            };
355            assert_eq!(metric, &expected_metric);
356        });
357    }
358
359    #[fuchsia::test]
360    fn test_failed_to_stop_logs_to_cobalt() {
361        let mut test_helper = setup_test();
362        let node = test_helper.create_inspect_node("wlan_mock_node");
363        let power_logger = PowerLogger::new(test_helper.cobalt_proxy.clone(), &node);
364
365        let mut test_fut = pin!(power_logger.chip_power_down_failure());
366        assert_eq!(
367            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
368            Poll::Ready(())
369        );
370
371        let logged_metrics =
372            test_helper.get_logged_metrics(metrics::STOP_FAILURE_OCCURRENCE_METRIC_ID);
373        assert_matches!(&logged_metrics[..], [metric] => {
374            let expected_metric = fidl_fuchsia_metrics::MetricEvent {
375                metric_id: metrics::STOP_FAILURE_OCCURRENCE_METRIC_ID,
376                event_codes: vec![],
377                payload: fidl_fuchsia_metrics::MetricEventPayload::Count(1),
378            };
379            assert_eq!(metric, &expected_metric);
380        });
381    }
382}