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