1use std::mem::size_of;
32use std::path::Path;
33
34use anyhow::{Context, Result};
35use libbpf_rs::MapCore;
36use libbpf_rs::MapFlags;
37use log::{info, warn};
38use scx_utils::get_primary_cpus;
39use scx_utils::Powermode;
40use scx_utils::Topology;
41
42use crate::bpf_intf::mlfq_bitmap;
43use crate::bpf_intf::mlfq_consts_MLFQ_BITMAP_WORDS;
44use crate::bpf_intf::mlfq_consts_MLFQ_MAX_CPUS;
45use crate::bpf_intf::mlfq_consts_MLFQ_MAX_LLCS;
46use crate::bpf_intf::mlfq_consts_MLFQ_MAX_LLC_CPUS;
47use crate::bpf_intf::mlfq_llc_cpu_list;
48
49const MAX_CPUS: usize = mlfq_consts_MLFQ_MAX_CPUS as usize;
51
52const MAX_LLCS: usize = mlfq_consts_MLFQ_MAX_LLCS as usize;
54
55const MAX_LLC_CPUS: usize = mlfq_consts_MLFQ_MAX_LLC_CPUS as usize;
58
59pub fn smt_enabled() -> Option<bool> {
65 let active = std::fs::read_to_string("/sys/devices/system/cpu/smt/active").ok()?;
66 active.trim().parse::<u8>().ok().map(|v| v == 1)
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct CapacityPlan {
73 pub primary_all: bool,
76 pub primary_cpus: Vec<u32>,
79}
80
81pub fn plan_primary_mask(primary_cpus: &[u32], nr_online: usize) -> CapacityPlan {
87 if primary_cpus.is_empty() || primary_cpus.len() >= nr_online {
88 CapacityPlan {
89 primary_all: true,
90 primary_cpus: Vec::new(),
91 }
92 } else {
93 let mut cpus = primary_cpus.to_vec();
94 cpus.sort_unstable();
95 cpus.dedup();
96 CapacityPlan {
97 primary_all: false,
98 primary_cpus: cpus,
99 }
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct LlcPlan {
107 pub nr_llcs: u32,
109 pub has_primary: [u8; MAX_LLCS],
111 pub llc_cpus: Vec<Vec<u32>>,
113 pub cpu_llc: [u32; MAX_CPUS],
117}
118
119pub fn plan_llcs(cpu_to_llc: &[(u32, u32)], primary_cpus: &[u32], max_llcs: usize) -> LlcPlan {
128 let mut plan = LlcPlan {
129 nr_llcs: 0,
130 has_primary: [0; MAX_LLCS],
131 llc_cpus: Vec::new(),
132 cpu_llc: [mlfq_consts_MLFQ_MAX_LLCS; MAX_CPUS],
133 };
134
135 if cpu_to_llc.is_empty() {
136 return plan;
137 }
138
139 let max_llc = cpu_to_llc.iter().map(|&(_, llc)| llc).max().unwrap();
140 let nr = match max_llc.checked_add(1) {
141 Some(v) => v,
142 None => {
143 warn!("LLC id u32::MAX wraps domain count, disabling LLC-aware placement");
144 return plan;
145 }
146 };
147 if nr as usize > max_llcs {
148 warn!(
149 "{} LLC domains exceed the supported maximum ({}), disabling LLC-aware placement",
150 nr, max_llcs
151 );
152 return plan;
153 }
154
155 plan.nr_llcs = nr;
156 plan.llc_cpus = vec![Vec::new(); nr as usize];
157 for &(cpu, llc) in cpu_to_llc {
158 if cpu as usize >= MAX_CPUS {
159 continue;
160 }
161 plan.cpu_llc[cpu as usize] = llc;
162 plan.llc_cpus[llc as usize].push(cpu);
163 }
164 for (llc, cpus) in plan.llc_cpus.iter().enumerate() {
165 if cpus.iter().any(|cpu| primary_cpus.contains(cpu)) {
166 plan.has_primary[llc] = 1;
167 }
168 }
169
170 plan
171}
172
173#[derive(Debug, Clone, PartialEq, Eq)]
176pub struct SiblingPlan {
177 pub smt_on: bool,
179 pub cpu_sibling: [u32; MAX_CPUS],
182 pub cpu_core: [u32; MAX_CPUS],
186}
187
188pub fn plan_sibling_table(cpu_to_core: &[(u32, u32)]) -> SiblingPlan {
198 let mut plan = SiblingPlan {
199 smt_on: false,
200 cpu_sibling: core::array::from_fn(|i| i as u32),
201 cpu_core: [mlfq_consts_MLFQ_MAX_CPUS; MAX_CPUS],
202 };
203 let mut cores: std::collections::BTreeMap<u32, Vec<u32>> = std::collections::BTreeMap::new();
204
205 for &(cpu, core) in cpu_to_core {
206 if cpu as usize >= MAX_CPUS {
207 continue;
208 }
209 cores.entry(core).or_default().push(cpu);
210 plan.cpu_core[cpu as usize] = core;
211 }
212
213 for cpus in cores.values_mut() {
214 cpus.sort_unstable();
215 cpus.dedup();
216 if cpus.len() < 2 {
217 continue;
218 }
219 plan.smt_on = true;
220 for &cpu in cpus.iter() {
221 let sib = cpus.iter().copied().find(|&c| c != cpu).unwrap_or(cpu);
224 plan.cpu_sibling[cpu as usize] = sib;
225 }
226 }
227
228 plan
229}
230
231fn parse_cache_size(size: &str) -> Option<u64> {
235 let s = size.trim();
236 let (num, mult) = if let Some(v) = s.strip_suffix('K') {
237 (v, 1024u64)
238 } else if let Some(v) = s.strip_suffix('M') {
239 (v, 1024u64 * 1024)
240 } else if let Some(v) = s.strip_suffix('G') {
241 (v, 1024u64 * 1024 * 1024)
242 } else {
243 (s, 1u64)
244 };
245 num.trim().parse::<u64>().ok().map(|n| n * mult)
246}
247
248fn llc_size_bytes(cache_path: &Path) -> Option<u64> {
259 let cpu_path = cache_path.parent()?;
260 let llc_id = std::fs::read_to_string(cpu_path.join("topology/llc_id"))
261 .ok()
262 .and_then(|s| s.trim().parse::<u64>().ok());
263
264 let mut best: Option<(u64, u64)> = None; for entry in std::fs::read_dir(cache_path).ok()?.flatten() {
266 let name = entry.file_name();
267 let Some(name) = name.to_str() else { continue };
268 if !name.starts_with("index") {
269 continue;
270 }
271 let dir = entry.path();
272 let Some(level) = std::fs::read_to_string(dir.join("level"))
273 .ok()
274 .and_then(|s| s.trim().parse::<u64>().ok())
275 else {
276 continue;
277 };
278 let id = std::fs::read_to_string(dir.join("id"))
279 .ok()
280 .and_then(|s| s.trim().parse::<u64>().ok());
281 if let Some(llc_id) = llc_id {
282 if id.is_some() && id != Some(llc_id) {
283 continue;
284 }
285 }
286 let Some(size) = std::fs::read_to_string(dir.join("size"))
287 .ok()
288 .and_then(|s| parse_cache_size(&s))
289 else {
290 continue;
291 };
292 if best.is_none_or(|(best_level, _)| level > best_level) {
293 best = Some((level, size));
294 }
295 }
296 best.map(|(_, size)| size)
297}
298
299pub fn pick_largest_llc(sizes: &[u64], nr_llcs: u32) -> Option<u32> {
308 if (nr_llcs as usize) < 2 {
309 return None;
310 }
311 let nr = nr_llcs as usize;
312 let max = sizes[..nr.min(sizes.len())].iter().copied().max()?;
313 let mut argmax = None;
314 let mut tied = false;
315
316 for (i, &s) in sizes[..nr.min(sizes.len())].iter().enumerate() {
317 if s == max {
318 if argmax.is_some() {
319 tied = true;
320 } else {
321 argmax = Some(i as u32);
322 }
323 }
324 }
325
326 if tied {
327 return None;
328 }
329 argmax
330}
331
332#[derive(Debug, Clone, PartialEq, Eq)]
334pub struct TopologyPlan {
335 pub capacity: CapacityPlan,
336 pub llcs: LlcPlan,
337}
338
339pub fn init_topology(skel: &mut crate::bpf_skel::OpenBpfSkel<'_>) -> Result<TopologyPlan> {
344 let topo = match Topology::new() {
345 Ok(topo) => topo,
346 Err(e) => {
347 warn!("CPU topology discovery failed, using uniform-capacity placement: {e}");
348 return Ok(TopologyPlan {
349 capacity: CapacityPlan {
350 primary_all: true,
351 primary_cpus: Vec::new(),
352 },
353 llcs: LlcPlan {
354 nr_llcs: 0,
355 has_primary: [0; MAX_LLCS],
356 llc_cpus: Vec::new(),
357 cpu_llc: [mlfq_consts_MLFQ_MAX_LLCS; MAX_CPUS],
358 },
359 });
360 }
361 };
362
363 let nr_online = topo.all_cpus.len();
364 let primaries: Vec<u32> = match get_primary_cpus(Powermode::Performance) {
365 Ok(cpus) => cpus.into_iter().map(|cpu| cpu as u32).collect(),
366 Err(e) => {
367 warn!("primary CPU discovery failed, using uniform-capacity placement: {e}");
368 Vec::new()
369 }
370 };
371
372 let capacity = plan_primary_mask(&primaries, nr_online);
373
374 let cpu_to_llc: Vec<(u32, u32)> = topo
375 .all_cpus
376 .iter()
377 .map(|(id, cpu)| (*id as u32, cpu.llc_id as u32))
378 .collect();
379 let llcs = plan_llcs(&cpu_to_llc, &capacity.primary_cpus, MAX_LLCS);
380 let mut llcs = llcs;
381 if capacity.primary_all {
382 llcs.has_primary.fill(1);
384 }
385
386 let sibling = plan_sibling_table(
388 &topo
389 .all_cpus
390 .iter()
391 .map(|(id, cpu)| (*id as u32, cpu.core_id as u32))
392 .collect::<Vec<_>>(),
393 );
394
395 let mut llc_sizes = vec![0u64; llcs.nr_llcs as usize];
400 for (llc, size) in llc_sizes.iter_mut().enumerate() {
401 if let Some(&cpu) = llcs.llc_cpus[llc].first() {
402 let cache_path = Path::new("/sys/devices/system/cpu").join(format!("cpu{cpu}/cache"));
403 *size = llc_size_bytes(&cache_path).unwrap_or(0);
404 }
405 }
406 let largest = pick_largest_llc(&llc_sizes, llcs.nr_llcs).unwrap_or(mlfq_consts_MLFQ_MAX_LLCS);
407
408 let rodata = skel
409 .maps
410 .rodata_data
411 .as_mut()
412 .context("rodata missing, the BPF object has no .rodata section")?;
413 rodata.mlfq_primary_all = capacity.primary_all;
414 rodata.mlfq_nr_llcs = llcs.nr_llcs;
415 rodata.mlfq_llc_has_primary = llcs.has_primary;
416 rodata.mlfq_cpu_llc = llcs.cpu_llc;
417 rodata.mlfq_smt_on = sibling.smt_on;
418 rodata.mlfq_cpu_sibling = sibling.cpu_sibling;
419 rodata.mlfq_cpu_core = sibling.cpu_core;
420 rodata.mlfq_llc_largest = largest;
421
422 if capacity.primary_all {
423 info!(
424 "Topology: {} online CPUs, uniform capacity, all treated as primary",
425 nr_online
426 );
427 } else {
428 info!(
429 "Topology: {} online CPUs, {} primary (big) cores",
430 nr_online,
431 capacity.primary_cpus.len()
432 );
433 }
434 if llcs.nr_llcs > 0 {
435 info!("Topology: {} LLC cache domains", llcs.nr_llcs);
436 }
437 if sibling.smt_on {
438 info!("Topology: SMT siblings detected, sibling preference enabled");
439 }
440 if largest < llcs.nr_llcs {
441 info!("Topology: LLC {largest} has the strictly-largest cache, Q1 bias enabled");
442 }
443
444 Ok(TopologyPlan { capacity, llcs })
445}
446
447pub fn web_cpu_static() -> Vec<crate::stats::PerCpuMetrics> {
462 let topo = match Topology::new() {
463 Ok(topo) => topo,
464 Err(e) => {
465 warn!("CPU topology discovery failed, the web UI reports no per-CPU data: {e}");
466 return Vec::new();
467 }
468 };
469
470 let primaries: Vec<u32> = match get_primary_cpus(Powermode::Performance) {
471 Ok(cpus) => cpus.into_iter().map(|cpu| cpu as u32).collect(),
472 Err(e) => {
473 warn!("primary CPU discovery failed, the web UI reports no per-CPU data: {e}");
474 Vec::new()
475 }
476 };
477
478 let cpu_to_llc: Vec<(u32, u32)> = topo
479 .all_cpus
480 .iter()
481 .map(|(id, cpu)| (*id as u32, cpu.llc_id as u32))
482 .collect();
483 let llcs = plan_llcs(&cpu_to_llc, &primaries, MAX_LLCS);
484
485 let cpu_to_core: Vec<(u32, u32)> = topo
492 .all_cpus
493 .iter()
494 .map(|(id, cpu)| (*id as u32, cpu.core_id as u32))
495 .collect();
496 let mut core_min: std::collections::BTreeMap<u32, u32> = std::collections::BTreeMap::new();
497 for &(cpu, core) in &cpu_to_core {
498 core_min
499 .entry(core)
500 .and_modify(|m| *m = (*m).min(cpu))
501 .or_insert(cpu);
502 }
503
504 topo.all_cpus
505 .iter()
506 .map(|(id, cpu)| {
507 let llc_id = llcs.cpu_llc.get(*id).copied().unwrap_or(0);
512 let llc_id = if llc_id == mlfq_consts_MLFQ_MAX_LLCS {
513 0
514 } else {
515 llc_id
516 };
517 crate::stats::PerCpuMetrics {
518 id: *id as u32,
519 freq_khz: cpu.max_freq as u64,
520 cur_freq_khz: 0,
521 llc_id,
522 smt: core_min
523 .get(&(cpu.core_id as u32))
524 .copied()
525 .unwrap_or(*id as u32)
526 != *id as u32,
527 running_queue: 0,
528 running_pid: 0,
529 rt_occupied: false,
530 running_gpu_submit: 0,
531 }
532 })
533 .collect()
534}
535
536pub fn current_freq_khz(cpu: u32) -> u64 {
541 std::fs::read_to_string(format!(
542 "/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_cur_freq"
543 ))
544 .ok()
545 .and_then(|s| s.trim().parse().ok())
546 .unwrap_or(0)
547}
548
549pub fn write_primary_bitmap(
556 skel: &mut crate::bpf_skel::BpfSkel<'_>,
557 plan: &CapacityPlan,
558) -> Result<()> {
559 if plan.primary_all {
560 return Ok(());
561 }
562
563 let bm = build_bitmap(&plan.primary_cpus);
564 write_bitmap_value(&skel.maps.mlfq_primary_bitmap, &bm, 0)
565 .context("failed to write the primary bitmap")
566}
567
568pub fn write_llc_bitmaps(skel: &mut crate::bpf_skel::BpfSkel<'_>, plan: &LlcPlan) -> Result<()> {
574 if plan.nr_llcs == 0 {
575 return Ok(());
576 }
577
578 for llc in 0..plan.nr_llcs as usize {
579 let bm = build_bitmap(&plan.llc_cpus[llc]);
580 write_bitmap_value(&skel.maps.mlfq_llc_bitmaps, &bm, llc as u32)
581 .with_context(|| format!("failed to write the LLC {llc} bitmap"))?;
582 }
583 Ok(())
584}
585
586pub fn write_llc_cpu_lists(skel: &mut crate::bpf_skel::BpfSkel<'_>, plan: &LlcPlan) -> Result<()> {
597 if plan.nr_llcs == 0 {
598 return Ok(());
599 }
600
601 for llc in 0..plan.nr_llcs as usize {
602 if plan.llc_cpus[llc].len() > MAX_LLC_CPUS {
603 warn!(
604 "LLC {llc} has {} CPUs, exceeding the supported maximum \
605({MAX_LLC_CPUS}); its Tier-A window is skipped and the full rotating window covers the domain",
606 plan.llc_cpus[llc].len()
607 );
608 }
609 let list = llc_cpu_list_for(&plan.llc_cpus[llc]);
610 write_llc_cpu_list_value(&skel.maps.mlfq_llc_cpus, &list, llc as u32)
611 .with_context(|| format!("failed to write the LLC {llc} CPU list"))?;
612 }
613 Ok(())
614}
615
616fn bitmap_word(cpu: usize) -> usize {
619 cpu >> 6
620}
621
622fn bitmap_mask(cpu: usize) -> u64 {
624 1u64 << (cpu & 63)
625}
626
627fn build_bitmap(cpus: &[u32]) -> mlfq_bitmap {
631 let mut bm = mlfq_bitmap {
632 words: [0; mlfq_consts_MLFQ_BITMAP_WORDS as usize],
633 };
634 for &cpu in cpus {
635 let cpu = cpu as usize;
636 if cpu >= MAX_CPUS {
637 continue;
638 }
639 bm.words[bitmap_word(cpu)] |= bitmap_mask(cpu);
640 }
641 bm
642}
643
644fn write_bitmap_value(map: &libbpf_rs::Map, value: &mlfq_bitmap, key: u32) -> Result<()> {
646 let key_bytes = key.to_ne_bytes();
647 let value_bytes = unsafe {
648 std::slice::from_raw_parts(value as *const _ as *const u8, size_of::<mlfq_bitmap>())
649 };
650 map.update(&key_bytes, value_bytes, MapFlags::ANY)?;
651 Ok(())
652}
653
654fn llc_cpu_list_for(cpus: &[u32]) -> mlfq_llc_cpu_list {
659 build_llc_cpu_list(cpus)
660}
661
662fn build_llc_cpu_list(cpus: &[u32]) -> mlfq_llc_cpu_list {
671 if cpus.len() > MAX_LLC_CPUS {
672 return mlfq_llc_cpu_list {
673 nr: 0,
674 cpus: [0; mlfq_consts_MLFQ_MAX_LLC_CPUS as usize],
675 };
676 }
677 let mut list = mlfq_llc_cpu_list {
678 nr: 0,
679 cpus: [0; mlfq_consts_MLFQ_MAX_LLC_CPUS as usize],
680 };
681 for &cpu in cpus {
682 if cpu as usize >= MAX_CPUS {
683 continue;
684 }
685 list.cpus[list.nr as usize] = cpu;
686 list.nr += 1;
687 }
688 list
689}
690
691fn write_llc_cpu_list_value(
693 map: &libbpf_rs::Map,
694 value: &mlfq_llc_cpu_list,
695 key: u32,
696) -> Result<()> {
697 let key_bytes = key.to_ne_bytes();
698 let value_bytes = unsafe {
699 std::slice::from_raw_parts(
700 value as *const _ as *const u8,
701 size_of::<mlfq_llc_cpu_list>(),
702 )
703 };
704 map.update(&key_bytes, value_bytes, MapFlags::ANY)?;
705 Ok(())
706}
707
708#[cfg(test)]
709mod tests {
710 use super::*;
711 use crate::bpf_intf::mlfq_consts_MLFQ_LLC_SCAN_MAX;
712
713 #[test]
714 fn empty_primary_list_falls_back_to_uniform() {
715 let plan = plan_primary_mask(&[], 8);
716 assert!(plan.primary_all);
717 assert!(plan.primary_cpus.is_empty());
718 }
719
720 #[test]
721 fn full_primary_list_is_uniform() {
722 let plan = plan_primary_mask(&[0, 1, 2, 3, 4, 5, 6, 7], 8);
724 assert!(plan.primary_all);
725 }
726
727 #[test]
728 fn subset_of_online_cpus_is_hybrid() {
729 let plan = plan_primary_mask(&[0, 1, 4, 5], 8);
730 assert!(!plan.primary_all);
731 assert_eq!(plan.primary_cpus, vec![0, 1, 4, 5]);
732 }
733
734 #[test]
735 fn hybrid_list_is_sorted_and_deduplicated() {
736 let plan = plan_primary_mask(&[5, 1, 4, 1, 0], 8);
737 assert!(!plan.primary_all);
738 assert_eq!(plan.primary_cpus, vec![0, 1, 4, 5]);
739 }
740
741 #[test]
742 fn max_cpus_matches_bpf_constant() {
743 assert_eq!(
745 MAX_CPUS,
746 crate::bpf_intf::mlfq_consts_MLFQ_MAX_CPUS as usize
747 );
748 assert_eq!(
749 MAX_LLCS,
750 crate::bpf_intf::mlfq_consts_MLFQ_MAX_LLCS as usize
751 );
752 }
753
754 #[test]
755 fn llc_grouping_from_cpu_map() {
756 let plan = plan_llcs(&[(0, 0), (1, 0), (2, 1), (3, 1)], &[], MAX_LLCS);
757 assert_eq!(plan.nr_llcs, 2);
758 assert_eq!(plan.llc_cpus, vec![vec![0, 1], vec![2, 3]]);
759 assert_eq!(plan.cpu_llc[0], 0);
760 assert_eq!(plan.cpu_llc[3], 1);
761 assert_eq!(plan.cpu_llc[4], mlfq_consts_MLFQ_MAX_LLCS);
763 }
764
765 #[test]
766 fn llc_has_primary_flags_domains_with_big_cores() {
767 let plan = plan_llcs(&[(0, 0), (1, 0), (2, 1), (3, 1)], &[1], MAX_LLCS);
768 assert_eq!(plan.has_primary[0], 1); assert_eq!(plan.has_primary[1], 0); }
771
772 #[test]
773 fn llc_map_without_primaries_has_no_flags() {
774 let plan = plan_llcs(&[(0, 0), (1, 1)], &[], MAX_LLCS);
775 assert_eq!(plan.has_primary, [0; MAX_LLCS]);
776 }
777
778 #[test]
779 fn llc_plan_disables_when_exceeding_cap() {
780 let map: Vec<(u32, u32)> = (0..33).map(|i| (i, i)).collect();
782 let plan = plan_llcs(&map, &[], MAX_LLCS);
783 assert_eq!(plan.nr_llcs, 0);
784 assert!(plan.llc_cpus.is_empty());
785 }
786
787 #[test]
788 fn llc_plan_saturates_on_u32_max_llc_id() {
789 let plan = plan_llcs(&[(0, 0), (1, u32::MAX)], &[], MAX_LLCS);
792 assert_eq!(plan.nr_llcs, 0);
793 assert!(plan.llc_cpus.is_empty());
794 }
795
796 #[test]
797 fn llc_plan_disables_on_empty_map() {
798 let plan = plan_llcs(&[], &[], MAX_LLCS);
799 assert_eq!(plan.nr_llcs, 0);
800 }
801
802 #[test]
803 fn llc_plan_skips_out_of_range_cpus() {
804 let plan = plan_llcs(&[(0, 0), (5000, 1)], &[], MAX_LLCS);
805 assert_eq!(plan.nr_llcs, 2);
807 assert_eq!(plan.cpu_llc[0], 0);
808 assert_eq!(plan.cpu_llc[1], mlfq_consts_MLFQ_MAX_LLCS);
810 assert!(plan.llc_cpus[1].is_empty());
811 }
812
813 #[test]
814 fn bitmap_word_index_math() {
815 assert_eq!(bitmap_word(0), 0);
816 assert_eq!(bitmap_word(63), 0);
817 assert_eq!(bitmap_word(64), 1);
818 assert_eq!(bitmap_word(127), 1);
819 assert_eq!(bitmap_word(1023), 15);
820 }
821
822 #[test]
823 fn bitmap_mask_math() {
824 assert_eq!(bitmap_mask(0), 1);
825 assert_eq!(bitmap_mask(5), 1 << 5);
826 assert_eq!(bitmap_mask(63), 1u64 << 63);
827 }
828
829 #[test]
830 fn build_bitmap_sets_expected_bits() {
831 let bm = build_bitmap(&[0, 1, 64, 1023]);
832 assert_eq!(bm.words[0], 0b11);
833 assert_eq!(bm.words[1], 1);
834 assert_eq!(bm.words[15], 1u64 << 63);
835 assert_eq!(bm.words[2], 0);
836 }
837
838 #[test]
839 fn build_bitmap_ignores_out_of_range_cpus() {
840 let bm = build_bitmap(&[1024, 5000]);
841 assert_eq!(bm.words, [0; mlfq_consts_MLFQ_BITMAP_WORDS as usize]);
842 }
843
844 #[test]
845 fn bitmap_words_count_matches_bpf_constant() {
846 assert_eq!(
847 mlfq_consts_MLFQ_BITMAP_WORDS,
848 (crate::bpf_intf::mlfq_consts_MLFQ_MAX_CPUS + 63) / 64
849 );
850 }
851
852 #[test]
853 fn smt_pair_table() {
854 let plan = plan_sibling_table(&[(0, 0), (1, 0), (2, 1), (3, 1)]);
857 assert!(plan.smt_on);
858 assert_eq!(&plan.cpu_sibling[..4], &[1, 0, 3, 2]);
859 }
860
861 #[test]
862 fn smt_off_with_distinct_cores() {
863 let plan = plan_sibling_table(&[(0, 0), (1, 1), (2, 2), (3, 3)]);
864 assert!(!plan.smt_on);
865 assert_eq!(&plan.cpu_sibling[..4], &[0, 1, 2, 3]);
866 }
867
868 #[test]
869 fn smt_unpaired_cpu_maps_to_self() {
870 let plan = plan_sibling_table(&[(0, 0), (1, 0), (2, 2)]);
871 assert!(plan.smt_on);
872 assert_eq!(plan.cpu_sibling[0], 1);
873 assert_eq!(plan.cpu_sibling[1], 0);
874 assert_eq!(plan.cpu_sibling[2], 2);
876 }
877
878 #[test]
879 fn smt_table_is_order_independent() {
880 let plan = plan_sibling_table(&[(1, 0), (3, 1), (0, 0), (2, 1)]);
881 assert_eq!(&plan.cpu_sibling[..4], &[1, 0, 3, 2]);
882 }
883
884 #[test]
885 fn smt_empty_input_stays_off() {
886 let plan = plan_sibling_table(&[]);
887 assert!(!plan.smt_on);
888 assert_eq!(plan.cpu_sibling[0], 0);
889 }
890
891 #[test]
892 fn largest_llc_unique_max_wins() {
893 assert_eq!(pick_largest_llc(&[16, 32, 8], 3), Some(1));
894 }
895
896 #[test]
897 fn largest_llc_tie_is_disabled() {
898 assert_eq!(pick_largest_llc(&[32, 32, 8], 3), None);
899 }
900
901 #[test]
902 fn largest_llc_single_domain_is_disabled() {
903 assert_eq!(pick_largest_llc(&[32], 1), None);
904 }
905
906 #[test]
907 fn largest_llc_zero_domains_is_disabled() {
908 assert_eq!(pick_largest_llc(&[], 0), None);
909 }
910
911 #[test]
912 fn largest_llc_all_zero_sizes_tie_to_disabled() {
913 assert_eq!(pick_largest_llc(&[0, 0, 0], 3), None);
915 }
916
917 #[test]
918 fn largest_llc_unreadable_domain_reads_zero() {
919 assert_eq!(pick_largest_llc(&[16, 0], 2), Some(0));
922 }
923
924 #[test]
925 fn parse_cache_size_suffixes() {
926 assert_eq!(parse_cache_size("16384K"), Some(16384 * 1024));
927 assert_eq!(parse_cache_size("32M"), Some(32 * 1024 * 1024));
928 assert_eq!(parse_cache_size("1G"), Some(1024 * 1024 * 1024));
929 assert_eq!(parse_cache_size("4096"), Some(4096));
930 assert_eq!(parse_cache_size("bogus"), None);
931 }
932
933 #[test]
934 fn llc_size_bytes_picks_the_llc_index_by_id() {
935 let dir = std::env::temp_dir().join(format!("scx_mlfq_cache_id_{}", std::process::id()));
938 std::fs::create_dir_all(dir.join("cpu0/cache/index0")).unwrap();
939 std::fs::create_dir_all(dir.join("cpu0/cache/index1")).unwrap();
940 std::fs::create_dir_all(dir.join("cpu0/topology")).unwrap();
941 std::fs::write(dir.join("cpu0/cache/index0/level"), "2\n").unwrap();
942 std::fs::write(dir.join("cpu0/cache/index0/size"), "512K\n").unwrap();
943 std::fs::write(dir.join("cpu0/cache/index0/id"), "4\n").unwrap();
944 std::fs::write(dir.join("cpu0/cache/index1/level"), "3\n").unwrap();
945 std::fs::write(dir.join("cpu0/cache/index1/size"), "32M\n").unwrap();
946 std::fs::write(dir.join("cpu0/cache/index1/id"), "7\n").unwrap();
947 std::fs::write(dir.join("cpu0/topology/llc_id"), "7\n").unwrap();
948
949 let size = llc_size_bytes(&dir.join("cpu0/cache"));
950 assert_eq!(size, Some(32 * 1024 * 1024));
951 std::fs::remove_dir_all(&dir).unwrap();
952 }
953
954 #[test]
955 fn llc_size_bytes_falls_back_to_deepest_level_without_id() {
956 let dir = std::env::temp_dir().join(format!("scx_mlfq_cache_noid_{}", std::process::id()));
958 std::fs::create_dir_all(dir.join("cpu0/cache/index0")).unwrap();
959 std::fs::create_dir_all(dir.join("cpu0/cache/index1")).unwrap();
960 std::fs::write(dir.join("cpu0/cache/index0/level"), "2\n").unwrap();
961 std::fs::write(dir.join("cpu0/cache/index0/size"), "512K\n").unwrap();
962 std::fs::write(dir.join("cpu0/cache/index1/level"), "3\n").unwrap();
963 std::fs::write(dir.join("cpu0/cache/index1/size"), "32M\n").unwrap();
964
965 let size = llc_size_bytes(&dir.join("cpu0/cache"));
966 assert_eq!(size, Some(32 * 1024 * 1024));
967 std::fs::remove_dir_all(&dir).unwrap();
968 }
969
970 #[test]
971 fn llc_size_bytes_missing_path_is_none() {
972 let dir =
973 std::env::temp_dir().join(format!("scx_mlfq_cache_missing_{}", std::process::id()));
974 assert_eq!(llc_size_bytes(&dir.join("cpu0/cache")), None);
975 }
976
977 #[test]
978 fn build_llc_cpu_list_layout() {
979 let list = build_llc_cpu_list(&[0, 1, 2, 1023]);
980 assert_eq!(list.nr, 4);
981 assert_eq!(&list.cpus[..4], &[0, 1, 2, 1023]);
982 assert_eq!(list.cpus[4], 0);
983 }
984
985 #[test]
986 fn build_llc_cpu_list_skips_out_of_range() {
987 let list = build_llc_cpu_list(&[0, 5000, 1]);
988 assert_eq!(list.nr, 2);
989 assert_eq!(&list.cpus[..2], &[0, 1]);
990 }
991
992 #[test]
993 fn max_llc_cpus_matches_bpf_constant() {
994 assert_eq!(MAX_LLC_CPUS, mlfq_consts_MLFQ_MAX_LLC_CPUS as usize);
995 }
996
997 #[test]
998 fn oversized_llc_domain_publishes_an_empty_list() {
999 let over: Vec<u32> = (0..MAX_LLC_CPUS as u32 + 1).collect();
1002 assert_eq!(llc_cpu_list_for(&over).nr, 0);
1003
1004 let fits: Vec<u32> = (0..MAX_LLC_CPUS as u32).collect();
1008 let list = llc_cpu_list_for(&fits);
1009 assert_eq!(list.nr, MAX_LLC_CPUS as u32);
1010 assert_eq!(MAX_LLC_CPUS, mlfq_consts_MLFQ_LLC_SCAN_MAX as usize);
1011 }
1012}