Skip to main content

scx_cake/
core_performance.rs

1// SPDX-License-Identifier: GPL-2.0
2//! Startup-only platform preference discovery. These are hints, not speed ratios.
3
4use std::cmp::Reverse;
5use std::path::Path;
6
7use scx_utils::Topology;
8
9const PREFERENCE_SOURCES: [&str; 2] = [
10    "cpufreq/amd_pstate_prefcore_ranking",
11    "acpi_cppc/highest_perf",
12];
13
14#[derive(Debug)]
15pub struct CoreIdentity {
16    pub id: usize,
17    pub package: usize,
18    pub llc: usize,
19    pub cpus: Vec<usize>,
20}
21
22#[derive(Debug)]
23pub struct CorePerformance {
24    pub identity: CoreIdentity,
25    pub capacity: Option<u32>,
26    pub preference: Option<u32>,
27    pub max_frequency_khz: Option<u32>,
28}
29
30#[derive(Debug)]
31pub struct CorePerformanceLayout {
32    /// Ordered by complete capacity data, then complete preferred-core data.
33    /// IDs only stabilize ties; they never imply different performance.
34    pub cores: Vec<CorePerformance>,
35    pub preference_source: Option<&'static str>,
36    pub capacity_complete: bool,
37}
38
39impl CorePerformanceLayout {
40    pub fn discover(topo: &Topology) -> Self {
41        let identities = topo
42            .all_cores
43            .values()
44            .filter_map(|core| {
45                let first = core.cpus.values().next()?;
46                Some(CoreIdentity {
47                    id: core.id,
48                    package: first.package_id,
49                    llc: core.llc_id,
50                    cpus: core.cpus.keys().copied().collect(),
51                })
52            })
53            .collect();
54        Self::read(identities, |cpu, suffix| {
55            std::fs::read_to_string(
56                Path::new("/sys/devices/system/cpu")
57                    .join(format!("cpu{cpu}"))
58                    .join(suffix),
59            )
60            .ok()?
61            .trim()
62            .parse::<u32>()
63            .ok()
64        })
65    }
66
67    fn read(
68        identities: Vec<CoreIdentity>,
69        mut read: impl FnMut(usize, &str) -> Option<u32>,
70    ) -> Self {
71        // Use one source across all cores. Partial or conflicting sibling
72        // readings must not silently rank unknown CPUs below known ones.
73        let mut read_core = |core: &CoreIdentity, suffix: &str| {
74            let mut values = core.cpus.iter().map(|cpu| read(*cpu, suffix));
75            let first = values.next().flatten().filter(|v| *v > 0)?;
76            values.all(|v| v == Some(first)).then_some(first)
77        };
78        let mut preference_source = None;
79        let mut preferences = vec![None; identities.len()];
80        for source in PREFERENCE_SOURCES {
81            let values: Vec<_> = identities.iter().map(|c| read_core(c, source)).collect();
82            if !values.is_empty() && values.iter().all(Option::is_some) {
83                preference_source = Some(source);
84                preferences = values;
85                break;
86            }
87        }
88        let mut cores: Vec<_> = identities
89            .into_iter()
90            .zip(preferences)
91            .map(|(identity, preference)| CorePerformance {
92                capacity: read_core(&identity, "cpu_capacity"),
93                max_frequency_khz: read_core(&identity, "cpufreq/cpuinfo_max_freq"),
94                identity,
95                preference,
96            })
97            .collect();
98        let capacity_complete = !cores.is_empty() && cores.iter().all(|c| c.capacity.is_some());
99        cores.sort_by_key(|c| {
100            (
101                Reverse(if capacity_complete { c.capacity } else { None }),
102                Reverse(c.preference),
103                c.identity.id,
104            )
105        });
106        Self {
107            cores,
108            preference_source,
109            capacity_complete,
110        }
111    }
112
113    /// Complete narrow-host tiers. Wide hosts retain the kernel picker.
114    pub fn rank_words(&self) -> Vec<u64> {
115        if self
116            .cores
117            .iter()
118            .any(|c| c.identity.cpus.iter().any(|cpu| *cpu >= 64))
119        {
120            return Vec::new();
121        }
122        let mut tiers: Vec<u64> = Vec::new();
123        let mut previous = None;
124        for c in &self.cores {
125            let key = (
126                if self.capacity_complete {
127                    c.capacity
128                } else {
129                    None
130                },
131                c.preference,
132            );
133            if previous != Some(key) {
134                tiers.push(0);
135                previous = Some(key);
136            }
137            if let Some(word) = tiers.last_mut() {
138                for cpu in &c.identity.cpus {
139                    *word |= 1u64 << cpu;
140                }
141            }
142        }
143        tiers
144    }
145
146    pub fn summary(&self) -> String {
147        let mut order = String::new();
148        let mut previous = None;
149        for c in &self.cores {
150            let key = (
151                if self.capacity_complete {
152                    c.capacity
153                } else {
154                    None
155                },
156                c.preference,
157            );
158            if let Some(prev) = previous {
159                order.push_str(if prev == key { " = " } else { " > " });
160            }
161            order.push_str(&format!("{:?}", c.identity.cpus));
162            previous = Some(key);
163        }
164        format!(
165            "platform core order {order}; capacity {}, preference {}; advisory startup snapshot",
166            if self.capacity_complete {
167                "available"
168            } else {
169                "unknown/partial"
170            },
171            self.preference_source.unwrap_or("unavailable (equal)")
172        )
173    }
174
175    pub fn details(&self) -> Vec<String> {
176        let value = |v: Option<u32>| v.map_or_else(|| "unknown".into(), |v| v.to_string());
177        self.cores.iter().map(|c| format!(
178            "core {} package {} LLC {} CPUs {:?}: capacity {}, preference {}, advertised max {} kHz",
179            c.identity.id, c.identity.package, c.identity.llc, c.identity.cpus,
180            value(c.capacity), value(c.preference), value(c.max_frequency_khz)
181        )).collect()
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    fn cores() -> Vec<CoreIdentity> {
190        vec![
191            CoreIdentity {
192                id: 4,
193                package: 0,
194                llc: 0,
195                cpus: vec![2, 70],
196            },
197            CoreIdentity {
198                id: 9,
199                package: 1,
200                llc: 1,
201                cpus: vec![5, 130],
202            },
203        ]
204    }
205
206    #[test]
207    fn sparse_smt_groups_follow_preference_not_cpu_id() {
208        let layout = CorePerformanceLayout::read(cores(), |cpu, source| match source {
209            "cpufreq/amd_pstate_prefcore_ranking" => {
210                Some(if cpu == 5 || cpu == 130 { 196 } else { 176 })
211            }
212            "cpu_capacity" => Some(1024),
213            _ => None,
214        });
215        assert_eq!(layout.cores[0].identity.cpus, [5, 130]);
216        assert!(layout.summary().contains("[5, 130] > [2, 70]"));
217    }
218
219    #[test]
220    fn missing_information_and_equal_ranks_do_not_invent_a_slow_core() {
221        for value in [None, Some(0), Some(100)] {
222            let layout = CorePerformanceLayout::read(cores(), |_, _| value);
223            assert!(layout.summary().contains("[2, 70] = [5, 130]"));
224        }
225    }
226
227    #[test]
228    fn incomplete_source_falls_back_as_a_whole() {
229        let layout = CorePerformanceLayout::read(cores(), |cpu, source| match source {
230            "cpufreq/amd_pstate_prefcore_ranking" if cpu == 2 || cpu == 70 => Some(196),
231            "acpi_cppc/highest_perf" => Some(100),
232            _ => None,
233        });
234        assert_eq!(layout.preference_source, Some("acpi_cppc/highest_perf"));
235        assert!(layout.summary().contains("[2, 70] = [5, 130]"));
236    }
237
238    #[test]
239    fn conflicting_siblings_are_unknown_not_two_different_cores() {
240        let layout = CorePerformanceLayout::read(cores(), |cpu, source| {
241            (source == "cpufreq/amd_pstate_prefcore_ranking").then_some(cpu as u32)
242        });
243        assert_eq!(layout.preference_source, None);
244        assert!(layout.summary().contains(" = "));
245    }
246
247    #[test]
248    fn capacity_is_distinct_from_preference_and_clock() {
249        let layout = CorePerformanceLayout::read(cores(), |cpu, source| {
250            let big = cpu == 2 || cpu == 70;
251            match source {
252                "cpu_capacity" => Some(if big { 1024 } else { 512 }),
253                "cpufreq/amd_pstate_prefcore_ranking" => Some(if big { 100 } else { 200 }),
254                "cpufreq/cpuinfo_max_freq" => Some(if big { 3000000 } else { 4000000 }),
255                _ => None,
256            }
257        });
258        assert_eq!(layout.cores[0].identity.id, 4);
259        assert!(layout.capacity_complete);
260    }
261
262    #[test]
263    fn partial_capacity_does_not_demote_unknown_core() {
264        let layout = CorePerformanceLayout::read(cores(), |cpu, source| {
265            (source == "cpu_capacity" && cpu == 2).then_some(1024)
266        });
267        assert!(!layout.capacity_complete);
268        assert!(layout.summary().contains(" = "));
269    }
270
271    #[test]
272    fn cold_rank_words_preserve_smt_ties_and_capacity_order() {
273        let ids = vec![
274            CoreIdentity {
275                id: 0,
276                package: 0,
277                llc: 0,
278                cpus: vec![0, 4],
279            },
280            CoreIdentity {
281                id: 1,
282                package: 0,
283                llc: 0,
284                cpus: vec![1, 5],
285            },
286            CoreIdentity {
287                id: 2,
288                package: 0,
289                llc: 1,
290                cpus: vec![2, 6],
291            },
292        ];
293        let layout = CorePerformanceLayout::read(ids, |cpu, source| match source {
294            "cpu_capacity" => Some(if cpu % 4 == 2 { 512 } else { 1024 }),
295            "cpufreq/amd_pstate_prefcore_ranking" => Some(if cpu % 4 == 2 { 200 } else { 100 }),
296            _ => None,
297        });
298        assert_eq!(layout.rank_words(), vec![0x33, 0x44]);
299    }
300
301    #[test]
302    fn rank_words_handle_cpu63_and_unknown_equal_hardware() {
303        let ids = vec![CoreIdentity {
304            id: 0,
305            package: 0,
306            llc: 0,
307            cpus: vec![3, 63],
308        }];
309        let layout = CorePerformanceLayout::read(ids, |_, _| None);
310        assert_eq!(layout.rank_words(), vec![(1u64 << 63) | 8]);
311    }
312
313    #[test]
314    fn wide_and_empty_layouts_keep_kernel_selection() {
315        assert!(
316            CorePerformanceLayout::read(cores(), |_, _| Some(100))
317                .rank_words()
318                .is_empty()
319        );
320        assert!(
321            CorePerformanceLayout::read(vec![], |_, _| None)
322                .rank_words()
323                .is_empty()
324        );
325    }
326}