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::Powermode;
39use scx_utils::Topology;
40use scx_utils::get_primary_cpus;
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_LLC_CPUS;
46use crate::bpf_intf::mlfq_consts_MLFQ_MAX_LLCS;
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 && id.is_some()
283 && id != Some(llc_id)
284 {
285 continue;
286 }
287 let Some(size) = std::fs::read_to_string(dir.join("size"))
288 .ok()
289 .and_then(|s| parse_cache_size(&s))
290 else {
291 continue;
292 };
293 if best.is_none_or(|(best_level, _)| level > best_level) {
294 best = Some((level, size));
295 }
296 }
297 best.map(|(_, size)| size)
298}
299
300pub fn pick_largest_llc(sizes: &[u64], nr_llcs: u32) -> Option<u32> {
309 if (nr_llcs as usize) < 2 {
310 return None;
311 }
312 let nr = nr_llcs as usize;
313 let max = sizes[..nr.min(sizes.len())].iter().copied().max()?;
314 let mut argmax = None;
315 let mut tied = false;
316
317 for (i, &s) in sizes[..nr.min(sizes.len())].iter().enumerate() {
318 if s == max {
319 if argmax.is_some() {
320 tied = true;
321 } else {
322 argmax = Some(i as u32);
323 }
324 }
325 }
326
327 if tied {
328 return None;
329 }
330 argmax
331}
332
333#[derive(Debug, Clone, PartialEq, Eq)]
335pub struct TopologyPlan {
336 pub capacity: CapacityPlan,
337 pub llcs: LlcPlan,
338}
339
340pub fn init_topology(skel: &mut crate::bpf_skel::OpenBpfSkel<'_>) -> Result<TopologyPlan> {
345 let topo = match Topology::new() {
346 Ok(topo) => topo,
347 Err(e) => {
348 warn!("CPU topology discovery failed, using uniform-capacity placement: {e}");
349 return Ok(TopologyPlan {
350 capacity: CapacityPlan {
351 primary_all: true,
352 primary_cpus: Vec::new(),
353 },
354 llcs: LlcPlan {
355 nr_llcs: 0,
356 has_primary: [0; MAX_LLCS],
357 llc_cpus: Vec::new(),
358 cpu_llc: [mlfq_consts_MLFQ_MAX_LLCS; MAX_CPUS],
359 },
360 });
361 }
362 };
363
364 let nr_online = topo.all_cpus.len();
365 let primaries: Vec<u32> = match get_primary_cpus(Powermode::Performance) {
366 Ok(cpus) => cpus.into_iter().map(|cpu| cpu as u32).collect(),
367 Err(e) => {
368 warn!("primary CPU discovery failed, using uniform-capacity placement: {e}");
369 Vec::new()
370 }
371 };
372
373 let capacity = plan_primary_mask(&primaries, nr_online);
374
375 let cpu_to_llc: Vec<(u32, u32)> = topo
376 .all_cpus
377 .iter()
378 .map(|(id, cpu)| (*id as u32, cpu.llc_id as u32))
379 .collect();
380 let llcs = plan_llcs(&cpu_to_llc, &capacity.primary_cpus, MAX_LLCS);
381 let mut llcs = llcs;
382 if capacity.primary_all {
383 llcs.has_primary.fill(1);
385 }
386
387 let sibling = plan_sibling_table(
389 &topo
390 .all_cpus
391 .iter()
392 .map(|(id, cpu)| (*id as u32, cpu.core_id as u32))
393 .collect::<Vec<_>>(),
394 );
395
396 let mut llc_sizes = vec![0u64; llcs.nr_llcs as usize];
401 for (llc, size) in llc_sizes.iter_mut().enumerate() {
402 if let Some(&cpu) = llcs.llc_cpus[llc].first() {
403 let cache_path = Path::new("/sys/devices/system/cpu").join(format!("cpu{cpu}/cache"));
404 *size = llc_size_bytes(&cache_path).unwrap_or(0);
405 }
406 }
407 let largest = pick_largest_llc(&llc_sizes, llcs.nr_llcs).unwrap_or(mlfq_consts_MLFQ_MAX_LLCS);
408
409 let rodata = skel
410 .maps
411 .rodata_data
412 .as_mut()
413 .context("rodata missing, the BPF object has no .rodata section")?;
414 rodata.mlfq_primary_all = capacity.primary_all;
415 rodata.mlfq_nr_llcs = llcs.nr_llcs;
416 rodata.mlfq_llc_has_primary = llcs.has_primary;
417 rodata.mlfq_cpu_llc = llcs.cpu_llc;
418 rodata.mlfq_smt_on = sibling.smt_on;
419 rodata.mlfq_cpu_sibling = sibling.cpu_sibling;
420 rodata.mlfq_cpu_core = sibling.cpu_core;
421 rodata.mlfq_llc_largest = largest;
422
423 if capacity.primary_all {
424 info!(
425 "Topology: {} online CPUs, uniform capacity, all treated as primary",
426 nr_online
427 );
428 } else {
429 info!(
430 "Topology: {} online CPUs, {} primary (big) cores",
431 nr_online,
432 capacity.primary_cpus.len()
433 );
434 }
435 if llcs.nr_llcs > 0 {
436 info!("Topology: {} LLC cache domains", llcs.nr_llcs);
437 }
438 if sibling.smt_on {
439 info!("Topology: SMT siblings detected, sibling preference enabled");
440 }
441 if largest < llcs.nr_llcs {
442 info!("Topology: LLC {largest} has the strictly-largest cache, Q1 bias enabled");
443 }
444
445 Ok(TopologyPlan { capacity, llcs })
446}
447
448pub fn web_cpu_static() -> Vec<crate::stats::PerCpuMetrics> {
463 let topo = match Topology::new() {
464 Ok(topo) => topo,
465 Err(e) => {
466 warn!("CPU topology discovery failed, the web UI reports no per-CPU data: {e}");
467 return Vec::new();
468 }
469 };
470
471 let primaries: Vec<u32> = match get_primary_cpus(Powermode::Performance) {
472 Ok(cpus) => cpus.into_iter().map(|cpu| cpu as u32).collect(),
473 Err(e) => {
474 warn!("primary CPU discovery failed, the web UI reports no per-CPU data: {e}");
475 Vec::new()
476 }
477 };
478
479 let cpu_to_llc: Vec<(u32, u32)> = topo
480 .all_cpus
481 .iter()
482 .map(|(id, cpu)| (*id as u32, cpu.llc_id as u32))
483 .collect();
484 let llcs = plan_llcs(&cpu_to_llc, &primaries, MAX_LLCS);
485
486 let cpu_to_core: Vec<(u32, u32)> = topo
493 .all_cpus
494 .iter()
495 .map(|(id, cpu)| (*id as u32, cpu.core_id as u32))
496 .collect();
497 let mut core_min: std::collections::BTreeMap<u32, u32> = std::collections::BTreeMap::new();
498 for &(cpu, core) in &cpu_to_core {
499 core_min
500 .entry(core)
501 .and_modify(|m| *m = (*m).min(cpu))
502 .or_insert(cpu);
503 }
504
505 topo.all_cpus
506 .iter()
507 .map(|(id, cpu)| {
508 let llc_id = llcs.cpu_llc.get(*id).copied().unwrap_or(0);
513 let llc_id = if llc_id == mlfq_consts_MLFQ_MAX_LLCS {
514 0
515 } else {
516 llc_id
517 };
518 crate::stats::PerCpuMetrics {
519 id: *id as u32,
520 freq_khz: cpu.max_freq as u64,
521 cur_freq_khz: 0,
522 llc_id,
523 smt: core_min
524 .get(&(cpu.core_id as u32))
525 .copied()
526 .unwrap_or(*id as u32)
527 != *id as u32,
528 running_queue: 0,
529 running_pid: 0,
530 rt_occupied: false,
531 running_gpu_submit: 0,
532 }
533 })
534 .collect()
535}
536
537pub fn current_freq_khz(cpu: u32) -> u64 {
542 std::fs::read_to_string(format!(
543 "/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_cur_freq"
544 ))
545 .ok()
546 .and_then(|s| s.trim().parse().ok())
547 .unwrap_or(0)
548}
549
550pub fn write_primary_bitmap(
557 skel: &mut crate::bpf_skel::BpfSkel<'_>,
558 plan: &CapacityPlan,
559) -> Result<()> {
560 if plan.primary_all {
561 return Ok(());
562 }
563
564 let bm = build_bitmap(&plan.primary_cpus);
565 write_bitmap_value(&skel.maps.mlfq_primary_bitmap, &bm, 0)
566 .context("failed to write the primary bitmap")
567}
568
569pub fn write_llc_bitmaps(skel: &mut crate::bpf_skel::BpfSkel<'_>, plan: &LlcPlan) -> Result<()> {
575 if plan.nr_llcs == 0 {
576 return Ok(());
577 }
578
579 for llc in 0..plan.nr_llcs as usize {
580 let bm = build_bitmap(&plan.llc_cpus[llc]);
581 write_bitmap_value(&skel.maps.mlfq_llc_bitmaps, &bm, llc as u32)
582 .with_context(|| format!("failed to write the LLC {llc} bitmap"))?;
583 }
584 Ok(())
585}
586
587pub fn write_llc_cpu_lists(skel: &mut crate::bpf_skel::BpfSkel<'_>, plan: &LlcPlan) -> Result<()> {
598 if plan.nr_llcs == 0 {
599 return Ok(());
600 }
601
602 for llc in 0..plan.nr_llcs as usize {
603 if plan.llc_cpus[llc].len() > MAX_LLC_CPUS {
604 warn!(
605 "LLC {llc} has {} CPUs, exceeding the supported maximum \
606({MAX_LLC_CPUS}); its Tier-A window is skipped and the full rotating window covers the domain",
607 plan.llc_cpus[llc].len()
608 );
609 }
610 let list = llc_cpu_list_for(&plan.llc_cpus[llc]);
611 write_llc_cpu_list_value(&skel.maps.mlfq_llc_cpus, &list, llc as u32)
612 .with_context(|| format!("failed to write the LLC {llc} CPU list"))?;
613 }
614 Ok(())
615}
616
617fn bitmap_word(cpu: usize) -> usize {
620 cpu >> 6
621}
622
623fn bitmap_mask(cpu: usize) -> u64 {
625 1u64 << (cpu & 63)
626}
627
628fn build_bitmap(cpus: &[u32]) -> mlfq_bitmap {
632 let mut bm = mlfq_bitmap {
633 words: [0; mlfq_consts_MLFQ_BITMAP_WORDS as usize],
634 };
635 for &cpu in cpus {
636 let cpu = cpu as usize;
637 if cpu >= MAX_CPUS {
638 continue;
639 }
640 bm.words[bitmap_word(cpu)] |= bitmap_mask(cpu);
641 }
642 bm
643}
644
645fn write_bitmap_value(map: &libbpf_rs::Map, value: &mlfq_bitmap, key: u32) -> Result<()> {
647 let key_bytes = key.to_ne_bytes();
648 let value_bytes = unsafe {
649 std::slice::from_raw_parts(value as *const _ as *const u8, size_of::<mlfq_bitmap>())
650 };
651 map.update(&key_bytes, value_bytes, MapFlags::ANY)?;
652 Ok(())
653}
654
655fn llc_cpu_list_for(cpus: &[u32]) -> mlfq_llc_cpu_list {
660 build_llc_cpu_list(cpus)
661}
662
663fn build_llc_cpu_list(cpus: &[u32]) -> mlfq_llc_cpu_list {
672 if cpus.len() > MAX_LLC_CPUS {
673 return mlfq_llc_cpu_list {
674 nr: 0,
675 cpus: [0; mlfq_consts_MLFQ_MAX_LLC_CPUS as usize],
676 };
677 }
678 let mut list = mlfq_llc_cpu_list {
679 nr: 0,
680 cpus: [0; mlfq_consts_MLFQ_MAX_LLC_CPUS as usize],
681 };
682 for &cpu in cpus {
683 if cpu as usize >= MAX_CPUS {
684 continue;
685 }
686 list.cpus[list.nr as usize] = cpu;
687 list.nr += 1;
688 }
689 list
690}
691
692fn write_llc_cpu_list_value(
694 map: &libbpf_rs::Map,
695 value: &mlfq_llc_cpu_list,
696 key: u32,
697) -> Result<()> {
698 let key_bytes = key.to_ne_bytes();
699 let value_bytes = unsafe {
700 std::slice::from_raw_parts(
701 value as *const _ as *const u8,
702 size_of::<mlfq_llc_cpu_list>(),
703 )
704 };
705 map.update(&key_bytes, value_bytes, MapFlags::ANY)?;
706 Ok(())
707}
708
709#[cfg(test)]
710mod tests {
711 use super::*;
712 use crate::bpf_intf::mlfq_consts_MLFQ_LLC_SCAN_MAX;
713
714 #[test]
715 fn empty_primary_list_falls_back_to_uniform() {
716 let plan = plan_primary_mask(&[], 8);
717 assert!(plan.primary_all);
718 assert!(plan.primary_cpus.is_empty());
719 }
720
721 #[test]
722 fn full_primary_list_is_uniform() {
723 let plan = plan_primary_mask(&[0, 1, 2, 3, 4, 5, 6, 7], 8);
725 assert!(plan.primary_all);
726 }
727
728 #[test]
729 fn subset_of_online_cpus_is_hybrid() {
730 let plan = plan_primary_mask(&[0, 1, 4, 5], 8);
731 assert!(!plan.primary_all);
732 assert_eq!(plan.primary_cpus, vec![0, 1, 4, 5]);
733 }
734
735 #[test]
736 fn hybrid_list_is_sorted_and_deduplicated() {
737 let plan = plan_primary_mask(&[5, 1, 4, 1, 0], 8);
738 assert!(!plan.primary_all);
739 assert_eq!(plan.primary_cpus, vec![0, 1, 4, 5]);
740 }
741
742 #[test]
743 fn max_cpus_matches_bpf_constant() {
744 assert_eq!(
746 MAX_CPUS,
747 crate::bpf_intf::mlfq_consts_MLFQ_MAX_CPUS as usize
748 );
749 assert_eq!(
750 MAX_LLCS,
751 crate::bpf_intf::mlfq_consts_MLFQ_MAX_LLCS as usize
752 );
753 }
754
755 #[test]
756 fn llc_grouping_from_cpu_map() {
757 let plan = plan_llcs(&[(0, 0), (1, 0), (2, 1), (3, 1)], &[], MAX_LLCS);
758 assert_eq!(plan.nr_llcs, 2);
759 assert_eq!(plan.llc_cpus, vec![vec![0, 1], vec![2, 3]]);
760 assert_eq!(plan.cpu_llc[0], 0);
761 assert_eq!(plan.cpu_llc[3], 1);
762 assert_eq!(plan.cpu_llc[4], mlfq_consts_MLFQ_MAX_LLCS);
764 }
765
766 #[test]
767 fn llc_has_primary_flags_domains_with_big_cores() {
768 let plan = plan_llcs(&[(0, 0), (1, 0), (2, 1), (3, 1)], &[1], MAX_LLCS);
769 assert_eq!(plan.has_primary[0], 1); assert_eq!(plan.has_primary[1], 0); }
772
773 #[test]
774 fn llc_map_without_primaries_has_no_flags() {
775 let plan = plan_llcs(&[(0, 0), (1, 1)], &[], MAX_LLCS);
776 assert_eq!(plan.has_primary, [0; MAX_LLCS]);
777 }
778
779 #[test]
780 fn llc_plan_disables_when_exceeding_cap() {
781 let map: Vec<(u32, u32)> = (0..33).map(|i| (i, i)).collect();
783 let plan = plan_llcs(&map, &[], MAX_LLCS);
784 assert_eq!(plan.nr_llcs, 0);
785 assert!(plan.llc_cpus.is_empty());
786 }
787
788 #[test]
789 fn llc_plan_saturates_on_u32_max_llc_id() {
790 let plan = plan_llcs(&[(0, 0), (1, u32::MAX)], &[], MAX_LLCS);
793 assert_eq!(plan.nr_llcs, 0);
794 assert!(plan.llc_cpus.is_empty());
795 }
796
797 #[test]
798 fn llc_plan_disables_on_empty_map() {
799 let plan = plan_llcs(&[], &[], MAX_LLCS);
800 assert_eq!(plan.nr_llcs, 0);
801 }
802
803 #[test]
804 fn llc_plan_skips_out_of_range_cpus() {
805 let plan = plan_llcs(&[(0, 0), (5000, 1)], &[], MAX_LLCS);
806 assert_eq!(plan.nr_llcs, 2);
808 assert_eq!(plan.cpu_llc[0], 0);
809 assert_eq!(plan.cpu_llc[1], mlfq_consts_MLFQ_MAX_LLCS);
811 assert!(plan.llc_cpus[1].is_empty());
812 }
813
814 #[test]
815 fn bitmap_word_index_math() {
816 assert_eq!(bitmap_word(0), 0);
817 assert_eq!(bitmap_word(63), 0);
818 assert_eq!(bitmap_word(64), 1);
819 assert_eq!(bitmap_word(127), 1);
820 assert_eq!(bitmap_word(1023), 15);
821 }
822
823 #[test]
824 fn bitmap_mask_math() {
825 assert_eq!(bitmap_mask(0), 1);
826 assert_eq!(bitmap_mask(5), 1 << 5);
827 assert_eq!(bitmap_mask(63), 1u64 << 63);
828 }
829
830 #[test]
831 fn build_bitmap_sets_expected_bits() {
832 let bm = build_bitmap(&[0, 1, 64, 1023]);
833 assert_eq!(bm.words[0], 0b11);
834 assert_eq!(bm.words[1], 1);
835 assert_eq!(bm.words[15], 1u64 << 63);
836 assert_eq!(bm.words[2], 0);
837 }
838
839 #[test]
840 fn build_bitmap_ignores_out_of_range_cpus() {
841 let bm = build_bitmap(&[1024, 5000]);
842 assert_eq!(bm.words, [0; mlfq_consts_MLFQ_BITMAP_WORDS as usize]);
843 }
844
845 #[test]
846 fn bitmap_words_count_matches_bpf_constant() {
847 assert_eq!(
848 mlfq_consts_MLFQ_BITMAP_WORDS,
849 crate::bpf_intf::mlfq_consts_MLFQ_MAX_CPUS.div_ceil(64)
850 );
851 }
852
853 #[test]
854 fn smt_pair_table() {
855 let plan = plan_sibling_table(&[(0, 0), (1, 0), (2, 1), (3, 1)]);
858 assert!(plan.smt_on);
859 assert_eq!(&plan.cpu_sibling[..4], &[1, 0, 3, 2]);
860 }
861
862 #[test]
863 fn smt_off_with_distinct_cores() {
864 let plan = plan_sibling_table(&[(0, 0), (1, 1), (2, 2), (3, 3)]);
865 assert!(!plan.smt_on);
866 assert_eq!(&plan.cpu_sibling[..4], &[0, 1, 2, 3]);
867 }
868
869 #[test]
870 fn smt_unpaired_cpu_maps_to_self() {
871 let plan = plan_sibling_table(&[(0, 0), (1, 0), (2, 2)]);
872 assert!(plan.smt_on);
873 assert_eq!(plan.cpu_sibling[0], 1);
874 assert_eq!(plan.cpu_sibling[1], 0);
875 assert_eq!(plan.cpu_sibling[2], 2);
877 }
878
879 #[test]
880 fn smt_table_is_order_independent() {
881 let plan = plan_sibling_table(&[(1, 0), (3, 1), (0, 0), (2, 1)]);
882 assert_eq!(&plan.cpu_sibling[..4], &[1, 0, 3, 2]);
883 }
884
885 #[test]
886 fn smt_empty_input_stays_off() {
887 let plan = plan_sibling_table(&[]);
888 assert!(!plan.smt_on);
889 assert_eq!(plan.cpu_sibling[0], 0);
890 }
891
892 #[test]
893 fn largest_llc_unique_max_wins() {
894 assert_eq!(pick_largest_llc(&[16, 32, 8], 3), Some(1));
895 }
896
897 #[test]
898 fn largest_llc_tie_is_disabled() {
899 assert_eq!(pick_largest_llc(&[32, 32, 8], 3), None);
900 }
901
902 #[test]
903 fn largest_llc_single_domain_is_disabled() {
904 assert_eq!(pick_largest_llc(&[32], 1), None);
905 }
906
907 #[test]
908 fn largest_llc_zero_domains_is_disabled() {
909 assert_eq!(pick_largest_llc(&[], 0), None);
910 }
911
912 #[test]
913 fn largest_llc_all_zero_sizes_tie_to_disabled() {
914 assert_eq!(pick_largest_llc(&[0, 0, 0], 3), None);
916 }
917
918 #[test]
919 fn largest_llc_unreadable_domain_reads_zero() {
920 assert_eq!(pick_largest_llc(&[16, 0], 2), Some(0));
923 }
924
925 #[test]
926 fn parse_cache_size_suffixes() {
927 assert_eq!(parse_cache_size("16384K"), Some(16384 * 1024));
928 assert_eq!(parse_cache_size("32M"), Some(32 * 1024 * 1024));
929 assert_eq!(parse_cache_size("1G"), Some(1024 * 1024 * 1024));
930 assert_eq!(parse_cache_size("4096"), Some(4096));
931 assert_eq!(parse_cache_size("bogus"), None);
932 }
933
934 #[test]
935 fn llc_size_bytes_picks_the_llc_index_by_id() {
936 let dir = std::env::temp_dir().join(format!("scx_mlfq_cache_id_{}", std::process::id()));
939 std::fs::create_dir_all(dir.join("cpu0/cache/index0")).unwrap();
940 std::fs::create_dir_all(dir.join("cpu0/cache/index1")).unwrap();
941 std::fs::create_dir_all(dir.join("cpu0/topology")).unwrap();
942 std::fs::write(dir.join("cpu0/cache/index0/level"), "2\n").unwrap();
943 std::fs::write(dir.join("cpu0/cache/index0/size"), "512K\n").unwrap();
944 std::fs::write(dir.join("cpu0/cache/index0/id"), "4\n").unwrap();
945 std::fs::write(dir.join("cpu0/cache/index1/level"), "3\n").unwrap();
946 std::fs::write(dir.join("cpu0/cache/index1/size"), "32M\n").unwrap();
947 std::fs::write(dir.join("cpu0/cache/index1/id"), "7\n").unwrap();
948 std::fs::write(dir.join("cpu0/topology/llc_id"), "7\n").unwrap();
949
950 let size = llc_size_bytes(&dir.join("cpu0/cache"));
951 assert_eq!(size, Some(32 * 1024 * 1024));
952 std::fs::remove_dir_all(&dir).unwrap();
953 }
954
955 #[test]
956 fn llc_size_bytes_falls_back_to_deepest_level_without_id() {
957 let dir = std::env::temp_dir().join(format!("scx_mlfq_cache_noid_{}", std::process::id()));
959 std::fs::create_dir_all(dir.join("cpu0/cache/index0")).unwrap();
960 std::fs::create_dir_all(dir.join("cpu0/cache/index1")).unwrap();
961 std::fs::write(dir.join("cpu0/cache/index0/level"), "2\n").unwrap();
962 std::fs::write(dir.join("cpu0/cache/index0/size"), "512K\n").unwrap();
963 std::fs::write(dir.join("cpu0/cache/index1/level"), "3\n").unwrap();
964 std::fs::write(dir.join("cpu0/cache/index1/size"), "32M\n").unwrap();
965
966 let size = llc_size_bytes(&dir.join("cpu0/cache"));
967 assert_eq!(size, Some(32 * 1024 * 1024));
968 std::fs::remove_dir_all(&dir).unwrap();
969 }
970
971 #[test]
972 fn llc_size_bytes_missing_path_is_none() {
973 let dir =
974 std::env::temp_dir().join(format!("scx_mlfq_cache_missing_{}", std::process::id()));
975 assert_eq!(llc_size_bytes(&dir.join("cpu0/cache")), None);
976 }
977
978 #[test]
979 fn build_llc_cpu_list_layout() {
980 let list = build_llc_cpu_list(&[0, 1, 2, 1023]);
981 assert_eq!(list.nr, 4);
982 assert_eq!(&list.cpus[..4], &[0, 1, 2, 1023]);
983 assert_eq!(list.cpus[4], 0);
984 }
985
986 #[test]
987 fn build_llc_cpu_list_skips_out_of_range() {
988 let list = build_llc_cpu_list(&[0, 5000, 1]);
989 assert_eq!(list.nr, 2);
990 assert_eq!(&list.cpus[..2], &[0, 1]);
991 }
992
993 #[test]
994 fn max_llc_cpus_matches_bpf_constant() {
995 assert_eq!(MAX_LLC_CPUS, mlfq_consts_MLFQ_MAX_LLC_CPUS as usize);
996 }
997
998 #[test]
999 fn oversized_llc_domain_publishes_an_empty_list() {
1000 let over: Vec<u32> = (0..MAX_LLC_CPUS as u32 + 1).collect();
1003 assert_eq!(llc_cpu_list_for(&over).nr, 0);
1004
1005 let fits: Vec<u32> = (0..MAX_LLC_CPUS as u32).collect();
1009 let list = llc_cpu_list_for(&fits);
1010 assert_eq!(list.nr, MAX_LLC_CPUS as u32);
1011 assert_eq!(MAX_LLC_CPUS, mlfq_consts_MLFQ_LLC_SCAN_MAX as usize);
1012 }
1013}