Skip to main content

scx_utils/
energy_model.rs

1// SPDX-License-Identifier: GPL-2.0
2//
3// Copyright (c) 2025 Valve Corporation.
4// Author: Changwoo Min <changwoo@igalia.com>
5
6// This software may be used and distributed according to the terms of the
7// GNU General Public License version 2.
8
9//! # SCX Energy Model
10//!
11//! A crate that allows schedulers to inspect and model the host's energy model,
12//! which is loaded from debugfs.
13
14use crate::compat;
15use crate::compat::ROOT_PREFIX;
16use crate::misc::read_from_file;
17use crate::Cpumask;
18use anyhow::bail;
19use anyhow::Result;
20use glob::glob;
21use num::clamp;
22use std::collections::BTreeMap;
23use std::fmt;
24use std::path::Path;
25use std::sync::Arc;
26
27#[derive(Debug, Clone, Eq, Hash, Ord, PartialOrd)]
28pub struct PerfState {
29    pub cost: usize,
30    pub frequency: usize,
31    pub inefficient: usize,
32    pub performance: usize,
33    pub power: usize,
34}
35
36#[derive(Debug, Clone, Eq, Hash, Ord, PartialOrd)]
37pub struct PerfDomain {
38    /// Monotonically increasing unique id.
39    pub id: usize,
40    /// Cpumask of all CPUs in this performance domain.
41    pub span: Cpumask,
42    /// Table of performance states indexed by performance.
43    pub perf_table: BTreeMap<usize, Arc<PerfState>>,
44}
45
46/// A set of performance domains sharing an identical performance table.
47///
48/// How many CPUs a performance domain covers is processor-specific: it can be a
49/// cluster, a core, or a single CPU. Intel hybrid processors, for instance, have
50/// one performance domain per CPU, so all the P-cores (or all the E-cores) are
51/// represented by separate performance domains even though they are identical.
52/// Such domains are interchangeable, so a search for the cheapest set of CPUs
53/// can consider how many CPUs to take from an equivalence performance domain
54/// rather than which performance domains to activate, reducing the search space
55/// from `2^nr_perf_doms` to `prod(weight_i + 1)`.
56#[derive(Debug, Clone)]
57pub struct EqPerfDomain {
58    /// Monotonically increasing unique id.
59    pub id: usize,
60    /// Member performance domains in ascending domain id order.
61    pub perf_doms: Vec<Arc<PerfDomain>>,
62    /// Cpumask of all CPUs in this equivalence performance domain.
63    pub span: Cpumask,
64    /// Table of performance states indexed by performance, shared by all
65    /// member performance domains.
66    pub perf_table: BTreeMap<usize, Arc<PerfState>>,
67}
68
69#[derive(Debug)]
70pub struct EnergyModel {
71    /// Performance domains indexed by domain id
72    pub perf_doms: BTreeMap<usize, Arc<PerfDomain>>,
73    /// Equivalence performance domains indexed by equivalence domain id
74    pub eq_perf_doms: BTreeMap<usize, Arc<EqPerfDomain>>,
75}
76
77impl EnergyModel {
78    pub fn has_energy_model() -> bool {
79        get_pd_paths().is_ok()
80    }
81
82    /// Build a complete EnergyModel
83    pub fn new() -> Result<EnergyModel> {
84        let mut perf_doms = BTreeMap::new();
85        let pd_paths = match get_pd_paths() {
86            Ok(pd_paths) => pd_paths,
87            Err(_) => {
88                bail!("Fail to locate the energy model directory");
89            }
90        };
91
92        for (pd_id, pd_path) in pd_paths {
93            let pd = PerfDomain::new(pd_id, pd_path)?;
94            perf_doms.insert(pd.id, pd.into());
95        }
96        let eq_perf_doms = Self::group_perf_doms(&perf_doms);
97
98        Ok(EnergyModel {
99            perf_doms,
100            eq_perf_doms,
101        })
102    }
103
104    /// Group performance domains sharing an identical performance table into
105    /// equivalence performance domains. Since @perf_doms is visited in
106    /// ascending domain id order, both the members of an equivalence
107    /// performance domain and the equivalence performance domains themselves
108    /// are ordered by performance domain id.
109    fn group_perf_doms(
110        perf_doms: &BTreeMap<usize, Arc<PerfDomain>>,
111    ) -> BTreeMap<usize, Arc<EqPerfDomain>> {
112        let mut eq_perf_doms: Vec<EqPerfDomain> = vec![];
113
114        for pd in perf_doms.values() {
115            match eq_perf_doms
116                .iter_mut()
117                .find(|eq_pd| eq_pd.perf_table == pd.perf_table)
118            {
119                Some(eq_pd) => {
120                    eq_pd.span = eq_pd.span.or(&pd.span);
121                    eq_pd.perf_doms.push(pd.clone());
122                }
123                None => {
124                    eq_perf_doms.push(EqPerfDomain {
125                        id: eq_perf_doms.len(),
126                        perf_doms: vec![pd.clone()],
127                        span: pd.span.clone(),
128                        perf_table: pd.perf_table.clone(),
129                    });
130                }
131            }
132        }
133
134        eq_perf_doms
135            .into_iter()
136            .map(|eq_pd| (eq_pd.id, eq_pd.into()))
137            .collect()
138    }
139
140    pub fn get_pd_by_cpu_id(&self, cpu_id: usize) -> Option<&PerfDomain> {
141        self.perf_doms
142            .values()
143            .find(|&pd| pd.span.test_cpu(cpu_id))
144            .map(|c| c as _)
145    }
146
147    pub fn perf_total(&self) -> usize {
148        let mut total = 0;
149
150        for (_, pd) in self.perf_doms.iter() {
151            total += pd.perf_total();
152        }
153
154        total
155    }
156}
157
158impl PerfDomain {
159    /// Build a PerfDomain
160    pub fn new(id: usize, root: String) -> Result<PerfDomain> {
161        let mut perf_table = BTreeMap::new();
162        let cpulist = std::fs::read_to_string(root.clone() + "/cpus")?;
163        let span = Cpumask::from_cpulist(&cpulist)?;
164
165        for ps_path in get_ps_paths(root)? {
166            let ps = PerfState::new(ps_path)?;
167            perf_table.insert(ps.performance, ps.into());
168        }
169
170        Ok(PerfDomain {
171            id,
172            span,
173            perf_table,
174        })
175    }
176
177    /// Lookup a performance state by a given CPU utilization.
178    /// @util is in %, ranging [0, 100].
179    pub fn select_perf_state(&self, util: f32) -> Option<&Arc<PerfState>> {
180        let util = clamp(util, 0.0, 100.0);
181        let (perf_max, _) = self.perf_table.last_key_value()?;
182        let perf_max = *perf_max as f32;
183        let req_perf = (perf_max * (util / 100.0)) as usize;
184        for (perf, ps) in self.perf_table.iter() {
185            if *perf >= req_perf {
186                return Some(ps);
187            }
188        }
189        None
190    }
191
192    pub fn perf_total(&self) -> usize {
193        let (_, ps) = self.perf_table.last_key_value().unwrap();
194        ps.performance * self.span.weight()
195    }
196}
197
198impl PartialEq for PerfDomain {
199    fn eq(&self, other: &Self) -> bool {
200        self.id == other.id && self.span == other.span && self.perf_table == other.perf_table
201    }
202}
203
204impl PerfState {
205    /// Build a PerfState
206    pub fn new(root: String) -> Result<PerfState> {
207        let cost = read_from_file(Path::new(&(root.clone() + "/cost")))?;
208        let frequency = read_from_file(Path::new(&(root.clone() + "/frequency")))?;
209        let inefficient = read_from_file(Path::new(&(root.clone() + "/inefficient")))?;
210        let performance = read_from_file(Path::new(&(root.clone() + "/performance")))?;
211        let power = read_from_file(Path::new(&(root.clone() + "/power")))?;
212
213        Ok(PerfState {
214            cost,
215            frequency,
216            inefficient,
217            performance,
218            power,
219        })
220    }
221}
222
223impl PartialEq for PerfState {
224    fn eq(&self, other: &Self) -> bool {
225        self.cost == other.cost
226            && self.frequency == other.frequency
227            && self.performance == other.performance
228            && self.power == other.power
229    }
230}
231
232impl fmt::Display for EnergyModel {
233    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
234        for (_, pd) in self.perf_doms.iter() {
235            writeln!(f, "{pd:#}")?;
236        }
237        for (_, eq_pd) in self.eq_perf_doms.iter() {
238            writeln!(f, "{eq_pd:#}")?;
239        }
240        Ok(())
241    }
242}
243
244impl fmt::Display for PerfDomain {
245    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
246        writeln!(f, "# perf domain: {:#}, cpus: {:#}", self.id, self.span)?;
247        writeln!(f, "cost, frequency, inefficient, performance, power")?;
248        for (_, ps) in self.perf_table.iter() {
249            writeln!(f, "{ps:#}")?;
250        }
251        Ok(())
252    }
253}
254
255impl fmt::Display for EqPerfDomain {
256    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
257        let pd_ids: Vec<String> = self.perf_doms.iter().map(|pd| pd.id.to_string()).collect();
258        writeln!(
259            f,
260            "# eq perf domain: {:#}, cpus: {:#}, perf domains: {}",
261            self.id,
262            self.span,
263            pd_ids.join(",")
264        )?;
265        writeln!(f, "cost, frequency, inefficient, performance, power")?;
266        for (_, ps) in self.perf_table.iter() {
267            writeln!(f, "{ps:#}")?;
268        }
269        Ok(())
270    }
271}
272
273impl fmt::Display for PerfState {
274    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
275        write!(
276            f,
277            "{}, {}, {}, {}, {}",
278            self.cost, self.frequency, self.inefficient, self.performance, self.power
279        )?;
280        Ok(())
281    }
282}
283
284/*********************************************************
285 * Helper structs/functions for creating the EnergyModel *
286 *********************************************************/
287fn get_ps_paths(root: String) -> Result<Vec<String>> {
288    let ps_paths = glob(&(root.clone() + "/ps:[0-9]*"))?;
289    let mut ps_vec = vec![];
290    for ps_path in ps_paths.filter_map(Result::ok) {
291        let ps_str = ps_path.to_string_lossy().into_owned();
292        ps_vec.push(ps_str);
293    }
294
295    Ok(ps_vec)
296}
297
298fn get_pd_paths() -> Result<Vec<(usize, String)>> {
299    let prefix = get_em_root()? + "/cpu";
300    let pd_paths = glob(&(prefix.clone() + "[0-9]*"))?;
301
302    let mut pd_vec = vec![];
303    for pd_path in pd_paths.filter_map(Result::ok) {
304        let pd_str = pd_path.to_string_lossy().into_owned();
305        let pd_id: usize = pd_str[prefix.len()..].parse()?;
306        pd_vec.push((pd_id, pd_str));
307    }
308    if pd_vec.is_empty() {
309        bail!("There is no performance domain.");
310    }
311    pd_vec.sort();
312
313    let mut pd_vec2 = vec![];
314    for (id, (_, pd_str)) in pd_vec.into_iter().enumerate() {
315        pd_vec2.push((id, pd_str));
316    }
317
318    Ok(pd_vec2)
319}
320
321fn get_em_root() -> Result<String> {
322    if ROOT_PREFIX.is_empty() {
323        let root = compat::debugfs_mount()?.join("energy_model");
324        Ok(root.display().to_string())
325    } else {
326        let root = format!("{}/sys/kernel/debug/energy_model", *ROOT_PREFIX);
327        Ok(root)
328    }
329}
330
331/// Tests for grouping performance domains into equivalence performance
332/// domains. The performance domains are built directly instead of read from
333/// debugfs, so the topologies of interest can be exercised on any host.
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    fn perf_table(states: &[(usize, usize)]) -> BTreeMap<usize, Arc<PerfState>> {
339        states
340            .iter()
341            .map(|&(performance, power)| {
342                let ps = PerfState {
343                    cost: performance,
344                    frequency: performance,
345                    inefficient: 0,
346                    performance,
347                    power,
348                };
349                (performance, ps.into())
350            })
351            .collect()
352    }
353
354    /// Build a performance domain covering a single CPU (@id), as on an Intel
355    /// hybrid processor.
356    fn perf_dom(id: usize, perf_table: BTreeMap<usize, Arc<PerfState>>) -> Arc<PerfDomain> {
357        PerfDomain {
358            id,
359            span: Cpumask::from_vec(vec![1u64 << id]),
360            perf_table,
361        }
362        .into()
363    }
364
365    /// A 28-thread hybrid CPU (8 P-cores, 16 E-cores, and 4 LP-E-cores) with
366    /// one performance domain per CPU thread, as reported in
367    /// <https://github.com/sched-ext/scx/issues/3340>. It collapses into three
368    /// equivalence performance domains.
369    #[test]
370    fn test_group_hybrid_perf_doms() {
371        let p_core = perf_table(&[(100, 50)]);
372        let e_core = perf_table(&[(60, 20)]);
373        let lpe_core = perf_table(&[(30, 5)]);
374
375        let mut perf_doms = BTreeMap::new();
376        for id in 0..28 {
377            let table = if id < 8 {
378                p_core.clone()
379            } else if id < 24 {
380                e_core.clone()
381            } else {
382                lpe_core.clone()
383            };
384            perf_doms.insert(id, perf_dom(id, table));
385        }
386
387        let eq_perf_doms = EnergyModel::group_perf_doms(&perf_doms);
388
389        let weights: Vec<usize> = eq_perf_doms.values().map(|e| e.span.weight()).collect();
390        assert_eq!(weights, vec![8, 16, 4]);
391
392        for (&id, eq_pd) in eq_perf_doms.iter() {
393            assert_eq!(eq_pd.id, id);
394            assert_eq!(eq_pd.perf_doms.len(), eq_pd.span.weight());
395
396            let member_ids: Vec<usize> = eq_pd.perf_doms.iter().map(|pd| pd.id).collect();
397            assert!(member_ids.windows(2).all(|ids| ids[0] < ids[1]));
398
399            for pd in eq_pd.perf_doms.iter() {
400                assert_eq!(pd.perf_table, eq_pd.perf_table);
401                assert!(eq_pd.span.test_cpu(pd.id));
402            }
403        }
404    }
405
406    /// Every performance domain has a distinct performance table, as per-core
407    /// binning could produce, so no grouping is possible.
408    #[test]
409    fn test_group_distinct_perf_doms() {
410        let mut perf_doms = BTreeMap::new();
411        for id in 0..24 {
412            perf_doms.insert(id, perf_dom(id, perf_table(&[(id + 1, id + 1)])));
413        }
414
415        let eq_perf_doms = EnergyModel::group_perf_doms(&perf_doms);
416
417        assert_eq!(eq_perf_doms.len(), 24);
418        for eq_pd in eq_perf_doms.values() {
419            assert_eq!(eq_pd.perf_doms.len(), 1);
420            assert_eq!(eq_pd.span.weight(), 1);
421        }
422    }
423
424    /// All the performance domains share one performance table, so they
425    /// collapse into a single equivalence performance domain.
426    #[test]
427    fn test_group_uniform_perf_doms() {
428        let table = perf_table(&[(100, 50)]);
429        let mut perf_doms = BTreeMap::new();
430        for id in 0..8 {
431            perf_doms.insert(id, perf_dom(id, table.clone()));
432        }
433
434        let eq_perf_doms = EnergyModel::group_perf_doms(&perf_doms);
435
436        assert_eq!(eq_perf_doms.len(), 1);
437        let eq_pd = eq_perf_doms.get(&0).unwrap();
438        assert_eq!(eq_pd.perf_doms.len(), 8);
439        assert_eq!(eq_pd.span.weight(), 8);
440    }
441}