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#[derive(Debug)]
47pub struct EnergyModel {
48    /// Performance domains indexed by domain id
49    pub perf_doms: BTreeMap<usize, Arc<PerfDomain>>,
50}
51
52impl EnergyModel {
53    pub fn has_energy_model() -> bool {
54        get_pd_paths().is_ok()
55    }
56
57    /// Build a complete EnergyModel
58    pub fn new() -> Result<EnergyModel> {
59        let mut perf_doms = BTreeMap::new();
60        let pd_paths = match get_pd_paths() {
61            Ok(pd_paths) => pd_paths,
62            Err(_) => {
63                bail!("Fail to locate the energy model directory");
64            }
65        };
66
67        for (pd_id, pd_path) in pd_paths {
68            let pd = PerfDomain::new(pd_id, pd_path).unwrap();
69            perf_doms.insert(pd.id, pd.into());
70        }
71
72        Ok(EnergyModel { perf_doms })
73    }
74
75    pub fn get_pd_by_cpu_id(&self, cpu_id: usize) -> Option<&PerfDomain> {
76        self.perf_doms
77            .values()
78            .find(|&pd| pd.span.test_cpu(cpu_id))
79            .map(|c| c as _)
80    }
81
82    pub fn perf_total(&self) -> usize {
83        let mut total = 0;
84
85        for (_, pd) in self.perf_doms.iter() {
86            total += pd.perf_total();
87        }
88
89        total
90    }
91}
92
93impl PerfDomain {
94    /// Build a PerfDomain
95    pub fn new(id: usize, root: String) -> Result<PerfDomain> {
96        let mut perf_table = BTreeMap::new();
97        let cpulist = std::fs::read_to_string(root.clone() + "/cpus")?;
98        let span = Cpumask::from_cpulist(&cpulist)?;
99
100        for ps_path in get_ps_paths(root).unwrap() {
101            let ps = PerfState::new(ps_path).unwrap();
102            perf_table.insert(ps.performance, ps.into());
103        }
104
105        Ok(PerfDomain {
106            id,
107            span,
108            perf_table,
109        })
110    }
111
112    /// Lookup a performance state by a given CPU utilization.
113    /// @util is in %, ranging [0, 100].
114    pub fn select_perf_state(&self, util: f32) -> Option<&Arc<PerfState>> {
115        let util = clamp(util, 0.0, 100.0);
116        let (perf_max, _) = self.perf_table.last_key_value().unwrap();
117        let perf_max = *perf_max as f32;
118        let req_perf = (perf_max * (util / 100.0)) as usize;
119        for (perf, ps) in self.perf_table.iter() {
120            if *perf >= req_perf {
121                return Some(ps);
122            }
123        }
124        None
125    }
126
127    pub fn perf_total(&self) -> usize {
128        let (_, ps) = self.perf_table.last_key_value().unwrap();
129        ps.performance * self.span.weight()
130    }
131}
132
133impl PartialEq for PerfDomain {
134    fn eq(&self, other: &Self) -> bool {
135        self.id == other.id && self.span == other.span && self.perf_table == other.perf_table
136    }
137}
138
139impl PerfState {
140    /// Build a PerfState
141    pub fn new(root: String) -> Result<PerfState> {
142        let cost = read_from_file(Path::new(&(root.clone() + "/cost")))?;
143        let frequency = read_from_file(Path::new(&(root.clone() + "/frequency")))?;
144        let inefficient = read_from_file(Path::new(&(root.clone() + "/inefficient")))?;
145        let performance = read_from_file(Path::new(&(root.clone() + "/performance")))?;
146        let power = read_from_file(Path::new(&(root.clone() + "/power")))?;
147
148        Ok(PerfState {
149            cost,
150            frequency,
151            inefficient,
152            performance,
153            power,
154        })
155    }
156}
157
158impl PartialEq for PerfState {
159    fn eq(&self, other: &Self) -> bool {
160        self.cost == other.cost
161            && self.frequency == other.frequency
162            && self.performance == other.performance
163            && self.power == other.power
164    }
165}
166
167impl fmt::Display for EnergyModel {
168    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
169        for (_, pd) in self.perf_doms.iter() {
170            writeln!(f, "{pd:#}")?;
171        }
172        Ok(())
173    }
174}
175
176impl fmt::Display for PerfDomain {
177    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
178        writeln!(f, "# perf domain: {:#}, cpus: {:#}", self.id, self.span)?;
179        writeln!(f, "cost, frequency, inefficient, performance, power")?;
180        for (_, ps) in self.perf_table.iter() {
181            writeln!(f, "{ps:#}")?;
182        }
183        Ok(())
184    }
185}
186
187impl fmt::Display for PerfState {
188    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
189        write!(
190            f,
191            "{}, {}, {}, {}, {}",
192            self.cost, self.frequency, self.inefficient, self.performance, self.power
193        )?;
194        Ok(())
195    }
196}
197
198/*********************************************************
199 * Helper structs/functions for creating the EnergyModel *
200 *********************************************************/
201fn get_ps_paths(root: String) -> Result<Vec<String>> {
202    let ps_paths = glob(&(root.clone() + "/ps:[0-9]*"))?;
203    let mut ps_vec = vec![];
204    for ps_path in ps_paths.filter_map(Result::ok) {
205        let ps_str = ps_path.into_os_string().into_string().unwrap();
206        ps_vec.push(ps_str);
207    }
208
209    Ok(ps_vec)
210}
211
212fn get_pd_paths() -> Result<Vec<(usize, String)>> {
213    let prefix = get_em_root().unwrap() + "/cpu";
214    let pd_paths = glob(&(prefix.clone() + "[0-9]*"))?;
215
216    let mut pd_vec = vec![];
217    for pd_path in pd_paths.filter_map(Result::ok) {
218        let pd_str = pd_path.into_os_string().into_string().unwrap();
219        let pd_id: usize = pd_str[prefix.len()..].parse().unwrap();
220        pd_vec.push((pd_id, pd_str));
221    }
222    if pd_vec.is_empty() {
223        bail!("There is no performance domain.");
224    }
225    pd_vec.sort();
226
227    let mut pd_vec2 = vec![];
228    for (id, (_, pd_str)) in pd_vec.into_iter().enumerate() {
229        pd_vec2.push((id, pd_str));
230    }
231
232    Ok(pd_vec2)
233}
234
235fn get_em_root() -> Result<String> {
236    if ROOT_PREFIX.is_empty() {
237        let root = compat::debugfs_mount().unwrap().join("energy_model");
238        Ok(root.display().to_string())
239    } else {
240        let root = format!("{}/sys/kernel/debug/energy_model", *ROOT_PREFIX);
241        Ok(root)
242    }
243}