Skip to main content

starnix_modules_power_residency/
lib.rs

1// Copyright 2026 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
5//! Exposes power residency entities from [`PowerStatsManager`] under platform devices in
6//! `/sys/devices/platform`, such as `powerdashboard` and `cap_sysfs`.
7
8use fidl_fuchsia_hardware_power_stats as fpowerstats;
9use starnix_core::device::power_stats::PowerStatsManager;
10use starnix_core::fs::sysfs::build_device_directory;
11use starnix_core::task::{CurrentTask, Kernel};
12use starnix_core::vfs::pseudo::simple_file::{BytesFile, BytesFileOps};
13use starnix_logging::log_warn;
14use starnix_uapi::errno;
15use starnix_uapi::errors::Errno;
16use starnix_uapi::file_mode::mode;
17use std::borrow::Cow;
18use std::collections::{BTreeMap, HashSet};
19use std::fmt::Write as _;
20
21/// Power entities whose names start with this prefix are served as performance state residency
22/// files of the same name.
23const PERF_STATE_FILE_PREFIX: &str = "lpcm_";
24
25/// Power entities whose names start with this prefix are served as power domain residency files
26/// of the same name.
27const POWER_DOMAIN_FILE_PREFIX: &str = "pwrblk_";
28
29/// Power entity served as the platform power state residency file.
30const PLATFORM_POWER_ENTITY: &str = "soc_power_state";
31
32/// Name of the file [`PLATFORM_POWER_ENTITY`] is served as.
33const PLATFORM_POWER_FILE: &str = "platform_power_residency";
34
35/// Name of the platform device under which the residency files are served.
36const RESIDENCY_DEVICE_NAME: &str = "powerdashboard";
37
38/// Power entity served as the power state stats file.
39const POWER_STATE_STATS_ENTITY: &str = "cap_power_state";
40
41/// Name of the file [`POWER_STATE_STATS_ENTITY`] is served as.
42const POWER_STATE_STATS_FILE: &str = "power_state_stats";
43
44/// Name of the platform device under which [`POWER_STATE_STATS_FILE`] is served.
45const POWER_STATE_STATS_DEVICE_NAME: &str = "cap_sysfs";
46
47/// Ticks per millisecond of the counter that some residency files report timestamps in.
48///
49/// Those files report raw values of the platform timer counter, which is expected to run at
50/// 38.4 MHz, so the provider's millisecond timestamps are converted at 38,400 ticks per
51/// millisecond.
52const TIMESTAMP_TICKS_PER_MS: u64 = 38_400;
53
54/// Formats the residency of a power entity as the contents of its residency file.
55type FormatResidency = fn(&fpowerstats::Stats) -> String;
56
57/// Where and how a power entity is served.
58struct ResidencyFileSpec {
59    /// Name of the platform device under which the file is served.
60    device: &'static str,
61    /// Name of the file within the device directory.
62    file: String,
63    /// Name of the power entity whose residency the file serves.
64    entity: String,
65    format: FormatResidency,
66}
67
68/// Serves a file for each power entity reported over `fuchsia.hardware.power.stats.Service` that
69/// [`residency_file_spec`] maps to one, under its platform device.
70///
71/// The entities are only known once a provider has started, so each device is added asynchronously
72/// via [`PowerStatsManager`], once a provider reports at least one entity served under it. Served
73/// entities for a device from other providers are ignored.
74pub fn init_power_residency_device(kernel: &Kernel) {
75    let mut added_devices: HashSet<&'static str> = HashSet::new();
76    let manager = PowerStatsManager::get_or_init(kernel);
77    let manager_for_files = manager.clone();
78    manager.watch_providers(kernel, move |kernel, provider| {
79        let mut files_by_device: BTreeMap<&'static str, Vec<ResidencyFileSpec>> = BTreeMap::new();
80        for spec in provider.entity_names().iter().filter_map(|name| residency_file_spec(name)) {
81            files_by_device.entry(spec.device).or_default().push(spec);
82        }
83        let registry = &kernel.device_registry;
84        for (device_name, files) in files_by_device {
85            if !added_devices.insert(device_name) {
86                log_warn!(
87                    "Ignoring {device_name} power residency entities from additional provider {}",
88                    provider.instance_name()
89                );
90                continue;
91            }
92            registry.add_bus_device(
93                device_name.into(),
94                Some(registry.objects.platform_device()),
95                registry.objects.platform_bus(),
96                |device, dir| {
97                    build_device_directory(device, dir);
98                    for spec in files {
99                        let file = ResidencyFile {
100                            entity: spec.entity,
101                            format: spec.format,
102                            manager: manager_for_files.clone(),
103                        };
104                        dir.entry(&spec.file, BytesFile::new_node(file), mode!(IFREG, 0o444));
105                    }
106                },
107            );
108        }
109    });
110}
111
112/// Returns where and how to serve the power entity named `entity`, or [`None`] if it is not
113/// served. Prefixed entities are only served if their names are safe to use as file names.
114fn residency_file_spec(entity: &str) -> Option<ResidencyFileSpec> {
115    let prefixed = |prefix: &str| {
116        entity.len() > prefix.len()
117            && entity.starts_with(prefix)
118            && entity.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
119    };
120    let (device, file, format): (_, _, FormatResidency) = if prefixed(PERF_STATE_FILE_PREFIX) {
121        (RESIDENCY_DEVICE_NAME, entity.to_string(), format_performance_states)
122    } else if prefixed(POWER_DOMAIN_FILE_PREFIX) {
123        (RESIDENCY_DEVICE_NAME, entity.to_string(), format_power_domain_states)
124    } else if entity == PLATFORM_POWER_ENTITY {
125        (RESIDENCY_DEVICE_NAME, PLATFORM_POWER_FILE.to_string(), format_platform_power_states)
126    } else if entity == POWER_STATE_STATS_ENTITY {
127        (
128            POWER_STATE_STATS_DEVICE_NAME,
129            POWER_STATE_STATS_FILE.to_string(),
130            format_power_state_stats,
131        )
132    } else {
133        return None;
134    };
135    Some(ResidencyFileSpec { device, file, entity: entity.to_string(), format })
136}
137
138/// Serves the residency of a single power entity, as formatted by `format`.
139struct ResidencyFile {
140    entity: String,
141    format: FormatResidency,
142    manager: PowerStatsManager,
143}
144
145impl BytesFileOps for ResidencyFile {
146    fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
147        let stats = self.manager.entity_stats(&self.entity)?.ok_or_else(|| {
148            log_warn!("No power stats provider registered for {}", self.entity);
149            errno!(EIO)
150        })?;
151        Ok((self.format)(&stats).into_bytes().into())
152    }
153}
154
155/// Returns the states in `stats` whose names are safe to write to a line-based residency file,
156/// logging a warning for any state left out.
157///
158/// States whose names contain control characters, such as newlines, are left out, as their names
159/// would break the line-based layout.
160fn safe_states(stats: &fpowerstats::Stats) -> impl Iterator<Item = &fpowerstats::StateResidency> {
161    stats.state_residency_data.iter().filter(|state| {
162        if state.name.chars().any(char::is_control) {
163            log_warn!("Leaving out {} state {} with an unsafe name", stats.entity, state.id);
164            return false;
165        }
166        true
167    })
168}
169
170/// Formats `stats` as a `num_pf_states` line followed by `pf_*` lines for each performance level.
171///
172/// The power stats API does not report times with sub-millisecond precision, so times are whole
173/// milliseconds converted to microseconds.
174fn format_performance_states(stats: &fpowerstats::Stats) -> String {
175    let mut out = String::new();
176    let _ = writeln!(out, "num_pf_states: {}", stats.state_residency_data.len());
177    for state in &stats.state_residency_data {
178        let time_in_state_us = state.total_time_ms.saturating_mul(1000);
179        let _ = writeln!(out, "pf_state: {}", state.id);
180        let _ = writeln!(out, "pf_entry_count: {}", state.entry_count);
181        let _ = writeln!(out, "pf_time_in_state: {time_in_state_us} (us)\n");
182    }
183    out
184}
185
186/// Formats `stats` as an `SSWRP lpb` header followed by `power_state` lines for each power
187/// state.
188///
189/// States whose names contain control characters, such as newlines, are left out, as their names
190/// would break the line-based layout.
191///
192/// The power stats API does not report times with sub-millisecond precision or when a state was
193/// last exited, so times are whole milliseconds converted to microseconds and last exit
194/// timestamps are 0.
195fn format_power_domain_states(stats: &fpowerstats::Stats) -> String {
196    let mut out = String::new();
197    let _ = writeln!(out, "\nSSWRP lpb\n");
198    for state in safe_states(stats) {
199        let time_in_state_us = state.total_time_ms.saturating_mul(1000);
200        let last_entry_us = state.last_entry_timestamp_ms.saturating_mul(1000);
201        let _ = writeln!(out, "power_state: {}", state.name);
202        let _ = writeln!(out, "entry_count: {}", state.entry_count);
203        let _ = writeln!(out, "time_in_state: {time_in_state_us} (us)");
204        let _ = writeln!(out, "last_entry_timestamp: {last_entry_us}");
205        let _ = writeln!(out, "last_exit_timestamp: 0");
206    }
207    out.push('\n');
208    out
209}
210
211/// Formats `stats` as a `power_state` line followed by counts and timestamps for each power
212/// state.
213///
214/// States whose names contain control characters, such as newlines, are left out, as their names
215/// would break the line-based layout.
216///
217/// The power stats API does not report which state is current or when a state was last exited,
218/// so there are no current state lines and last exit timestamps are 0.
219fn format_platform_power_states(stats: &fpowerstats::Stats) -> String {
220    let mut out = String::new();
221    for state in safe_states(stats) {
222        let last_entry_ticks = state.last_entry_timestamp_ms.saturating_mul(TIMESTAMP_TICKS_PER_MS);
223        let _ = writeln!(out, "power_state: {}", state.name);
224        let _ = writeln!(out, "entry_count: {}", state.entry_count);
225        let _ = writeln!(out, "time_in_state_ms: {}", state.total_time_ms);
226        let _ = writeln!(out, "last_entry_timestamp_ticks: {last_entry_ticks}");
227        let _ = writeln!(out, "last_exit_timestamp_ticks: 0\n");
228    }
229    out
230}
231
232/// Formats `stats` as `---`-delimited records of `state`, timestamps, residency, and entry count
233/// for each power state.
234///
235/// States whose names contain control characters, such as newlines, are left out, as their names
236/// would break the line-based layout.
237///
238/// The power stats API does not report when a state was last exited, so last exit timestamps are
239/// 0.
240fn format_power_state_stats(stats: &fpowerstats::Stats) -> String {
241    let mut out = String::new();
242    for state in safe_states(stats) {
243        let last_entry_ticks = state.last_entry_timestamp_ms.saturating_mul(TIMESTAMP_TICKS_PER_MS);
244        let _ = writeln!(out, "---");
245        let _ = writeln!(out, "state: {}", state.name);
246        let _ = writeln!(out, "last_entry_ts_ticks: {last_entry_ticks}");
247        let _ = writeln!(out, "last_exit_ts_ticks: 0");
248        let _ = writeln!(out, "total_state_residency_ms: {}", state.total_time_ms);
249        let _ = writeln!(out, "last_state_entry_counts: {}", state.entry_count);
250    }
251    out
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257    use fidl::endpoints::{ServerEnd, create_sync_proxy};
258    use fuchsia_async as fasync;
259    use futures::StreamExt;
260    use starnix_core::device::power_stats::PowerStatsProvider;
261    use starnix_core::testing::spawn_kernel_and_run_sync;
262    use starnix_uapi::error;
263
264    fn state(id: i32, entry_count: u32, total_time_ms: u64) -> fpowerstats::StateResidency {
265        fpowerstats::StateResidency {
266            id,
267            name: id.to_string(),
268            total_time_ms,
269            entry_count,
270            last_entry_timestamp_ms: 0,
271        }
272    }
273
274    fn stats(
275        entity: &str,
276        state_residency_data: Vec<fpowerstats::StateResidency>,
277    ) -> fpowerstats::Stats {
278        fpowerstats::Stats { entity: entity.to_string(), state_residency_data }
279    }
280
281    /// Serves a fake provider that reports `reports` on `stream`.
282    async fn serve_provider(
283        stream: fpowerstats::DeviceRequestStream,
284        reports: Vec<fpowerstats::Stats>,
285    ) {
286        let _ = stream
287            .for_each(|request| {
288                let reports = &reports;
289                async move {
290                    if let Ok(fpowerstats::DeviceRequest::GetStats { responder }) = request {
291                        let _ = responder.send(reports);
292                    }
293                }
294            })
295            .await;
296    }
297
298    /// Runs `serve` on its own thread, as residency files call providers synchronously.
299    fn spawn_server<F: Future<Output = ()> + 'static>(serve: impl FnOnce() -> F + Send + 'static) {
300        std::thread::spawn(move || fasync::LocalExecutor::default().run_singlethreaded(serve()));
301    }
302
303    /// Returns a [`PowerStatsProvider`] reporting `entity_names` for tests that only exercise
304    /// device registration.
305    fn provider_with_entities(
306        instance_name: &str,
307        entity_names: impl IntoIterator<Item = impl Into<String>>,
308    ) -> PowerStatsProvider {
309        fake_provider(
310            instance_name,
311            entity_names.into_iter().map(|name| stats(&name.into(), vec![])).collect(),
312        )
313    }
314
315    /// Returns a fake [`PowerStatsProvider`] named `instance_name` that serves `reports`.
316    fn fake_provider(instance_name: &str, reports: Vec<fpowerstats::Stats>) -> PowerStatsProvider {
317        let entity_names: Vec<_> = reports.iter().map(|stats| stats.entity.clone()).collect();
318        let (proxy, server) = create_sync_proxy::<fpowerstats::DeviceMarker>();
319        spawn_server(move || serve_provider(server.into_stream(), reports));
320        PowerStatsProvider::new(instance_name, entity_names, proxy)
321    }
322
323    /// Returns a performance state residency file serving `entity` from `manager`.
324    fn perf_file(entity: &str, manager: PowerStatsManager) -> ResidencyFile {
325        ResidencyFile { entity: entity.to_string(), format: format_performance_states, manager }
326    }
327
328    fn exists(kernel: &Kernel, path: &str) -> bool {
329        kernel.device_registry.objects.root.lookup(path.into()).is_some()
330    }
331
332    #[::fuchsia::test]
333    async fn device_is_added_with_served_entities() {
334        spawn_kernel_and_run_sync(|current_task| {
335            let kernel = current_task.kernel();
336            let prefix = PERF_STATE_FILE_PREFIX;
337            PowerStatsManager::add_provider_for_testing(
338                kernel,
339                provider_with_entities(
340                    "default",
341                    [
342                        format!("{prefix}a"),
343                        format!("{prefix}b"),
344                        PLATFORM_POWER_ENTITY.to_string(),
345                        "other".to_string(),
346                    ],
347                ),
348            );
349            init_power_residency_device(kernel);
350
351            let device = format!("devices/platform/{RESIDENCY_DEVICE_NAME}");
352            assert!(exists(kernel, &device));
353            assert!(exists(kernel, &format!("bus/platform/devices/{RESIDENCY_DEVICE_NAME}")));
354            assert!(exists(kernel, &format!("{device}/uevent")));
355            assert!(exists(kernel, &format!("{device}/{prefix}a")));
356            assert!(exists(kernel, &format!("{device}/{prefix}b")));
357            assert!(exists(kernel, &format!("{device}/{PLATFORM_POWER_FILE}")));
358            assert!(!exists(kernel, &format!("{device}/other")));
359        })
360        .await;
361    }
362
363    #[::fuchsia::test]
364    async fn no_device_without_served_entities() {
365        spawn_kernel_and_run_sync(|current_task| {
366            let kernel = current_task.kernel();
367            PowerStatsManager::add_provider_for_testing(
368                kernel,
369                provider_with_entities("default", ["other"]),
370            );
371            init_power_residency_device(kernel);
372
373            assert!(!exists(kernel, &format!("devices/platform/{RESIDENCY_DEVICE_NAME}")));
374            assert!(!exists(kernel, &format!("devices/platform/{POWER_STATE_STATS_DEVICE_NAME}")));
375        })
376        .await;
377    }
378
379    #[::fuchsia::test]
380    async fn device_is_added_when_later_provider_reports_served_entities() {
381        spawn_kernel_and_run_sync(|current_task| {
382            let kernel = current_task.kernel();
383            let prefix = PERF_STATE_FILE_PREFIX;
384            let device = format!("devices/platform/{RESIDENCY_DEVICE_NAME}");
385
386            let _ = PowerStatsManager::get_or_init_for_testing(kernel);
387            init_power_residency_device(kernel);
388            assert!(!exists(kernel, &device));
389
390            PowerStatsManager::add_provider_for_testing(
391                kernel,
392                provider_with_entities("unserved", ["other"]),
393            );
394            assert!(!exists(kernel, &device));
395
396            PowerStatsManager::add_provider_for_testing(
397                kernel,
398                provider_with_entities(
399                    "served",
400                    [format!("{prefix}a"), PLATFORM_POWER_ENTITY.to_string()],
401                ),
402            );
403            assert!(exists(kernel, &device));
404            assert!(exists(kernel, &format!("{device}/{prefix}a")));
405            assert!(exists(kernel, &format!("{device}/{PLATFORM_POWER_FILE}")));
406        })
407        .await;
408    }
409
410    #[::fuchsia::test]
411    async fn only_first_provider_is_served() {
412        spawn_kernel_and_run_sync(|current_task| {
413            let kernel = current_task.kernel();
414            let prefix = PERF_STATE_FILE_PREFIX;
415            PowerStatsManager::add_provider_for_testing(
416                kernel,
417                provider_with_entities("first", [format!("{prefix}a")]),
418            );
419            init_power_residency_device(kernel);
420            PowerStatsManager::add_provider_for_testing(
421                kernel,
422                provider_with_entities("second", [format!("{prefix}b")]),
423            );
424
425            let device = format!("devices/platform/{RESIDENCY_DEVICE_NAME}");
426            assert!(exists(kernel, &format!("{device}/{prefix}a")));
427            assert!(!exists(kernel, &format!("{device}/{prefix}b")));
428        })
429        .await;
430    }
431
432    #[::fuchsia::test]
433    async fn each_device_is_added_for_first_provider_serving_it() {
434        spawn_kernel_and_run_sync(|current_task| {
435            let kernel = current_task.kernel();
436            let prefix = PERF_STATE_FILE_PREFIX;
437            PowerStatsManager::add_provider_for_testing(
438                kernel,
439                provider_with_entities("first", [format!("{prefix}a")]),
440            );
441            init_power_residency_device(kernel);
442            PowerStatsManager::add_provider_for_testing(
443                kernel,
444                provider_with_entities(
445                    "second",
446                    [format!("{prefix}b"), POWER_STATE_STATS_ENTITY.to_string()],
447                ),
448            );
449
450            let residency_device = format!("devices/platform/{RESIDENCY_DEVICE_NAME}");
451            assert!(exists(kernel, &format!("{residency_device}/{prefix}a")));
452            assert!(!exists(kernel, &format!("{residency_device}/{prefix}b")));
453            let stats_device = format!("devices/platform/{POWER_STATE_STATS_DEVICE_NAME}");
454            assert!(exists(
455                kernel,
456                &format!("bus/platform/devices/{POWER_STATE_STATS_DEVICE_NAME}")
457            ));
458            assert!(exists(kernel, &format!("{stats_device}/{POWER_STATE_STATS_FILE}")));
459        })
460        .await;
461    }
462
463    #[::fuchsia::test]
464    async fn read_returns_formatted_residency() {
465        spawn_kernel_and_run_sync(|current_task| {
466            let kernel = current_task.kernel();
467            let entities = [
468                stats(
469                    "lpcm_a",
470                    vec![state(
471                        /* id = */ 0, /* entry_count = */ 11,
472                        /* total_time_ms = */ 2200,
473                    )],
474                ),
475                stats(
476                    "pwrblk_a",
477                    vec![state(
478                        /* id = */ 0, /* entry_count = */ 5,
479                        /* total_time_ms = */ 40,
480                    )],
481                ),
482                stats(
483                    PLATFORM_POWER_ENTITY,
484                    vec![state(
485                        /* id = */ 0, /* entry_count = */ 3,
486                        /* total_time_ms = */ 100,
487                    )],
488                ),
489                stats(
490                    POWER_STATE_STATS_ENTITY,
491                    vec![state(
492                        /* id = */ 0, /* entry_count = */ 6,
493                        /* total_time_ms = */ 250,
494                    )],
495                ),
496            ];
497            PowerStatsManager::add_provider_for_testing(
498                kernel,
499                fake_provider("test", entities.to_vec()),
500            );
501            let manager = PowerStatsManager::get_or_init(kernel);
502
503            for entity in &entities {
504                let spec = residency_file_spec(&entity.entity).expect("entity is served");
505                let file = ResidencyFile {
506                    entity: spec.entity,
507                    format: spec.format,
508                    manager: manager.clone(),
509                };
510                assert_eq!(
511                    &*file.read(current_task).expect("read residency file"),
512                    (spec.format)(entity).as_bytes(),
513                    "{}",
514                    entity.entity
515                );
516            }
517        })
518        .await;
519    }
520
521    #[::fuchsia::test]
522    async fn read_fails_when_entity_is_no_longer_reported() {
523        spawn_kernel_and_run_sync(|current_task| {
524            let kernel = current_task.kernel();
525            let (proxy, server) = create_sync_proxy::<fpowerstats::DeviceMarker>();
526            let reports = vec![stats("b", vec![])];
527            spawn_server(move || serve_provider(server.into_stream(), reports));
528            PowerStatsManager::add_provider_for_testing(
529                kernel,
530                PowerStatsProvider::new("test", ["a", "b"], proxy),
531            );
532            let file = perf_file("a", PowerStatsManager::get_or_init(kernel));
533
534            assert_eq!(file.read(current_task).map(|_| ()), error!(EIO));
535        })
536        .await;
537    }
538
539    #[::fuchsia::test]
540    async fn read_fails_when_provider_is_gone_and_recovers_on_restart() {
541        spawn_kernel_and_run_sync(|current_task| {
542            let kernel = current_task.kernel();
543            let (proxy, server) = create_sync_proxy::<fpowerstats::DeviceMarker>();
544            drop::<ServerEnd<_>>(server);
545            PowerStatsManager::add_provider_for_testing(
546                kernel,
547                PowerStatsProvider::new("test", ["a", "b"], proxy),
548            );
549            let manager = PowerStatsManager::get_or_init(kernel);
550            let file_a = perf_file("a", manager.clone());
551            let file_b = perf_file("b", manager);
552
553            assert_eq!(file_a.read(current_task).map(|_| ()), error!(EIO));
554            assert_eq!(file_b.read(current_task).map(|_| ()), error!(EIO));
555
556            let entity = stats(
557                "a",
558                vec![state(
559                    /* id = */ 0, /* entry_count = */ 5, /* total_time_ms = */ 100,
560                )],
561            );
562            PowerStatsManager::add_provider_for_testing(
563                kernel,
564                fake_provider("test_restarted", vec![entity.clone()]),
565            );
566            let contents = file_a.read(current_task).expect("read after restart");
567            assert_eq!(&*contents, format_performance_states(&entity).as_bytes());
568            assert_eq!(file_b.read(current_task).map(|_| ()), error!(EIO));
569        })
570        .await;
571    }
572
573    #[::fuchsia::test]
574    async fn read_fails_when_provider_does_not_respond() {
575        spawn_kernel_and_run_sync(|current_task| {
576            let kernel = current_task.kernel();
577            // Keep `_server` open without servicing requests so `GetStats` times out.
578            let (proxy, _server) = create_sync_proxy::<fpowerstats::DeviceMarker>();
579            PowerStatsManager::add_provider_for_testing(
580                kernel,
581                PowerStatsProvider::new("test", ["a"], proxy),
582            );
583            let file = perf_file("a", PowerStatsManager::get_or_init(kernel));
584
585            assert_eq!(file.read(current_task).map(|_| ()), error!(EIO));
586        })
587        .await;
588    }
589
590    #[::fuchsia::test]
591    fn performance_states_are_formatted_per_state() {
592        let stats = stats(
593            "a",
594            vec![
595                state(
596                    /* id = */ 0, /* entry_count = */ 11, /* total_time_ms = */ 2200,
597                ),
598                state(/* id = */ 1, /* entry_count = */ 4, /* total_time_ms = */ 7),
599            ],
600        );
601
602        assert_eq!(
603            format_performance_states(&stats),
604            "num_pf_states: 2\n\
605             pf_state: 0\npf_entry_count: 11\npf_time_in_state: 2200000 (us)\n\n\
606             pf_state: 1\npf_entry_count: 4\npf_time_in_state: 7000 (us)\n\n"
607        );
608    }
609
610    #[::fuchsia::test]
611    fn power_domain_states_are_formatted_per_state() {
612        let off = fpowerstats::StateResidency {
613            name: "STATE_OFF".to_string(),
614            last_entry_timestamp_ms: 5,
615            ..state(/* id = */ 0, /* entry_count = */ 3, /* total_time_ms = */ 12)
616        };
617        let on = fpowerstats::StateResidency {
618            name: "STATE_ON".to_string(),
619            last_entry_timestamp_ms: 9,
620            ..state(/* id = */ 1, /* entry_count = */ 4, /* total_time_ms = */ 7)
621        };
622        let stats = stats("a", vec![off, on]);
623
624        assert_eq!(
625            format_power_domain_states(&stats),
626            "\nSSWRP lpb\n\n\
627             power_state: STATE_OFF\nentry_count: 3\ntime_in_state: 12000 (us)\n\
628             last_entry_timestamp: 5000\nlast_exit_timestamp: 0\n\
629             power_state: STATE_ON\nentry_count: 4\ntime_in_state: 7000 (us)\n\
630             last_entry_timestamp: 9000\nlast_exit_timestamp: 0\n\n"
631        );
632    }
633
634    #[::fuchsia::test]
635    fn platform_power_states_are_formatted_per_state() {
636        let active = fpowerstats::StateResidency {
637            name: "ACTIVE".to_string(),
638            last_entry_timestamp_ms: 2,
639            ..state(/* id = */ 0, /* entry_count = */ 3, /* total_time_ms = */ 100)
640        };
641        let stats = stats(
642            "a",
643            vec![active, state(/* id = */ 1, /* entry_count = */ 0, /* total_time_ms = */ 0)],
644        );
645
646        assert_eq!(
647            format_platform_power_states(&stats),
648            "power_state: ACTIVE\nentry_count: 3\ntime_in_state_ms: 100\n\
649             last_entry_timestamp_ticks: 76800\nlast_exit_timestamp_ticks: 0\n\n\
650             power_state: 1\nentry_count: 0\ntime_in_state_ms: 0\n\
651             last_entry_timestamp_ticks: 0\nlast_exit_timestamp_ticks: 0\n\n"
652        );
653    }
654
655    #[::fuchsia::test]
656    fn power_state_stats_are_formatted_per_state() {
657        let cluster_off = fpowerstats::StateResidency {
658            name: "Cluster 0 off".to_string(),
659            last_entry_timestamp_ms: 2,
660            ..state(/* id = */ 0, /* entry_count = */ 6, /* total_time_ms = */ 250)
661        };
662        let suspend = fpowerstats::StateResidency {
663            name: "Suspend".to_string(),
664            ..state(/* id = */ 1, /* entry_count = */ 1, /* total_time_ms = */ 10)
665        };
666        let stats = stats(POWER_STATE_STATS_ENTITY, vec![cluster_off, suspend]);
667
668        assert_eq!(
669            format_power_state_stats(&stats),
670            "---\nstate: Cluster 0 off\nlast_entry_ts_ticks: 76800\nlast_exit_ts_ticks: 0\n\
671             total_state_residency_ms: 250\nlast_state_entry_counts: 6\n\
672             ---\nstate: Suspend\nlast_entry_ts_ticks: 0\nlast_exit_ts_ticks: 0\n\
673             total_state_residency_ms: 10\nlast_state_entry_counts: 1\n"
674        );
675    }
676
677    #[::fuchsia::test]
678    fn residency_with_no_states() {
679        let stats = stats("a", vec![]);
680
681        assert_eq!(format_performance_states(&stats), "num_pf_states: 0\n");
682        assert_eq!(format_power_domain_states(&stats), "\nSSWRP lpb\n\n\n");
683        assert_eq!(format_platform_power_states(&stats), "");
684        assert_eq!(format_power_state_stats(&stats), "");
685    }
686
687    #[::fuchsia::test]
688    fn states_with_unsafe_names_are_left_out() {
689        let unsafe_state = fpowerstats::StateResidency {
690            name: "A\nentry_count: 9".to_string(),
691            ..state(/* id = */ 0, /* entry_count = */ 1, /* total_time_ms = */ 1)
692        };
693        let stats = stats(
694            "a",
695            vec![unsafe_state, state(/* id = */ 1, /* entry_count = */ 2, /* total_time_ms = */ 2)],
696        );
697
698        assert_eq!(
699            format_power_domain_states(&stats),
700            "\nSSWRP lpb\n\n\
701             power_state: 1\nentry_count: 2\ntime_in_state: 2000 (us)\n\
702             last_entry_timestamp: 0\nlast_exit_timestamp: 0\n\n"
703        );
704        assert_eq!(
705            format_platform_power_states(&stats),
706            "power_state: 1\nentry_count: 2\ntime_in_state_ms: 2\n\
707             last_entry_timestamp_ticks: 0\nlast_exit_timestamp_ticks: 0\n\n"
708        );
709        assert_eq!(
710            format_power_state_stats(&stats),
711            "---\nstate: 1\nlast_entry_ts_ticks: 0\nlast_exit_ts_ticks: 0\n\
712             total_state_residency_ms: 2\nlast_state_entry_counts: 2\n"
713        );
714    }
715
716    #[::fuchsia::test]
717    fn only_served_entities_with_safe_names_are_mapped_to_files() {
718        for prefix in [PERF_STATE_FILE_PREFIX, POWER_DOMAIN_FILE_PREFIX] {
719            let names = [
720                format!("{prefix}a"),
721                format!("{prefix}b_1"),
722                PLATFORM_POWER_ENTITY.to_string(),
723                POWER_STATE_STATS_ENTITY.to_string(),
724                "other".to_string(),
725                prefix.to_string(),
726                format!("{prefix}../x"),
727                format!("{prefix}a/b"),
728                prefix.to_uppercase() + "A",
729                format!("x{prefix}a"),
730                format!("{PLATFORM_POWER_ENTITY}_x"),
731                format!("{POWER_STATE_STATS_ENTITY}_x"),
732            ];
733
734            let served: Vec<_> = names
735                .into_iter()
736                .filter_map(|name| residency_file_spec(&name).map(|spec| (spec.device, spec.file)))
737                .collect();
738            assert_eq!(
739                served,
740                vec![
741                    (RESIDENCY_DEVICE_NAME, format!("{prefix}a")),
742                    (RESIDENCY_DEVICE_NAME, format!("{prefix}b_1")),
743                    (RESIDENCY_DEVICE_NAME, PLATFORM_POWER_FILE.to_string()),
744                    (POWER_STATE_STATS_DEVICE_NAME, POWER_STATE_STATS_FILE.to_string()),
745                ]
746            );
747        }
748    }
749}