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_broker as fbroker;
16use fidl_fuchsia_power_cpu as fcpu;
17use fuchsia_component::client::connect_to_protocol_sync;
18use itertools::Itertools;
19use starnix_logging::{bug_ref, log_warn};
20use starnix_sync::{
21    CpuC2ProxyCacheLock, CpuC2SuspendAccountingLock, CpuFreqProxyCacheLock, LockDepMutex,
22};
23use starnix_uapi::errors::Errno;
24use starnix_uapi::file_mode::mode;
25use starnix_uapi::{errno, error, from_status_like_fdio};
26use std::borrow::Cow;
27use std::collections::HashMap;
28use std::sync::Arc;
29use std::sync::atomic::{AtomicU64, Ordering};
30use zx;
31
32#[derive(Clone)]
33struct CpuFreqDomains(Arc<[Arc<CpuFreqDomain>]>);
34
35/// Returns the per-domain CPU frequency state shared across sysfs `cpufreq` nodes and thermal
36/// cooling devices for `kernel`.
37pub fn get_cpu_freq_domains(kernel: &Kernel) -> Arc<[Arc<CpuFreqDomain>]> {
38    kernel.expando.get_or_init(|| CpuFreqDomains(get_cpu_domains().into())).0.clone()
39}
40
41/// Registers `/sys/devices/system/cpu` and the per-core `cpuN` bus devices beneath it.
42pub fn register_cpu_devices(kernel: &Kernel) {
43    // Each domain is wrapped once and shared across `cpuN/cpufreq`, `cpufreq/policyN`, and
44    // thermal `cooling_device*` nodes so all voters aggregate on the same domain state.
45    let cpu_domains = get_cpu_freq_domains(kernel);
46
47    let mut core_to_domain_map: Vec<(u64, &Arc<CpuFreqDomain>)> = cpu_domains
48        .iter()
49        .flat_map(|domain| domain.core_ids.iter().map(move |id| (*id, domain)))
50        .collect();
51    core_to_domain_map.sort_by_key(|(id, _)| *id);
52    core_to_domain_map.dedup_by_key(|(id, _)| *id);
53
54    let core_count = core_to_domain_map.len();
55    let registry = &kernel.device_registry;
56    let system_device = registry.objects.system_device();
57    let cpu_root =
58        registry.add_subsystemless_device("cpu".into(), Some(system_device), |device, dir| {
59            build_device_directory(device, dir);
60            build_cpu_subsystem_directory(dir, &cpu_domains, core_count);
61        });
62
63    let cpu_bus = registry.objects.get_or_create_bus("cpu".into());
64    let c2_provider = kernel.expando.get::<CpuC2Provider>();
65    for (core_id, domain) in &core_to_domain_map {
66        let name: FsString = format!("cpu{}", core_id).into();
67        let c2_provider = c2_provider.clone();
68        registry.add_bus_device(
69            name.as_ref(),
70            Some(cpu_root.clone()),
71            cpu_bus.clone(),
72            |device, dir| {
73                build_device_directory(device, dir);
74                build_cpu_directory(dir, *core_id, domain, c2_provider);
75            },
76        );
77    }
78}
79
80/// Populates the subsystem-wide attributes in `/sys/devices/system/cpu`.
81fn build_cpu_subsystem_directory(
82    dir: &SimpleDirectoryMutator,
83    cpu_domains: &[Arc<CpuFreqDomain>],
84    core_count: usize,
85) {
86    dir.entry(
87        "online",
88        BytesFile::new_node(format!("0-{}\n", core_count.saturating_sub(1)).into_bytes()),
89        mode!(IFREG, 0o444),
90    );
91    dir.entry(
92        "possible",
93        BytesFile::new_node(format!("0-{}\n", core_count.saturating_sub(1)).into_bytes()),
94        mode!(IFREG, 0o444),
95    );
96    dir.subdir("vulnerabilities", 0o755, |dir| {
97        for (name, contents) in VULNERABILITIES {
98            let contents = contents.to_string();
99            dir.entry(name, BytesFile::new_node(contents.into_bytes()), mode!(IFREG, 0o444));
100        }
101    });
102    dir.subdir("cpufreq", 0o755, |dir| {
103        for domain in cpu_domains.iter() {
104            let min_core_id = domain.core_ids.iter().min().expect("core_ids is empty");
105            let name = format!("policy{}", min_core_id);
106            dir.subdir(&name, 0o755, |dir| build_cpufreq_directory(dir, domain));
107        }
108    });
109    dir.subdir("soc", 0o755, |dir| {
110        dir.subdir("0", 0o755, |dir| {
111            dir.entry(
112                "machine",
113                StubEmptyFile::new_node(bug_ref!("https://fxbug.dev/452096300")),
114                mode!(IFREG, 0o444),
115            );
116        });
117    });
118}
119
120/// Retrieves CPU topology and domain information with a tiered fallback approach.
121///
122/// 1. Get CPU domains from the `fuchsia.power.cpu.DomainController` FIDL protocol.
123/// 2. If that fails, use the `fuchsia.hardware.cpu.ctrl.Service` FIDL service to connect to
124///    individual CPU control devices.
125/// 3. If that fails, get CPU information from the kernel directly. In this case, no CPU control
126///    or topological information is available, so only a limited set of sysfs entries
127///    will be populated.
128fn get_cpu_domains() -> Vec<Arc<CpuFreqDomain>> {
129    // Tier 1: Try DomainController
130    if let Ok(domain_controller) = connect_to_protocol_sync::<fcpu::DomainControllerMarker>() {
131        if let Ok(mut domains) = domain_controller.list_domains(zx::MonotonicInstant::INFINITE) {
132            // Remove any domains without an ID or empty core_ids.
133            domains
134                .retain(|d| d.id.is_some() && d.core_ids.as_ref().map_or(false, |c| !c.is_empty()));
135            if !domains.is_empty() {
136                return domains
137                    .iter()
138                    .map(|domain| Arc::new(CpuFreqDomain::new(domain, Some(&domain_controller))))
139                    .collect();
140            }
141        }
142    }
143
144    log_warn!(
145        "Could not retrieve CPU domains from fuchsia.power.cpu.DomainController, using CPU control devices instead."
146    );
147
148    // Tier 2: Try cpu.ctrl devices
149    if let Ok(proxies) = connect_to_cpu_devices() {
150        let mut domains = Vec::new();
151
152        for proxy in proxies {
153            let cpu_count = match proxy.get_num_logical_cores(zx::MonotonicInstant::INFINITE) {
154                Ok(count) => count,
155                Err(e) => {
156                    log_warn!("get_num_logical_cores returned error: {}", e);
157                    continue;
158                }
159            };
160            let domain_id = match proxy.get_domain_id(zx::MonotonicInstant::INFINITE) {
161                Ok(id) => id as u64,
162                Err(e) => {
163                    log_warn!("get_domain_id returned error: {}", e);
164                    continue;
165                }
166            };
167
168            let mut core_ids = Vec::with_capacity(cpu_count as usize);
169            let mut get_core_failed = false;
170            for i in 0..cpu_count {
171                let core_id = match proxy.get_logical_core_id(i, zx::MonotonicInstant::INFINITE) {
172                    Ok(id) => id,
173                    Err(e) => {
174                        log_warn!("get_logical_core_id error in domain {}: {}", domain_id, e);
175                        get_core_failed = true;
176                        break;
177                    }
178                };
179                core_ids.push(core_id);
180            }
181            if get_core_failed || core_ids.is_empty() {
182                log_warn!("get_logical_core_id failed in domain {}, skipping", domain_id);
183                continue;
184            }
185
186            let available_frequencies_hz =
187                match proxy.get_operating_point_count(zx::MonotonicInstant::INFINITE) {
188                    Ok(Ok(count)) => {
189                        let mut freqs = Vec::with_capacity(count as usize);
190                        for i in 0..count {
191                            if let Ok(Ok(info)) =
192                                proxy.get_operating_point_info(i, zx::MonotonicInstant::INFINITE)
193                            {
194                                if info.frequency_hz > 0 {
195                                    freqs.push(info.frequency_hz as u64);
196                                }
197                            }
198                        }
199                        freqs.sort();
200                        freqs.dedup();
201                        Some(freqs)
202                    }
203                    _ => None,
204                };
205
206            domains.push(fcpu::DomainInfo {
207                id: Some(domain_id),
208                core_ids: Some(core_ids),
209                available_frequencies_hz,
210                ..Default::default()
211            });
212        }
213
214        if !domains.is_empty() {
215            return domains
216                .iter()
217                .map(|domain| Arc::new(CpuFreqDomain::new(domain, None)))
218                .collect();
219        }
220    }
221
222    log_warn!(
223        "Could not connect to CPU control devices, using default domain info from kernel CPU count."
224    );
225
226    // Tier 3: Fallback to kernel CPU count
227    let cpu_count = zx::system_get_num_cpus();
228    vec![Arc::new(CpuFreqDomain::new(
229        &fcpu::DomainInfo {
230            id: Some(0),
231            core_ids: Some((0..cpu_count as u64).collect()),
232            available_frequencies_hz: None,
233            name: None,
234            ..Default::default()
235        },
236        None,
237    ))]
238}
239
240fn hz_to_khz(hz: u64) -> u64 {
241    hz / 1000
242}
243
244/// The only CPU frequency governor Starnix reports.
245///
246/// Frequency selection is not user-tunable on Fuchsia, so `scaling_governor` is a fixed value
247/// rather than a real choice.
248const SCALING_GOVERNOR: &str = "performance";
249
250/// Formats a frequency for a sysfs node, leaving the value empty when the domain reports none.
251fn format_freq_khz(khz: Option<u64>) -> String {
252    format!("{}\n", khz.map(|f| f.to_string()).unwrap_or_default())
253}
254
255fn build_cpu_directory(
256    dir: &SimpleDirectoryMutator,
257    core_id: u64,
258    domain: &Arc<CpuFreqDomain>,
259    c2_provider: Arc<CpuC2Provider>,
260) {
261    let cluster_id = domain.domain_id;
262
263    dir.entry(
264        "cpu_capacity",
265        StubEmptyFile::new_node(bug_ref!("https://fxbug.dev/452096300")),
266        mode!(IFREG, 0o444),
267    );
268    dir.subdir("cpuidle", 0o755, |dir| {
269        build_cpuidle_directory(dir, core_id, c2_provider);
270    });
271    dir.subdir("cpufreq", 0o755, |dir| {
272        build_cpufreq_directory(dir, domain);
273    });
274    dir.subdir("topology", 0o755, |dir| {
275        dir.entry(
276            "cluster_id",
277            BytesFile::new_node(format!("{cluster_id}\n").into_bytes()),
278            mode!(IFREG, 0o444),
279        );
280        dir.entry(
281            "physical_package_id",
282            BytesFile::new_node(format!("{cluster_id}\n").into_bytes()),
283            mode!(IFREG, 0o444),
284        );
285    });
286}
287
288fn build_cpuidle_directory(
289    dir: &SimpleDirectoryMutator,
290    core_id: u64,
291    c2_provider: Arc<CpuC2Provider>,
292) {
293    let state0_c2_provider = c2_provider.clone();
294    dir.subdir("state0", 0o755, |dir| {
295        dir.entry("name", BytesFile::new_node(b"C1\n".to_vec()), mode!(IFREG, 0o444));
296        dir.entry("desc", BytesFile::new_node(b"idle\n".to_vec()), mode!(IFREG, 0o444));
297        dir.entry(
298            "time",
299            BytesFile::new_node(CpuIdleStatFile::new_time(core_id, state0_c2_provider)),
300            mode!(IFREG, 0o444),
301        );
302        dir.entry(
303            "usage",
304            BytesFile::new_node(CpuIdleStatFile::new_usage(core_id)),
305            mode!(IFREG, 0o444),
306        );
307    });
308    dir.subdir("state1", 0o755, |dir| {
309        dir.entry("name", BytesFile::new_node(b"C2\n".to_vec()), mode!(IFREG, 0o444));
310        dir.entry("desc", BytesFile::new_node(b"deep idle\n".to_vec()), mode!(IFREG, 0o444));
311        dir.entry(
312            "time",
313            BytesFile::new_node(CpuIdleC2StatFile {
314                core_id,
315                stat_type: CpuIdleStatType::Time,
316                provider: c2_provider.clone(),
317            }),
318            mode!(IFREG, 0o444),
319        );
320        dir.entry(
321            "usage",
322            BytesFile::new_node(CpuIdleC2StatFile {
323                core_id,
324                stat_type: CpuIdleStatType::Usage,
325                provider: c2_provider,
326            }),
327            mode!(IFREG, 0o444),
328        );
329    });
330}
331
332const SOFT_MAX_FREQ_VOTER: &str = "soft_max_freq";
333
334/// Max-frequency voter nodes under `cpufreq/vote_manager/`.
335const VOTE_MANAGER_MAX_FREQ_NODES: &[&str] =
336    &["powerhint_max_freq", "thermal_max_freq", SOFT_MAX_FREQ_VOTER, "debug_max_freq"];
337
338/// Min-frequency voter nodes under `cpufreq/vote_manager/`.
339const VOTE_MANAGER_MIN_FREQ_NODES: &[&str] =
340    &["powerhint_min_freq", "soft_min_freq", "debug_min_freq"];
341
342fn add_max_freq_entry(
343    dir: &SimpleDirectoryMutator,
344    name: &'static str,
345    domain: &Arc<CpuFreqDomain>,
346) {
347    // A domain that reports no operating points cannot be capped, so fall back to an inert node
348    // rather than one that would fail every write.
349    if domain.is_tunable() {
350        dir.entry(name, ScalingMaxFreqFile::new_node(domain, name), mode!(IFREG, 0o644));
351    } else {
352        dir.entry(
353            name,
354            InertFile::new_node(format_freq_khz(domain.highest_khz())),
355            mode!(IFREG, 0o644),
356        );
357    }
358}
359
360fn add_min_freq_entry(
361    dir: &SimpleDirectoryMutator,
362    name: &'static str,
363    domain: &Arc<CpuFreqDomain>,
364) {
365    // A domain that reports no operating points cannot have its minimum frequency controlled, so
366    // fall back to an inert node rather than one that would fail every write.
367    if domain.is_tunable() {
368        dir.entry(name, ScalingMinFreqFile::new_node(domain, name), mode!(IFREG, 0o644));
369    } else {
370        dir.entry(
371            name,
372            InertFile::new_node(format_freq_khz(domain.lowest_khz())),
373            mode!(IFREG, 0o644),
374        );
375    }
376}
377
378fn build_cpufreq_directory(dir: &SimpleDirectoryMutator, domain: &Arc<CpuFreqDomain>) {
379    dir.subdir("stats", 0o755, |dir| {
380        dir.entry("reset", CpuFreqStatsResetFile::new_node(), mode!(IFREG, 0o200));
381        dir.entry(
382            "time_in_state",
383            StubEmptyFile::new_node(bug_ref!("https://fxbug.dev/452096300")),
384            mode!(IFREG, 0o444),
385        );
386    });
387
388    // Android Power HAL and thermal/debug tooling vote through `cpufreq/vote_manager/*`. Each
389    // max/min-frequency node (including `scaling_max_freq` and `scaling_min_freq`) holds its own
390    // persistent `DomainController` lease in `CpuFreqDomain`.
391    dir.subdir("vote_manager", 0o755, |dir| {
392        for name in VOTE_MANAGER_MAX_FREQ_NODES {
393            add_max_freq_entry(dir, name, domain);
394        }
395        for name in VOTE_MANAGER_MIN_FREQ_NODES {
396            add_min_freq_entry(dir, name, domain);
397        }
398    });
399
400    let related_cpus_str = format!("{}\n", domain.core_ids.iter().sorted().join(" "));
401    dir.entry(
402        "related_cpus",
403        BytesFile::new_node(related_cpus_str.into_bytes()),
404        mode!(IFREG, 0o444),
405    );
406    dir.entry(
407        "scaling_cur_freq",
408        create_scaling_cur_freq_file(domain.domain_id),
409        mode!(IFREG, 0o444),
410    );
411
412    add_min_freq_entry(dir, "scaling_min_freq", domain);
413    add_max_freq_entry(dir, "scaling_max_freq", domain);
414
415    dir.entry(
416        "scaling_available_frequencies",
417        BytesFile::new_node((domain.sorted_khz.iter().join(" ") + "\n").into_bytes()),
418        mode!(IFREG, 0o444),
419    );
420    dir.entry(
421        "scaling_available_governors",
422        BytesFile::new_node(format!("{SCALING_GOVERNOR}\n").into_bytes()),
423        mode!(IFREG, 0o444),
424    );
425    dir.entry(
426        "scaling_governor",
427        InertFile::new_node(format!("{SCALING_GOVERNOR}\n")),
428        mode!(IFREG, 0o644),
429    );
430    dir.entry(
431        "cpuinfo_min_freq",
432        BytesFile::new_node(format_freq_khz(domain.lowest_khz()).into_bytes()),
433        mode!(IFREG, 0o444),
434    );
435    dir.entry(
436        "cpuinfo_max_freq",
437        BytesFile::new_node(format_freq_khz(domain.highest_khz()).into_bytes()),
438        mode!(IFREG, 0o444),
439    );
440}
441
442const VULNERABILITIES: &[(&str, &str)] = &[
443    ("gather_data_sampling", "Not affected\n"),
444    ("itlb_multihit", "Not affected\n"),
445    ("l1tf", "Not affected\n"),
446    ("mds", "Not affected\n"),
447    ("meltdown", "Not affected\n"),
448    ("mmio_stale_data", "Not affected\n"),
449    ("retbleed", "Not affected\n"),
450    ("spec_rstack_overflow", "Not affected\n"),
451    ("spec_store_bypass", "Not affected\n"),
452    ("spectre_v1", "Not affected\n"),
453    ("spectre_v2", "Not affected\n"),
454    ("srbds", "Not affected\n"),
455    ("tsx_async_abort", "Not affected\n"),
456];
457
458struct CpuFreqStatsResetFile {}
459
460impl CpuFreqStatsResetFile {
461    pub fn new_node() -> impl FsNodeOps {
462        BytesFile::new_node(Self {})
463    }
464}
465
466impl BytesFileOps for CpuFreqStatsResetFile {
467    // Currently a no-op. The value written to this node does not matter.
468    fn write(&self, _current_task: &CurrentTask, _data: Vec<u8>) -> Result<(), Errno> {
469        Ok(())
470    }
471}
472
473#[derive(Default)]
474struct CpuFreqDomainState {
475    /// Active ceiling leases keyed by voter name (`voter -> (opp_index, lease_token)`).
476    /// Each voter holds its own lease on `DomainController`; dropping the `fbroker::LeaseToken`
477    /// releases that voter's ceiling lease in `cpu-manager`.
478    votes: HashMap<&'static str, (u64, fbroker::LeaseToken)>,
479
480    /// Active floor leases keyed by voter name (`voter -> (opp_index, lease_token)`).
481    /// Each voter holds its own lease on `DomainController`; dropping the `fbroker::LeaseToken`
482    /// releases that voter's floor lease in `cpu-manager`.
483    min_votes: HashMap<&'static str, (u64, fbroker::LeaseToken)>,
484
485    /// Lazily established connection to the server, dropped if a call fails at the transport
486    /// level so that the next caller reconnects.
487    domain_controller: Option<fcpu::DomainControllerSynchronousProxy>,
488}
489
490fn is_lease_open(token: &fbroker::LeaseToken) -> bool {
491    token.wait_one(zx::Signals::EVENTPAIR_PEER_CLOSED, zx::MonotonicInstant::INFINITE_PAST).is_err()
492}
493
494impl CpuFreqDomainState {
495    /// Queries the domain's current ceiling on `domain_controller` and seeds any pre-existing cap
496    /// (such as a `boot_max_opp` limit configured in `cpu-manager`) into `soft_max_freq`, matching
497    /// how Linux `google_vote_manager` initializes `soft_max_freq` from the platform boot ceiling.
498    fn from_domain_controller(
499        domain_id: u64,
500        opps_hz: &[u64],
501        domain_controller: &fcpu::DomainControllerSynchronousProxy,
502    ) -> Self {
503        let max_hz = opps_hz.iter().max().copied();
504        match domain_controller.get_max_frequency(domain_id, zx::MonotonicInstant::INFINITE) {
505            Ok(Ok(opp_index)) => match opps_hz.get(opp_index as usize).copied() {
506                Some(hz) if Some(hz) == max_hz => Self::default(),
507                Some(_) => {
508                    let lease_name = format!("starnix:{SOFT_MAX_FREQ_VOTER}");
509                    match domain_controller.lease_max_frequency(
510                        domain_id,
511                        opp_index,
512                        &lease_name,
513                        zx::MonotonicInstant::INFINITE,
514                    ) {
515                        Ok(Ok(token)) => {
516                            match domain_controller
517                                .clear_max_frequency(domain_id, zx::MonotonicInstant::INFINITE)
518                            {
519                                Ok(Ok(())) => {}
520                                other => log_warn!(
521                                    "Failed to clear initial max frequency for domain {}: {:?}",
522                                    domain_id,
523                                    other
524                                ),
525                            }
526                            let mut votes = HashMap::new();
527                            votes.insert(SOFT_MAX_FREQ_VOTER, (opp_index, token));
528                            Self { votes, ..Self::default() }
529                        }
530                        other => {
531                            log_warn!(
532                                "Failed to lease initial soft_max_freq for domain {}: {:?}",
533                                domain_id,
534                                other
535                            );
536                            Self::default()
537                        }
538                    }
539                }
540                None => {
541                    log_warn!(
542                        "Initial max frequency index {} out of range for domain {} (len {})",
543                        opp_index,
544                        domain_id,
545                        opps_hz.len()
546                    );
547                    Self::default()
548                }
549            },
550            other => {
551                log_warn!(
552                    "Failed to query initial max frequency for domain {}: {:?}",
553                    domain_id,
554                    other
555                );
556                Self::default()
557            }
558        }
559    }
560}
561
562/// Per-domain state shared by a domain's cpufreq nodes and by its corresponding thermal
563/// `cooling_device`.
564pub struct CpuFreqDomain {
565    domain_id: u64,
566
567    /// Optional name of the domain/cluster from `DomainInfo.name`.
568    name: Option<String>,
569
570    /// The IDs of the CPU cores that are part of this domain.
571    core_ids: Vec<u64>,
572
573    /// Available frequencies in Hz, in `DomainInfo.available_frequencies_hz` order.
574    ///
575    /// This is operating point order: `DomainController.LeaseMaxFrequency` and
576    /// `LeaseMinFrequency` take an index into this list. It is deliberately kept apart from
577    /// `sorted_khz` below, which is what `scaling_available_frequencies` reports, because sorting
578    /// reorders the entries. The two lists are not positionally related, so a frequency must be
579    /// matched by value to find its operating point.
580    opps_hz: Vec<u64>,
581
582    /// `opps_hz` in kHz, sorted ascending and deduplicated.
583    sorted_khz: Vec<u64>,
584
585    /// Whether `DomainController` is serving this domain (as opposed to the `cpu.ctrl` or
586    /// kernel-CPU-count fallbacks).
587    has_domain_controller: bool,
588
589    state: LockDepMutex<CpuFreqDomainState, CpuFreqProxyCacheLock>,
590}
591
592impl CpuFreqDomain {
593    fn new(
594        domain: &fcpu::DomainInfo,
595        domain_controller: Option<&fcpu::DomainControllerSynchronousProxy>,
596    ) -> Self {
597        let domain_id = domain.id.expect("id not available");
598        let opps_hz = domain.available_frequencies_hz.clone().unwrap_or_default();
599        let sorted_khz = opps_hz.iter().map(|hz| hz_to_khz(*hz)).sorted().dedup().collect();
600        let state = match domain_controller {
601            Some(dc) if !opps_hz.is_empty() => {
602                CpuFreqDomainState::from_domain_controller(domain_id, &opps_hz, dc)
603            }
604            _ => CpuFreqDomainState::default(),
605        };
606        Self {
607            domain_id,
608            name: domain.name.clone(),
609            core_ids: domain.core_ids.clone().expect("core_ids not available"),
610            opps_hz,
611            sorted_khz,
612            has_domain_controller: domain_controller.is_some(),
613            state: LockDepMutex::new(state),
614        }
615    }
616
617    pub fn name(&self) -> Option<&str> {
618        self.name.as_deref()
619    }
620
621    pub fn opp_count(&self) -> usize {
622        self.opps_hz.len()
623    }
624
625    pub fn get_vote(&self, voter: &'static str) -> Option<u64> {
626        self.state
627            .lock()
628            .votes
629            .get(voter)
630            .and_then(|(index, token)| is_lease_open(token).then_some(*index))
631    }
632
633    /// Returns `voter`'s active ceiling in kHz, or the domain's highest frequency when `voter` has
634    /// no active cap.
635    fn voter_frequency_khz(&self, voter: &'static str) -> Option<u64> {
636        self.get_vote(voter)
637            .and_then(|index| self.opps_hz.get(index as usize))
638            .map(|&hz| hz_to_khz(hz))
639            .or_else(|| self.highest_khz())
640    }
641
642    fn get_min_vote(&self, voter: &'static str) -> Option<u64> {
643        self.state
644            .lock()
645            .min_votes
646            .get(voter)
647            .and_then(|(index, token)| is_lease_open(token).then_some(*index))
648    }
649
650    /// Returns `voter`'s active floor in kHz, or the domain's lowest frequency when `voter` has
651    /// no active floor.
652    fn min_voter_frequency_khz(&self, voter: &'static str) -> Option<u64> {
653        self.get_min_vote(voter)
654            .and_then(|index| self.opps_hz.get(index as usize))
655            .map(|&hz| hz_to_khz(hz))
656            .or_else(|| self.lowest_khz())
657    }
658
659    /// Whether the domain is backed by `DomainController` and reports operating points, and can
660    /// therefore have its frequency bounds controlled.
661    pub fn is_tunable(&self) -> bool {
662        self.has_domain_controller && !self.opps_hz.is_empty()
663    }
664
665    fn lowest_khz(&self) -> Option<u64> {
666        self.sorted_khz.first().copied()
667    }
668
669    fn highest_khz(&self) -> Option<u64> {
670        self.sorted_khz.last().copied()
671    }
672
673    /// Maps a frequency in kHz to the operating point that caps the domain there.
674    ///
675    /// Returns `None` when the request is at or above the domain's highest operating point,
676    /// meaning "no cap". A client releases a cap by writing a sentinel far above any real
677    /// frequency, which lands here.
678    ///
679    /// Otherwise the request is snapped down to the highest operating point that does not
680    /// exceed it, or to the lowest operating point if it undershoots them all.
681    fn opp_index_for_khz(&self, khz: u64) -> Option<u64> {
682        if khz >= self.highest_khz()? {
683            return None;
684        }
685        self.opps_hz
686            .iter()
687            .enumerate()
688            .filter(|(_, hz)| hz_to_khz(**hz) <= khz)
689            .max_by_key(|(_, hz)| **hz)
690            .or_else(|| self.opps_hz.iter().enumerate().min_by_key(|(_, hz)| **hz))
691            .map(|(index, _)| index as u64)
692    }
693
694    /// Maps a frequency in kHz to the operating point that sets the domain's floor there.
695    ///
696    /// Returns `None` when the request is at or below the domain's lowest operating point,
697    /// meaning "no floor". A client releases a floor by writing `0` or the domain's minimum
698    /// frequency (`cpuinfo_min_freq`), which lands here.
699    ///
700    /// Otherwise the request is snapped up to the lowest operating point that meets or exceeds
701    /// it, or to the highest operating point if it overshoots them all.
702    fn opp_index_for_min_khz(&self, khz: u64) -> Option<u64> {
703        if khz <= self.lowest_khz()? {
704            return None;
705        }
706        self.opps_hz
707            .iter()
708            .enumerate()
709            .filter(|(_, hz)| hz_to_khz(**hz) >= khz)
710            .min_by_key(|(_, hz)| **hz)
711            .or_else(|| self.opps_hz.iter().enumerate().max_by_key(|(_, hz)| **hz))
712            .map(|(index, _)| index as u64)
713    }
714
715    /// Updates `voter`'s ceiling lease (`Some(opp_index)` or `None` to release) on
716    /// `DomainController`.
717    pub fn update_vote(&self, voter: &'static str, opp_index: Option<u64>) -> Result<(), Errno> {
718        self.update_lease_vote(voter, opp_index, |s| &mut s.votes, Self::lease_max_frequency)
719    }
720
721    /// Updates `voter`'s floor lease (`Some(opp_index)` or `None` to release) on
722    /// `DomainController`.
723    fn update_min_vote(&self, voter: &'static str, opp_index: Option<u64>) -> Result<(), Errno> {
724        self.update_lease_vote(voter, opp_index, |s| &mut s.min_votes, Self::lease_min_frequency)
725    }
726
727    fn update_lease_vote(
728        &self,
729        voter: &'static str,
730        opp_index: Option<u64>,
731        votes_mut: impl Fn(
732            &mut CpuFreqDomainState,
733        ) -> &mut HashMap<&'static str, (u64, fbroker::LeaseToken)>,
734        acquire_lease: impl FnOnce(
735            &Self,
736            &mut CpuFreqDomainState,
737            u64,
738            &str,
739        ) -> Result<fbroker::LeaseToken, Errno>,
740    ) -> Result<(), Errno> {
741        if let Some(index) = opp_index {
742            if index as usize >= self.opps_hz.len() {
743                return error!(EINVAL);
744            }
745        }
746        let mut state = self.state.lock();
747        match opp_index {
748            Some(index) => {
749                if let Some((existing_index, existing_token)) = votes_mut(&mut state).get(voter) {
750                    if *existing_index == index && is_lease_open(existing_token) {
751                        return Ok(());
752                    }
753                }
754                let lease_name = format!("starnix:{voter}");
755                let token = acquire_lease(self, &mut state, index, &lease_name)?;
756                votes_mut(&mut state).insert(voter, (index, token));
757            }
758            None => {
759                votes_mut(&mut state).remove(voter);
760            }
761        }
762        Ok(())
763    }
764
765    /// Runs `call` against a lazily connected `DomainController`.
766    ///
767    /// A transport failure invalidates the cached connection; a protocol-level error leaves the
768    /// connection in place for the caller to interpret.
769    fn with_domain_controller<T, E: std::fmt::Debug>(
770        &self,
771        state: &mut CpuFreqDomainState,
772        call: impl FnOnce(&fcpu::DomainControllerSynchronousProxy) -> Result<T, E>,
773    ) -> Result<T, Errno> {
774        if state.domain_controller.is_none() {
775            state.domain_controller =
776                Some(connect_to_protocol_sync::<fcpu::DomainControllerMarker>().map_err(|e| {
777                    log_warn!("Failed to connect to DomainController: {:?}", e);
778                    errno!(ENODEV)
779                })?);
780        }
781        match call(state.domain_controller.as_ref().expect("connection was just established")) {
782            Ok(value) => Ok(value),
783            Err(e) => {
784                log_warn!("DomainController call failed for domain {}: {:?}", self.domain_id, e);
785                state.domain_controller = None;
786                error!(EIO)
787            }
788        }
789    }
790
791    /// Reads back the frequency in kHz that the domain is currently capped to.
792    fn max_frequency_khz(&self) -> Result<u64, Errno> {
793        let mut state = self.state.lock();
794        let index = self
795            .with_domain_controller(&mut state, |dc| {
796                dc.get_max_frequency(self.domain_id, zx::MonotonicInstant::INFINITE)
797            })?
798            .map_err(|e| {
799                log_warn!("get_max_frequency failed for domain {}: {:?}", self.domain_id, e);
800                errno!(EINVAL)
801            })?;
802        let hz = self.opps_hz.get(index as usize).ok_or_else(|| errno!(EINVAL))?;
803        Ok(hz_to_khz(*hz))
804    }
805
806    fn lease_max_frequency(
807        &self,
808        state: &mut CpuFreqDomainState,
809        index: u64,
810        lease_name: &str,
811    ) -> Result<fbroker::LeaseToken, Errno> {
812        self.with_domain_controller(state, |dc| {
813            dc.lease_max_frequency(
814                self.domain_id,
815                index,
816                lease_name,
817                zx::MonotonicInstant::INFINITE,
818            )
819        })?
820        .map_err(|e| {
821            log_warn!("lease_max_frequency failed for domain {}: {:?}", self.domain_id, e);
822            errno!(EINVAL)
823        })
824    }
825
826    /// Reads back the frequency in kHz that the domain's minimum is currently set to.
827    fn min_frequency_khz(&self) -> Result<u64, Errno> {
828        let mut state = self.state.lock();
829        let index = self
830            .with_domain_controller(&mut state, |dc| {
831                dc.get_min_frequency(self.domain_id, zx::MonotonicInstant::INFINITE)
832            })?
833            .map_err(|e| {
834                log_warn!("get_min_frequency failed for domain {}: {:?}", self.domain_id, e);
835                errno!(EINVAL)
836            })?;
837        let hz = self.opps_hz.get(index as usize).ok_or_else(|| errno!(EINVAL))?;
838        Ok(hz_to_khz(*hz))
839    }
840
841    fn lease_min_frequency(
842        &self,
843        state: &mut CpuFreqDomainState,
844        index: u64,
845        lease_name: &str,
846    ) -> Result<fbroker::LeaseToken, Errno> {
847        self.with_domain_controller(state, |dc| {
848            dc.lease_min_frequency(
849                self.domain_id,
850                index,
851                lease_name,
852                zx::MonotonicInstant::INFINITE,
853            )
854        })?
855        .map_err(|e| {
856            log_warn!("lease_min_frequency failed for domain {}: {:?}", self.domain_id, e);
857            errno!(EINVAL)
858        })
859    }
860}
861
862/// A writable max-frequency node (`scaling_max_freq` and `vote_manager/*_max_freq`), backed by
863/// `fuchsia.power.cpu/DomainController`.
864///
865/// Votes are keyed by the sysfs node identity (`voter`) in `CpuFreqDomain` and persist across
866/// `close()` until overwritten or cleared with a value at or above the domain maximum.
867struct ScalingMaxFreqFile {
868    domain: Arc<CpuFreqDomain>,
869    voter: &'static str,
870}
871
872impl ScalingMaxFreqFile {
873    fn new_node(domain: &Arc<CpuFreqDomain>, voter: &'static str) -> impl FsNodeOps {
874        BytesFile::new_node(Self { domain: domain.clone(), voter })
875    }
876}
877
878impl BytesFileOps for ScalingMaxFreqFile {
879    fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
880        let content = if self.voter == "scaling_max_freq" {
881            format!("{}\n", self.domain.max_frequency_khz()?)
882        } else {
883            format_freq_khz(self.domain.voter_frequency_khz(self.voter))
884        };
885        Ok(content.into_bytes().into())
886    }
887
888    fn write(&self, _current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
889        let opp_index = self.domain.opp_index_for_khz(parse_unsigned_file::<u64>(&data)?);
890        self.domain.update_vote(self.voter, opp_index)
891    }
892}
893
894/// A writable min-frequency node (`scaling_min_freq` and `vote_manager/*_min_freq`), backed by
895/// `fuchsia.power.cpu/DomainController`.
896///
897/// Votes are keyed by the sysfs node identity (`voter`) in `CpuFreqDomain` and persist across
898/// `close()` until overwritten or cleared with a value at or below the domain minimum.
899struct ScalingMinFreqFile {
900    domain: Arc<CpuFreqDomain>,
901    voter: &'static str,
902}
903
904impl ScalingMinFreqFile {
905    fn new_node(domain: &Arc<CpuFreqDomain>, voter: &'static str) -> impl FsNodeOps {
906        BytesFile::new_node(Self { domain: domain.clone(), voter })
907    }
908}
909
910impl BytesFileOps for ScalingMinFreqFile {
911    fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
912        let content = if self.voter == "scaling_min_freq" {
913            format!("{}\n", self.domain.min_frequency_khz()?)
914        } else {
915            format_freq_khz(self.domain.min_voter_frequency_khz(self.voter))
916        };
917        Ok(content.into_bytes().into())
918    }
919
920    fn write(&self, _current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
921        let opp_index = self.domain.opp_index_for_min_khz(parse_unsigned_file::<u64>(&data)?);
922        self.domain.update_min_vote(self.voter, opp_index)
923    }
924}
925
926/// A sysfs node that accepts writes, discards them, and reads back a fixed value.
927///
928/// This is for cpufreq nodes that the Android Power HAL writes but that Fuchsia cannot act on.
929/// Leaving such a node read-only is worse than making it inert: libperfmgr suppresses its
930/// warning and its 500ms retry clamp only when `open()` fails with `ENOENT`, so a node that
931/// exists but rejects writes produces log spam and a busy retry loop on every hint. Removing
932/// the node instead would return `ENOENT` to every reader, not just to libperfmgr.
933struct InertFile(Vec<u8>);
934
935impl InertFile {
936    fn new_node(contents: String) -> impl FsNodeOps {
937        BytesFile::new_node(Self(contents.into_bytes()))
938    }
939}
940
941impl BytesFileOps for InertFile {
942    fn write(&self, _current_task: &CurrentTask, _data: Vec<u8>) -> Result<(), Errno> {
943        Ok(())
944    }
945
946    fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
947        Ok(self.0.as_slice().into())
948    }
949}
950
951const CPU_DIRECTORY: &str = "/svc/fuchsia.hardware.cpu.ctrl.Service";
952
953fn connect_to_cpu_devices() -> Result<Vec<fcpuctrl::DeviceSynchronousProxy>, Errno> {
954    let dir = std::fs::read_dir(CPU_DIRECTORY).map_err(|_| errno!(EINVAL))?;
955
956    let proxies: Vec<_> = dir
957        .filter_map(|r| r.ok())
958        .filter_map(|entry| {
959            let path = entry.path().join("device").into_os_string().into_string().ok()?;
960            let (client, server) = zx::Channel::create();
961            fdio::service_connect(&path, server).ok()?;
962            Some(fcpuctrl::DeviceSynchronousProxy::new(client))
963        })
964        .collect();
965
966    if proxies.is_empty() { error!(ENOENT) } else { Ok(proxies) }
967}
968
969fn connect_to_cpu_device_by_domain_id(
970    domain_id: u64,
971) -> Result<fcpuctrl::DeviceSynchronousProxy, Errno> {
972    let dir = std::fs::read_dir(CPU_DIRECTORY).map_err(|_| errno!(EINVAL))?;
973
974    dir.filter_map(|r| r.ok())
975        .find_map(|entry| {
976            let path = entry.path().join("device").into_os_string().into_string().ok()?;
977            let (client, server) = zx::Channel::create();
978            fdio::service_connect(&path, server).ok()?;
979            let proxy = fcpuctrl::DeviceSynchronousProxy::new(client);
980
981            let dev_domain_id = proxy.get_domain_id(zx::MonotonicInstant::INFINITE).ok()?;
982            if domain_id == dev_domain_id as u64 { Some(proxy) } else { None }
983        })
984        .ok_or_else(|| errno!(ENOENT))
985}
986
987fn create_scaling_cur_freq_file(domain_id: u64) -> impl FsNodeOps {
988    let proxy_cache =
989        LockDepMutex::<_, CpuFreqProxyCacheLock>::new(None::<fcpuctrl::DeviceSynchronousProxy>);
990    SimpleFileNode::new(move |_| {
991        let mut guard = proxy_cache.lock();
992        if guard.is_none() {
993            let proxy = connect_to_cpu_device_by_domain_id(domain_id)?;
994            *guard = Some(proxy);
995        }
996        let proxy = guard.as_ref().expect("must have a valid proxy");
997        let opp = match proxy.get_current_operating_point(zx::MonotonicInstant::INFINITE) {
998            Ok(opp) => opp,
999            Err(_) => {
1000                *guard = None;
1001                return error!(EINVAL);
1002            }
1003        };
1004        let info = match proxy.get_operating_point_info(opp, zx::MonotonicInstant::INFINITE) {
1005            Ok(info) => info,
1006            Err(_) => {
1007                *guard = None;
1008                return error!(EINVAL);
1009            }
1010        };
1011        let info = info.map_err(|e| from_status_like_fdio!(zx::Status::err_from_raw(e)))?;
1012        if info.frequency_hz <= 0 {
1013            return error!(EINVAL);
1014        }
1015        let freq_khz = hz_to_khz(info.frequency_hz as u64);
1016        Ok(BytesFile::new(format!("{}\n", freq_khz).into_bytes()))
1017    })
1018}
1019
1020enum CpuIdleStatType {
1021    Time,
1022    Usage,
1023}
1024
1025/// Returns the total time the system has spent suspended since boot.
1026///
1027/// The monotonic clock pauses while the system is suspended, whereas the boot clock keeps
1028/// advancing, so the difference between the two is the accumulated suspended time. Reading the
1029/// monotonic clock before the boot clock ensures that any execution delay or suspend between the
1030/// two reads inflates (rather than reduces) `boot_ns - mono_ns`.
1031fn suspended_time() -> zx::BootDuration {
1032    let mono_ns = zx::MonotonicInstant::get().into_nanos();
1033    let boot_ns = zx::BootInstant::get().into_nanos();
1034    zx::BootDuration::from_nanos(boot_ns.saturating_sub(mono_ns))
1035}
1036
1037/// Per-core firmware C2 residency, annotated with the portion accrued while the system was
1038/// awake.
1039#[derive(Clone, Copy, Debug, Default, PartialEq)]
1040struct CoreC2Residency {
1041    /// Core identifier matching the firmware C2 state id.
1042    id: i32,
1043    /// Total C2 residency reported by firmware.
1044    total_time: zx::BootDuration,
1045    /// Number of C2 entries reported by firmware.
1046    entry_count: u64,
1047    /// Portion of `total_time` accrued while the system was awake.
1048    awake_time: zx::MonotonicDuration,
1049}
1050
1051/// Computes C1 residency from Zircon's per-CPU `total_idle` duration by subtracting the C2
1052/// residency accrued while the system was awake.
1053///
1054/// Zircon's `idle_time` is measured on the monotonic timeline and therefore excludes time spent in
1055/// system suspend, whereas firmware may or may not count system suspend as C2 residency (and this
1056/// may differ between cores and between suspends). Only C2 residency accrued while awake overlaps
1057/// with `total_idle`.
1058///
1059/// When `c2_residency` is `None` (because C2 residency reporting is unsupported on the platform or
1060/// not reported for the core), all of `total_idle` is reported as C1 residency.
1061fn compute_c1_residency(
1062    total_idle: zx::MonotonicDuration,
1063    c2_residency: Option<CoreC2Residency>,
1064) -> zx::MonotonicDuration {
1065    let awake_c2 = c2_residency.map_or(zx::MonotonicDuration::ZERO, |c2| c2.awake_time);
1066    (total_idle - awake_c2).max(zx::MonotonicDuration::ZERO)
1067}
1068
1069/// Per-core accounting state for [`C2SuspendAccounting`].
1070#[derive(Clone, Copy, Default)]
1071struct CoreC2Accounting {
1072    /// Accumulated suspended time at the previous sample for this core.
1073    last_suspended: zx::BootDuration,
1074    /// Firmware C2 residency at the previous sample for this core.
1075    last_c2: zx::BootDuration,
1076    /// Accumulated C2 residency accrued while the system was awake.
1077    awake_c2: zx::MonotonicDuration,
1078}
1079
1080/// Tracks per-core firmware C2 residency accrued while the system was awake, excluding C2
1081/// residency accrued during system suspend.
1082///
1083/// Between two samples for a core, C2 growth up to the suspended time that elapsed between those
1084/// samples is attributed to suspend; any excess growth (`c2_delta - suspended_delta`) was accrued
1085/// while awake. Cores whose firmware counts suspend as C2 thus have the suspend excluded from
1086/// `awake_c2`, whereas cores whose firmware does not count it retain their awake C2 growth outside
1087/// the suspend window.
1088///
1089/// Sampling immediately before and after container suspension (see
1090/// [`sample_cpu_c2_residency`]) keeps the window around each suspend tight, so little awake C2
1091/// time is lost on cores that do not count suspend as C2.
1092#[derive(Default)]
1093struct C2SuspendAccounting {
1094    /// Per-core accounting keyed by the firmware C2 state id.
1095    cores: HashMap<i32, CoreC2Accounting>,
1096}
1097
1098impl C2SuspendAccounting {
1099    fn boot_duration_from_ms(ms: u64) -> zx::BootDuration {
1100        zx::BootDuration::from_millis(i64::try_from(ms).unwrap_or(i64::MAX))
1101    }
1102
1103    /// Records a sample of accumulated suspended time and per-core C2 residencies.
1104    ///
1105    /// Samples older than a previously recorded one contribute no additional awake residency.
1106    fn record(&mut self, suspended: zx::BootDuration, residencies: &[fpowerstats::StateResidency]) {
1107        // Quantize suspended time to milliseconds to match firmware C2 residency granularity.
1108        let suspended = zx::BootDuration::from_millis(suspended.into_millis().max(0));
1109        for residency in residencies {
1110            let total_c2 = Self::boot_duration_from_ms(residency.total_time_ms);
1111            let core = self.cores.entry(residency.id).or_default();
1112            let suspended_delta = (suspended - core.last_suspended).max(zx::BootDuration::ZERO);
1113            let c2_delta = (total_c2 - core.last_c2).max(zx::BootDuration::ZERO);
1114            let awake_delta =
1115                zx::MonotonicDuration::from_nanos((c2_delta - suspended_delta).into_nanos().max(0));
1116            core.awake_c2 += awake_delta;
1117            core.last_suspended = core.last_suspended.max(suspended);
1118            core.last_c2 = core.last_c2.max(total_c2);
1119        }
1120    }
1121
1122    /// Returns the C2 residency accrued while awake for `core_id`.
1123    fn awake_c2(&self, core_id: i32) -> zx::MonotonicDuration {
1124        self.cores.get(&core_id).map_or(zx::MonotonicDuration::ZERO, |core| core.awake_c2)
1125    }
1126
1127    /// Pairs raw firmware residencies with the current per-core awake C2 accounting.
1128    fn snapshot(&self, residencies: Vec<fpowerstats::StateResidency>) -> Vec<CoreC2Residency> {
1129        residencies
1130            .into_iter()
1131            .map(|state| CoreC2Residency {
1132                id: state.id,
1133                total_time: Self::boot_duration_from_ms(state.total_time_ms),
1134                entry_count: u64::from(state.entry_count),
1135                awake_time: self.awake_c2(state.id),
1136            })
1137            .collect()
1138    }
1139}
1140
1141/// Statistic type and state for a `/sys/devices/system/cpu/cpuN/cpuidle/state0/{time,usage}` node.
1142enum CpuIdleC1Stat {
1143    Time {
1144        c2_provider: Arc<CpuC2Provider>,
1145        /// Highest C1 residency (in microseconds) reported so far.
1146        ///
1147        /// Zircon idle time and firmware C2 residency are sampled at slightly different instants
1148        /// (and C2 at millisecond granularity), so the computed C1 residency may transiently
1149        /// regress. Userspace treats `time` as a monotonic counter, so reads never report a value
1150        /// lower than a previously reported one.
1151        last_time_us: AtomicU64,
1152    },
1153    Usage,
1154}
1155
1156/// Backing [`BytesFileOps`] implementation for `/sys/devices/system/cpu/cpuN/cpuidle/state0/time`
1157/// and `state0/usage`, reporting per-core C1 residency (in microseconds) and reschedule count
1158/// from Zircon's `PerCpuStats`, subtracting awake C2 residency when available.
1159struct CpuIdleStatFile {
1160    core_id: u64,
1161    stat: CpuIdleC1Stat,
1162}
1163
1164impl CpuIdleStatFile {
1165    fn new_time(core_id: u64, c2_provider: Arc<CpuC2Provider>) -> Self {
1166        Self { core_id, stat: CpuIdleC1Stat::Time { c2_provider, last_time_us: AtomicU64::new(0) } }
1167    }
1168
1169    fn new_usage(core_id: u64) -> Self {
1170        Self { core_id, stat: CpuIdleC1Stat::Usage }
1171    }
1172
1173    fn clamp_c1_residency_us(last_time_us: &AtomicU64, c1: zx::MonotonicDuration) -> u64 {
1174        let c1_us = u64::try_from(c1.into_micros()).unwrap_or(0);
1175        last_time_us.fetch_max(c1_us, Ordering::Relaxed).max(c1_us)
1176    }
1177}
1178
1179impl BytesFileOps for CpuIdleStatFile {
1180    fn read(&self, current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
1181        let cpu_stats = current_task
1182            .kernel()
1183            .stats
1184            .get()
1185            .get_cpu_stats(zx::MonotonicInstant::INFINITE)
1186            .map_err(|_| errno!(EINVAL))?;
1187        let per_cpu_stats = cpu_stats.per_cpu_stats.ok_or_else(|| errno!(EINVAL))?;
1188        let per_cpu = per_cpu_stats.get(self.core_id as usize).ok_or_else(|| errno!(EINVAL))?;
1189        let value = match &self.stat {
1190            CpuIdleC1Stat::Time { c2_provider, last_time_us } => {
1191                let total_idle = zx::MonotonicDuration::from_nanos(
1192                    per_cpu.idle_time.ok_or_else(|| errno!(EINVAL))?,
1193                );
1194                let c2 = c2_provider.residency(self.core_id)?;
1195                let c1 = compute_c1_residency(total_idle, c2);
1196                Self::clamp_c1_residency_us(last_time_us, c1)
1197            }
1198            CpuIdleC1Stat::Usage => per_cpu.reschedules.ok_or_else(|| errno!(EINVAL))?,
1199        };
1200        Ok(format!("{value}\n").into_bytes().into())
1201    }
1202}
1203
1204const POWER_STATS_SERVICE_DIRECTORY: &str = "/svc/fuchsia.hardware.power.stats.Service";
1205const CPU_C2_ENTITY_NAME: &str = "cpu_c2";
1206
1207/// Maximum time to wait for a synchronous `fuchsia.hardware.power.stats.Device` call.
1208const POWER_STATS_FIDL_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(2);
1209
1210/// Maximum difference between back-to-back [`suspended_time`] reads attributed to non-atomic
1211/// reading of the boot and monotonic clocks rather than an intervening system suspend.
1212const CLOCK_READ_JITTER_TOLERANCE: zx::BootDuration = zx::BootDuration::from_millis(1);
1213
1214/// Connection state of [`CpuC2Provider`] to the `fuchsia.hardware.power.stats.Service` instance
1215/// that reports `"cpu_c2"`.
1216#[derive(Default)]
1217enum ProviderState {
1218    /// No initial connection has been attempted yet.
1219    #[default]
1220    Unconnected,
1221    /// Connected to a `fuchsia.hardware.power.stats.Service` instance that reports `"cpu_c2"`.
1222    Connected(Arc<fpowerstats::DeviceSynchronousProxy>),
1223    /// A previous connection failed with a transport error; subsequent reads attempt to reconnect
1224    /// and fail with `EIO` until the service recovers, avoiding non-monotonic `0` counter drops.
1225    Disconnected,
1226    /// No `fuchsia.hardware.power.stats.Service` instance in `/svc` reported `"cpu_c2"` during
1227    /// initial discovery, so C2 residency is unsupported on this platform.
1228    Unavailable,
1229}
1230
1231/// Provides per-core CPU C2 ("deep idle") state residency from
1232/// `fuchsia.hardware.power.stats.Service`.
1233///
1234/// A single `CpuC2Provider` instance is shared via `Arc` across all CPU cores and across the
1235/// `state0/time`, `state1/time`, and `state1/usage` sysfs nodes.
1236///
1237/// # Caching and Recovery
1238/// Connection discovery in `/svc/fuchsia.hardware.power.stats.Service` is performed lazily on
1239/// the first read or suspend sample:
1240/// - If an instance reporting the `"cpu_c2"` entity is found, its proxy is cached in
1241///   [`ProviderState::Connected`].
1242/// - If no such instance exists during initial discovery, [`ProviderState::Unavailable`] is cached
1243///   so subsequent reads return `Ok(None)` without repeatedly scanning `/svc`.
1244/// - If a `GetStats` call fails after a connection was established, the state transitions to
1245///   [`ProviderState::Disconnected`] and the read fails with `EIO` so userspace does not observe
1246///   monotonic counters dropping to `0`. Subsequent reads attempt to reconnect without permanently
1247///   disabling C2 reporting if the driver is still restarting.
1248///
1249/// # Concurrency
1250/// `state` is locked only briefly to inspect or update [`ProviderState`] and clone the `Arc`
1251/// wrapping the synchronous proxy. The lock is released before scanning `/svc` or issuing FIDL
1252/// calls so concurrent sysfs reads across CPU cores do not serialize behind the mutex.
1253/// `suspend_accounting` is likewise locked only to record a sample and snapshot the accounting;
1254/// the two locks are never held at the same time.
1255#[derive(Default)]
1256struct CpuC2Provider {
1257    state: LockDepMutex<ProviderState, CpuC2ProxyCacheLock>,
1258    suspend_accounting: LockDepMutex<C2SuspendAccounting, CpuC2SuspendAccountingLock>,
1259}
1260
1261impl CpuC2Provider {
1262    fn connect() -> Option<fpowerstats::DeviceSynchronousProxy> {
1263        let dir = std::fs::read_dir(POWER_STATS_SERVICE_DIRECTORY).ok()?;
1264        dir.filter_map(|entry| entry.ok()).find_map(|entry| {
1265            let path = entry.path().join("device").into_os_string().into_string().ok()?;
1266            let (client, server) = zx::Channel::create();
1267            fdio::service_connect(&path, server).ok()?;
1268            let proxy = fpowerstats::DeviceSynchronousProxy::new(client);
1269            let deadline = zx::MonotonicInstant::after(POWER_STATS_FIDL_TIMEOUT);
1270            let entities = proxy.get_entities(deadline).ok()?;
1271            entities.iter().any(|entity| entity.name == CPU_C2_ENTITY_NAME).then_some(proxy)
1272        })
1273    }
1274
1275    fn proxy(&self) -> Result<Option<Arc<fpowerstats::DeviceSynchronousProxy>>, Errno> {
1276        if matches!(*self.state.lock(), ProviderState::Unconnected | ProviderState::Disconnected) {
1277            let connected_proxy = Self::connect().map(Arc::new);
1278            let mut state = self.state.lock();
1279            match (&*state, connected_proxy) {
1280                (ProviderState::Unconnected | ProviderState::Disconnected, Some(proxy)) => {
1281                    *state = ProviderState::Connected(proxy);
1282                }
1283                (ProviderState::Unconnected, None) => {
1284                    *state = ProviderState::Unavailable;
1285                }
1286                (ProviderState::Disconnected, None)
1287                | (ProviderState::Connected(_) | ProviderState::Unavailable, _) => {}
1288            }
1289        }
1290        match &*self.state.lock() {
1291            ProviderState::Connected(proxy) => Ok(Some(proxy.clone())),
1292            ProviderState::Unavailable | ProviderState::Unconnected => Ok(None),
1293            ProviderState::Disconnected => error!(EIO),
1294        }
1295    }
1296
1297    /// Updates `suspend_accounting` if the sample did not race a system suspend, and returns a
1298    /// consistent [`CoreC2Residency`] snapshot while holding the accounting lock.
1299    fn record_sample(
1300        &self,
1301        suspended_before: zx::BootDuration,
1302        suspended_after: zx::BootDuration,
1303        residencies: Vec<fpowerstats::StateResidency>,
1304    ) -> Vec<CoreC2Residency> {
1305        let mut accounting = self.suspend_accounting.lock();
1306        // If the system suspended while the stats were being read, it is unknown whether the
1307        // residencies include that suspend, so do not use them for attribution. The tolerance
1308        // absorbs jitter from reading the boot and monotonic clocks non-atomically.
1309        if suspended_after - suspended_before <= CLOCK_READ_JITTER_TOLERANCE {
1310            accounting.record(suspended_after, &residencies);
1311        }
1312        accounting.snapshot(residencies)
1313    }
1314
1315    /// Reads the per-core C2 residencies from `proxy`, updates `suspend_accounting`, and returns
1316    /// the resulting [`CoreC2Residency`] snapshot.
1317    fn sample(
1318        &self,
1319        proxy: &Arc<fpowerstats::DeviceSynchronousProxy>,
1320    ) -> Result<Vec<CoreC2Residency>, Errno> {
1321        let suspended_before = suspended_time();
1322        let deadline = zx::MonotonicInstant::after(POWER_STATS_FIDL_TIMEOUT);
1323        let reports = match proxy.get_stats(deadline) {
1324            Ok(reports) => reports,
1325            Err(e) => {
1326                log_warn!("Failed to read power stats for {CPU_C2_ENTITY_NAME}: {e}");
1327                let mut state = self.state.lock();
1328                if matches!(
1329                    &*state,
1330                    ProviderState::Connected(current) if Arc::ptr_eq(current, proxy)
1331                ) {
1332                    *state = ProviderState::Disconnected;
1333                }
1334                return error!(EIO);
1335            }
1336        };
1337        let suspended_after = suspended_time();
1338        let residencies = reports
1339            .into_iter()
1340            .find(|stats| stats.entity == CPU_C2_ENTITY_NAME)
1341            .map_or_else(Vec::new, |stats| stats.state_residency_data);
1342        Ok(self.record_sample(suspended_before, suspended_after, residencies))
1343    }
1344
1345    /// Connects if needed and records a C2 residency sample when C2 reporting is available.
1346    fn sample_all(&self) {
1347        if let Ok(Some(proxy)) = self.proxy() {
1348            let _ = self.sample(&proxy);
1349        }
1350    }
1351
1352    fn residency(&self, core_id: u64) -> Result<Option<CoreC2Residency>, Errno> {
1353        let core_id = i32::try_from(core_id).map_err(|_| errno!(EINVAL))?;
1354        let Some(proxy) = self.proxy()? else {
1355            return Ok(None);
1356        };
1357        Ok(self.sample(&proxy)?.into_iter().find(|s| s.id == core_id))
1358    }
1359}
1360
1361/// Samples per-core CPU C2 residency so that C2 residency accrued during system suspend is
1362/// excluded from awake C2 residency rather than subtracted from C1 idle time.
1363///
1364/// Call immediately before and after suspending the container so the sampling window tightly
1365/// brackets each suspend.
1366pub fn sample_cpu_c2_residency(kernel: &Kernel) {
1367    kernel.expando.get::<CpuC2Provider>().sample_all();
1368}
1369
1370/// Backing [`BytesFileOps`] implementation for `/sys/devices/system/cpu/cpuN/cpuidle/state1/time`
1371/// and `state1/usage`, reporting per-core C2 residency (in microseconds) and entry count from
1372/// [`CpuC2Provider`], or `ENODEV` when C2 residency is unavailable for the core.
1373struct CpuIdleC2StatFile {
1374    core_id: u64,
1375    stat_type: CpuIdleStatType,
1376    provider: Arc<CpuC2Provider>,
1377}
1378
1379impl CpuIdleC2StatFile {
1380    fn read_value(&self) -> Result<u64, Errno> {
1381        let state = self.provider.residency(self.core_id)?.ok_or_else(|| errno!(ENODEV))?;
1382        Ok(match self.stat_type {
1383            CpuIdleStatType::Time => u64::try_from(state.total_time.into_micros()).unwrap_or(0),
1384            CpuIdleStatType::Usage => state.entry_count,
1385        })
1386    }
1387}
1388
1389impl BytesFileOps for CpuIdleC2StatFile {
1390    fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
1391        let value = self.read_value()?;
1392        Ok(format!("{value}\n").into_bytes().into())
1393    }
1394}
1395
1396#[cfg(test)]
1397mod tests {
1398    use super::*;
1399    use fidl::endpoints::RequestStream;
1400    use futures::TryStreamExt;
1401
1402    #[::fuchsia::test]
1403    fn cpu_c2_provider_reads_firmware_residencies() {
1404        let (client, server) = zx::Channel::create();
1405        let server_thread = std::thread::spawn(move || {
1406            let mut executor = fuchsia_async::LocalExecutor::default();
1407            executor.run_singlethreaded(async move {
1408                let mut stream = fpowerstats::DeviceRequestStream::from_channel(
1409                    fuchsia_async::Channel::from_channel(server),
1410                );
1411                if let Some(fpowerstats::DeviceRequest::GetStats { responder }) =
1412                    stream.try_next().await.expect("failed to read request")
1413                {
1414                    let reports = vec![fpowerstats::Stats {
1415                        entity: CPU_C2_ENTITY_NAME.to_string(),
1416                        state_residency_data: vec![
1417                            fpowerstats::StateResidency {
1418                                id: 0,
1419                                name: "C2_CPU0".to_string(),
1420                                total_time_ms: 34,
1421                                entry_count: 12,
1422                                last_entry_timestamp_ms: 0,
1423                            },
1424                            fpowerstats::StateResidency {
1425                                id: 1,
1426                                name: "C2_CPU1".to_string(),
1427                                total_time_ms: 78,
1428                                entry_count: 56,
1429                                last_entry_timestamp_ms: 0,
1430                            },
1431                        ],
1432                    }];
1433                    responder.send(&reports).expect("failed to send response");
1434                }
1435            });
1436        });
1437
1438        let provider = CpuC2Provider {
1439            state: LockDepMutex::new(ProviderState::Connected(Arc::new(
1440                fpowerstats::DeviceSynchronousProxy::new(client),
1441            ))),
1442            suspend_accounting: Default::default(),
1443        };
1444        let core1 = provider
1445            .residency(/* core_id = */ 1)
1446            .expect("residency call must succeed")
1447            .expect("core 1 residency must be present");
1448        assert_eq!(core1.entry_count, 56);
1449        assert_eq!(core1.total_time, zx::BootDuration::from_millis(78));
1450
1451        server_thread.join().expect("server thread panicked");
1452
1453        // After the server closes the channel, reads return EIO and transition to Disconnected;
1454        // subsequent reads while the service is still absent stay Disconnected (rather than
1455        // permanently transitioning to Unavailable) and continue returning EIO.
1456        assert_eq!(provider.residency(/* core_id = */ 0), error!(EIO));
1457        assert!(matches!(*provider.state.lock(), ProviderState::Disconnected));
1458        assert_eq!(provider.residency(/* core_id = */ 0), error!(EIO));
1459        assert!(matches!(*provider.state.lock(), ProviderState::Disconnected));
1460
1461        // When no service is present on initial discovery, the provider transitions from
1462        // Unconnected to Unavailable and returns Ok(None), while CpuIdleC2StatFile fails with
1463        // ENODEV rather than reporting 0.
1464        let unconfigured = Arc::new(CpuC2Provider::default());
1465        assert_eq!(unconfigured.residency(/* core_id = */ 0), Ok(None));
1466        assert!(matches!(*unconfigured.state.lock(), ProviderState::Unavailable));
1467        let c2_file = CpuIdleC2StatFile {
1468            core_id: 0,
1469            stat_type: CpuIdleStatType::Time,
1470            provider: unconfigured,
1471        };
1472        assert_eq!(c2_file.read_value(), error!(ENODEV));
1473    }
1474
1475    fn c2_residency(id: i32, total_time_ms: u64) -> fpowerstats::StateResidency {
1476        fpowerstats::StateResidency {
1477            id,
1478            name: format!("C2_CPU{id}"),
1479            total_time_ms,
1480            entry_count: 1,
1481            last_entry_timestamp_ms: 0,
1482        }
1483    }
1484
1485    #[::fuchsia::test]
1486    fn c1_time_subtracts_awake_c2_residency_and_saturates_at_zero() {
1487        let c2 = CoreC2Residency {
1488            id: 0,
1489            total_time: zx::BootDuration::from_millis(1_899),
1490            entry_count: 1,
1491            awake_time: zx::MonotonicDuration::from_millis(1_899),
1492        };
1493
1494        // 5,000,000 us idle - 1,899,000 us awake C2 = 3,101,000 us C1.
1495        assert_eq!(
1496            compute_c1_residency(zx::MonotonicDuration::from_seconds(5), Some(c2)),
1497            zx::MonotonicDuration::from_micros(3_101_000)
1498        );
1499
1500        // When C2 residency is unavailable, report all of total_idle as C1.
1501        assert_eq!(
1502            compute_c1_residency(zx::MonotonicDuration::from_seconds(5), None),
1503            zx::MonotonicDuration::from_seconds(5)
1504        );
1505
1506        // Saturate at 0 if awake C2 residency exceeds idle time due to sampling skew.
1507        assert_eq!(
1508            compute_c1_residency(zx::MonotonicDuration::from_seconds(1), Some(c2)),
1509            zx::MonotonicDuration::ZERO
1510        );
1511
1512        // C2 residency accrued during suspend (excluded from awake_time) is not subtracted.
1513        let c2 = CoreC2Residency {
1514            id: 0,
1515            total_time: zx::BootDuration::from_millis(154_160),
1516            entry_count: 2,
1517            awake_time: zx::MonotonicDuration::from_millis(1_860),
1518        };
1519        assert_eq!(
1520            compute_c1_residency(zx::MonotonicDuration::from_millis(252_370), Some(c2)),
1521            zx::MonotonicDuration::from_micros(250_510_000)
1522        );
1523    }
1524
1525    #[::fuchsia::test]
1526    fn c2_suspend_accounting_attributes_suspend_per_core() {
1527        let mut accounting = C2SuspendAccounting::default();
1528
1529        // Before suspend: core 0 accrued 10 s of awake C2, core 1 accrued 2 s.
1530        accounting.record(
1531            zx::BootDuration::from_millis(0),
1532            &[c2_residency(0, 10_000), c2_residency(1, 2_000)],
1533        );
1534        assert_eq!(accounting.awake_c2(0), zx::MonotonicDuration::from_millis(10_000));
1535        assert_eq!(accounting.awake_c2(1), zx::MonotonicDuration::from_millis(2_000));
1536
1537        // A 152 s suspend. Firmware counts it as C2 on core 1 (plus 0.5 s of awake C2 within the
1538        // sampling window) but not on core 0, which only accrued 0.2 s of awake C2.
1539        accounting.record(
1540            zx::BootDuration::from_millis(152_000),
1541            &[c2_residency(0, 10_200), c2_residency(1, 154_500)],
1542        );
1543        assert_eq!(accounting.awake_c2(0), zx::MonotonicDuration::from_millis(10_000));
1544        assert_eq!(accounting.awake_c2(1), zx::MonotonicDuration::from_millis(2_500));
1545
1546        // Awake C2 accrued without further suspend is added in full. Core 2 appears for the first
1547        // time after the 152 s suspend with 153 s of total C2; its per-core last_suspended starts
1548        // at 0 so the 152 s suspend is still deducted.
1549        accounting.record(
1550            zx::BootDuration::from_millis(152_000),
1551            &[c2_residency(0, 70_000), c2_residency(1, 160_000), c2_residency(2, 153_000)],
1552        );
1553        assert_eq!(accounting.awake_c2(0), zx::MonotonicDuration::from_millis(69_800));
1554        assert_eq!(accounting.awake_c2(1), zx::MonotonicDuration::from_millis(8_000));
1555        assert_eq!(accounting.awake_c2(2), zx::MonotonicDuration::from_millis(1_000));
1556
1557        // A stale sample (older suspended time and residencies) adds no awake residency.
1558        accounting.record(
1559            zx::BootDuration::from_millis(100_000),
1560            &[c2_residency(0, 10_000), c2_residency(1, 100_000)],
1561        );
1562        assert_eq!(accounting.awake_c2(0), zx::MonotonicDuration::from_millis(69_800));
1563        assert_eq!(accounting.awake_c2(1), zx::MonotonicDuration::from_millis(8_000));
1564
1565        // Unknown cores have no awake C2 residency.
1566        assert_eq!(accounting.awake_c2(7), zx::MonotonicDuration::ZERO);
1567    }
1568
1569    #[::fuchsia::test]
1570    fn record_sample_discards_sample_that_raced_suspend() {
1571        let provider = CpuC2Provider::default();
1572
1573        // Initial sample while awake is recorded.
1574        let snapshot = provider.record_sample(
1575            zx::BootDuration::from_millis(0),
1576            zx::BootDuration::from_millis(0),
1577            vec![c2_residency(0, 10_000)],
1578        );
1579        assert_eq!(
1580            snapshot,
1581            vec![CoreC2Residency {
1582                id: 0,
1583                total_time: zx::BootDuration::from_millis(10_000),
1584                entry_count: 1,
1585                awake_time: zx::MonotonicDuration::from_millis(10_000),
1586            }]
1587        );
1588
1589        // A sample across a 50 s suspend exceeds CLOCK_READ_JITTER_TOLERANCE: accounting is not
1590        // updated, and the snapshot preserves the previously recorded awake_time rather than
1591        // misattributing the ambiguous sample.
1592        let raced = provider.record_sample(
1593            zx::BootDuration::from_millis(0),
1594            zx::BootDuration::from_millis(50_000),
1595            vec![c2_residency(0, 60_000)],
1596        );
1597        assert_eq!(
1598            raced,
1599            vec![CoreC2Residency {
1600                id: 0,
1601                total_time: zx::BootDuration::from_millis(60_000),
1602                entry_count: 1,
1603                awake_time: zx::MonotonicDuration::from_millis(10_000),
1604            }]
1605        );
1606
1607        // The next un-raced sample (with 1 ms of clock-read jitter within
1608        // CLOCK_READ_JITTER_TOLERANCE) attributes the 50 s suspend and records the 500 ms of
1609        // awake C2 accrued since the initial sample.
1610        let after = provider.record_sample(
1611            zx::BootDuration::from_millis(49_999),
1612            zx::BootDuration::from_millis(50_000),
1613            vec![c2_residency(0, 60_500)],
1614        );
1615        assert_eq!(
1616            after,
1617            vec![CoreC2Residency {
1618                id: 0,
1619                total_time: zx::BootDuration::from_millis(60_500),
1620                entry_count: 1,
1621                awake_time: zx::MonotonicDuration::from_millis(10_500),
1622            }]
1623        );
1624    }
1625
1626    #[::fuchsia::test]
1627    fn c1_residency_clamps_to_high_water_mark() {
1628        let last_time_us = AtomicU64::new(0);
1629        assert_eq!(
1630            CpuIdleStatFile::clamp_c1_residency_us(
1631                &last_time_us,
1632                zx::MonotonicDuration::from_micros(3_101_000),
1633            ),
1634            3_101_000
1635        );
1636        // Transient regression from sampling skew is clamped to the previous high-water mark.
1637        assert_eq!(
1638            CpuIdleStatFile::clamp_c1_residency_us(
1639                &last_time_us,
1640                zx::MonotonicDuration::from_micros(3_100_500),
1641            ),
1642            3_101_000
1643        );
1644        // Subsequent growth advances the high-water mark.
1645        assert_eq!(
1646            CpuIdleStatFile::clamp_c1_residency_us(
1647                &last_time_us,
1648                zx::MonotonicDuration::from_micros(3_200_000),
1649            ),
1650            3_200_000
1651        );
1652    }
1653
1654    #[::fuchsia::test]
1655    fn min_freq_votes_manage_domain_controller_leases() {
1656        let (client, server) = zx::Channel::create();
1657        let (lease_tx, lease_rx) = std::sync::mpsc::channel();
1658        let (prev_token_tx, prev_token_rx) = std::sync::mpsc::channel::<zx::EventPair>();
1659        let server_thread = std::thread::spawn(move || {
1660            let mut executor = fuchsia_async::LocalExecutor::default();
1661            executor.run_singlethreaded(async move {
1662                let mut stream = fcpu::DomainControllerRequestStream::from_channel(
1663                    fuchsia_async::Channel::from_channel(server),
1664                );
1665                while let Some(request) =
1666                    stream.try_next().await.expect("failed to read DomainController request")
1667                {
1668                    match request {
1669                        fcpu::DomainControllerRequest::GetMaxFrequency { domain_id, responder } => {
1670                            assert_eq!(domain_id, 0);
1671                            responder.send(Ok(2)).expect("failed to send max frequency");
1672                        }
1673                        fcpu::DomainControllerRequest::LeaseMinFrequency {
1674                            domain_id,
1675                            frequency_index,
1676                            lease_name,
1677                            responder,
1678                        } => {
1679                            assert_eq!(domain_id, 0);
1680                            if let Ok(prev_token) = prev_token_rx.try_recv() {
1681                                assert!(is_lease_open(&prev_token));
1682                            }
1683                            let (client_token, server_token) = zx::EventPair::create();
1684                            responder.send(Ok(client_token)).expect("failed to send lease token");
1685                            lease_tx
1686                                .send((lease_name, frequency_index, server_token))
1687                                .expect("failed to forward lease token");
1688                        }
1689                        other => panic!("unexpected request: {other:?}"),
1690                    }
1691                }
1692            });
1693        });
1694
1695        let proxy = fcpu::DomainControllerSynchronousProxy::new(client);
1696        let domain = CpuFreqDomain::new(
1697            &fcpu::DomainInfo {
1698                id: Some(0),
1699                core_ids: Some(vec![0, 1]),
1700                available_frequencies_hz: Some(vec![820_000_000, 1_200_000_000, 1_632_000_000]),
1701                name: Some("cluster0".to_string()),
1702                ..Default::default()
1703            },
1704            Some(&proxy),
1705        );
1706        domain.state.lock().domain_controller = Some(proxy);
1707
1708        // Voting above cpuinfo_min_freq acquires a per-voter LeaseMinFrequency token.
1709        assert_eq!(
1710            domain.update_min_vote("powerhint_min_freq", domain.opp_index_for_min_khz(1_632_000)),
1711            Ok(())
1712        );
1713        assert_eq!(domain.min_voter_frequency_khz("powerhint_min_freq"), Some(1_632_000));
1714        assert_eq!(domain.min_voter_frequency_khz("soft_min_freq"), Some(820_000));
1715        let (name1, opp1, token1) = lease_rx.recv().expect("expected first lease token");
1716        assert_eq!(name1, "starnix:powerhint_min_freq");
1717        assert_eq!(opp1, 2);
1718        assert!(is_lease_open(&token1));
1719
1720        // Re-applying the same OPP index while the token is open does not issue a duplicate
1721        // LeaseMinFrequency call or drop the existing lease.
1722        assert_eq!(
1723            domain.update_min_vote("powerhint_min_freq", domain.opp_index_for_min_khz(1_632_000)),
1724            Ok(())
1725        );
1726        assert!(lease_rx.try_recv().is_err());
1727        assert!(is_lease_open(&token1));
1728
1729        // A second voter acquires an independent LeaseMinFrequency token while keeping the first alive.
1730        assert_eq!(
1731            domain.update_min_vote("soft_min_freq", domain.opp_index_for_min_khz(1_200_000)),
1732            Ok(())
1733        );
1734        assert_eq!(domain.min_voter_frequency_khz("soft_min_freq"), Some(1_200_000));
1735        let (name2, opp2, token2) = lease_rx.recv().expect("expected second lease token");
1736        assert_eq!(name2, "starnix:soft_min_freq");
1737        assert_eq!(opp2, 1);
1738        assert!(is_lease_open(&token1));
1739        assert!(is_lease_open(&token2));
1740
1741        // Updating an existing voter from one active OPP to another acquires the new lease before
1742        // token1 observes EVENTPAIR_PEER_CLOSED, and then drops token1.
1743        prev_token_tx
1744            .send(token1.duplicate_handle(zx::Rights::SAME_RIGHTS).expect("duplicate token1"))
1745            .expect("send previous token");
1746        assert_eq!(
1747            domain.update_min_vote("powerhint_min_freq", domain.opp_index_for_min_khz(1_200_000)),
1748            Ok(())
1749        );
1750        assert_eq!(domain.min_voter_frequency_khz("powerhint_min_freq"), Some(1_200_000));
1751        let (name3, opp3, token3) = lease_rx.recv().expect("expected replacement lease token");
1752        assert_eq!(name3, "starnix:powerhint_min_freq");
1753        assert_eq!(opp3, 1);
1754        assert_eq!(
1755            token1.wait_one(zx::Signals::EVENTPAIR_PEER_CLOSED, zx::MonotonicInstant::INFINITE),
1756            zx::WaitResult::Ok(zx::Signals::EVENTPAIR_PEER_CLOSED)
1757        );
1758        assert!(is_lease_open(&token3));
1759        assert!(is_lease_open(&token2));
1760
1761        // Dropping the server peer token invalidates the cached vote until re-applied, at which
1762        // point re-applying the same OPP index acquires a new lease.
1763        drop(token3);
1764        assert_eq!(domain.get_min_vote("powerhint_min_freq"), None);
1765        assert_eq!(domain.min_voter_frequency_khz("powerhint_min_freq"), Some(820_000));
1766        assert_eq!(
1767            domain.update_min_vote("powerhint_min_freq", domain.opp_index_for_min_khz(1_200_000)),
1768            Ok(())
1769        );
1770        assert_eq!(domain.min_voter_frequency_khz("powerhint_min_freq"), Some(1_200_000));
1771        let (name4, opp4, token4) = lease_rx.recv().expect("expected re-leased token");
1772        assert_eq!(name4, "starnix:powerhint_min_freq");
1773        assert_eq!(opp4, 1);
1774        assert!(is_lease_open(&token4));
1775
1776        // Lowering powerhint_min_freq to cpuinfo_min_freq drops only its lease while soft_min_freq stays active.
1777        assert_eq!(
1778            domain.update_min_vote("powerhint_min_freq", domain.opp_index_for_min_khz(820_000)),
1779            Ok(())
1780        );
1781        assert_eq!(domain.min_voter_frequency_khz("powerhint_min_freq"), Some(820_000));
1782        assert_eq!(
1783            token4.wait_one(zx::Signals::EVENTPAIR_PEER_CLOSED, zx::MonotonicInstant::INFINITE),
1784            zx::WaitResult::Ok(zx::Signals::EVENTPAIR_PEER_CLOSED)
1785        );
1786        assert!(is_lease_open(&token2));
1787
1788        // Clearing soft_min_freq drops the remaining lease.
1789        assert_eq!(
1790            domain.update_min_vote("soft_min_freq", domain.opp_index_for_min_khz(0)),
1791            Ok(())
1792        );
1793        assert_eq!(domain.min_voter_frequency_khz("soft_min_freq"), Some(820_000));
1794        assert_eq!(
1795            token2.wait_one(zx::Signals::EVENTPAIR_PEER_CLOSED, zx::MonotonicInstant::INFINITE),
1796            zx::WaitResult::Ok(zx::Signals::EVENTPAIR_PEER_CLOSED)
1797        );
1798
1799        drop(domain);
1800        server_thread.join().expect("server thread panicked");
1801    }
1802}