Skip to main content

starnix_core/fs/sysfs/
cpu_class_directory.rs

1// Copyright 2023 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
5use crate::fs::sysfs::build_device_directory;
6use crate::task::{CurrentTask, Kernel};
7use crate::vfs::pseudo::simple_directory::SimpleDirectoryMutator;
8use crate::vfs::pseudo::simple_file::{
9    BytesFile, BytesFileOps, SimpleFileNode, parse_unsigned_file,
10};
11use crate::vfs::pseudo::stub_empty_file::StubEmptyFile;
12use crate::vfs::{FsNodeOps, FsString};
13use fidl_fuchsia_hardware_cpu_ctrl as fcpuctrl;
14use fidl_fuchsia_hardware_power_stats as fpowerstats;
15use fidl_fuchsia_power_cpu as fcpu;
16use fidl_fuchsia_power_cpu_manager as fcpumanager;
17use fuchsia_component::client::connect_to_protocol_sync;
18use itertools::Itertools;
19use starnix_logging::{bug_ref, log_warn};
20use starnix_sync::{CpuC2ProxyCacheLock, CpuFreqProxyCacheLock, LockDepMutex};
21use starnix_uapi::errors::Errno;
22use starnix_uapi::file_mode::mode;
23use starnix_uapi::{errno, error, from_status_like_fdio};
24use std::borrow::Cow;
25use std::collections::HashMap;
26use std::sync::Arc;
27use zx;
28
29#[derive(Clone)]
30struct CpuFreqDomains(Arc<[Arc<CpuFreqDomain>]>);
31
32/// Returns the per-domain CPU frequency state shared across sysfs `cpufreq` nodes and thermal
33/// cooling devices for `kernel`.
34pub fn get_cpu_freq_domains(kernel: &Kernel) -> Arc<[Arc<CpuFreqDomain>]> {
35    kernel.expando.get_or_init(|| CpuFreqDomains(get_cpu_domains().into())).0.clone()
36}
37
38/// Registers `/sys/devices/system/cpu` and the per-core `cpuN` bus devices beneath it.
39pub fn register_cpu_devices(kernel: &Kernel) {
40    // Each domain is wrapped once and shared across `cpuN/cpufreq`, `cpufreq/policyN`, and
41    // thermal `cooling_device*` nodes so all voters aggregate on the same domain state.
42    let cpu_domains = get_cpu_freq_domains(kernel);
43
44    let mut core_to_domain_map: Vec<(u64, &Arc<CpuFreqDomain>)> = cpu_domains
45        .iter()
46        .flat_map(|domain| domain.core_ids.iter().map(move |id| (*id, domain)))
47        .collect();
48    core_to_domain_map.sort_by_key(|(id, _)| *id);
49    core_to_domain_map.dedup_by_key(|(id, _)| *id);
50
51    let core_count = core_to_domain_map.len();
52    let registry = &kernel.device_registry;
53    let system_device = registry.objects.system_device();
54    let cpu_root =
55        registry.add_subsystemless_device("cpu".into(), Some(system_device), |device, dir| {
56            build_device_directory(device, dir);
57            build_cpu_subsystem_directory(dir, &cpu_domains, core_count);
58        });
59
60    let cpu_bus = registry.objects.get_or_create_bus("cpu".into());
61    let c2_provider = Arc::new(CpuC2Provider::default());
62    for (core_id, domain) in &core_to_domain_map {
63        let name: FsString = format!("cpu{}", core_id).into();
64        let c2_provider = c2_provider.clone();
65        registry.add_bus_device(
66            name.as_ref(),
67            Some(cpu_root.clone()),
68            cpu_bus.clone(),
69            |device, dir| {
70                build_device_directory(device, dir);
71                build_cpu_directory(dir, *core_id, domain, c2_provider);
72            },
73        );
74    }
75}
76
77/// Populates the subsystem-wide attributes in `/sys/devices/system/cpu`.
78fn build_cpu_subsystem_directory(
79    dir: &SimpleDirectoryMutator,
80    cpu_domains: &[Arc<CpuFreqDomain>],
81    core_count: usize,
82) {
83    dir.entry(
84        "online",
85        BytesFile::new_node(format!("0-{}\n", core_count.saturating_sub(1)).into_bytes()),
86        mode!(IFREG, 0o444),
87    );
88    dir.entry(
89        "possible",
90        BytesFile::new_node(format!("0-{}\n", core_count.saturating_sub(1)).into_bytes()),
91        mode!(IFREG, 0o444),
92    );
93    dir.subdir("vulnerabilities", 0o755, |dir| {
94        for (name, contents) in VULNERABILITIES {
95            let contents = contents.to_string();
96            dir.entry(name, BytesFile::new_node(contents.into_bytes()), mode!(IFREG, 0o444));
97        }
98    });
99    dir.subdir("cpufreq", 0o755, |dir| {
100        for domain in cpu_domains.iter() {
101            let min_core_id = domain.core_ids.iter().min().expect("core_ids is empty");
102            let name = format!("policy{}", min_core_id);
103            dir.subdir(&name, 0o755, |dir| build_cpufreq_directory(dir, domain));
104        }
105    });
106    dir.subdir("soc", 0o755, |dir| {
107        dir.subdir("0", 0o755, |dir| {
108            dir.entry(
109                "machine",
110                StubEmptyFile::new_node(bug_ref!("https://fxbug.dev/452096300")),
111                mode!(IFREG, 0o444),
112            );
113        });
114    });
115}
116
117/// Retrieves CPU topology and domain information with a tiered fallback approach.
118///
119/// 1. Get CPU domains from the `fuchsia.power.cpu.DomainController` FIDL protocol.
120/// 2. If that fails, use the `fuchsia.hardware.cpu.ctrl.Service` FIDL service to connect to
121///    individual CPU control devices.
122/// 3. If that fails, get CPU information from the kernel directly. In this case, no CPU control
123///    or topological information is available, so only a limited set of sysfs entries
124///    will be populated.
125fn get_cpu_domains() -> Vec<Arc<CpuFreqDomain>> {
126    // Tier 1: Try DomainController
127    if let Ok(domain_controller) = connect_to_protocol_sync::<fcpu::DomainControllerMarker>() {
128        if let Ok(mut domains) = domain_controller.list_domains(zx::MonotonicInstant::INFINITE) {
129            // Remove any domains without an ID or empty core_ids.
130            domains
131                .retain(|d| d.id.is_some() && d.core_ids.as_ref().map_or(false, |c| !c.is_empty()));
132            if !domains.is_empty() {
133                return domains
134                    .iter()
135                    .map(|domain| Arc::new(CpuFreqDomain::new(domain, Some(&domain_controller))))
136                    .collect();
137            }
138        }
139    }
140
141    log_warn!(
142        "Could not retrieve CPU domains from fuchsia.power.cpu.DomainController, using CPU control devices instead."
143    );
144
145    // Tier 2: Try cpu.ctrl devices
146    if let Ok(proxies) = connect_to_cpu_devices() {
147        let mut domains = Vec::new();
148
149        for proxy in proxies {
150            let cpu_count = match proxy.get_num_logical_cores(zx::MonotonicInstant::INFINITE) {
151                Ok(count) => count,
152                Err(e) => {
153                    log_warn!("get_num_logical_cores returned error: {}", e);
154                    continue;
155                }
156            };
157            let domain_id = match proxy.get_domain_id(zx::MonotonicInstant::INFINITE) {
158                Ok(id) => id as u64,
159                Err(e) => {
160                    log_warn!("get_domain_id returned error: {}", e);
161                    continue;
162                }
163            };
164
165            let mut core_ids = Vec::with_capacity(cpu_count as usize);
166            let mut get_core_failed = false;
167            for i in 0..cpu_count {
168                let core_id = match proxy.get_logical_core_id(i, zx::MonotonicInstant::INFINITE) {
169                    Ok(id) => id,
170                    Err(e) => {
171                        log_warn!("get_logical_core_id error in domain {}: {}", domain_id, e);
172                        get_core_failed = true;
173                        break;
174                    }
175                };
176                core_ids.push(core_id);
177            }
178            if get_core_failed || core_ids.is_empty() {
179                log_warn!("get_logical_core_id failed in domain {}, skipping", domain_id);
180                continue;
181            }
182
183            let available_frequencies_hz =
184                match proxy.get_operating_point_count(zx::MonotonicInstant::INFINITE) {
185                    Ok(Ok(count)) => {
186                        let mut freqs = Vec::with_capacity(count as usize);
187                        for i in 0..count {
188                            if let Ok(Ok(info)) =
189                                proxy.get_operating_point_info(i, zx::MonotonicInstant::INFINITE)
190                            {
191                                if info.frequency_hz > 0 {
192                                    freqs.push(info.frequency_hz as u64);
193                                }
194                            }
195                        }
196                        freqs.sort();
197                        freqs.dedup();
198                        Some(freqs)
199                    }
200                    _ => None,
201                };
202
203            domains.push(fcpu::DomainInfo {
204                id: Some(domain_id),
205                core_ids: Some(core_ids),
206                available_frequencies_hz,
207                ..Default::default()
208            });
209        }
210
211        if !domains.is_empty() {
212            return domains
213                .iter()
214                .map(|domain| Arc::new(CpuFreqDomain::new(domain, None)))
215                .collect();
216        }
217    }
218
219    log_warn!(
220        "Could not connect to CPU control devices, using default domain info from kernel CPU count."
221    );
222
223    // Tier 3: Fallback to kernel CPU count
224    let cpu_count = zx::system_get_num_cpus();
225    vec![Arc::new(CpuFreqDomain::new(
226        &fcpu::DomainInfo {
227            id: Some(0),
228            core_ids: Some((0..cpu_count as u64).collect()),
229            available_frequencies_hz: None,
230            name: None,
231            ..Default::default()
232        },
233        None,
234    ))]
235}
236
237fn hz_to_khz(hz: u64) -> u64 {
238    hz / 1000
239}
240
241/// The only CPU frequency governor Starnix reports.
242///
243/// Frequency selection is not user-tunable on Fuchsia, so `scaling_governor` is a fixed value
244/// rather than a real choice.
245const SCALING_GOVERNOR: &str = "performance";
246
247/// Formats a frequency for a sysfs node, leaving the value empty when the domain reports none.
248fn format_freq_khz(khz: Option<u64>) -> String {
249    format!("{}\n", khz.map(|f| f.to_string()).unwrap_or_default())
250}
251
252fn build_cpu_directory(
253    dir: &SimpleDirectoryMutator,
254    core_id: u64,
255    domain: &Arc<CpuFreqDomain>,
256    c2_provider: Arc<CpuC2Provider>,
257) {
258    let cluster_id = domain.domain_id;
259
260    dir.entry(
261        "cpu_capacity",
262        StubEmptyFile::new_node(bug_ref!("https://fxbug.dev/452096300")),
263        mode!(IFREG, 0o444),
264    );
265    dir.subdir("cpuidle", 0o755, |dir| {
266        build_cpuidle_directory(dir, core_id, c2_provider);
267    });
268    dir.subdir("cpufreq", 0o755, |dir| {
269        build_cpufreq_directory(dir, domain);
270    });
271    dir.subdir("topology", 0o755, |dir| {
272        dir.entry(
273            "cluster_id",
274            BytesFile::new_node(format!("{cluster_id}\n").into_bytes()),
275            mode!(IFREG, 0o444),
276        );
277        dir.entry(
278            "physical_package_id",
279            BytesFile::new_node(format!("{cluster_id}\n").into_bytes()),
280            mode!(IFREG, 0o444),
281        );
282    });
283}
284
285fn build_cpuidle_directory(
286    dir: &SimpleDirectoryMutator,
287    core_id: u64,
288    c2_provider: Arc<CpuC2Provider>,
289) {
290    let state0_c2_provider = c2_provider.clone();
291    dir.subdir("state0", 0o755, |dir| {
292        dir.entry("name", BytesFile::new_node(b"C1\n".to_vec()), mode!(IFREG, 0o444));
293        dir.entry("desc", BytesFile::new_node(b"idle\n".to_vec()), mode!(IFREG, 0o444));
294        dir.entry(
295            "time",
296            BytesFile::new_node(CpuIdleStatFile {
297                core_id,
298                stat_type: CpuIdleStatType::Time,
299                c2_provider: state0_c2_provider.clone(),
300            }),
301            mode!(IFREG, 0o444),
302        );
303        dir.entry(
304            "usage",
305            BytesFile::new_node(CpuIdleStatFile {
306                core_id,
307                stat_type: CpuIdleStatType::Usage,
308                c2_provider: state0_c2_provider,
309            }),
310            mode!(IFREG, 0o444),
311        );
312    });
313    dir.subdir("state1", 0o755, |dir| {
314        dir.entry("name", BytesFile::new_node(b"C2\n".to_vec()), mode!(IFREG, 0o444));
315        dir.entry("desc", BytesFile::new_node(b"deep idle\n".to_vec()), mode!(IFREG, 0o444));
316        dir.entry(
317            "time",
318            BytesFile::new_node(CpuIdleC2StatFile {
319                core_id,
320                stat_type: CpuIdleStatType::Time,
321                provider: c2_provider.clone(),
322            }),
323            mode!(IFREG, 0o444),
324        );
325        dir.entry(
326            "usage",
327            BytesFile::new_node(CpuIdleC2StatFile {
328                core_id,
329                stat_type: CpuIdleStatType::Usage,
330                provider: c2_provider,
331            }),
332            mode!(IFREG, 0o444),
333        );
334    });
335}
336
337const SOFT_MAX_FREQ_VOTER: &str = "soft_max_freq";
338
339/// Max-frequency voter nodes under `cpufreq/vote_manager/`.
340const VOTE_MANAGER_MAX_FREQ_NODES: &[&str] =
341    &["powerhint_max_freq", "thermal_max_freq", SOFT_MAX_FREQ_VOTER, "debug_max_freq"];
342
343/// Min-frequency voter nodes under `cpufreq/vote_manager/`.
344const VOTE_MANAGER_MIN_FREQ_NODES: &[&str] =
345    &["powerhint_min_freq", "soft_min_freq", "debug_min_freq"];
346
347fn add_max_freq_entry(
348    dir: &SimpleDirectoryMutator,
349    name: &'static str,
350    domain: &Arc<CpuFreqDomain>,
351) {
352    // A domain that reports no operating points cannot be capped, so fall back to an inert node
353    // rather than one that would fail every write.
354    if domain.is_tunable() {
355        dir.entry(name, ScalingMaxFreqFile::new_node(domain, name), mode!(IFREG, 0o644));
356    } else {
357        dir.entry(
358            name,
359            InertFile::new_node(format_freq_khz(domain.highest_khz())),
360            mode!(IFREG, 0o644),
361        );
362    }
363}
364
365fn add_min_freq_entry(
366    dir: &SimpleDirectoryMutator,
367    name: &'static str,
368    domain: &Arc<CpuFreqDomain>,
369) {
370    // Mirror `add_max_freq_entry`: only expose active min-frequency voting when the domain is
371    // tunable, falling back to an inert node reporting `lowest_khz()` otherwise.
372    if domain.is_tunable() {
373        dir.entry(name, ScalingMinFreqFile::new_node(domain, name), mode!(IFREG, 0o644));
374    } else {
375        dir.entry(
376            name,
377            InertFile::new_node(format_freq_khz(domain.lowest_khz())),
378            mode!(IFREG, 0o644),
379        );
380    }
381}
382
383fn build_cpufreq_directory(dir: &SimpleDirectoryMutator, domain: &Arc<CpuFreqDomain>) {
384    dir.subdir("stats", 0o755, |dir| {
385        dir.entry("reset", CpuFreqStatsResetFile::new_node(), mode!(IFREG, 0o200));
386        dir.entry(
387            "time_in_state",
388            StubEmptyFile::new_node(bug_ref!("https://fxbug.dev/452096300")),
389            mode!(IFREG, 0o444),
390        );
391    });
392
393    // Android Power HAL and thermal/debug tooling vote through `cpufreq/vote_manager/*`. Each
394    // max-frequency node (including `scaling_max_freq`) registers its own persistent vote in
395    // `CpuFreqDomain`, which enforces the most restrictive ceiling across all active voters.
396    dir.subdir("vote_manager", 0o755, |dir| {
397        for name in VOTE_MANAGER_MAX_FREQ_NODES {
398            add_max_freq_entry(dir, name, domain);
399        }
400        for name in VOTE_MANAGER_MIN_FREQ_NODES {
401            add_min_freq_entry(dir, name, domain);
402        }
403    });
404
405    let related_cpus_str = format!("{}\n", domain.core_ids.iter().sorted().join(" "));
406    dir.entry(
407        "related_cpus",
408        BytesFile::new_node(related_cpus_str.into_bytes()),
409        mode!(IFREG, 0o444),
410    );
411    dir.entry(
412        "scaling_cur_freq",
413        create_scaling_cur_freq_file(domain.domain_id),
414        mode!(IFREG, 0o444),
415    );
416
417    add_min_freq_entry(dir, "scaling_min_freq", domain);
418
419    add_max_freq_entry(dir, "scaling_max_freq", domain);
420
421    dir.entry(
422        "scaling_available_frequencies",
423        BytesFile::new_node((domain.sorted_khz.iter().join(" ") + "\n").into_bytes()),
424        mode!(IFREG, 0o444),
425    );
426    dir.entry(
427        "scaling_available_governors",
428        BytesFile::new_node(format!("{SCALING_GOVERNOR}\n").into_bytes()),
429        mode!(IFREG, 0o444),
430    );
431    dir.entry(
432        "scaling_governor",
433        InertFile::new_node(format!("{SCALING_GOVERNOR}\n")),
434        mode!(IFREG, 0o644),
435    );
436    dir.entry(
437        "cpuinfo_min_freq",
438        BytesFile::new_node(format_freq_khz(domain.lowest_khz()).into_bytes()),
439        mode!(IFREG, 0o444),
440    );
441    dir.entry(
442        "cpuinfo_max_freq",
443        BytesFile::new_node(format_freq_khz(domain.highest_khz()).into_bytes()),
444        mode!(IFREG, 0o444),
445    );
446}
447
448const VULNERABILITIES: &[(&str, &str)] = &[
449    ("gather_data_sampling", "Not affected\n"),
450    ("itlb_multihit", "Not affected\n"),
451    ("l1tf", "Not affected\n"),
452    ("mds", "Not affected\n"),
453    ("meltdown", "Not affected\n"),
454    ("mmio_stale_data", "Not affected\n"),
455    ("retbleed", "Not affected\n"),
456    ("spec_rstack_overflow", "Not affected\n"),
457    ("spec_store_bypass", "Not affected\n"),
458    ("spectre_v1", "Not affected\n"),
459    ("spectre_v2", "Not affected\n"),
460    ("srbds", "Not affected\n"),
461    ("tsx_async_abort", "Not affected\n"),
462];
463
464struct CpuFreqStatsResetFile {}
465
466impl CpuFreqStatsResetFile {
467    pub fn new_node() -> impl FsNodeOps {
468        BytesFile::new_node(Self {})
469    }
470}
471
472impl BytesFileOps for CpuFreqStatsResetFile {
473    // Currently a no-op. The value written to this node does not matter.
474    fn write(&self, _current_task: &CurrentTask, _data: Vec<u8>) -> Result<(), Errno> {
475        Ok(())
476    }
477}
478
479#[derive(Default)]
480struct CpuFreqDomainState {
481    /// Active ceiling votes keyed by voter name (`voter -> opp_index`), aggregated using Linux
482    /// `freq_qos` / `vote_manager` semantics (the lowest maximum frequency wins).
483    votes: HashMap<&'static str, u64>,
484
485    /// Active floor votes in kHz keyed by voter name (`voter -> khz` where `khz > lowest_khz`),
486    /// aggregated using Linux `freq_qos` / `vote_manager` semantics (the highest minimum
487    /// frequency wins).
488    min_votes: HashMap<&'static str, u64>,
489
490    /// The effective ceiling (`Some(opp_index)` or `None` when uncapped) last applied to
491    /// `DomainController`, or `None` when unknown, either because no vote has been applied yet
492    /// or because the last call failed.
493    applied_opp: Option<Option<u64>>,
494
495    /// Lazily established connection to the server, dropped if a call fails at the transport
496    /// level so that the next caller reconnects.
497    domain_controller: Option<fcpu::DomainControllerSynchronousProxy>,
498
499    /// Lazily established connection to `fuchsia.power.cpu.manager.Boost`, dropped if a call
500    /// fails at the transport level so that the next caller reconnects.
501    boost_proxy: Option<fcpumanager::BoostSynchronousProxy>,
502
503    /// Active boost token held while `min_votes` is non-empty. Dropping this token signals
504    /// `ZX_EVENTPAIR_PEER_CLOSED` to `cpu-manager`, releasing this domain's boost vote.
505    boost_token: Option<zx::EventPair>,
506}
507
508impl CpuFreqDomainState {
509    /// Queries the domain's current ceiling on `domain_controller` and seeds any pre-existing cap
510    /// (such as a `boot_max_opp` limit configured in `cpu-manager`) into `soft_max_freq`, matching
511    /// how Linux `google_vote_manager` initializes `soft_max_freq` from the platform boot ceiling.
512    fn from_domain_controller(
513        domain_id: u64,
514        opps_hz: &[u64],
515        domain_controller: &fcpu::DomainControllerSynchronousProxy,
516    ) -> Self {
517        let max_hz = opps_hz.iter().max().copied();
518        match domain_controller.get_max_frequency(domain_id, zx::MonotonicInstant::INFINITE) {
519            Ok(Ok(opp_index)) => match opps_hz.get(opp_index as usize).copied() {
520                Some(hz) if Some(hz) == max_hz => {
521                    Self { applied_opp: Some(None), ..Self::default() }
522                }
523                Some(_) => {
524                    let mut votes = HashMap::new();
525                    votes.insert(SOFT_MAX_FREQ_VOTER, opp_index);
526                    Self { votes, applied_opp: Some(Some(opp_index)), ..Self::default() }
527                }
528                None => {
529                    log_warn!(
530                        "Initial max frequency index {} out of range for domain {} (len {})",
531                        opp_index,
532                        domain_id,
533                        opps_hz.len()
534                    );
535                    Self::default()
536                }
537            },
538            other => {
539                log_warn!(
540                    "Failed to query initial max frequency for domain {}: {:?}",
541                    domain_id,
542                    other
543                );
544                Self::default()
545            }
546        }
547    }
548}
549
550/// Per-domain state shared by a domain's cpufreq nodes and by its corresponding thermal
551/// `cooling_device`.
552pub struct CpuFreqDomain {
553    domain_id: u64,
554
555    /// Optional name of the domain/cluster from `DomainInfo.name`.
556    name: Option<String>,
557
558    /// The IDs of the CPU cores that are part of this domain.
559    core_ids: Vec<u64>,
560
561    /// Available frequencies in Hz, in `DomainInfo.available_frequencies_hz` order.
562    ///
563    /// This is operating point order: `DomainController.SetMaxFrequency` takes an index into
564    /// this list. It is deliberately kept apart from `sorted_khz` below, which is what
565    /// `scaling_available_frequencies` reports, because sorting reorders the entries. The two
566    /// lists are not positionally related, so a frequency must be matched by value to find its
567    /// operating point.
568    opps_hz: Vec<u64>,
569
570    /// `opps_hz` in kHz, sorted ascending and deduplicated.
571    sorted_khz: Vec<u64>,
572
573    /// Whether `DomainController` is serving this domain (as opposed to the `cpu.ctrl` or
574    /// kernel-CPU-count fallbacks).
575    has_domain_controller: bool,
576
577    state: LockDepMutex<CpuFreqDomainState, CpuFreqProxyCacheLock>,
578}
579
580impl CpuFreqDomain {
581    fn new(
582        domain: &fcpu::DomainInfo,
583        domain_controller: Option<&fcpu::DomainControllerSynchronousProxy>,
584    ) -> Self {
585        let domain_id = domain.id.expect("id not available");
586        let opps_hz = domain.available_frequencies_hz.clone().unwrap_or_default();
587        let sorted_khz = opps_hz.iter().map(|hz| hz_to_khz(*hz)).sorted().dedup().collect();
588        let state = match domain_controller {
589            Some(dc) if !opps_hz.is_empty() => {
590                CpuFreqDomainState::from_domain_controller(domain_id, &opps_hz, dc)
591            }
592            _ => CpuFreqDomainState::default(),
593        };
594        Self {
595            domain_id,
596            name: domain.name.clone(),
597            core_ids: domain.core_ids.clone().expect("core_ids not available"),
598            opps_hz,
599            sorted_khz,
600            has_domain_controller: domain_controller.is_some(),
601            state: LockDepMutex::new(state),
602        }
603    }
604
605    pub fn name(&self) -> Option<&str> {
606        self.name.as_deref()
607    }
608
609    pub fn opp_count(&self) -> usize {
610        self.opps_hz.len()
611    }
612
613    pub fn get_vote(&self, voter: &'static str) -> Option<u64> {
614        self.state.lock().votes.get(voter).copied()
615    }
616
617    /// Returns `voter`'s active ceiling in kHz, or the domain's highest frequency when `voter` has
618    /// no active cap.
619    fn voter_frequency_khz(&self, voter: &'static str) -> Option<u64> {
620        self.get_vote(voter)
621            .and_then(|index| self.opps_hz.get(index as usize))
622            .map(|&hz| hz_to_khz(hz))
623            .or_else(|| self.highest_khz())
624    }
625
626    /// Whether the domain is backed by `DomainController` and reports operating points, and can
627    /// therefore have its maximum frequency controlled.
628    pub fn is_tunable(&self) -> bool {
629        self.has_domain_controller && !self.opps_hz.is_empty()
630    }
631
632    fn lowest_khz(&self) -> Option<u64> {
633        self.sorted_khz.first().copied()
634    }
635
636    fn highest_khz(&self) -> Option<u64> {
637        self.sorted_khz.last().copied()
638    }
639
640    /// Maps a frequency in kHz to the operating point that caps the domain there.
641    ///
642    /// Returns `None` when the request is at or above the domain's highest operating point,
643    /// meaning "no cap". A client releases a cap by writing a sentinel far above any real
644    /// frequency, which lands here.
645    ///
646    /// Otherwise the request is snapped down to the highest operating point that does not
647    /// exceed it, or to the lowest operating point if it undershoots them all.
648    fn opp_index_for_khz(&self, khz: u64) -> Option<u64> {
649        if khz >= self.highest_khz()? {
650            return None;
651        }
652        self.opps_hz
653            .iter()
654            .enumerate()
655            .filter(|(_, hz)| hz_to_khz(**hz) <= khz)
656            .max_by_key(|(_, hz)| **hz)
657            .or_else(|| self.opps_hz.iter().enumerate().min_by_key(|(_, hz)| **hz))
658            .map(|(index, _)| index as u64)
659    }
660
661    /// Updates `voter`'s ceiling vote (`Some(opp_index)` or `None` to release) and applies the
662    /// most restrictive ceiling across all active voters on this domain.
663    pub fn update_vote(&self, voter: &'static str, opp_index: Option<u64>) -> Result<(), Errno> {
664        if let Some(index) = opp_index {
665            if index as usize >= self.opps_hz.len() {
666                return error!(EINVAL);
667            }
668        }
669        let mut state = self.state.lock();
670        let previous = match opp_index {
671            Some(index) => state.votes.insert(voter, index),
672            None => state.votes.remove(voter),
673        };
674        let result = self.apply_effective_vote(&mut state);
675        if result.is_err() {
676            match previous {
677                Some(prev) => {
678                    state.votes.insert(voter, prev);
679                }
680                None => {
681                    state.votes.remove(voter);
682                }
683            }
684        }
685        result
686    }
687
688    fn apply_effective_vote(&self, state: &mut CpuFreqDomainState) -> Result<(), Errno> {
689        let effective_opp = state
690            .votes
691            .values()
692            .copied()
693            .min_by_key(|&index| (self.opps_hz[index as usize], std::cmp::Reverse(index)));
694        if state.applied_opp == Some(effective_opp) {
695            return Ok(());
696        }
697        state.applied_opp = None;
698        match effective_opp {
699            Some(index) => self.set_max_frequency(state, index)?,
700            None => self.clear_max_frequency(state)?,
701        }
702        state.applied_opp = Some(effective_opp);
703        Ok(())
704    }
705
706    /// Runs `call` against a lazily connected `DomainController`.
707    ///
708    /// A transport failure invalidates the cached connection and `applied_opp`; a protocol-level
709    /// error leaves the connection in place for the caller to interpret.
710    fn with_domain_controller<T, E: std::fmt::Debug>(
711        &self,
712        state: &mut CpuFreqDomainState,
713        call: impl FnOnce(&fcpu::DomainControllerSynchronousProxy) -> Result<T, E>,
714    ) -> Result<T, Errno> {
715        if state.domain_controller.is_none() {
716            state.domain_controller =
717                Some(connect_to_protocol_sync::<fcpu::DomainControllerMarker>().map_err(|e| {
718                    log_warn!("Failed to connect to DomainController: {:?}", e);
719                    errno!(ENODEV)
720                })?);
721        }
722        match call(state.domain_controller.as_ref().expect("connection was just established")) {
723            Ok(value) => Ok(value),
724            Err(e) => {
725                log_warn!("DomainController call failed for domain {}: {:?}", self.domain_id, e);
726                state.domain_controller = None;
727                state.applied_opp = None;
728                error!(EIO)
729            }
730        }
731    }
732
733    /// Reads back the frequency in kHz that the domain is currently capped to.
734    fn max_frequency_khz(&self) -> Result<u64, Errno> {
735        let mut state = self.state.lock();
736        let index = self
737            .with_domain_controller(&mut state, |dc| {
738                dc.get_max_frequency(self.domain_id, zx::MonotonicInstant::INFINITE)
739            })?
740            .map_err(|e| {
741                log_warn!("get_max_frequency failed for domain {}: {:?}", self.domain_id, e);
742                errno!(EINVAL)
743            })?;
744        let hz = self.opps_hz.get(index as usize).ok_or_else(|| errno!(EINVAL))?;
745        Ok(hz_to_khz(*hz))
746    }
747
748    fn set_max_frequency(&self, state: &mut CpuFreqDomainState, index: u64) -> Result<(), Errno> {
749        self.with_domain_controller(state, |dc| {
750            dc.set_max_frequency(self.domain_id, index, zx::MonotonicInstant::INFINITE)
751        })?
752        .map_err(|e| {
753            log_warn!("set_max_frequency failed for domain {}: {:?}", self.domain_id, e);
754            errno!(EINVAL)
755        })
756    }
757
758    fn clear_max_frequency(&self, state: &mut CpuFreqDomainState) -> Result<(), Errno> {
759        self.with_domain_controller(state, |dc| {
760            dc.clear_max_frequency(self.domain_id, zx::MonotonicInstant::INFINITE)
761        })?
762        .map_err(|e| {
763            log_warn!("clear_max_frequency failed for domain {}: {:?}", self.domain_id, e);
764            errno!(EINVAL)
765        })
766    }
767
768    /// Returns the effective minimum frequency in kHz for `scaling_min_freq`, or `voter`'s active
769    /// floor in kHz for `vote_manager/*_min_freq`, falling back to the domain's lowest frequency.
770    fn min_frequency_khz(&self, voter: &'static str) -> Option<u64> {
771        let state = self.state.lock();
772        let active_vote = if voter == "scaling_min_freq" {
773            state.min_votes.values().copied().max()
774        } else {
775            state.min_votes.get(voter).copied()
776        };
777        active_vote.or_else(|| self.lowest_khz())
778    }
779
780    /// Updates `voter`'s floor vote in kHz and acquires or releases a `cpu-manager` `Boost` token
781    /// for this domain when the set of active floor votes transitions between empty and non-empty.
782    pub fn update_min_vote(
783        &self,
784        kernel: Option<&Kernel>,
785        voter: &'static str,
786        khz: u64,
787    ) -> Result<(), Errno> {
788        let lowest_khz = self.lowest_khz().unwrap_or(0);
789        let highest_khz = self.highest_khz().unwrap_or(u64::MAX);
790        let mut state = self.state.lock();
791        if khz <= lowest_khz {
792            state.min_votes.remove(voter);
793        } else {
794            state.min_votes.insert(voter, khz.min(highest_khz));
795        }
796
797        if !state.min_votes.is_empty() && state.boost_token.is_none() {
798            if state.boost_proxy.is_none() {
799                state.boost_proxy = kernel
800                    .and_then(|k| {
801                        k.connect_to_protocol_at_container_svc::<fcpumanager::BoostMarker>()
802                            .ok()
803                            .map(|c| c.into_sync_proxy())
804                    })
805                    .or_else(|| connect_to_protocol_sync::<fcpumanager::BoostMarker>().ok());
806            }
807            if let Some(ref proxy) = state.boost_proxy {
808                match proxy.boost(zx::MonotonicInstant::INFINITE) {
809                    Ok(Ok(token)) => {
810                        state.boost_token = Some(token);
811                    }
812                    Ok(Err(e)) => {
813                        log_warn!(
814                            "cpu-manager Boost returned error for domain {}: {:?}",
815                            self.domain_id,
816                            e
817                        );
818                    }
819                    Err(e) => {
820                        log_warn!(
821                            "cpu-manager Boost FIDL call failed for domain {}: {:?}",
822                            self.domain_id,
823                            e
824                        );
825                        state.boost_proxy = None;
826                    }
827                }
828            }
829        } else if state.min_votes.is_empty() && state.boost_token.is_some() {
830            state.boost_token = None;
831        }
832        Ok(())
833    }
834}
835
836/// A writable max-frequency node (`scaling_max_freq` and `vote_manager/*_max_freq`), backed by
837/// `fuchsia.power.cpu/DomainController`.
838///
839/// Votes are keyed by the sysfs node identity (`voter`) in `CpuFreqDomain` and persist across
840/// `close()` until overwritten or cleared with a value at or above the domain maximum.
841struct ScalingMaxFreqFile {
842    domain: Arc<CpuFreqDomain>,
843    voter: &'static str,
844}
845
846impl ScalingMaxFreqFile {
847    fn new_node(domain: &Arc<CpuFreqDomain>, voter: &'static str) -> impl FsNodeOps {
848        BytesFile::new_node(Self { domain: domain.clone(), voter })
849    }
850}
851
852impl BytesFileOps for ScalingMaxFreqFile {
853    fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
854        let content = if self.voter == "scaling_max_freq" {
855            format!("{}\n", self.domain.max_frequency_khz()?)
856        } else {
857            format_freq_khz(self.domain.voter_frequency_khz(self.voter))
858        };
859        Ok(content.into_bytes().into())
860    }
861
862    fn write(&self, _current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
863        let opp_index = self.domain.opp_index_for_khz(parse_unsigned_file::<u64>(&data)?);
864        self.domain.update_vote(self.voter, opp_index)
865    }
866}
867
868/// A writable min-frequency node (`scaling_min_freq` and `vote_manager/*_min_freq`), backed by
869/// `fuchsia.power.cpu.manager/Boost`.
870///
871/// While any voter on a domain requests a floor above `lowest_khz`, the domain holds a
872/// ref-counted `Boost` token with `cpu-manager`; when all floor votes are cleared
873/// (`<= lowest_khz`), the token is dropped.
874struct ScalingMinFreqFile {
875    domain: Arc<CpuFreqDomain>,
876    voter: &'static str,
877}
878
879impl ScalingMinFreqFile {
880    fn new_node(domain: &Arc<CpuFreqDomain>, voter: &'static str) -> impl FsNodeOps {
881        BytesFile::new_node(Self { domain: domain.clone(), voter })
882    }
883}
884
885impl BytesFileOps for ScalingMinFreqFile {
886    fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
887        Ok(format_freq_khz(self.domain.min_frequency_khz(self.voter)).into_bytes().into())
888    }
889
890    fn write(&self, current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
891        let khz = parse_unsigned_file::<u64>(&data)?;
892        self.domain.update_min_vote(Some(current_task.kernel()), self.voter, khz)
893    }
894}
895
896/// A sysfs node that accepts writes, discards them, and reads back a fixed value.
897///
898/// This is for cpufreq nodes that the Android Power HAL writes but that Fuchsia cannot act on.
899/// Leaving such a node read-only is worse than making it inert: libperfmgr suppresses its
900/// warning and its 500ms retry clamp only when `open()` fails with `ENOENT`, so a node that
901/// exists but rejects writes produces log spam and a busy retry loop on every hint. Removing
902/// the node instead would return `ENOENT` to every reader, not just to libperfmgr.
903struct InertFile(Vec<u8>);
904
905impl InertFile {
906    fn new_node(contents: String) -> impl FsNodeOps {
907        BytesFile::new_node(Self(contents.into_bytes()))
908    }
909}
910
911impl BytesFileOps for InertFile {
912    fn write(&self, _current_task: &CurrentTask, _data: Vec<u8>) -> Result<(), Errno> {
913        Ok(())
914    }
915
916    fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
917        Ok(self.0.as_slice().into())
918    }
919}
920
921const CPU_DIRECTORY: &str = "/svc/fuchsia.hardware.cpu.ctrl.Service";
922
923fn connect_to_cpu_devices() -> Result<Vec<fcpuctrl::DeviceSynchronousProxy>, Errno> {
924    let dir = std::fs::read_dir(CPU_DIRECTORY).map_err(|_| errno!(EINVAL))?;
925
926    let proxies: Vec<_> = dir
927        .filter_map(|r| r.ok())
928        .filter_map(|entry| {
929            let path = entry.path().join("device").into_os_string().into_string().ok()?;
930            let (client, server) = zx::Channel::create();
931            fdio::service_connect(&path, server).ok()?;
932            Some(fcpuctrl::DeviceSynchronousProxy::new(client))
933        })
934        .collect();
935
936    if proxies.is_empty() { error!(ENOENT) } else { Ok(proxies) }
937}
938
939fn connect_to_cpu_device_by_domain_id(
940    domain_id: u64,
941) -> Result<fcpuctrl::DeviceSynchronousProxy, Errno> {
942    let dir = std::fs::read_dir(CPU_DIRECTORY).map_err(|_| errno!(EINVAL))?;
943
944    dir.filter_map(|r| r.ok())
945        .find_map(|entry| {
946            let path = entry.path().join("device").into_os_string().into_string().ok()?;
947            let (client, server) = zx::Channel::create();
948            fdio::service_connect(&path, server).ok()?;
949            let proxy = fcpuctrl::DeviceSynchronousProxy::new(client);
950
951            let dev_domain_id = proxy.get_domain_id(zx::MonotonicInstant::INFINITE).ok()?;
952            if domain_id == dev_domain_id as u64 { Some(proxy) } else { None }
953        })
954        .ok_or_else(|| errno!(ENOENT))
955}
956
957fn create_scaling_cur_freq_file(domain_id: u64) -> impl FsNodeOps {
958    let proxy_cache =
959        LockDepMutex::<_, CpuFreqProxyCacheLock>::new(None::<fcpuctrl::DeviceSynchronousProxy>);
960    SimpleFileNode::new(move |_| {
961        let mut guard = proxy_cache.lock();
962        if guard.is_none() {
963            let proxy = connect_to_cpu_device_by_domain_id(domain_id)?;
964            *guard = Some(proxy);
965        }
966        let proxy = guard.as_ref().expect("must have a valid proxy");
967        let opp = match proxy.get_current_operating_point(zx::MonotonicInstant::INFINITE) {
968            Ok(opp) => opp,
969            Err(_) => {
970                *guard = None;
971                return error!(EINVAL);
972            }
973        };
974        let info = match proxy.get_operating_point_info(opp, zx::MonotonicInstant::INFINITE) {
975            Ok(info) => info,
976            Err(_) => {
977                *guard = None;
978                return error!(EINVAL);
979            }
980        };
981        let info = info.map_err(|e| from_status_like_fdio!(zx::Status::err_from_raw(e)))?;
982        if info.frequency_hz <= 0 {
983            return error!(EINVAL);
984        }
985        let freq_khz = hz_to_khz(info.frequency_hz as u64);
986        Ok(BytesFile::new(format!("{}\n", freq_khz).into_bytes()))
987    })
988}
989
990enum CpuIdleStatType {
991    Time,
992    Usage,
993}
994
995/// Computes C1 residency in microseconds from Zircon's aggregate `idle_time` (in nanoseconds)
996/// by subtracting C2 residency (reported in milliseconds) when available.
997fn c1_time_us(zircon_idle_ns: i64, c2_residency: Option<&fpowerstats::StateResidency>) -> u64 {
998    let total_idle_us = u64::try_from(zircon_idle_ns).unwrap_or(0) / 1000;
999    let c2_time_us = c2_residency.map_or(0, |state| state.total_time_ms.saturating_mul(1000));
1000    total_idle_us.saturating_sub(c2_time_us)
1001}
1002
1003/// Backing [`BytesFileOps`] implementation for `/sys/devices/system/cpu/cpuN/cpuidle/state0/time`
1004/// and `state0/usage`, reporting per-core C1 residency (in microseconds) and reschedule count
1005/// from Zircon's `PerCpuStats`, subtracting C2 residency when available.
1006struct CpuIdleStatFile {
1007    core_id: u64,
1008    stat_type: CpuIdleStatType,
1009    c2_provider: Arc<CpuC2Provider>,
1010}
1011
1012impl BytesFileOps for CpuIdleStatFile {
1013    fn read(&self, current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
1014        let cpu_stats = current_task
1015            .kernel()
1016            .stats
1017            .get()
1018            .get_cpu_stats(zx::MonotonicInstant::INFINITE)
1019            .map_err(|_| errno!(EINVAL))?;
1020        let per_cpu_stats = cpu_stats.per_cpu_stats.ok_or_else(|| errno!(EINVAL))?;
1021        let per_cpu = per_cpu_stats.get(self.core_id as usize).ok_or_else(|| errno!(EINVAL))?;
1022        let value = match self.stat_type {
1023            CpuIdleStatType::Time => {
1024                let idle_ns = per_cpu.idle_time.ok_or_else(|| errno!(EINVAL))?;
1025                let c2 = self.c2_provider.residency(self.core_id)?;
1026                c1_time_us(idle_ns, c2.as_ref())
1027            }
1028            CpuIdleStatType::Usage => per_cpu.reschedules.ok_or_else(|| errno!(EINVAL))?,
1029        };
1030        Ok(format!("{value}\n").into_bytes().into())
1031    }
1032}
1033
1034const POWER_STATS_SERVICE_DIRECTORY: &str = "/svc/fuchsia.hardware.power.stats.Service";
1035const CPU_C2_ENTITY_NAME: &str = "cpu_c2";
1036
1037/// Maximum time to wait for a synchronous `fuchsia.hardware.power.stats.Device` call.
1038const POWER_STATS_FIDL_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(2);
1039
1040/// Connection state of [`CpuC2Provider`] to the `fuchsia.hardware.power.stats.Service` instance
1041/// that reports `"cpu_c2"`.
1042#[derive(Default)]
1043enum ProviderState {
1044    /// No initial connection has been attempted yet.
1045    #[default]
1046    Unconnected,
1047    /// Connected to a `fuchsia.hardware.power.stats.Service` instance that reports `"cpu_c2"`.
1048    Connected(Arc<fpowerstats::DeviceSynchronousProxy>),
1049    /// A previous connection failed with a transport error; subsequent reads attempt to reconnect
1050    /// and fail with `EIO` until the service recovers, avoiding non-monotonic `0` counter drops.
1051    Disconnected,
1052    /// No `fuchsia.hardware.power.stats.Service` instance in `/svc` reported `"cpu_c2"` during
1053    /// initial discovery, so C2 residency is unsupported on this platform and reads report `0`.
1054    Unavailable,
1055}
1056
1057/// Provides per-core CPU C2 ("deep idle") state residency from
1058/// `fuchsia.hardware.power.stats.Service`.
1059///
1060/// A single `CpuC2Provider` instance is shared via `Arc` across all CPU cores and across the
1061/// `state0/time`, `state1/time`, and `state1/usage` sysfs nodes.
1062///
1063/// # Caching and Recovery
1064/// Connection discovery in `/svc/fuchsia.hardware.power.stats.Service` is performed lazily on
1065/// the first read:
1066/// - If an instance reporting the `"cpu_c2"` entity is found, its proxy is cached in
1067///   [`ProviderState::Connected`].
1068/// - If no such instance exists during initial discovery, [`ProviderState::Unavailable`] is cached
1069///   so subsequent reads return `Ok(None)` (`0` residency) without repeatedly scanning `/svc`.
1070/// - If a `GetStats` call fails after a connection was established, the state transitions to
1071///   [`ProviderState::Disconnected`] and the read fails with `EIO` so userspace does not observe
1072///   monotonic counters dropping to `0`. Subsequent reads attempt to reconnect without permanently
1073///   disabling C2 reporting if the driver is still restarting.
1074///
1075/// # Concurrency
1076/// `state` is locked only briefly to inspect or update [`ProviderState`] and clone the `Arc`
1077/// wrapping the synchronous proxy. The lock is released before scanning `/svc` or issuing FIDL
1078/// calls so concurrent sysfs reads across CPU cores do not serialize behind the mutex.
1079#[derive(Default)]
1080struct CpuC2Provider {
1081    state: LockDepMutex<ProviderState, CpuC2ProxyCacheLock>,
1082}
1083
1084impl CpuC2Provider {
1085    fn connect() -> Option<fpowerstats::DeviceSynchronousProxy> {
1086        let dir = std::fs::read_dir(POWER_STATS_SERVICE_DIRECTORY).ok()?;
1087        dir.filter_map(|entry| entry.ok()).find_map(|entry| {
1088            let path = entry.path().join("device").into_os_string().into_string().ok()?;
1089            let (client, server) = zx::Channel::create();
1090            fdio::service_connect(&path, server).ok()?;
1091            let proxy = fpowerstats::DeviceSynchronousProxy::new(client);
1092            let deadline = zx::MonotonicInstant::after(POWER_STATS_FIDL_TIMEOUT);
1093            let entities = proxy.get_entities(deadline).ok()?;
1094            entities.iter().any(|entity| entity.name == CPU_C2_ENTITY_NAME).then_some(proxy)
1095        })
1096    }
1097
1098    fn proxy(&self) -> Result<Option<Arc<fpowerstats::DeviceSynchronousProxy>>, Errno> {
1099        if matches!(*self.state.lock(), ProviderState::Unconnected | ProviderState::Disconnected) {
1100            let connected_proxy = Self::connect().map(Arc::new);
1101            let mut state = self.state.lock();
1102            match (&*state, connected_proxy) {
1103                (ProviderState::Unconnected | ProviderState::Disconnected, Some(proxy)) => {
1104                    *state = ProviderState::Connected(proxy);
1105                }
1106                (ProviderState::Unconnected, None) => {
1107                    *state = ProviderState::Unavailable;
1108                }
1109                (ProviderState::Disconnected, None)
1110                | (ProviderState::Connected(_) | ProviderState::Unavailable, _) => {}
1111            }
1112        }
1113        match &*self.state.lock() {
1114            ProviderState::Connected(proxy) => Ok(Some(proxy.clone())),
1115            ProviderState::Unavailable | ProviderState::Unconnected => Ok(None),
1116            ProviderState::Disconnected => error!(EIO),
1117        }
1118    }
1119
1120    fn residency(&self, core_id: u64) -> Result<Option<fpowerstats::StateResidency>, Errno> {
1121        let core_id = i32::try_from(core_id).map_err(|_| errno!(EINVAL))?;
1122        let Some(proxy) = self.proxy()? else {
1123            return Ok(None);
1124        };
1125        let deadline = zx::MonotonicInstant::after(POWER_STATS_FIDL_TIMEOUT);
1126        let reports = match proxy.get_stats(deadline) {
1127            Ok(reports) => reports,
1128            Err(e) => {
1129                log_warn!("Failed to read power stats for {CPU_C2_ENTITY_NAME}: {e}");
1130                let mut state = self.state.lock();
1131                if matches!(
1132                    &*state,
1133                    ProviderState::Connected(current) if Arc::ptr_eq(current, &proxy)
1134                ) {
1135                    *state = ProviderState::Disconnected;
1136                }
1137                return error!(EIO);
1138            }
1139        };
1140        Ok(reports
1141            .into_iter()
1142            .find(|stats| stats.entity == CPU_C2_ENTITY_NAME)
1143            .and_then(|stats| stats.state_residency_data.into_iter().find(|s| s.id == core_id)))
1144    }
1145}
1146
1147/// Backing [`BytesFileOps`] implementation for `/sys/devices/system/cpu/cpuN/cpuidle/state1/time`
1148/// and `state1/usage`, reporting per-core C2 residency (in microseconds) and entry count from
1149/// [`CpuC2Provider`], or `0` when C2 residency is unsupported on the platform.
1150struct CpuIdleC2StatFile {
1151    core_id: u64,
1152    stat_type: CpuIdleStatType,
1153    provider: Arc<CpuC2Provider>,
1154}
1155
1156impl BytesFileOps for CpuIdleC2StatFile {
1157    fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
1158        let value =
1159            self.provider.residency(self.core_id)?.map_or(0, |state| match self.stat_type {
1160                CpuIdleStatType::Time => state.total_time_ms.saturating_mul(1000),
1161                CpuIdleStatType::Usage => u64::from(state.entry_count),
1162            });
1163        Ok(format!("{value}\n").into_bytes().into())
1164    }
1165}
1166
1167#[cfg(test)]
1168mod tests {
1169    use super::*;
1170    use fidl::endpoints::RequestStream;
1171    use futures::TryStreamExt;
1172
1173    #[::fuchsia::test]
1174    fn cpu_c2_provider_reads_firmware_residencies() {
1175        let (client, server) = zx::Channel::create();
1176        let server_thread = std::thread::spawn(move || {
1177            let mut executor = fuchsia_async::LocalExecutor::default();
1178            executor.run_singlethreaded(async move {
1179                let mut stream = fpowerstats::DeviceRequestStream::from_channel(
1180                    fuchsia_async::Channel::from_channel(server),
1181                );
1182                if let Some(fpowerstats::DeviceRequest::GetStats { responder }) =
1183                    stream.try_next().await.expect("failed to read request")
1184                {
1185                    let reports = vec![fpowerstats::Stats {
1186                        entity: CPU_C2_ENTITY_NAME.to_string(),
1187                        state_residency_data: vec![
1188                            fpowerstats::StateResidency {
1189                                id: 0,
1190                                name: "C2_CPU0".to_string(),
1191                                total_time_ms: 34,
1192                                entry_count: 12,
1193                                last_entry_timestamp_ms: 0,
1194                            },
1195                            fpowerstats::StateResidency {
1196                                id: 1,
1197                                name: "C2_CPU1".to_string(),
1198                                total_time_ms: 78,
1199                                entry_count: 56,
1200                                last_entry_timestamp_ms: 0,
1201                            },
1202                        ],
1203                    }];
1204                    responder.send(&reports).expect("failed to send response");
1205                }
1206            });
1207        });
1208
1209        let provider = CpuC2Provider {
1210            state: LockDepMutex::new(ProviderState::Connected(Arc::new(
1211                fpowerstats::DeviceSynchronousProxy::new(client),
1212            ))),
1213        };
1214        let core1 = provider
1215            .residency(/* core_id = */ 1)
1216            .expect("residency call must succeed")
1217            .expect("core 1 residency must be present");
1218        assert_eq!(core1.entry_count, 56);
1219        assert_eq!(core1.total_time_ms, 78);
1220
1221        server_thread.join().expect("server thread panicked");
1222
1223        // After the server closes the channel, reads return EIO and transition to Disconnected;
1224        // subsequent reads while the service is still absent stay Disconnected (rather than
1225        // permanently transitioning to Unavailable) and continue returning EIO.
1226        assert_eq!(provider.residency(/* core_id = */ 0), error!(EIO));
1227        assert!(matches!(*provider.state.lock(), ProviderState::Disconnected));
1228        assert_eq!(provider.residency(/* core_id = */ 0), error!(EIO));
1229        assert!(matches!(*provider.state.lock(), ProviderState::Disconnected));
1230
1231        // When no service is present on initial discovery, the provider transitions from
1232        // Unconnected to Unavailable and returns Ok(None).
1233        let unconfigured = CpuC2Provider::default();
1234        assert_eq!(unconfigured.residency(/* core_id = */ 0), Ok(None));
1235        assert!(matches!(*unconfigured.state.lock(), ProviderState::Unavailable));
1236    }
1237
1238    #[::fuchsia::test]
1239    fn c1_time_subtracts_c2_residency_and_saturates_at_zero() {
1240        let c2 = fpowerstats::StateResidency {
1241            id: 0,
1242            name: "C2_CPU0".to_string(),
1243            total_time_ms: 1_899,
1244            entry_count: 1,
1245            last_entry_timestamp_ms: 0,
1246        };
1247
1248        // 5,000,000 us total idle - 1,899,000 us C2 = 3,101,000 us C1.
1249        assert_eq!(c1_time_us(/* zircon_idle_ns = */ 5_000_000_000, Some(&c2)), 3_101_000);
1250
1251        // When C2 residency is unavailable, report aggregate idle time.
1252        assert_eq!(c1_time_us(/* zircon_idle_ns = */ 5_000_000_000, None), 5_000_000);
1253
1254        // Saturate at 0 if C2 residency exceeds aggregate idle time due to sampling skew.
1255        assert_eq!(c1_time_us(/* zircon_idle_ns = */ 1_000_000_000, Some(&c2)), 0);
1256    }
1257
1258    #[::fuchsia::test]
1259    fn min_freq_votes_manage_boost_token_lifecycle() {
1260        let (client, server) = zx::Channel::create();
1261        let (token_tx, token_rx) = std::sync::mpsc::channel::<zx::EventPair>();
1262        let server_thread = std::thread::spawn(move || {
1263            let mut executor = fuchsia_async::LocalExecutor::default();
1264            executor.run_singlethreaded(async move {
1265                let mut stream = fcpumanager::BoostRequestStream::from_channel(
1266                    fuchsia_async::Channel::from_channel(server),
1267                );
1268                while let Some(fcpumanager::BoostRequest::Boost { responder }) =
1269                    stream.try_next().await.expect("failed to read Boost request")
1270                {
1271                    let (client_token, server_token) = zx::EventPair::create();
1272                    responder.send(Ok(client_token)).expect("failed to send Boost response");
1273                    token_tx.send(server_token).expect("failed to forward server token");
1274                }
1275            });
1276        });
1277
1278        let domain = CpuFreqDomain::new(
1279            &fcpu::DomainInfo {
1280                id: Some(0),
1281                core_ids: Some(vec![0, 1]),
1282                available_frequencies_hz: Some(vec![820_000_000, 1_200_000_000, 1_632_000_000]),
1283                name: Some("little".to_string()),
1284                ..Default::default()
1285            },
1286            None,
1287        );
1288        domain.state.lock().boost_proxy = Some(fcpumanager::BoostSynchronousProxy::new(client));
1289
1290        assert_eq!(domain.min_frequency_khz("powerhint_min_freq"), Some(820_000));
1291        assert_eq!(domain.min_frequency_khz("soft_min_freq"), Some(820_000));
1292        assert_eq!(domain.min_frequency_khz("scaling_min_freq"), Some(820_000));
1293
1294        // 1. Writing khz > lowest_khz to powerhint_min_freq requests a Boost token and updates
1295        //    powerhint_min_freq and scaling_min_freq reads.
1296        domain
1297            .update_min_vote(None, "powerhint_min_freq", 1_632_000)
1298            .expect("update_min_vote must succeed");
1299        let server_token = token_rx.recv().expect("must receive a Boost token");
1300        assert_eq!(
1301            server_token
1302                .wait_one(zx::Signals::EVENTPAIR_PEER_CLOSED, zx::MonotonicInstant::INFINITE_PAST),
1303            zx::WaitResult::TimedOut(zx::Signals::NONE)
1304        );
1305        assert_eq!(domain.min_frequency_khz("powerhint_min_freq"), Some(1_632_000));
1306        assert_eq!(domain.min_frequency_khz("soft_min_freq"), Some(820_000));
1307        assert_eq!(domain.min_frequency_khz("scaling_min_freq"), Some(1_632_000));
1308
1309        // 2. While another voter (soft_min_freq) also holds a vote > lowest_khz, clearing
1310        //    powerhint_min_freq (<= lowest_khz) keeps the boost token alive.
1311        domain
1312            .update_min_vote(None, "soft_min_freq", 1_200_000)
1313            .expect("update_min_vote must succeed");
1314        assert!(token_rx.try_recv().is_err(), "must reuse existing Boost token");
1315        assert_eq!(domain.min_frequency_khz("soft_min_freq"), Some(1_200_000));
1316        assert_eq!(domain.min_frequency_khz("scaling_min_freq"), Some(1_632_000));
1317
1318        domain
1319            .update_min_vote(None, "powerhint_min_freq", 820_000)
1320            .expect("clearing powerhint_min_freq must succeed");
1321        assert_eq!(domain.min_frequency_khz("powerhint_min_freq"), Some(820_000));
1322        assert_eq!(domain.min_frequency_khz("soft_min_freq"), Some(1_200_000));
1323        assert_eq!(domain.min_frequency_khz("scaling_min_freq"), Some(1_200_000));
1324        assert_eq!(
1325            server_token
1326                .wait_one(zx::Signals::EVENTPAIR_PEER_CLOSED, zx::MonotonicInstant::INFINITE_PAST),
1327            zx::WaitResult::TimedOut(zx::Signals::NONE)
1328        );
1329
1330        // 3. Clearing all min votes (<= lowest_khz or 0) drops the zx::EventPair token
1331        //    (ZX_EVENTPAIR_PEER_CLOSED observed by the server end) and restores scaling_min_freq
1332        //    to lowest_khz.
1333        domain
1334            .update_min_vote(None, "soft_min_freq", 0)
1335            .expect("clearing soft_min_freq must succeed");
1336        assert_eq!(domain.min_frequency_khz("soft_min_freq"), Some(820_000));
1337        assert_eq!(domain.min_frequency_khz("scaling_min_freq"), Some(820_000));
1338        let closed_signals = server_token
1339            .wait_one(zx::Signals::EVENTPAIR_PEER_CLOSED, zx::MonotonicInstant::INFINITE_PAST)
1340            .to_result()
1341            .expect("server_token must observe ZX_EVENTPAIR_PEER_CLOSED");
1342        assert!(closed_signals.contains(zx::Signals::EVENTPAIR_PEER_CLOSED));
1343
1344        drop(domain);
1345        server_thread.join().expect("server thread panicked");
1346    }
1347}