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_1dot1;
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_1dot1_proxy: fidl_fuchsia_metrics::MetricEventLoggerProxy,
32    iface_power_states: Arc<Mutex<HashMap<u16, IfacePowerLevel>>>,
33}
34
35impl PowerLogger {
36    pub fn new(
37        cobalt_1dot1_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_1dot1_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_1dot1!(
73                self.cobalt_1dot1_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_1dot1!(
91            self.cobalt_1dot1_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
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::testing::setup_test;
108    use diagnostics_assertions::{assert_data_tree, AnyNumericProperty};
109    use futures::pin_mut;
110    use futures::task::Poll;
111    use std::pin::pin;
112    use wlan_common::assert_variant;
113
114    #[fuchsia::test]
115    fn test_iface_power_event_in_inspect() {
116        let mut test_helper = setup_test();
117        let node = test_helper.create_inspect_node("wlan_mock_node");
118        let power_logger = PowerLogger::new(test_helper.cobalt_1dot1_proxy.clone(), &node);
119
120        let test_fut = power_logger.log_iface_power_event(IfacePowerLevel::NoPowerSavings, 11);
121        pin_mut!(test_fut);
122        assert_eq!(
123            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
124            Poll::Ready(())
125        );
126        let test_fut = power_logger.log_iface_power_event(IfacePowerLevel::NoPowerSavings, 22);
127        pin_mut!(test_fut);
128        assert_eq!(
129            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
130            Poll::Ready(())
131        );
132        let mut test_fut = pin!(power_logger.log_iface_power_event(IfacePowerLevel::Normal, 33));
133        assert_eq!(
134            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
135            Poll::Ready(())
136        );
137
138        // Update the first one
139        let test_fut = power_logger.log_iface_power_event(IfacePowerLevel::SuspendMode, 11);
140        pin_mut!(test_fut);
141        assert_eq!(
142            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
143            Poll::Ready(())
144        );
145
146        assert_data_tree!(test_helper.inspector, root: contains {
147            wlan_mock_node: {
148                iface_power_events: {
149                    "0": {
150                        "power_level": "NoPowerSavings",
151                        "iface_id": 11_u64,
152                        "@time": AnyNumericProperty
153                    },
154                    "1": {
155                        "power_level": "NoPowerSavings",
156                        "iface_id": 22_u64,
157                        "@time": AnyNumericProperty
158                    },
159                    "2": {
160                        "power_level": "Normal",
161                        "iface_id": 33_u64,
162                        "@time": AnyNumericProperty
163                    },
164                    "3": {
165                        "power_level": "SuspendMode",
166                        "iface_id": 11_u64,
167                        "@time": AnyNumericProperty
168                    },
169                }
170            }
171        });
172    }
173
174    #[fuchsia::test]
175    fn test_iface_power_event_adds_to_internal_hashmap() {
176        let mut test_helper = setup_test();
177        let node = test_helper.create_inspect_node("wlan_mock_node");
178        let power_logger = PowerLogger::new(test_helper.cobalt_1dot1_proxy.clone(), &node);
179
180        let test_fut = power_logger.log_iface_power_event(IfacePowerLevel::NoPowerSavings, 11);
181        pin_mut!(test_fut);
182        assert_eq!(
183            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
184            Poll::Ready(())
185        );
186        let test_fut = power_logger.log_iface_power_event(IfacePowerLevel::NoPowerSavings, 22);
187        pin_mut!(test_fut);
188        assert_eq!(
189            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
190            Poll::Ready(())
191        );
192        let mut test_fut = pin!(power_logger.log_iface_power_event(IfacePowerLevel::Normal, 33));
193        assert_eq!(
194            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
195            Poll::Ready(())
196        );
197
198        // Send a new value for the first iface_id, ensuring it gets updated
199        let test_fut = power_logger.log_iface_power_event(IfacePowerLevel::SuspendMode, 11);
200        pin_mut!(test_fut);
201        assert_eq!(
202            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
203            Poll::Ready(())
204        );
205
206        assert_eq!(power_logger.iface_power_states.try_lock().unwrap().len(), 3);
207        assert_eq!(
208            power_logger.iface_power_states.try_lock().unwrap().get(&11),
209            Some(&IfacePowerLevel::SuspendMode)
210        );
211        assert_eq!(
212            power_logger.iface_power_states.try_lock().unwrap().get(&22),
213            Some(&IfacePowerLevel::NoPowerSavings)
214        );
215        assert_eq!(
216            power_logger.iface_power_states.try_lock().unwrap().get(&33),
217            Some(&IfacePowerLevel::Normal)
218        );
219    }
220
221    #[fuchsia::test]
222    fn test_disconnect_updates_internal_hashmap() {
223        let mut test_helper = setup_test();
224        let node = test_helper.create_inspect_node("wlan_mock_node");
225        let power_logger = PowerLogger::new(test_helper.cobalt_1dot1_proxy.clone(), &node);
226
227        let _ =
228            power_logger.iface_power_states.try_lock().unwrap().insert(33, IfacePowerLevel::Normal);
229        assert_eq!(power_logger.iface_power_states.try_lock().unwrap().len(), 1);
230
231        let mut test_fut = pin!(power_logger.handle_iface_disconnect(33));
232        assert_eq!(
233            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
234            Poll::Ready(())
235        );
236        assert_eq!(power_logger.iface_power_states.try_lock().unwrap().len(), 1);
237        assert_eq!(
238            power_logger.iface_power_states.try_lock().unwrap().get(&33),
239            Some(&IfacePowerLevel::Disconnected)
240        );
241    }
242
243    #[fuchsia::test]
244    fn test_destroy_removes_from_internal_hashmap() {
245        let mut test_helper = setup_test();
246        let node = test_helper.create_inspect_node("wlan_mock_node");
247        let power_logger = PowerLogger::new(test_helper.cobalt_1dot1_proxy.clone(), &node);
248
249        let _ =
250            power_logger.iface_power_states.try_lock().unwrap().insert(33, IfacePowerLevel::Normal);
251        assert_eq!(power_logger.iface_power_states.try_lock().unwrap().len(), 1);
252
253        let mut test_fut = pin!(power_logger.handle_iface_destroyed(33));
254        assert_eq!(
255            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
256            Poll::Ready(())
257        );
258        assert_eq!(power_logger.iface_power_states.try_lock().unwrap().len(), 0);
259    }
260
261    #[fuchsia::test]
262    fn test_imminent_suspension_logs_to_cobalt() {
263        let mut test_helper = setup_test();
264        let node = test_helper.create_inspect_node("wlan_mock_node");
265        let power_logger = PowerLogger::new(test_helper.cobalt_1dot1_proxy.clone(), &node);
266
267        let _ =
268            power_logger.iface_power_states.try_lock().unwrap().insert(33, IfacePowerLevel::Normal);
269        let mut test_fut = pin!(power_logger.handle_suspend_imminent());
270        assert_eq!(
271            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
272            Poll::Ready(())
273        );
274
275        let logged_metrics =
276            test_helper.get_logged_metrics(metrics::POWER_LEVEL_AT_SUSPEND_METRIC_ID);
277        assert_variant!(&logged_metrics[..], [metric] => {
278            let expected_metric = fidl_fuchsia_metrics::MetricEvent {
279                metric_id: metrics::POWER_LEVEL_AT_SUSPEND_METRIC_ID,
280                event_codes: vec![metrics::PowerLevelAtSuspendMetricDimensionPowerLevel::PowerSaveMode
281                    .as_event_code()],
282                payload: fidl_fuchsia_metrics::MetricEventPayload::Count(1),
283            };
284            assert_eq!(metric, &expected_metric);
285        });
286    }
287
288    #[fuchsia::test]
289    fn test_unclear_power_demand_logs_to_cobalt() {
290        let mut test_helper = setup_test();
291        let node = test_helper.create_inspect_node("wlan_mock_node");
292        let power_logger = PowerLogger::new(test_helper.cobalt_1dot1_proxy.clone(), &node);
293
294        let mut test_fut = pin!(power_logger.handle_unclear_power_demand(
295            UnclearPowerDemand::PowerSaveRequestedWhileSuspendModeEnabled
296        ));
297        assert_eq!(
298            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
299            Poll::Ready(())
300        );
301
302        let logged_metrics =
303            test_helper.get_logged_metrics(metrics::UNCLEAR_POWER_LEVEL_DEMAND_METRIC_ID);
304        assert_variant!(&logged_metrics[..], [metric] => {
305            let expected_metric = fidl_fuchsia_metrics::MetricEvent {
306                metric_id: metrics::UNCLEAR_POWER_LEVEL_DEMAND_METRIC_ID,
307                event_codes: vec![metrics::UnclearPowerLevelDemandMetricDimensionReason::PowerSaveRequestedWhileSuspendModeEnabled
308                    .as_event_code()],
309                payload: fidl_fuchsia_metrics::MetricEventPayload::Count(1),
310            };
311            assert_eq!(metric, &expected_metric);
312        });
313    }
314}