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    pub async fn chip_power_down_failure(&self) {
111        log_cobalt!(
112            self.cobalt_proxy,
113            log_occurrence,
114            metrics::STOP_FAILURE_OCCURRENCE_METRIC_ID,
115            1,
116            &[]
117        )
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::testing::setup_test;
125    use assert_matches::assert_matches;
126    use diagnostics_assertions::{AnyNumericProperty, assert_data_tree};
127    use futures::pin_mut;
128    use futures::task::Poll;
129    use std::pin::pin;
130
131    #[fuchsia::test]
132    fn test_iface_power_event_in_inspect() {
133        let mut test_helper = setup_test();
134        let node = test_helper.create_inspect_node("wlan_mock_node");
135        let power_logger = PowerLogger::new(test_helper.filtered_cobalt_logger(), &node);
136
137        let test_fut = power_logger.log_iface_power_event(IfacePowerLevel::NoPowerSavings, 11);
138        pin_mut!(test_fut);
139        assert_eq!(
140            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
141            Poll::Ready(())
142        );
143        let test_fut = power_logger.log_iface_power_event(IfacePowerLevel::NoPowerSavings, 22);
144        pin_mut!(test_fut);
145        assert_eq!(
146            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
147            Poll::Ready(())
148        );
149        let mut test_fut = pin!(power_logger.log_iface_power_event(IfacePowerLevel::Normal, 33));
150        assert_eq!(
151            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
152            Poll::Ready(())
153        );
154
155        // Update the first one
156        let test_fut = power_logger.log_iface_power_event(IfacePowerLevel::SuspendMode, 11);
157        pin_mut!(test_fut);
158        assert_eq!(
159            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
160            Poll::Ready(())
161        );
162
163        assert_data_tree!(@executor test_helper.exec, test_helper.inspector, root: contains {
164            wlan_mock_node: {
165                iface_power_events: {
166                    "0": {
167                        "power_level": "NoPowerSavings",
168                        "iface_id": 11_u64,
169                        "@time": AnyNumericProperty
170                    },
171                    "1": {
172                        "power_level": "NoPowerSavings",
173                        "iface_id": 22_u64,
174                        "@time": AnyNumericProperty
175                    },
176                    "2": {
177                        "power_level": "Normal",
178                        "iface_id": 33_u64,
179                        "@time": AnyNumericProperty
180                    },
181                    "3": {
182                        "power_level": "SuspendMode",
183                        "iface_id": 11_u64,
184                        "@time": AnyNumericProperty
185                    },
186                }
187            }
188        });
189    }
190
191    #[fuchsia::test]
192    fn test_iface_power_event_adds_to_internal_hashmap() {
193        let mut test_helper = setup_test();
194        let node = test_helper.create_inspect_node("wlan_mock_node");
195        let power_logger = PowerLogger::new(test_helper.filtered_cobalt_logger(), &node);
196
197        let test_fut = power_logger.log_iface_power_event(IfacePowerLevel::NoPowerSavings, 11);
198        pin_mut!(test_fut);
199        assert_eq!(
200            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
201            Poll::Ready(())
202        );
203        let test_fut = power_logger.log_iface_power_event(IfacePowerLevel::NoPowerSavings, 22);
204        pin_mut!(test_fut);
205        assert_eq!(
206            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
207            Poll::Ready(())
208        );
209        let mut test_fut = pin!(power_logger.log_iface_power_event(IfacePowerLevel::Normal, 33));
210        assert_eq!(
211            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
212            Poll::Ready(())
213        );
214
215        // Send a new value for the first iface_id, ensuring it gets updated
216        let test_fut = power_logger.log_iface_power_event(IfacePowerLevel::SuspendMode, 11);
217        pin_mut!(test_fut);
218        assert_eq!(
219            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
220            Poll::Ready(())
221        );
222
223        assert_eq!(power_logger.iface_power_states.try_lock().unwrap().len(), 3);
224        assert_eq!(
225            power_logger.iface_power_states.try_lock().unwrap().get(&11),
226            Some(&IfacePowerLevel::SuspendMode)
227        );
228        assert_eq!(
229            power_logger.iface_power_states.try_lock().unwrap().get(&22),
230            Some(&IfacePowerLevel::NoPowerSavings)
231        );
232        assert_eq!(
233            power_logger.iface_power_states.try_lock().unwrap().get(&33),
234            Some(&IfacePowerLevel::Normal)
235        );
236    }
237
238    #[fuchsia::test]
239    fn test_disconnect_updates_internal_hashmap() {
240        let mut test_helper = setup_test();
241        let node = test_helper.create_inspect_node("wlan_mock_node");
242        let power_logger = PowerLogger::new(test_helper.filtered_cobalt_logger(), &node);
243
244        let _ =
245            power_logger.iface_power_states.try_lock().unwrap().insert(33, IfacePowerLevel::Normal);
246        assert_eq!(power_logger.iface_power_states.try_lock().unwrap().len(), 1);
247
248        let mut test_fut = pin!(power_logger.handle_iface_disconnect(33));
249        assert_eq!(
250            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
251            Poll::Ready(())
252        );
253        assert_eq!(power_logger.iface_power_states.try_lock().unwrap().len(), 1);
254        assert_eq!(
255            power_logger.iface_power_states.try_lock().unwrap().get(&33),
256            Some(&IfacePowerLevel::Disconnected)
257        );
258    }
259
260    #[fuchsia::test]
261    fn test_destroy_removes_from_internal_hashmap() {
262        let mut test_helper = setup_test();
263        let node = test_helper.create_inspect_node("wlan_mock_node");
264        let power_logger = PowerLogger::new(test_helper.filtered_cobalt_logger(), &node);
265
266        let _ =
267            power_logger.iface_power_states.try_lock().unwrap().insert(33, IfacePowerLevel::Normal);
268        assert_eq!(power_logger.iface_power_states.try_lock().unwrap().len(), 1);
269
270        let mut test_fut = pin!(power_logger.handle_iface_destroyed(33));
271        assert_eq!(
272            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
273            Poll::Ready(())
274        );
275        assert_eq!(power_logger.iface_power_states.try_lock().unwrap().len(), 0);
276    }
277
278    #[fuchsia::test]
279    fn test_imminent_suspension_logs_to_cobalt() {
280        let mut test_helper = setup_test();
281        let node = test_helper.create_inspect_node("wlan_mock_node");
282        let power_logger = PowerLogger::new(test_helper.filtered_cobalt_logger(), &node);
283
284        let _ =
285            power_logger.iface_power_states.try_lock().unwrap().insert(33, IfacePowerLevel::Normal);
286        let mut test_fut = pin!(power_logger.handle_suspend_imminent());
287        assert_eq!(
288            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
289            Poll::Ready(())
290        );
291
292        let logged_metrics =
293            test_helper.get_logged_metrics(metrics::POWER_LEVEL_AT_SUSPEND_METRIC_ID);
294        assert_matches!(&logged_metrics[..], [metric] => {
295            let expected_metric = fidl_fuchsia_metrics::MetricEvent {
296                metric_id: metrics::POWER_LEVEL_AT_SUSPEND_METRIC_ID,
297                event_codes: vec![metrics::PowerLevelAtSuspendMetricDimensionPowerLevel::PowerSaveMode
298                    .as_event_code()],
299                payload: fidl_fuchsia_metrics::MetricEventPayload::Count(1),
300            };
301            assert_eq!(metric, &expected_metric);
302        });
303    }
304
305    #[fuchsia::test]
306    fn test_unclear_power_demand_logs_to_cobalt() {
307        let mut test_helper = setup_test();
308        let node = test_helper.create_inspect_node("wlan_mock_node");
309        let power_logger = PowerLogger::new(test_helper.filtered_cobalt_logger(), &node);
310
311        let mut test_fut = pin!(power_logger.handle_unclear_power_demand(
312            UnclearPowerDemand::PowerSaveRequestedWhileSuspendModeEnabled
313        ));
314        assert_eq!(
315            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
316            Poll::Ready(())
317        );
318
319        let logged_metrics =
320            test_helper.get_logged_metrics(metrics::UNCLEAR_POWER_LEVEL_DEMAND_METRIC_ID);
321        assert_matches!(&logged_metrics[..], [metric] => {
322            let expected_metric = fidl_fuchsia_metrics::MetricEvent {
323                metric_id: metrics::UNCLEAR_POWER_LEVEL_DEMAND_METRIC_ID,
324                event_codes: vec![metrics::UnclearPowerLevelDemandMetricDimensionReason::PowerSaveRequestedWhileSuspendModeEnabled
325                    .as_event_code()],
326                payload: fidl_fuchsia_metrics::MetricEventPayload::Count(1),
327            };
328            assert_eq!(metric, &expected_metric);
329        });
330    }
331
332    #[fuchsia::test]
333    fn test_chip_power_up_failure_logs_to_cobalt() {
334        let mut test_helper = setup_test();
335        let node = test_helper.create_inspect_node("wlan_mock_node");
336        let power_logger = PowerLogger::new(test_helper.filtered_cobalt_logger(), &node);
337
338        let mut test_fut = pin!(power_logger.handle_chip_power_up_failure());
339        assert_eq!(
340            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
341            Poll::Ready(())
342        );
343
344        let logged_metrics =
345            test_helper.get_logged_metrics(metrics::CHIP_POWER_UP_FAILURE_METRIC_ID);
346        assert_matches!(&logged_metrics[..], [metric] => {
347            let expected_metric = fidl_fuchsia_metrics::MetricEvent {
348                metric_id: metrics::CHIP_POWER_UP_FAILURE_METRIC_ID,
349                event_codes: vec![],
350                payload: fidl_fuchsia_metrics::MetricEventPayload::Count(1),
351            };
352            assert_eq!(metric, &expected_metric);
353        });
354    }
355
356    #[fuchsia::test]
357    fn test_failed_to_stop_logs_to_cobalt() {
358        let mut test_helper = setup_test();
359        let node = test_helper.create_inspect_node("wlan_mock_node");
360        let power_logger = PowerLogger::new(test_helper.filtered_cobalt_logger(), &node);
361
362        let mut test_fut = pin!(power_logger.chip_power_down_failure());
363        assert_eq!(
364            test_helper.run_until_stalled_drain_cobalt_events(&mut test_fut),
365            Poll::Ready(())
366        );
367
368        let logged_metrics =
369            test_helper.get_logged_metrics(metrics::STOP_FAILURE_OCCURRENCE_METRIC_ID);
370        assert_matches!(&logged_metrics[..], [metric] => {
371            let expected_metric = fidl_fuchsia_metrics::MetricEvent {
372                metric_id: metrics::STOP_FAILURE_OCCURRENCE_METRIC_ID,
373                event_codes: vec![],
374                payload: fidl_fuchsia_metrics::MetricEventPayload::Count(1),
375            };
376            assert_eq!(metric, &expected_metric);
377        });
378    }
379}