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//!
8//! These devices are not generic Linux sysfs interfaces. They emulate the SoC-specific vendor
9//! devices that the Android PowerStats HAL reads residency from: the CPM power dashboard and the
10//! CAP (CPU Auxiliary Processor) stats device. The DSU's devfreq device is also emulated, as the
11//! HAL reads the DSU's frequencies from it to label the DSU's DVFS residency.
12
13use fidl_fuchsia_hardware_power_stats as fpowerstats;
14use starnix_core::device::power_stats::PowerStatsManager;
15use starnix_core::fs::sysfs::build_device_directory;
16use starnix_core::task::{CurrentTask, Kernel};
17use starnix_core::vfs::pseudo::simple_file::{BytesFile, BytesFileOps};
18use starnix_logging::log_warn;
19use starnix_uapi::errno;
20use starnix_uapi::errors::Errno;
21use starnix_uapi::file_mode::mode;
22use std::borrow::Cow;
23use std::collections::{BTreeMap, HashSet};
24use std::fmt::Write as _;
25
26/// Power entities whose names start with this prefix are served as performance state residency
27/// files of the same name.
28const PERF_STATE_FILE_PREFIX: &str = "lpcm_";
29
30/// Power entities whose names start with this prefix are served as power domain residency files
31/// of the same name.
32const POWER_DOMAIN_FILE_PREFIX: &str = "pwrblk_";
33
34/// Power entity served as the platform power state residency file.
35const PLATFORM_POWER_ENTITY: &str = "soc_power_state";
36
37/// Name of the file [`PLATFORM_POWER_ENTITY`] is served as.
38const PLATFORM_POWER_FILE: &str = "platform_power_residency";
39
40/// Name of the platform device under which the residency files are served, emulating the
41/// SoC-specific CPM power dashboard device read by the PowerStats HAL.
42const RESIDENCY_DEVICE_NAME: &str = "powerdashboard";
43
44/// Power entity served as the power state stats file.
45const POWER_STATE_STATS_ENTITY: &str = "cap_power_state";
46
47/// Name of the file [`POWER_STATE_STATS_ENTITY`] is served as.
48const POWER_STATE_STATS_FILE: &str = "power_state_stats";
49
50/// Name of the platform device under which [`POWER_STATE_STATS_FILE`] and the DVFS stats files are
51/// served, emulating the SoC-specific CAP stats device read by the PowerStats HAL.
52const CAP_SYSFS_DEVICE_NAME: &str = "cap_sysfs";
53
54/// Power entities whose names start with this prefix, followed by the name of a clock domain, are
55/// served as DVFS stats files. Like [`POWER_STATE_STATS_ENTITY`], the prefix names CAP, so that
56/// DVFS entities of other components are not served under [`CAP_SYSFS_DEVICE_NAME`].
57const DVFS_STATS_ENTITY_PREFIX: &str = "cap_dvfs_";
58
59/// Prefix of the name of the DVFS stats file of each clock domain, which is followed by the name of
60/// the clock domain.
61const DVFS_STATS_FILE_PREFIX: &str = "dvfs_stats_";
62
63/// Power entity reporting the DSU's DVFS residency. When a provider reports it, the DSU's devfreq
64/// device is also served, as the frequency list for that residency.
65const DSU_DVFS_ENTITY: &str = "cap_dvfs_clk_dsu";
66
67/// Name of the DSU's platform device, and of its devfreq device.
68const DSU_DEVFREQ_DEVICE_NAME: &str = "200c0780.dsufreq";
69
70/// Frequencies of the DSU's operating points in Hz, from slowest to fastest.
71///
72/// The DSU has no frequency driver, so the frequencies are those its devfreq device lists on
73/// Linux. The fastest one is CAP operating level 3 and the slowest is level 23.
74// TODO(b/578170862): Get the frequencies from the power stats provider.
75const DSU_AVAILABLE_FREQUENCIES_HZ: [u64; 21] = [
76    115_000_000,
77    268_000_000,
78    400_000_000,
79    537_000_000,
80    691_000_000,
81    768_000_000,
82    844_000_000,
83    960_000_000,
84    1_056_000_000,
85    1_132_000_000,
86    1_228_000_000,
87    1_324_000_000,
88    1_420_000_000,
89    1_516_000_000,
90    1_612_000_000,
91    1_728_000_000,
92    1_843_000_000,
93    1_920_000_000,
94    1_996_000_000,
95    2_092_000_000,
96    2_188_000_000,
97];
98
99/// Ticks per millisecond of the counter that some residency files report timestamps in.
100///
101/// Those files report raw values of the platform timer counter, which is expected to run at
102/// 38.4 MHz, so the provider's millisecond timestamps are converted at 38,400 ticks per
103/// millisecond.
104const TIMESTAMP_TICKS_PER_MS: u64 = 38_400;
105
106/// Formats the residency of a power entity as the contents of its residency file.
107type FormatResidency = fn(&fpowerstats::Stats) -> String;
108
109/// Where and how a power entity is served.
110struct ResidencyFileSpec {
111    /// Name of the platform device under which the file is served.
112    device: &'static str,
113    /// Name of the file within the device directory.
114    file: String,
115    /// Name of the power entity whose residency the file serves.
116    entity: String,
117    format: FormatResidency,
118}
119
120/// Serves a file for each power entity reported over `fuchsia.hardware.power.stats.Service` that
121/// [`residency_file_spec`] maps to one, under its platform device.
122///
123/// The entities are only known once a provider has started, so each device is added asynchronously
124/// via [`PowerStatsManager`], once a provider reports at least one entity served under it. Served
125/// entities for a device from other providers are ignored.
126///
127/// The DSU's devfreq device is added along with [`DSU_DVFS_ENTITY`]'s file.
128pub fn init_power_residency_device(kernel: &Kernel) {
129    let mut added_devices: HashSet<&'static str> = HashSet::new();
130    let manager = PowerStatsManager::get_or_init(kernel);
131    let manager_for_files = manager.clone();
132    manager.watch_providers(kernel, move |kernel, provider| {
133        let mut files_by_device: BTreeMap<&'static str, Vec<ResidencyFileSpec>> = BTreeMap::new();
134        for spec in provider.entity_names().iter().filter_map(|name| residency_file_spec(name)) {
135            files_by_device.entry(spec.device).or_default().push(spec);
136        }
137        let registry = &kernel.device_registry;
138        for (device_name, files) in files_by_device {
139            if !added_devices.insert(device_name) {
140                log_warn!(
141                    "Ignoring {device_name} power residency entities from additional provider {}",
142                    provider.instance_name()
143                );
144                continue;
145            }
146            registry.add_bus_device(
147                device_name.into(),
148                Some(registry.objects.platform_device()),
149                registry.objects.platform_bus(),
150                |device, dir| {
151                    build_device_directory(device, dir);
152                    for spec in files {
153                        let file = ResidencyFile {
154                            entity: spec.entity,
155                            format: spec.format,
156                            manager: manager_for_files.clone(),
157                        };
158                        dir.entry(&spec.file, BytesFile::new_node(file), mode!(IFREG, 0o444));
159                    }
160                },
161            );
162        }
163        if provider.entity_names().iter().any(|name| name == DSU_DVFS_ENTITY)
164            && added_devices.insert(DSU_DEVFREQ_DEVICE_NAME)
165        {
166            add_dsu_devfreq_device(kernel);
167        }
168    });
169}
170
171/// Adds the DSU's platform device and its devfreq device, which lists the DSU's frequencies in
172/// `available_frequencies`.
173fn add_dsu_devfreq_device(kernel: &Kernel) {
174    let registry = &kernel.device_registry;
175    let platform_device = registry.add_bus_device(
176        DSU_DEVFREQ_DEVICE_NAME.into(),
177        Some(registry.objects.platform_device()),
178        registry.objects.platform_bus(),
179        |device, dir| build_device_directory(device, dir),
180    );
181    let devfreq_class = registry.objects.get_or_create_class("devfreq".into());
182    registry.add_numberless_device(
183        DSU_DEVFREQ_DEVICE_NAME.into(),
184        Some(platform_device),
185        devfreq_class,
186        |device, dir| {
187            build_device_directory(device, dir);
188            dir.entry(
189                "available_frequencies",
190                BytesFile::new_node(dsu_available_frequencies().into_bytes()),
191                mode!(IFREG, 0o444),
192            );
193        },
194    );
195}
196
197/// Returns the contents of the DSU's `available_frequencies` file: its frequencies in Hz, from
198/// slowest to fastest, separated by spaces.
199fn dsu_available_frequencies() -> String {
200    let frequencies: Vec<_> = DSU_AVAILABLE_FREQUENCIES_HZ.iter().map(u64::to_string).collect();
201    frequencies.join(" ") + "\n"
202}
203
204/// Returns where and how to serve the power entity named `entity`, or [`None`] if it is not
205/// served. Prefixed entities are only served if their names are safe to use as file names.
206fn residency_file_spec(entity: &str) -> Option<ResidencyFileSpec> {
207    let prefixed = |prefix: &str| {
208        entity.len() > prefix.len()
209            && entity.starts_with(prefix)
210            && entity.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
211    };
212    let (device, file, format): (_, _, FormatResidency) = if prefixed(PERF_STATE_FILE_PREFIX) {
213        (RESIDENCY_DEVICE_NAME, entity.to_string(), format_performance_states)
214    } else if prefixed(POWER_DOMAIN_FILE_PREFIX) {
215        (RESIDENCY_DEVICE_NAME, entity.to_string(), format_power_domain_states)
216    } else if entity == PLATFORM_POWER_ENTITY {
217        (RESIDENCY_DEVICE_NAME, PLATFORM_POWER_FILE.to_string(), format_platform_power_states)
218    } else if prefixed(DVFS_STATS_ENTITY_PREFIX) {
219        let file = format!("{DVFS_STATS_FILE_PREFIX}{}", &entity[DVFS_STATS_ENTITY_PREFIX.len()..]);
220        (CAP_SYSFS_DEVICE_NAME, file, format_dvfs_stats)
221    } else if entity == POWER_STATE_STATS_ENTITY {
222        (CAP_SYSFS_DEVICE_NAME, POWER_STATE_STATS_FILE.to_string(), format_power_state_stats)
223    } else {
224        return None;
225    };
226    Some(ResidencyFileSpec { device, file, entity: entity.to_string(), format })
227}
228
229/// Serves the residency of a single power entity, as formatted by `format`.
230struct ResidencyFile {
231    entity: String,
232    format: FormatResidency,
233    manager: PowerStatsManager,
234}
235
236impl BytesFileOps for ResidencyFile {
237    fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
238        let stats = self.manager.entity_stats(&self.entity)?.ok_or_else(|| {
239            log_warn!("No power stats provider registered for {}", self.entity);
240            errno!(EIO)
241        })?;
242        Ok((self.format)(&stats).into_bytes().into())
243    }
244}
245
246/// Returns the states in `stats` whose names are safe to write to a line-based residency file,
247/// logging a warning for any state left out.
248///
249/// States whose names contain control characters, such as newlines, are left out, as their names
250/// would break the line-based layout.
251fn safe_states(stats: &fpowerstats::Stats) -> impl Iterator<Item = &fpowerstats::StateResidency> {
252    stats.state_residency_data.iter().filter(|state| {
253        if state.name.chars().any(char::is_control) {
254            log_warn!("Leaving out {} state {} with an unsafe name", stats.entity, state.id);
255            return false;
256        }
257        true
258    })
259}
260
261/// Formats `stats` as a `num_pf_states` line followed by `pf_*` lines for each performance level.
262///
263/// The power stats API does not report times with sub-millisecond precision, so times are whole
264/// milliseconds converted to microseconds.
265fn format_performance_states(stats: &fpowerstats::Stats) -> String {
266    let mut out = String::new();
267    let _ = writeln!(out, "num_pf_states: {}", stats.state_residency_data.len());
268    for state in &stats.state_residency_data {
269        let time_in_state_us = state.total_time_ms.saturating_mul(1000);
270        let _ = writeln!(out, "pf_state: {}", state.id);
271        let _ = writeln!(out, "pf_entry_count: {}", state.entry_count);
272        let _ = writeln!(out, "pf_time_in_state: {time_in_state_us} (us)\n");
273    }
274    out
275}
276
277/// Formats `stats` as an `SSWRP lpb` header followed by `power_state` lines for each power
278/// state.
279///
280/// States whose names contain control characters, such as newlines, are left out, as their names
281/// would break the line-based layout.
282///
283/// The power stats API does not report times with sub-millisecond precision or when a state was
284/// last exited, so times are whole milliseconds converted to microseconds and last exit
285/// timestamps are 0.
286fn format_power_domain_states(stats: &fpowerstats::Stats) -> String {
287    let mut out = String::new();
288    let _ = writeln!(out, "\nSSWRP lpb\n");
289    for state in safe_states(stats) {
290        let time_in_state_us = state.total_time_ms.saturating_mul(1000);
291        let last_entry_us = state.last_entry_timestamp_ms.saturating_mul(1000);
292        let _ = writeln!(out, "power_state: {}", state.name);
293        let _ = writeln!(out, "entry_count: {}", state.entry_count);
294        let _ = writeln!(out, "time_in_state: {time_in_state_us} (us)");
295        let _ = writeln!(out, "last_entry_timestamp: {last_entry_us}");
296        let _ = writeln!(out, "last_exit_timestamp: 0");
297    }
298    out.push('\n');
299    out
300}
301
302/// Formats `stats` as a `power_state` line followed by counts and timestamps for each power
303/// state.
304///
305/// States whose names contain control characters, such as newlines, are left out, as their names
306/// would break the line-based layout.
307///
308/// The power stats API does not report which state is current or when a state was last exited,
309/// so there are no current state lines and last exit timestamps are 0.
310fn format_platform_power_states(stats: &fpowerstats::Stats) -> String {
311    let mut out = String::new();
312    for state in safe_states(stats) {
313        let last_entry_ticks = state.last_entry_timestamp_ms.saturating_mul(TIMESTAMP_TICKS_PER_MS);
314        let _ = writeln!(out, "power_state: {}", state.name);
315        let _ = writeln!(out, "entry_count: {}", state.entry_count);
316        let _ = writeln!(out, "time_in_state_ms: {}", state.total_time_ms);
317        let _ = writeln!(out, "last_entry_timestamp_ticks: {last_entry_ticks}");
318        let _ = writeln!(out, "last_exit_timestamp_ticks: 0\n");
319    }
320    out
321}
322
323/// Formats `stats` as `---`-delimited records of `state`, timestamps, residency, and entry count
324/// for each power state.
325///
326/// States whose names contain control characters, such as newlines, are left out, as their names
327/// would break the line-based layout.
328///
329/// The power stats API does not report when a state was last exited, so last exit timestamps are
330/// 0.
331fn format_power_state_stats(stats: &fpowerstats::Stats) -> String {
332    let mut out = String::new();
333    for state in safe_states(stats) {
334        let last_entry_ticks = state.last_entry_timestamp_ms.saturating_mul(TIMESTAMP_TICKS_PER_MS);
335        let _ = writeln!(out, "---");
336        let _ = writeln!(out, "state: {}", state.name);
337        let _ = writeln!(out, "last_entry_ts_ticks: {last_entry_ticks}");
338        let _ = writeln!(out, "last_exit_ts_ticks: 0");
339        let _ = writeln!(out, "total_state_residency_ms: {}", state.total_time_ms);
340        let _ = writeln!(out, "last_state_entry_counts: {}", state.entry_count);
341    }
342    out
343}
344
345/// Formats `stats` as a record that starts with `---` and names the clock domain, followed by the
346/// entry count, residency, and last entry timestamp of each operating level.
347///
348/// Operating levels are identified by their state IDs. The power stats API does not report the
349/// level a clock domain is at or is moving to, so there are no current or target level lines.
350fn format_dvfs_stats(stats: &fpowerstats::Stats) -> String {
351    let clock_domain = stats.entity.strip_prefix(DVFS_STATS_ENTITY_PREFIX).unwrap_or(&stats.entity);
352    let mut out = String::new();
353    let _ = writeln!(out, "---");
354    let _ = writeln!(out, "clock_domain: {clock_domain}");
355    let _ = writeln!(out, "per_op_level_stats: ");
356    for state in &stats.state_residency_data {
357        let last_entry_ticks = state.last_entry_timestamp_ms.saturating_mul(TIMESTAMP_TICKS_PER_MS);
358        let _ = writeln!(out, "  - oplevel: {}", state.id);
359        let _ = writeln!(out, "    entry_count: {}", state.entry_count);
360        let _ = writeln!(out, "    total_residency_ms: {}", state.total_time_ms);
361        let _ = writeln!(out, "    last_entry_timestamp_ticks: {last_entry_ticks}");
362    }
363    out
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369    use fidl::endpoints::{ServerEnd, create_sync_proxy};
370    use fuchsia_async as fasync;
371    use futures::StreamExt;
372    use starnix_core::device::power_stats::PowerStatsProvider;
373    use starnix_core::testing::spawn_kernel_and_run_sync;
374    use starnix_uapi::error;
375
376    fn state(id: i32, entry_count: u32, total_time_ms: u64) -> fpowerstats::StateResidency {
377        fpowerstats::StateResidency {
378            id,
379            name: id.to_string(),
380            total_time_ms,
381            entry_count,
382            last_entry_timestamp_ms: 0,
383        }
384    }
385
386    fn stats(
387        entity: &str,
388        state_residency_data: Vec<fpowerstats::StateResidency>,
389    ) -> fpowerstats::Stats {
390        fpowerstats::Stats { entity: entity.to_string(), state_residency_data }
391    }
392
393    /// Serves a fake provider that reports `reports` on `stream`.
394    async fn serve_provider(
395        stream: fpowerstats::DeviceRequestStream,
396        reports: Vec<fpowerstats::Stats>,
397    ) {
398        let _ = stream
399            .for_each(|request| {
400                let reports = &reports;
401                async move {
402                    if let Ok(fpowerstats::DeviceRequest::GetStats { responder }) = request {
403                        let _ = responder.send(reports);
404                    }
405                }
406            })
407            .await;
408    }
409
410    /// Runs `serve` on its own thread, as residency files call providers synchronously.
411    fn spawn_server<F: Future<Output = ()> + 'static>(serve: impl FnOnce() -> F + Send + 'static) {
412        std::thread::spawn(move || fasync::LocalExecutor::default().run_singlethreaded(serve()));
413    }
414
415    /// Returns a [`PowerStatsProvider`] reporting `entity_names` for tests that only exercise
416    /// device registration.
417    fn provider_with_entities(
418        instance_name: &str,
419        entity_names: impl IntoIterator<Item = impl Into<String>>,
420    ) -> PowerStatsProvider {
421        fake_provider(
422            instance_name,
423            entity_names.into_iter().map(|name| stats(&name.into(), vec![])).collect(),
424        )
425    }
426
427    /// Returns a fake [`PowerStatsProvider`] named `instance_name` that serves `reports`.
428    fn fake_provider(instance_name: &str, reports: Vec<fpowerstats::Stats>) -> PowerStatsProvider {
429        let entity_names: Vec<_> = reports.iter().map(|stats| stats.entity.clone()).collect();
430        let (proxy, server) = create_sync_proxy::<fpowerstats::DeviceMarker>();
431        spawn_server(move || serve_provider(server.into_stream(), reports));
432        PowerStatsProvider::new(instance_name, entity_names, proxy)
433    }
434
435    /// Returns a performance state residency file serving `entity` from `manager`.
436    fn perf_file(entity: &str, manager: PowerStatsManager) -> ResidencyFile {
437        ResidencyFile { entity: entity.to_string(), format: format_performance_states, manager }
438    }
439
440    fn exists(kernel: &Kernel, path: &str) -> bool {
441        kernel.device_registry.objects.root.lookup(path.into()).is_some()
442    }
443
444    #[::fuchsia::test]
445    async fn device_is_added_with_served_entities() {
446        spawn_kernel_and_run_sync(|current_task| {
447            let kernel = current_task.kernel();
448            let prefix = PERF_STATE_FILE_PREFIX;
449            PowerStatsManager::add_provider_for_testing(
450                kernel,
451                provider_with_entities(
452                    "default",
453                    [
454                        format!("{prefix}a"),
455                        format!("{prefix}b"),
456                        PLATFORM_POWER_ENTITY.to_string(),
457                        "other".to_string(),
458                    ],
459                ),
460            );
461            init_power_residency_device(kernel);
462
463            let device = format!("devices/platform/{RESIDENCY_DEVICE_NAME}");
464            assert!(exists(kernel, &device));
465            assert!(exists(kernel, &format!("bus/platform/devices/{RESIDENCY_DEVICE_NAME}")));
466            assert!(exists(kernel, &format!("{device}/uevent")));
467            assert!(exists(kernel, &format!("{device}/{prefix}a")));
468            assert!(exists(kernel, &format!("{device}/{prefix}b")));
469            assert!(exists(kernel, &format!("{device}/{PLATFORM_POWER_FILE}")));
470            assert!(!exists(kernel, &format!("{device}/other")));
471        })
472        .await;
473    }
474
475    #[::fuchsia::test]
476    async fn no_device_without_served_entities() {
477        spawn_kernel_and_run_sync(|current_task| {
478            let kernel = current_task.kernel();
479            PowerStatsManager::add_provider_for_testing(
480                kernel,
481                provider_with_entities("default", ["other"]),
482            );
483            init_power_residency_device(kernel);
484
485            assert!(!exists(kernel, &format!("devices/platform/{RESIDENCY_DEVICE_NAME}")));
486            assert!(!exists(kernel, &format!("devices/platform/{CAP_SYSFS_DEVICE_NAME}")));
487        })
488        .await;
489    }
490
491    #[::fuchsia::test]
492    async fn device_is_added_when_later_provider_reports_served_entities() {
493        spawn_kernel_and_run_sync(|current_task| {
494            let kernel = current_task.kernel();
495            let prefix = PERF_STATE_FILE_PREFIX;
496            let device = format!("devices/platform/{RESIDENCY_DEVICE_NAME}");
497
498            let _ = PowerStatsManager::get_or_init_for_testing(kernel);
499            init_power_residency_device(kernel);
500            assert!(!exists(kernel, &device));
501
502            PowerStatsManager::add_provider_for_testing(
503                kernel,
504                provider_with_entities("unserved", ["other"]),
505            );
506            assert!(!exists(kernel, &device));
507
508            PowerStatsManager::add_provider_for_testing(
509                kernel,
510                provider_with_entities(
511                    "served",
512                    [format!("{prefix}a"), PLATFORM_POWER_ENTITY.to_string()],
513                ),
514            );
515            assert!(exists(kernel, &device));
516            assert!(exists(kernel, &format!("{device}/{prefix}a")));
517            assert!(exists(kernel, &format!("{device}/{PLATFORM_POWER_FILE}")));
518        })
519        .await;
520    }
521
522    #[::fuchsia::test]
523    async fn only_first_provider_is_served() {
524        spawn_kernel_and_run_sync(|current_task| {
525            let kernel = current_task.kernel();
526            let prefix = PERF_STATE_FILE_PREFIX;
527            PowerStatsManager::add_provider_for_testing(
528                kernel,
529                provider_with_entities("first", [format!("{prefix}a")]),
530            );
531            init_power_residency_device(kernel);
532            PowerStatsManager::add_provider_for_testing(
533                kernel,
534                provider_with_entities("second", [format!("{prefix}b")]),
535            );
536
537            let device = format!("devices/platform/{RESIDENCY_DEVICE_NAME}");
538            assert!(exists(kernel, &format!("{device}/{prefix}a")));
539            assert!(!exists(kernel, &format!("{device}/{prefix}b")));
540        })
541        .await;
542    }
543
544    #[::fuchsia::test]
545    async fn each_device_is_added_for_first_provider_serving_it() {
546        spawn_kernel_and_run_sync(|current_task| {
547            let kernel = current_task.kernel();
548            let prefix = PERF_STATE_FILE_PREFIX;
549            PowerStatsManager::add_provider_for_testing(
550                kernel,
551                provider_with_entities("first", [format!("{prefix}a")]),
552            );
553            init_power_residency_device(kernel);
554            PowerStatsManager::add_provider_for_testing(
555                kernel,
556                provider_with_entities(
557                    "second",
558                    [format!("{prefix}b"), POWER_STATE_STATS_ENTITY.to_string()],
559                ),
560            );
561
562            let residency_device = format!("devices/platform/{RESIDENCY_DEVICE_NAME}");
563            assert!(exists(kernel, &format!("{residency_device}/{prefix}a")));
564            assert!(!exists(kernel, &format!("{residency_device}/{prefix}b")));
565            let stats_device = format!("devices/platform/{CAP_SYSFS_DEVICE_NAME}");
566            assert!(exists(kernel, &format!("bus/platform/devices/{CAP_SYSFS_DEVICE_NAME}")));
567            assert!(exists(kernel, &format!("{stats_device}/{POWER_STATE_STATS_FILE}")));
568        })
569        .await;
570    }
571
572    #[::fuchsia::test]
573    async fn read_returns_formatted_residency() {
574        spawn_kernel_and_run_sync(|current_task| {
575            let kernel = current_task.kernel();
576            let entities = [
577                stats(
578                    "lpcm_a",
579                    vec![state(
580                        /* id = */ 0, /* entry_count = */ 11,
581                        /* total_time_ms = */ 2200,
582                    )],
583                ),
584                stats(
585                    "pwrblk_a",
586                    vec![state(
587                        /* id = */ 0, /* entry_count = */ 5,
588                        /* total_time_ms = */ 40,
589                    )],
590                ),
591                stats(
592                    PLATFORM_POWER_ENTITY,
593                    vec![state(
594                        /* id = */ 0, /* entry_count = */ 3,
595                        /* total_time_ms = */ 100,
596                    )],
597                ),
598                stats(
599                    POWER_STATE_STATS_ENTITY,
600                    vec![state(
601                        /* id = */ 0, /* entry_count = */ 6,
602                        /* total_time_ms = */ 250,
603                    )],
604                ),
605            ];
606            PowerStatsManager::add_provider_for_testing(
607                kernel,
608                fake_provider("test", entities.to_vec()),
609            );
610            let manager = PowerStatsManager::get_or_init(kernel);
611
612            for entity in &entities {
613                let spec = residency_file_spec(&entity.entity).expect("entity is served");
614                let file = ResidencyFile {
615                    entity: spec.entity,
616                    format: spec.format,
617                    manager: manager.clone(),
618                };
619                assert_eq!(
620                    &*file.read(current_task).expect("read residency file"),
621                    (spec.format)(entity).as_bytes(),
622                    "{}",
623                    entity.entity
624                );
625            }
626        })
627        .await;
628    }
629
630    #[::fuchsia::test]
631    async fn read_fails_when_entity_is_no_longer_reported() {
632        spawn_kernel_and_run_sync(|current_task| {
633            let kernel = current_task.kernel();
634            let (proxy, server) = create_sync_proxy::<fpowerstats::DeviceMarker>();
635            let reports = vec![stats("b", vec![])];
636            spawn_server(move || serve_provider(server.into_stream(), reports));
637            PowerStatsManager::add_provider_for_testing(
638                kernel,
639                PowerStatsProvider::new("test", ["a", "b"], proxy),
640            );
641            let file = perf_file("a", PowerStatsManager::get_or_init(kernel));
642
643            assert_eq!(file.read(current_task).map(|_| ()), error!(EIO));
644        })
645        .await;
646    }
647
648    #[::fuchsia::test]
649    async fn read_fails_when_provider_is_gone_and_recovers_on_restart() {
650        spawn_kernel_and_run_sync(|current_task| {
651            let kernel = current_task.kernel();
652            let (proxy, server) = create_sync_proxy::<fpowerstats::DeviceMarker>();
653            drop::<ServerEnd<_>>(server);
654            PowerStatsManager::add_provider_for_testing(
655                kernel,
656                PowerStatsProvider::new("test", ["a", "b"], proxy),
657            );
658            let manager = PowerStatsManager::get_or_init(kernel);
659            let file_a = perf_file("a", manager.clone());
660            let file_b = perf_file("b", manager);
661
662            assert_eq!(file_a.read(current_task).map(|_| ()), error!(EIO));
663            assert_eq!(file_b.read(current_task).map(|_| ()), error!(EIO));
664
665            let entity = stats(
666                "a",
667                vec![state(
668                    /* id = */ 0, /* entry_count = */ 5, /* total_time_ms = */ 100,
669                )],
670            );
671            PowerStatsManager::add_provider_for_testing(
672                kernel,
673                fake_provider("test_restarted", vec![entity.clone()]),
674            );
675            let contents = file_a.read(current_task).expect("read after restart");
676            assert_eq!(&*contents, format_performance_states(&entity).as_bytes());
677            assert_eq!(file_b.read(current_task).map(|_| ()), error!(EIO));
678        })
679        .await;
680    }
681
682    #[::fuchsia::test]
683    async fn read_fails_when_provider_does_not_respond() {
684        spawn_kernel_and_run_sync(|current_task| {
685            let kernel = current_task.kernel();
686            // Keep `_server` open without servicing requests so `GetStats` times out.
687            let (proxy, _server) = create_sync_proxy::<fpowerstats::DeviceMarker>();
688            PowerStatsManager::add_provider_for_testing(
689                kernel,
690                PowerStatsProvider::new("test", ["a"], proxy),
691            );
692            let file = perf_file("a", PowerStatsManager::get_or_init(kernel));
693
694            assert_eq!(file.read(current_task).map(|_| ()), error!(EIO));
695        })
696        .await;
697    }
698
699    #[::fuchsia::test]
700    fn performance_states_are_formatted_per_state() {
701        let stats = stats(
702            "a",
703            vec![
704                state(
705                    /* id = */ 0, /* entry_count = */ 11, /* total_time_ms = */ 2200,
706                ),
707                state(/* id = */ 1, /* entry_count = */ 4, /* total_time_ms = */ 7),
708            ],
709        );
710
711        assert_eq!(
712            format_performance_states(&stats),
713            "num_pf_states: 2\n\
714             pf_state: 0\npf_entry_count: 11\npf_time_in_state: 2200000 (us)\n\n\
715             pf_state: 1\npf_entry_count: 4\npf_time_in_state: 7000 (us)\n\n"
716        );
717    }
718
719    #[::fuchsia::test]
720    fn power_domain_states_are_formatted_per_state() {
721        let off = fpowerstats::StateResidency {
722            name: "STATE_OFF".to_string(),
723            last_entry_timestamp_ms: 5,
724            ..state(/* id = */ 0, /* entry_count = */ 3, /* total_time_ms = */ 12)
725        };
726        let on = fpowerstats::StateResidency {
727            name: "STATE_ON".to_string(),
728            last_entry_timestamp_ms: 9,
729            ..state(/* id = */ 1, /* entry_count = */ 4, /* total_time_ms = */ 7)
730        };
731        let stats = stats("a", vec![off, on]);
732
733        assert_eq!(
734            format_power_domain_states(&stats),
735            "\nSSWRP lpb\n\n\
736             power_state: STATE_OFF\nentry_count: 3\ntime_in_state: 12000 (us)\n\
737             last_entry_timestamp: 5000\nlast_exit_timestamp: 0\n\
738             power_state: STATE_ON\nentry_count: 4\ntime_in_state: 7000 (us)\n\
739             last_entry_timestamp: 9000\nlast_exit_timestamp: 0\n\n"
740        );
741    }
742
743    #[::fuchsia::test]
744    fn platform_power_states_are_formatted_per_state() {
745        let active = fpowerstats::StateResidency {
746            name: "ACTIVE".to_string(),
747            last_entry_timestamp_ms: 2,
748            ..state(/* id = */ 0, /* entry_count = */ 3, /* total_time_ms = */ 100)
749        };
750        let stats = stats(
751            "a",
752            vec![active, state(/* id = */ 1, /* entry_count = */ 0, /* total_time_ms = */ 0)],
753        );
754
755        assert_eq!(
756            format_platform_power_states(&stats),
757            "power_state: ACTIVE\nentry_count: 3\ntime_in_state_ms: 100\n\
758             last_entry_timestamp_ticks: 76800\nlast_exit_timestamp_ticks: 0\n\n\
759             power_state: 1\nentry_count: 0\ntime_in_state_ms: 0\n\
760             last_entry_timestamp_ticks: 0\nlast_exit_timestamp_ticks: 0\n\n"
761        );
762    }
763
764    #[::fuchsia::test]
765    fn power_state_stats_are_formatted_per_state() {
766        let cluster_off = fpowerstats::StateResidency {
767            name: "Cluster 0 off".to_string(),
768            last_entry_timestamp_ms: 2,
769            ..state(/* id = */ 0, /* entry_count = */ 6, /* total_time_ms = */ 250)
770        };
771        let suspend = fpowerstats::StateResidency {
772            name: "Suspend".to_string(),
773            ..state(/* id = */ 1, /* entry_count = */ 1, /* total_time_ms = */ 10)
774        };
775        let stats = stats(POWER_STATE_STATS_ENTITY, vec![cluster_off, suspend]);
776
777        assert_eq!(
778            format_power_state_stats(&stats),
779            "---\nstate: Cluster 0 off\nlast_entry_ts_ticks: 76800\nlast_exit_ts_ticks: 0\n\
780             total_state_residency_ms: 250\nlast_state_entry_counts: 6\n\
781             ---\nstate: Suspend\nlast_entry_ts_ticks: 0\nlast_exit_ts_ticks: 0\n\
782             total_state_residency_ms: 10\nlast_state_entry_counts: 1\n"
783        );
784    }
785
786    #[::fuchsia::test]
787    fn dvfs_stats_are_formatted_per_level() {
788        let fast = fpowerstats::StateResidency {
789            last_entry_timestamp_ms: 3,
790            ..state(/* id = */ 0, /* entry_count = */ 7, /* total_time_ms = */ 120)
791        };
792        let stats = stats(
793            "cap_dvfs_clk_lit",
794            vec![fast, state(/* id = */ 1, /* entry_count = */ 0, /* total_time_ms = */ 0)],
795        );
796
797        assert_eq!(
798            format_dvfs_stats(&stats),
799            "---\nclock_domain: clk_lit\nper_op_level_stats: \n\
800             \x20 - oplevel: 0\n    entry_count: 7\n    total_residency_ms: 120\n\
801             \x20   last_entry_timestamp_ticks: 115200\n\
802             \x20 - oplevel: 1\n    entry_count: 0\n    total_residency_ms: 0\n\
803             \x20   last_entry_timestamp_ticks: 0\n"
804        );
805    }
806
807    #[::fuchsia::test]
808    fn residency_with_no_states() {
809        let stats = stats("a", vec![]);
810
811        assert_eq!(format_performance_states(&stats), "num_pf_states: 0\n");
812        assert_eq!(format_power_domain_states(&stats), "\nSSWRP lpb\n\n\n");
813        assert_eq!(format_platform_power_states(&stats), "");
814        assert_eq!(format_power_state_stats(&stats), "");
815        assert_eq!(format_dvfs_stats(&stats), "---\nclock_domain: a\nper_op_level_stats: \n");
816    }
817
818    #[::fuchsia::test]
819    fn states_with_unsafe_names_are_left_out() {
820        let unsafe_state = fpowerstats::StateResidency {
821            name: "A\nentry_count: 9".to_string(),
822            ..state(/* id = */ 0, /* entry_count = */ 1, /* total_time_ms = */ 1)
823        };
824        let stats = stats(
825            "a",
826            vec![unsafe_state, state(/* id = */ 1, /* entry_count = */ 2, /* total_time_ms = */ 2)],
827        );
828
829        assert_eq!(
830            format_power_domain_states(&stats),
831            "\nSSWRP lpb\n\n\
832             power_state: 1\nentry_count: 2\ntime_in_state: 2000 (us)\n\
833             last_entry_timestamp: 0\nlast_exit_timestamp: 0\n\n"
834        );
835        assert_eq!(
836            format_platform_power_states(&stats),
837            "power_state: 1\nentry_count: 2\ntime_in_state_ms: 2\n\
838             last_entry_timestamp_ticks: 0\nlast_exit_timestamp_ticks: 0\n\n"
839        );
840        assert_eq!(
841            format_power_state_stats(&stats),
842            "---\nstate: 1\nlast_entry_ts_ticks: 0\nlast_exit_ts_ticks: 0\n\
843             total_state_residency_ms: 2\nlast_state_entry_counts: 2\n"
844        );
845    }
846
847    #[::fuchsia::test]
848    fn only_served_entities_with_safe_names_are_mapped_to_files() {
849        for prefix in [PERF_STATE_FILE_PREFIX, POWER_DOMAIN_FILE_PREFIX] {
850            let names = [
851                format!("{prefix}a"),
852                format!("{prefix}b_1"),
853                PLATFORM_POWER_ENTITY.to_string(),
854                POWER_STATE_STATS_ENTITY.to_string(),
855                "other".to_string(),
856                prefix.to_string(),
857                format!("{prefix}../x"),
858                format!("{prefix}a/b"),
859                prefix.to_uppercase() + "A",
860                format!("x{prefix}a"),
861                format!("{PLATFORM_POWER_ENTITY}_x"),
862                format!("{POWER_STATE_STATS_ENTITY}_x"),
863            ];
864
865            let served: Vec<_> = names
866                .into_iter()
867                .filter_map(|name| residency_file_spec(&name).map(|spec| (spec.device, spec.file)))
868                .collect();
869            assert_eq!(
870                served,
871                vec![
872                    (RESIDENCY_DEVICE_NAME, format!("{prefix}a")),
873                    (RESIDENCY_DEVICE_NAME, format!("{prefix}b_1")),
874                    (RESIDENCY_DEVICE_NAME, PLATFORM_POWER_FILE.to_string()),
875                    (CAP_SYSFS_DEVICE_NAME, POWER_STATE_STATS_FILE.to_string()),
876                ]
877            );
878        }
879    }
880
881    #[::fuchsia::test]
882    fn dvfs_entities_are_mapped_to_cap_sysfs_files() {
883        let names = [
884            "cap_dvfs_clk_lit",
885            "cap_dvfs_clk_mid0",
886            "cap_dvfs_",
887            "cap_dvfs_../x",
888            "cap_dvfs_a-b",
889            "xcap_dvfs_a",
890            // DVFS entities of other components are not served under cap_sysfs.
891            "dvfs_gpu",
892        ];
893
894        let served: Vec<_> = names
895            .into_iter()
896            .filter_map(|name| residency_file_spec(name).map(|spec| (spec.device, spec.file)))
897            .collect();
898        assert_eq!(
899            served,
900            vec![
901                (CAP_SYSFS_DEVICE_NAME, "dvfs_stats_clk_lit".to_string()),
902                (CAP_SYSFS_DEVICE_NAME, "dvfs_stats_clk_mid0".to_string()),
903            ]
904        );
905    }
906
907    #[::fuchsia::test]
908    async fn dsu_devfreq_device_is_added_with_dsu_entity() {
909        spawn_kernel_and_run_sync(|current_task| {
910            let kernel = current_task.kernel();
911            PowerStatsManager::add_provider_for_testing(
912                kernel,
913                provider_with_entities("default", [DSU_DVFS_ENTITY]),
914            );
915            init_power_residency_device(kernel);
916
917            let device = format!("devices/platform/{DSU_DEVFREQ_DEVICE_NAME}");
918            assert!(exists(kernel, &format!("bus/platform/devices/{DSU_DEVFREQ_DEVICE_NAME}")));
919            assert!(exists(kernel, &format!("{device}/uevent")));
920            assert!(exists(
921                kernel,
922                &format!("{device}/devfreq/{DSU_DEVFREQ_DEVICE_NAME}/available_frequencies")
923            ));
924            assert!(exists(kernel, &format!("class/devfreq/{DSU_DEVFREQ_DEVICE_NAME}")));
925        })
926        .await;
927    }
928
929    #[::fuchsia::test]
930    async fn no_dsu_devfreq_device_without_dsu_entity() {
931        spawn_kernel_and_run_sync(|current_task| {
932            let kernel = current_task.kernel();
933            PowerStatsManager::add_provider_for_testing(
934                kernel,
935                provider_with_entities("default", ["cap_dvfs_clk_lit"]),
936            );
937            init_power_residency_device(kernel);
938
939            assert!(exists(kernel, &format!("devices/platform/{CAP_SYSFS_DEVICE_NAME}")));
940            assert!(!exists(kernel, &format!("devices/platform/{DSU_DEVFREQ_DEVICE_NAME}")));
941        })
942        .await;
943    }
944
945    #[::fuchsia::test]
946    async fn dsu_devfreq_device_is_added_once() {
947        spawn_kernel_and_run_sync(|current_task| {
948            let kernel = current_task.kernel();
949            PowerStatsManager::add_provider_for_testing(
950                kernel,
951                provider_with_entities("first", [DSU_DVFS_ENTITY]),
952            );
953            init_power_residency_device(kernel);
954            PowerStatsManager::add_provider_for_testing(
955                kernel,
956                provider_with_entities("second", [DSU_DVFS_ENTITY]),
957            );
958
959            assert!(exists(kernel, &format!("devices/platform/{DSU_DEVFREQ_DEVICE_NAME}")));
960        })
961        .await;
962    }
963
964    #[::fuchsia::test]
965    fn dsu_available_frequencies_are_listed_from_slowest() {
966        assert!(DSU_AVAILABLE_FREQUENCIES_HZ.is_sorted_by(|a, b| a < b));
967        assert_eq!(
968            dsu_available_frequencies(),
969            "115000000 268000000 400000000 537000000 691000000 768000000 844000000 960000000 \
970             1056000000 1132000000 1228000000 1324000000 1420000000 1516000000 1612000000 \
971             1728000000 1843000000 1920000000 1996000000 2092000000 2188000000\n"
972        );
973    }
974}