1use 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 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 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 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}