1use crate::Cpumask;
73use crate::compat::ROOT_PREFIX;
74use crate::cpumask::read_cpulist;
75use crate::misc::find_best_split_size;
76use crate::misc::read_file_byte;
77use crate::misc::read_file_usize_vec;
78use crate::misc::read_from_file;
79use anyhow::Result;
80use anyhow::bail;
81use glob::glob;
82use log::debug;
83use log::info;
84use log::warn;
85use sscanf::sscanf;
86use std::cmp::min;
87use std::collections::{BTreeMap, BTreeSet};
88use std::io::Write;
89use std::path::Path;
90use std::sync::{Arc, OnceLock};
91
92#[cfg(feature = "gpu-topology")]
93use crate::gpu::{Gpu, GpuIndex, create_gpus};
94
95lazy_static::lazy_static! {
96 pub static ref NR_CPU_IDS: usize = read_cpu_ids().unwrap().last().unwrap() + 1;
103
104 pub static ref NR_CPUS_POSSIBLE: usize = libbpf_rs::num_possible_cpus().unwrap();
111
112 pub static ref NR_PARTITION_MIN_CORES: usize = 2;
118 pub static ref NR_PARTITION_MAX_CORES: usize = 8;
119}
120
121#[derive(Debug, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
122pub enum CoreType {
123 Big { turbo: bool },
124 Little,
125}
126
127#[derive(Debug, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
128pub enum Powermode {
129 Turbo,
130 Performance,
131 Powersave,
132 Any,
133}
134
135pub fn get_primary_cpus(mode: Powermode) -> std::io::Result<Vec<usize>> {
143 let topo = Topology::new().unwrap();
144
145 let cpus: Vec<usize> = topo
146 .all_cores
147 .values()
148 .flat_map(|core| &core.cpus)
149 .filter_map(|(cpu_id, cpu)| match (&mode, &cpu.core_type) {
150 (Powermode::Turbo, CoreType::Big { turbo: true })
151 | (Powermode::Performance, CoreType::Big { .. })
152 | (Powermode::Powersave, CoreType::Little) => Some(*cpu_id),
153 (Powermode::Any, ..) => Some(*cpu_id),
154 _ => None,
155 })
156 .collect();
157
158 Ok(cpus)
159}
160
161#[derive(Debug, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
162pub struct Cpu {
163 pub id: usize,
164 pub min_freq: usize,
165 pub max_freq: usize,
166 pub base_freq: usize,
169 pub cpu_capacity: usize,
171 pub kernel_cpu_capacity: Option<usize>,
177 pub smt_level: usize,
178 pub pm_qos_resume_latency_us: usize,
180 pub trans_lat_ns: usize,
181 pub l2_id: usize,
182 pub l3_id: usize,
183 pub cache_size: usize,
185 pub core_type: CoreType,
186
187 pub core_id: usize,
189 pub llc_id: usize,
190 pub node_id: usize,
191 pub package_id: usize,
192 pub cluster_id: isize,
193}
194
195#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
196pub struct Core {
197 pub id: usize,
199 pub kernel_id: usize,
201 pub cluster_id: isize,
202 pub cpus: BTreeMap<usize, Arc<Cpu>>,
203 pub span: Cpumask,
205 pub core_type: CoreType,
206
207 pub llc_id: usize,
209 pub node_id: usize,
210}
211
212#[derive(Debug, Clone)]
213pub struct Llc {
214 pub id: usize,
216 pub kernel_id: usize,
218 pub cores: BTreeMap<usize, Arc<Core>>,
219 pub span: Cpumask,
221
222 pub node_id: usize,
224
225 pub all_cpus: BTreeMap<usize, Arc<Cpu>>,
227}
228
229#[derive(Debug, Clone)]
230pub struct Node {
231 pub id: usize,
232 pub distance: Vec<usize>,
233 pub llcs: BTreeMap<usize, Arc<Llc>>,
234 pub span: Cpumask,
236
237 pub all_cores: BTreeMap<usize, Arc<Core>>,
239 pub all_cpus: BTreeMap<usize, Arc<Cpu>>,
240
241 #[cfg(feature = "gpu-topology")]
242 pub gpus: BTreeMap<GpuIndex, Gpu>,
243}
244
245#[derive(Debug, Clone, Copy, Eq, PartialEq)]
247pub enum SchedDomainSource {
248 Schedstat,
250 Topology,
252}
253
254#[derive(Debug, Clone, Copy, Eq, PartialEq)]
256pub struct SchedDomainInfo {
257 pub fork_span: usize,
259 pub wake_affine_span: usize,
261 pub asym_capacity_span: usize,
263 pub source: SchedDomainSource,
265}
266
267const SCHEDSTAT_VERSION: usize = 17;
268const NUMA_RECLAIM_DISTANCE: usize = 30;
269
270#[derive(Debug)]
271pub struct Topology {
272 pub nodes: BTreeMap<usize, Node>,
273 pub span: Cpumask,
275 pub smt_enabled: bool,
277
278 pub all_llcs: BTreeMap<usize, Arc<Llc>>,
280 pub all_cores: BTreeMap<usize, Arc<Core>>,
281 pub all_cpus: BTreeMap<usize, Arc<Cpu>>,
282
283 sched_domains: OnceLock<Option<BTreeMap<usize, Vec<Cpumask>>>>,
288}
289
290impl Topology {
291 fn instantiate(span: Cpumask, mut nodes: BTreeMap<usize, Node>) -> Result<Self> {
292 let mut topo_llcs = BTreeMap::new();
296 let mut topo_cores = BTreeMap::new();
297 let mut topo_cpus = BTreeMap::new();
298
299 for node in nodes.values_mut() {
300 let mut node_cores = BTreeMap::new();
301 let mut node_cpus = BTreeMap::new();
302
303 for (&llc_id, llc) in node.llcs.iter_mut() {
304 let llc_mut = Arc::get_mut(llc).unwrap();
305 let mut llc_cpus = BTreeMap::new();
306
307 for (&core_id, core) in llc_mut.cores.iter_mut() {
308 let core_mut = Arc::get_mut(core).unwrap();
309 let smt_level = core_mut.cpus.len();
310
311 for (&cpu_id, cpu) in core_mut.cpus.iter_mut() {
312 let cpu_mut = Arc::get_mut(cpu).unwrap();
313 cpu_mut.smt_level = smt_level;
314
315 if topo_cpus
316 .insert(cpu_id, cpu.clone())
317 .or(node_cpus.insert(cpu_id, cpu.clone()))
318 .or(llc_cpus.insert(cpu_id, cpu.clone()))
319 .is_some()
320 {
321 bail!("Duplicate CPU ID {}", cpu_id);
322 }
323 }
324
325 topo_cores
328 .insert(core_id, core.clone())
329 .or(node_cores.insert(core_id, core.clone()));
330 }
331
332 llc_mut.all_cpus = llc_cpus;
333
334 if topo_llcs.insert(llc_id, llc.clone()).is_some() {
335 bail!("Duplicate LLC ID {}", llc_id);
336 }
337 }
338
339 node.all_cores = node_cores;
340 node.all_cpus = node_cpus;
341 }
342
343 Ok(Topology {
344 nodes,
345 span,
346 smt_enabled: is_smt_active().unwrap_or(false),
347 all_llcs: topo_llcs,
348 all_cores: topo_cores,
349 all_cpus: topo_cpus,
350 sched_domains: OnceLock::new(),
351 })
352 }
353
354 fn sched_domains(&self) -> Option<&BTreeMap<usize, Vec<Cpumask>>> {
355 self.sched_domains
356 .get_or_init(|| {
357 let path = format!("{}/proc/schedstat", *ROOT_PREFIX);
358 match std::fs::read_to_string(&path).and_then(|data| {
359 parse_schedstat(&data).map_err(|err| std::io::Error::other(err.to_string()))
360 }) {
361 Ok(domains)
362 if self
363 .all_cpus
364 .keys()
365 .all(|cpu| domains.get(cpu).is_some_and(|spans| !spans.is_empty())) =>
366 {
367 Some(domains)
368 }
369 Ok(_) => {
370 debug!("{path} has incomplete scheduler domains; using topology fallback");
371 None
372 }
373 Err(err) => {
374 debug!("cannot use {path}: {err}; using topology fallback");
375 None
376 }
377 }
378 })
379 .as_ref()
380 }
381
382 pub fn new() -> Result<Topology> {
384 Self::with_virt_llcs(None)
385 }
386
387 pub fn with_virt_llcs(nr_cores_per_vllc: Option<(usize, usize)>) -> Result<Topology> {
388 let span = cpus_online()?;
389 let mut topo_ctx = TopoCtx::new();
390
391 let path = format!("{}/sys/devices/system/node", *ROOT_PREFIX);
395 let nodes = if Path::new(&path).exists() {
396 create_numa_nodes(&span, &mut topo_ctx, nr_cores_per_vllc)?
397 } else {
398 create_default_node(&span, &mut topo_ctx, false, nr_cores_per_vllc)?
399 };
400
401 Self::instantiate(span, nodes)
402 }
403
404 pub fn with_flattened_llc_node() -> Result<Topology> {
405 let span = cpus_online()?;
406 let mut topo_ctx = TopoCtx::new();
407 let nodes = create_default_node(&span, &mut topo_ctx, true, None)?;
408 Self::instantiate(span, nodes)
409 }
410
411 pub fn with_args(topology_args: &crate::cli::TopologyArgs) -> Result<Topology> {
415 topology_args.validate()?;
417
418 let nr_cores_per_vllc = topology_args.get_nr_cores_per_vllc();
420
421 Self::with_virt_llcs(nr_cores_per_vllc)
423 }
424
425 #[cfg(feature = "gpu-topology")]
427 pub fn gpus(&self) -> BTreeMap<GpuIndex, &Gpu> {
428 let mut gpus = BTreeMap::new();
429 for node in self.nodes.values() {
430 for (idx, gpu) in &node.gpus {
431 gpus.insert(*idx, gpu);
432 }
433 }
434 gpus
435 }
436
437 pub fn has_little_cores(&self) -> bool {
439 self.all_cores
440 .values()
441 .any(|c| c.core_type == CoreType::Little)
442 }
443
444 pub fn sibling_cpus(&self) -> Vec<i32> {
451 let mut sibling_cpu = vec![-1i32; *NR_CPUS_POSSIBLE];
452 for core in self.all_cores.values() {
453 let mut first = -1i32;
454 for &cpu in core.cpus.keys() {
455 if first < 0 {
456 first = cpu as i32;
457 } else {
458 sibling_cpu[first as usize] = cpu as i32;
459 sibling_cpu[cpu] = first;
460 break;
461 }
462 }
463 }
464 sibling_cpu
465 }
466
467 pub fn cpumask_nr_cores(&self, cpumask: &Cpumask) -> usize {
469 let mut count = 0;
470 for core in self.all_cores.values() {
471 if core.cpus.keys().any(|&cpu_id| cpumask.test_cpu(cpu_id)) {
472 count += 1;
473 }
474 }
475 count
476 }
477
478 pub fn sched_domain_info(&self, cpu_id: usize) -> Result<SchedDomainInfo> {
487 let cpu = self
488 .all_cpus
489 .get(&cpu_id)
490 .ok_or_else(|| anyhow::anyhow!("CPU {cpu_id} is not in the topology"))?;
491
492 let (mut domains, source) = match self.sched_domains().and_then(|all| all.get(&cpu_id)) {
493 Some(domains) => (domains.clone(), SchedDomainSource::Schedstat),
494 None => {
495 let mut domains = vec![
496 self.all_cores[&cpu.core_id].span.clone(),
497 self.all_llcs[&cpu.llc_id].span.clone(),
498 self.nodes[&cpu.node_id].span.clone(),
499 self.span.clone(),
500 ];
501 domains.sort_by_key(Cpumask::weight);
502 domains.dedup();
503 (domains, SchedDomainSource::Topology)
504 }
505 };
506 domains = domains
507 .into_iter()
508 .map(|span| span.and(&self.span))
509 .filter(|span| span.test_cpu(cpu_id))
510 .collect();
511 domains.sort_by_key(Cpumask::weight);
512 domains.dedup();
513
514 let balance_span = domains
515 .iter()
516 .filter(|span| self.within_numa_reclaim_distance(cpu.node_id, span))
517 .map(Cpumask::weight)
518 .max()
519 .unwrap_or(1);
520
521 let capacities: Option<BTreeMap<_, _>> = self
522 .all_cpus
523 .iter()
524 .map(|(&id, cpu)| Some((id, cpu.kernel_cpu_capacity?)))
525 .collect();
526 let asym_capacity_span = capacities
527 .as_ref()
528 .map(|capacities| smallest_full_capacity_span(&domains, capacities))
529 .unwrap_or(0);
530
531 Ok(SchedDomainInfo {
532 fork_span: balance_span,
533 wake_affine_span: balance_span,
534 asym_capacity_span,
535 source,
536 })
537 }
538
539 fn within_numa_reclaim_distance(&self, source_node: usize, span: &Cpumask) -> bool {
540 let Some(source) = self.nodes.get(&source_node) else {
541 return false;
542 };
543
544 span.iter().all(|cpu_id| {
545 self.all_cpus
546 .get(&cpu_id)
547 .and_then(|cpu| source.distance.get(cpu.node_id))
548 .is_some_and(|&distance| distance <= NUMA_RECLAIM_DISTANCE)
549 })
550 }
551
552 pub fn format_cpumask_grid<W: Write>(
564 &self,
565 w: &mut W,
566 cpumask: &Cpumask,
567 indent: &str,
568 max_width: usize,
569 ) -> Result<()> {
570 for node in self.nodes.values() {
571 let mut llc_segments: Vec<(usize, String)> = Vec::new();
574
575 for llc in node.llcs.values() {
576 let mut seg = String::new();
577 let nr_cores = llc.cores.len();
578 let nr_groups = nr_cores.div_ceil(8);
579 let base = nr_cores / nr_groups;
580 let rem = nr_cores % nr_groups;
581 let mut next_break = if rem > 0 { base + 1 } else { base };
583 let mut group_idx = 0;
584 for (i, core) in llc.cores.values().enumerate() {
585 if i > 0 && i == next_break {
586 seg.push(' ');
587 group_idx += 1;
588 next_break += if group_idx < rem { base + 1 } else { base };
589 }
590 let nr_cpus = core.cpus.len();
591 let cpu_ids: Vec<usize> = core.cpus.keys().copied().collect();
592 let nr_set: usize = cpu_ids.iter().filter(|&&c| cpumask.test_cpu(c)).count();
593
594 let ch = if nr_cpus == 1 {
595 if nr_set > 0 { '█' } else { '░' }
596 } else if nr_cpus == 2 {
597 let first_set = cpumask.test_cpu(cpu_ids[0]);
598 let second_set = cpumask.test_cpu(cpu_ids[1]);
599 match (first_set, second_set) {
600 (false, false) => '░',
601 (true, false) => '▀',
602 (false, true) => '▄',
603 (true, true) => '█',
604 }
605 } else {
606 if nr_set == 0 {
608 '░'
609 } else if nr_set == nr_cpus {
610 '█'
611 } else {
612 '▄'
613 }
614 };
615 seg.push(ch);
616 }
617 llc_segments.push((llc.id, seg));
618 }
619
620 if llc_segments.is_empty() {
621 continue;
622 }
623
624 let first_llc_id = llc_segments[0].0;
626 let prefix = format!("{}N{} L{:02}: ", indent, node.id, first_llc_id);
627 let prefix_width = prefix.chars().count();
628 let cont_indent = format!(
629 "{}{}",
630 indent,
631 " ".repeat(prefix_width - indent.chars().count())
632 );
633
634 let mut line = prefix.clone();
636 let mut first_llc = true;
637
638 for (_, seg) in &llc_segments {
639 let seg_width = seg.chars().count();
640 let separator = if first_llc { "" } else { "|" };
641 let sep_width = separator.chars().count();
642 let current_line_width = line.chars().count();
643
644 if !first_llc && current_line_width + sep_width + seg_width > max_width {
645 writeln!(w, "{}", line)?;
646 line = format!("{}{}", cont_indent, seg);
647 } else {
648 line = format!("{}{}{}", line, separator, seg);
649 }
650 first_llc = false;
651 }
652 writeln!(w, "{}", line)?;
653 }
654 Ok(())
655 }
656
657 pub fn format_cpumask_header(&self, cpumask: &Cpumask, min_cpus: u32, max_cpus: u32) -> String {
659 let nr_cpus = cpumask.weight();
660 let nr_cores = self.cpumask_nr_cores(cpumask);
661 format!(
662 "cpus={:3}({:3}c) [{:3},{:3}]",
663 nr_cpus, nr_cores, min_cpus, max_cpus
664 )
665 }
666}
667
668fn parse_schedstat(data: &str) -> Result<BTreeMap<usize, Vec<Cpumask>>> {
669 let mut lines = data.lines();
670 let version = lines
671 .next()
672 .and_then(|line| line.strip_prefix("version "))
673 .ok_or_else(|| anyhow::anyhow!("missing schedstat version"))?
674 .parse::<usize>()?;
675 if version != SCHEDSTAT_VERSION {
676 bail!("unsupported schedstat version {version}");
677 }
678
679 let mut domains = BTreeMap::<usize, Vec<Cpumask>>::new();
680 let mut current_cpu = None;
681 for line in lines {
682 let mut fields = line.split_ascii_whitespace();
683 let Some(kind) = fields.next() else {
684 continue;
685 };
686 if let Some(cpu) = kind.strip_prefix("cpu") {
687 let cpu = cpu.parse::<usize>()?;
688 domains.entry(cpu).or_default();
689 current_cpu = Some(cpu);
690 continue;
691 }
692 if !kind.starts_with("domain") {
693 continue;
694 }
695
696 let cpu = current_cpu.ok_or_else(|| anyhow::anyhow!("domain before CPU record"))?;
697 let _name = fields
698 .next()
699 .ok_or_else(|| anyhow::anyhow!("missing scheduler-domain name"))?;
700 let mask = fields
701 .next()
702 .ok_or_else(|| anyhow::anyhow!("missing scheduler-domain mask"))?
703 .replace(',', "");
704 domains
705 .entry(cpu)
706 .or_default()
707 .push(Cpumask::from_str(&mask)?);
708 }
709
710 for spans in domains.values_mut() {
711 spans.sort_by_key(Cpumask::weight);
712 spans.dedup();
713 }
714 Ok(domains)
715}
716
717fn smallest_full_capacity_span(domains: &[Cpumask], capacities: &BTreeMap<usize, usize>) -> usize {
718 let all = capacities.values().copied().collect::<BTreeSet<_>>();
719 if all.len() <= 1 {
720 return 0;
721 }
722
723 domains
724 .iter()
725 .find(|span| {
726 span.iter()
727 .filter_map(|cpu| capacities.get(&cpu).copied())
728 .collect::<BTreeSet<_>>()
729 == all
730 })
731 .map(Cpumask::weight)
732 .unwrap_or(0)
733}
734
735struct TopoCtx {
740 node_core_kernel_ids: BTreeMap<(usize, usize, usize), usize>,
742 node_llc_kernel_ids: BTreeMap<(usize, usize, usize), usize>,
744 l2_ids: BTreeMap<String, usize>,
746 l3_ids: BTreeMap<String, usize>,
748}
749
750impl TopoCtx {
751 fn new() -> TopoCtx {
752 let core_kernel_ids = BTreeMap::new();
753 let llc_kernel_ids = BTreeMap::new();
754 let l2_ids = BTreeMap::new();
755 let l3_ids = BTreeMap::new();
756 TopoCtx {
757 node_core_kernel_ids: core_kernel_ids,
758 node_llc_kernel_ids: llc_kernel_ids,
759 l2_ids,
760 l3_ids,
761 }
762 }
763}
764
765fn cpus_online() -> Result<Cpumask> {
766 let path = format!("{}/sys/devices/system/cpu/online", *ROOT_PREFIX);
767 let online = std::fs::read_to_string(path)?;
768 Cpumask::from_cpulist(&online)
769}
770
771fn get_cache_id(topo_ctx: &mut TopoCtx, cache_level_path: &Path, cache_level: usize) -> usize {
772 let id_map = match cache_level {
774 2 => &mut topo_ctx.l2_ids,
775 3 => &mut topo_ctx.l3_ids,
776 _ => return usize::MAX,
777 };
778
779 let path = &cache_level_path.join("shared_cpu_list");
780 let key = match std::fs::read_to_string(path) {
781 Ok(key) => key,
782 Err(_) => return usize::MAX,
783 };
784
785 let id = *id_map.get(&key).unwrap_or(&usize::MAX);
786 if id != usize::MAX {
787 return id;
788 }
789
790 let id = read_from_file(&cache_level_path.join("id")).unwrap_or(usize::MAX);
792 if id != usize::MAX {
793 id_map.insert(key, id);
795 return id;
796 }
797
798 let id = id_map.len();
800 id_map.insert(key, id);
801
802 id
803}
804
805fn get_per_cpu_cache_size(cache_path: &Path) -> Result<usize> {
806 let path_str = cache_path.to_str().unwrap();
807 let paths = glob(&(path_str.to_owned() + "/index[0-9]*"))?;
808 let mut tot_size = 0;
809
810 for index in paths.filter_map(Result::ok) {
811 let size = read_file_byte(&index.join("size")).unwrap_or(1024_usize);
816 let cpulist: String = read_from_file(&index.join("shared_cpu_list"))?;
817 let num_cpus = read_cpulist(&cpulist)?.len();
818 tot_size += size / num_cpus;
819 }
820
821 Ok(tot_size)
822}
823
824#[allow(clippy::too_many_arguments)]
825fn create_insert_cpu(
826 id: usize,
827 node: &mut Node,
828 online_mask: &Cpumask,
829 topo_ctx: &mut TopoCtx,
830 cs: &CapacitySource,
831 flatten_llc: bool,
832) -> Result<()> {
833 if !online_mask.test_cpu(id) {
837 return Ok(());
838 }
839
840 let cpu_str = format!("{}/sys/devices/system/cpu/cpu{}", *ROOT_PREFIX, id);
841 let cpu_path = Path::new(&cpu_str);
842
843 let top_path = cpu_path.join("topology");
845 let core_kernel_id = read_from_file(&top_path.join("core_id"))?;
846 let package_id = read_from_file(&top_path.join("physical_package_id"))?;
847 let cluster_id = read_from_file(&top_path.join("cluster_id"))?;
848
849 let cache_path = cpu_path.join("cache");
855 let l2_id = get_cache_id(topo_ctx, &cache_path.join(format!("index{}", 2)), 2);
856 let l3_id = get_cache_id(topo_ctx, &cache_path.join(format!("index{}", 3)), 3);
857 let llc_kernel_id = if flatten_llc {
858 0
859 } else if l3_id == usize::MAX {
860 l2_id
861 } else {
862 l3_id
863 };
864
865 let cache_size = get_per_cpu_cache_size(&cache_path).unwrap_or(0_usize);
867
868 let freq_path = cpu_path.join("cpufreq");
871 let min_freq = read_from_file(&freq_path.join("scaling_min_freq")).unwrap_or(0_usize);
872 let max_freq = read_from_file(&freq_path.join("scaling_max_freq")).unwrap_or(0_usize);
873 let base_freq = read_from_file(&freq_path.join("base_frequency")).unwrap_or(max_freq);
874 let trans_lat_ns =
875 read_from_file(&freq_path.join("cpuinfo_transition_latency")).unwrap_or(0_usize);
876
877 let kernel_cpu_capacity = read_from_file(&cpu_path.join("cpu_capacity"))
880 .ok()
881 .filter(|capacity| *capacity > 0);
882 let cap_path = cpu_path.join(cs.suffix.clone());
883 let rcap = read_from_file(&cap_path).unwrap_or(cs.max_rcap);
884 let cpu_capacity = (rcap * 1024) / cs.max_rcap;
885
886 let power_path = cpu_path.join("power");
888 let pm_qos_resume_latency_us =
889 read_from_file(&power_path.join("pm_qos_resume_latency_us")).unwrap_or(0_usize);
890
891 let num_llcs = topo_ctx.node_llc_kernel_ids.len();
892 let llc_id = topo_ctx
893 .node_llc_kernel_ids
894 .entry((node.id, package_id, llc_kernel_id))
895 .or_insert(num_llcs);
896
897 let llc = node.llcs.entry(*llc_id).or_insert(Arc::new(Llc {
898 id: *llc_id,
899 cores: BTreeMap::new(),
900 span: Cpumask::new(),
901 all_cpus: BTreeMap::new(),
902
903 node_id: node.id,
904 kernel_id: llc_kernel_id,
905 }));
906 let llc_mut = Arc::get_mut(llc).unwrap();
907
908 let core_type = if cs.avg_rcap < cs.max_rcap && rcap == cs.max_rcap {
909 CoreType::Big { turbo: true }
910 } else if !cs.has_biglittle || rcap >= cs.avg_rcap {
911 CoreType::Big { turbo: false }
912 } else {
913 CoreType::Little
914 };
915
916 let num_cores = topo_ctx.node_core_kernel_ids.len();
917 let core_id = topo_ctx
918 .node_core_kernel_ids
919 .entry((node.id, package_id, core_kernel_id))
920 .or_insert(num_cores);
921
922 let core = llc_mut.cores.entry(*core_id).or_insert(Arc::new(Core {
923 id: *core_id,
924 cpus: BTreeMap::new(),
925 span: Cpumask::new(),
926 core_type: core_type.clone(),
927
928 llc_id: *llc_id,
929 node_id: node.id,
930 kernel_id: core_kernel_id,
931 cluster_id,
932 }));
933 let core_mut = Arc::get_mut(core).unwrap();
934
935 core_mut.cpus.insert(
936 id,
937 Arc::new(Cpu {
938 id,
939 min_freq,
940 max_freq,
941 base_freq,
942 cpu_capacity,
943 kernel_cpu_capacity,
944 smt_level: 0, pm_qos_resume_latency_us,
946 trans_lat_ns,
947 l2_id,
948 l3_id,
949 cache_size,
950 core_type: core_type.clone(),
951
952 core_id: *core_id,
953 llc_id: *llc_id,
954 node_id: node.id,
955 package_id,
956 cluster_id,
957 }),
958 );
959
960 if node.span.test_cpu(id) {
961 bail!("Node {} already had CPU {}", node.id, id);
962 }
963
964 core_mut.span.set_cpu(id)?;
966 llc_mut.span.set_cpu(id)?;
967 node.span.set_cpu(id)?;
968
969 Ok(())
970}
971
972fn read_cpu_ids() -> Result<Vec<usize>> {
973 let mut cpu_ids = vec![];
974 let path = format!("{}/sys/devices/system/cpu/cpu[0-9]*", *ROOT_PREFIX);
975 let cpu_paths = glob(&path)?;
976 for cpu_path in cpu_paths.filter_map(Result::ok) {
977 let cpu_str = cpu_path.to_str().unwrap().trim();
978 if ROOT_PREFIX.is_empty() {
979 match sscanf!(cpu_str, "/sys/devices/system/cpu/cpu{usize}") {
980 Some(val) => cpu_ids.push(val),
981 None => {
982 bail!("Failed to parse cpu ID {}", cpu_str);
983 }
984 }
985 } else {
986 match sscanf!(cpu_str, "{str}/sys/devices/system/cpu/cpu{usize}") {
987 Some((_, val)) => cpu_ids.push(val),
988 None => {
989 bail!("Failed to parse cpu ID {}", cpu_str);
990 }
991 }
992 }
993 }
994 cpu_ids.sort();
995 Ok(cpu_ids)
996}
997
998struct CapacitySource {
999 suffix: String,
1001 avg_rcap: usize,
1003 max_rcap: usize,
1005 has_biglittle: bool,
1007}
1008
1009fn get_capacity_source() -> Option<CapacitySource> {
1010 let sources = [
1013 "cpufreq/amd_pstate_prefcore_ranking",
1014 "cpufreq/amd_pstate_highest_perf",
1015 "acpi_cppc/highest_perf",
1016 "cpu_capacity",
1017 "cpufreq/cpuinfo_max_freq",
1018 ];
1019
1020 let prefix = format!("{}/sys/devices/system/cpu/cpu0", *ROOT_PREFIX);
1022 let mut raw_capacity;
1023 let mut suffix = sources[sources.len() - 1];
1024 'outer: for src in sources {
1025 let path_str = [prefix.clone(), src.to_string()].join("/");
1026 let path = Path::new(&path_str);
1027 raw_capacity = read_from_file(path).unwrap_or(0_usize);
1028 if raw_capacity > 0 {
1029 suffix = src;
1031 let path = format!("{}/sys/devices/system/cpu/cpu[0-9]*", *ROOT_PREFIX);
1033 let cpu_paths = glob(&path).ok()?;
1034 for cpu_path in cpu_paths.filter_map(Result::ok) {
1035 let raw_capacity2 = read_from_file(&cpu_path.join(suffix)).unwrap_or(0_usize);
1036 if raw_capacity != raw_capacity2 {
1037 break 'outer;
1038 }
1039 }
1040 }
1045 }
1046
1047 let mut max_rcap = 0;
1049 let mut min_rcap = usize::MAX;
1050 let mut avg_rcap = 0;
1051 let mut nr_cpus = 0;
1052 let mut has_biglittle = false;
1053 let path = format!("{}/sys/devices/system/cpu/cpu[0-9]*", *ROOT_PREFIX);
1054 let cpu_paths = glob(&path).ok()?;
1055 for cpu_path in cpu_paths.filter_map(Result::ok) {
1056 let rcap = read_from_file(&cpu_path.join(suffix)).unwrap_or(0_usize);
1057 if max_rcap < rcap {
1058 max_rcap = rcap;
1059 }
1060 if min_rcap > rcap {
1061 min_rcap = rcap;
1062 }
1063 avg_rcap += rcap;
1064 nr_cpus += 1;
1065 }
1066
1067 if nr_cpus == 0 || max_rcap == 0 {
1068 suffix = "";
1069 avg_rcap = 1024;
1070 max_rcap = 1024;
1071 warn!("CPU capacity information is not available under sysfs.");
1072 } else {
1073 avg_rcap /= nr_cpus;
1074 has_biglittle = max_rcap as f32 >= (1.3 * min_rcap as f32);
1083 }
1084
1085 Some(CapacitySource {
1086 suffix: suffix.to_string(),
1087 avg_rcap,
1088 max_rcap,
1089 has_biglittle,
1090 })
1091}
1092
1093fn is_smt_active() -> Option<bool> {
1094 let path = format!("{}/sys/devices/system/cpu/smt/active", *ROOT_PREFIX);
1095 let smt_on: u8 = read_from_file(Path::new(&path)).ok()?;
1096 Some(smt_on == 1)
1097}
1098
1099fn replace_with_virt_llcs(
1100 node: &mut Node,
1101 min_cores: usize,
1102 max_cores: usize,
1103 start_id: usize,
1104) -> Result<usize> {
1105 let mut next_id = start_id;
1106 let mut core_to_partition: BTreeMap<usize, usize> = BTreeMap::new();
1107 let mut partition_to_kernel_id: BTreeMap<usize, usize> = BTreeMap::new();
1108 let num_orig_llcs = node.llcs.len();
1109
1110 for llc in node.llcs.values() {
1113 let mut cores_by_type: BTreeMap<bool, Vec<usize>> = BTreeMap::new();
1115
1116 for (core_id, core) in llc.cores.iter() {
1117 let core_type = core.core_type == CoreType::Little;
1118 cores_by_type.entry(core_type).or_default().push(*core_id);
1119 }
1120
1121 for core_ids in cores_by_type.values() {
1122 let num_cores_in_bucket = core_ids.len();
1123
1124 let best_split = find_best_split_size(num_cores_in_bucket, min_cores, max_cores);
1126 let num_partitions = num_cores_in_bucket / best_split;
1127
1128 for (bucket_idx, &core_id) in core_ids.iter().enumerate() {
1130 let partition_idx = min(bucket_idx / best_split, num_partitions - 1);
1131 let current_partition_id = next_id + partition_idx;
1132 core_to_partition.insert(core_id, current_partition_id);
1133 partition_to_kernel_id.insert(current_partition_id, llc.kernel_id);
1134 }
1135
1136 next_id += num_partitions;
1137 }
1138 }
1139
1140 let mut virt_llcs: BTreeMap<usize, Arc<Llc>> = BTreeMap::new();
1142
1143 for vllc_id in start_id..next_id {
1144 let kernel_id = partition_to_kernel_id.get(&vllc_id).copied().unwrap();
1145 virt_llcs.insert(
1146 vllc_id,
1147 Arc::new(Llc {
1148 id: vllc_id,
1149 kernel_id,
1150 cores: BTreeMap::new(),
1151 span: Cpumask::new(),
1152 node_id: node.id,
1153 all_cpus: BTreeMap::new(),
1154 }),
1155 );
1156 }
1157
1158 for llc in node.llcs.values_mut() {
1160 for (core_id, core) in llc.cores.iter() {
1161 if let Some(&target_partition_id) = core_to_partition.get(core_id)
1162 && let Some(target_llc) = virt_llcs.get_mut(&target_partition_id)
1163 {
1164 let target_llc_mut = Arc::get_mut(target_llc).unwrap();
1165
1166 let mut new_core = (**core).clone();
1168 new_core.llc_id = target_partition_id;
1169
1170 let mut updated_cpus = BTreeMap::new();
1172 for (cpu_id, cpu) in new_core.cpus.iter() {
1173 let mut new_cpu = (**cpu).clone();
1174 new_cpu.llc_id = target_partition_id;
1175
1176 target_llc_mut.span.set_cpu(*cpu_id)?;
1178
1179 updated_cpus.insert(*cpu_id, Arc::new(new_cpu));
1180 }
1181 new_core.cpus = updated_cpus;
1182
1183 target_llc_mut.cores.insert(*core_id, Arc::new(new_core));
1185 }
1186 }
1187 }
1188
1189 node.llcs = virt_llcs;
1191
1192 let num_virt_llcs = next_id - start_id;
1193 let vllc_sizes: Vec<usize> = node.llcs.values().map(|llc| llc.cores.len()).collect();
1194
1195 if vllc_sizes.is_empty() {
1196 return Ok(next_id);
1197 }
1198
1199 let common_size = vllc_sizes[0];
1201 let last_size = *vllc_sizes.last().unwrap();
1202
1203 if common_size == last_size {
1204 info!(
1205 "Node {}: split {} LLC(s) into {} virtual LLCs with {} cores each",
1206 node.id, num_orig_llcs, num_virt_llcs, common_size
1207 );
1208 } else {
1209 info!(
1210 "Node {}: split {} LLC(s) into {} virtual LLCs with {} cores each (last with {})",
1211 node.id, num_orig_llcs, num_virt_llcs, common_size, last_size
1212 );
1213 }
1214
1215 Ok(next_id)
1216}
1217
1218fn create_default_node(
1219 online_mask: &Cpumask,
1220 topo_ctx: &mut TopoCtx,
1221 flatten_llc: bool,
1222 nr_cores_per_vllc: Option<(usize, usize)>,
1223) -> Result<BTreeMap<usize, Node>> {
1224 let mut nodes = BTreeMap::<usize, Node>::new();
1225
1226 let mut node = Node {
1227 id: 0,
1228 distance: vec![],
1229 llcs: BTreeMap::new(),
1230 span: Cpumask::new(),
1231 #[cfg(feature = "gpu-topology")]
1232 gpus: BTreeMap::new(),
1233 all_cores: BTreeMap::new(),
1234 all_cpus: BTreeMap::new(),
1235 };
1236
1237 #[cfg(feature = "gpu-topology")]
1238 {
1239 let system_gpus = create_gpus();
1240 if let Some(gpus) = system_gpus.get(&0) {
1241 for gpu in gpus {
1242 node.gpus.insert(gpu.index, gpu.clone());
1243 }
1244 }
1245 }
1246
1247 let path = format!("{}/sys/devices/system/cpu", *ROOT_PREFIX);
1248 if !Path::new(&path).exists() {
1249 bail!("/sys/devices/system/cpu sysfs node not found");
1250 }
1251
1252 let cs = get_capacity_source().unwrap();
1253 let cpu_ids = read_cpu_ids()?;
1254 for cpu_id in cpu_ids.iter() {
1255 create_insert_cpu(*cpu_id, &mut node, online_mask, topo_ctx, &cs, flatten_llc)?;
1256 }
1257
1258 if let Some((min_cores_val, max_cores_val)) = nr_cores_per_vllc {
1259 replace_with_virt_llcs(&mut node, min_cores_val, max_cores_val, 0)?;
1260 }
1261
1262 nodes.insert(node.id, node);
1263
1264 Ok(nodes)
1265}
1266
1267fn create_numa_nodes(
1268 online_mask: &Cpumask,
1269 topo_ctx: &mut TopoCtx,
1270 nr_cores_per_vllc: Option<(usize, usize)>,
1271) -> Result<BTreeMap<usize, Node>> {
1272 let mut nodes = BTreeMap::<usize, Node>::new();
1273 let mut next_virt_llc_id = 0;
1274
1275 #[cfg(feature = "gpu-topology")]
1276 let system_gpus = create_gpus();
1277
1278 let path = format!("{}/sys/devices/system/node/node*", *ROOT_PREFIX);
1279 let numa_paths = glob(&path)?;
1280 for numa_path in numa_paths.filter_map(Result::ok) {
1281 let numa_str = numa_path.to_str().unwrap().trim();
1282 let node_id = if ROOT_PREFIX.is_empty() {
1283 match sscanf!(numa_str, "/sys/devices/system/node/node{usize}") {
1284 Some(val) => val,
1285 None => {
1286 bail!("Failed to parse NUMA node ID {}", numa_str);
1287 }
1288 }
1289 } else {
1290 match sscanf!(numa_str, "{str}/sys/devices/system/node/node{usize}") {
1291 Some((_, val)) => val,
1292 None => {
1293 bail!("Failed to parse NUMA node ID {}", numa_str);
1294 }
1295 }
1296 };
1297
1298 let distance = read_file_usize_vec(
1299 Path::new(&format!(
1300 "{}/sys/devices/system/node/node{}/distance",
1301 *ROOT_PREFIX, node_id
1302 )),
1303 ' ',
1304 )?;
1305 let mut node = Node {
1306 id: node_id,
1307 distance,
1308 llcs: BTreeMap::new(),
1309 span: Cpumask::new(),
1310
1311 all_cores: BTreeMap::new(),
1312 all_cpus: BTreeMap::new(),
1313
1314 #[cfg(feature = "gpu-topology")]
1315 gpus: BTreeMap::new(),
1316 };
1317
1318 #[cfg(feature = "gpu-topology")]
1319 {
1320 if let Some(gpus) = system_gpus.get(&node_id) {
1321 for gpu in gpus {
1322 node.gpus.insert(gpu.index, gpu.clone());
1323 }
1324 }
1325 }
1326
1327 let cpu_pattern = numa_path.join("cpu[0-9]*");
1328 let cpu_paths = glob(cpu_pattern.to_string_lossy().as_ref())?;
1329 let cs = get_capacity_source().unwrap();
1330 let mut cpu_ids = vec![];
1331 for cpu_path in cpu_paths.filter_map(Result::ok) {
1332 let cpu_str = cpu_path.to_str().unwrap().trim();
1333 let cpu_id = if ROOT_PREFIX.is_empty() {
1334 match sscanf!(cpu_str, "/sys/devices/system/node/node{usize}/cpu{usize}") {
1335 Some((_, val)) => val,
1336 None => {
1337 bail!("Failed to parse cpu ID {}", cpu_str);
1338 }
1339 }
1340 } else {
1341 match sscanf!(
1342 cpu_str,
1343 "{str}/sys/devices/system/node/node{usize}/cpu{usize}"
1344 ) {
1345 Some((_, _, val)) => val,
1346 None => {
1347 bail!("Failed to parse cpu ID {}", cpu_str);
1348 }
1349 }
1350 };
1351 cpu_ids.push(cpu_id);
1352 }
1353 cpu_ids.sort();
1354
1355 for cpu_id in cpu_ids {
1356 create_insert_cpu(cpu_id, &mut node, online_mask, topo_ctx, &cs, false)?;
1357 }
1358
1359 if let Some((min_cores_val, max_cores_val)) = nr_cores_per_vllc {
1360 next_virt_llc_id =
1361 replace_with_virt_llcs(&mut node, min_cores_val, max_cores_val, next_virt_llc_id)?;
1362 }
1363
1364 nodes.insert(node.id, node);
1365 }
1366 Ok(nodes)
1367}
1368
1369#[cfg(any(test, feature = "testutils"))]
1376pub mod testutils {
1377 use super::*;
1378 use crate::set_cpumask_test_width;
1379
1380 pub fn test_cpu(id: usize, core_id: usize, llc_id: usize, node_id: usize) -> Cpu {
1382 Cpu {
1383 id,
1384 core_id,
1385 llc_id,
1386 node_id,
1387 min_freq: 0,
1388 max_freq: 0,
1389 base_freq: 0,
1390 cpu_capacity: 1024,
1391 kernel_cpu_capacity: Some(1024),
1392 smt_level: 0, pm_qos_resume_latency_us: 0,
1394 trans_lat_ns: 0,
1395 l2_id: 0,
1396 l3_id: llc_id,
1397 cache_size: 0,
1398 core_type: CoreType::Big { turbo: false },
1399 package_id: node_id,
1400 cluster_id: 0,
1401 }
1402 }
1403
1404 pub fn test_core(
1406 id: usize,
1407 cpus: BTreeMap<usize, Arc<Cpu>>,
1408 llc_id: usize,
1409 node_id: usize,
1410 ) -> Core {
1411 let mut span = Cpumask::new();
1412 for &cpu_id in cpus.keys() {
1413 span.set_cpu(cpu_id).unwrap();
1414 }
1415 Core {
1416 id,
1417 kernel_id: id,
1418 cluster_id: 0,
1419 cpus,
1420 span,
1421 core_type: CoreType::Big { turbo: false },
1422 llc_id,
1423 node_id,
1424 }
1425 }
1426
1427 pub fn test_llc(id: usize, cores: BTreeMap<usize, Arc<Core>>, node_id: usize) -> Llc {
1429 let mut span = Cpumask::new();
1430 for core in cores.values() {
1431 for &cpu_id in core.cpus.keys() {
1432 span.set_cpu(cpu_id).unwrap();
1433 }
1434 }
1435 Llc {
1436 id,
1437 kernel_id: id,
1438 cores,
1439 span,
1440 node_id,
1441 all_cpus: BTreeMap::new(), }
1443 }
1444
1445 pub fn test_node(id: usize, llcs: BTreeMap<usize, Arc<Llc>>, nr_nodes: usize) -> Node {
1447 let mut span = Cpumask::new();
1448 for llc in llcs.values() {
1449 for core in llc.cores.values() {
1450 for &cpu_id in core.cpus.keys() {
1451 span.set_cpu(cpu_id).unwrap();
1452 }
1453 }
1454 }
1455 Node {
1456 id,
1457 distance: vec![10; nr_nodes],
1458 llcs,
1459 span,
1460 all_cores: BTreeMap::new(), all_cpus: BTreeMap::new(), #[cfg(feature = "gpu-topology")]
1463 gpus: BTreeMap::new(),
1464 }
1465 }
1466
1467 pub fn make_test_topo(
1476 nr_nodes: usize,
1477 llcs_per_node: usize,
1478 cores_per_llc: usize,
1479 hts_per_core: usize,
1480 ) -> (Topology, usize) {
1481 let total_cpus = nr_nodes * llcs_per_node * cores_per_llc * hts_per_core;
1482 set_cpumask_test_width(total_cpus);
1483
1484 let mut cpu_id = 0usize;
1485 let mut core_id = 0usize;
1486 let mut llc_id = 0usize;
1487 let mut nodes = BTreeMap::new();
1488
1489 for node_idx in 0..nr_nodes {
1490 let mut llcs = BTreeMap::new();
1491 for _ in 0..llcs_per_node {
1492 let mut cores = BTreeMap::new();
1493 for _ in 0..cores_per_llc {
1494 let mut cpus = BTreeMap::new();
1495 for _ in 0..hts_per_core {
1496 cpus.insert(
1497 cpu_id,
1498 Arc::new(test_cpu(cpu_id, core_id, llc_id, node_idx)),
1499 );
1500 cpu_id += 1;
1501 }
1502 cores.insert(
1503 core_id,
1504 Arc::new(test_core(core_id, cpus, llc_id, node_idx)),
1505 );
1506 core_id += 1;
1507 }
1508 llcs.insert(llc_id, Arc::new(test_llc(llc_id, cores, node_idx)));
1509 llc_id += 1;
1510 }
1511 nodes.insert(node_idx, test_node(node_idx, llcs, nr_nodes));
1512 }
1513
1514 let mut span = Cpumask::new();
1515 for i in 0..total_cpus {
1516 span.set_cpu(i).unwrap();
1517 }
1518
1519 (Topology::instantiate(span, nodes).unwrap(), total_cpus)
1520 }
1521
1522 pub fn make_het_test_topo(
1523 cores_per_llc: &[&[usize]],
1524 hts_per_core: usize,
1525 ) -> (Topology, usize) {
1526 let nr_nodes = cores_per_llc.len();
1527
1528 let total_cpus = cores_per_llc
1529 .iter()
1530 .map(|node| node.iter().sum::<usize>())
1531 .sum::<usize>()
1532 * hts_per_core;
1533
1534 set_cpumask_test_width(total_cpus);
1535
1536 let mut cpu_id = 0usize;
1537 let mut core_id = 0usize;
1538 let mut llc_id = 0usize;
1539 let mut nodes = BTreeMap::new();
1540
1541 for (node_id, llcs_in_node) in cores_per_llc.iter().enumerate() {
1542 let mut llcs = BTreeMap::new();
1543
1544 for &cores_in_llc in *llcs_in_node {
1545 let mut cores = BTreeMap::new();
1546
1547 for _ in 0..cores_in_llc {
1548 let mut cpus = BTreeMap::new();
1549
1550 for _ in 0..hts_per_core {
1551 cpus.insert(cpu_id, Arc::new(test_cpu(cpu_id, core_id, llc_id, node_id)));
1552 cpu_id += 1;
1553 }
1554
1555 cores.insert(core_id, Arc::new(test_core(core_id, cpus, llc_id, node_id)));
1556 core_id += 1;
1557 }
1558
1559 llcs.insert(llc_id, Arc::new(test_llc(llc_id, cores, node_id)));
1560 llc_id += 1;
1561 }
1562
1563 nodes.insert(node_id, test_node(node_id, llcs, nr_nodes));
1564 }
1565
1566 let mut span = Cpumask::new();
1567 for i in 0..total_cpus {
1568 span.set_cpu(i).unwrap();
1569 }
1570
1571 (Topology::instantiate(span, nodes).unwrap(), total_cpus)
1572 }
1573
1574 pub fn mask_from_bits(_total: usize, bits: &[usize]) -> Cpumask {
1576 let mut mask = Cpumask::new();
1577 for &b in bits {
1578 mask.set_cpu(b).unwrap();
1579 }
1580 mask
1581 }
1582}
1583
1584#[cfg(test)]
1585mod tests {
1586 use super::testutils::*;
1587 use super::*;
1588 use crate::set_cpumask_test_width;
1589
1590 fn grid_output(topo: &Topology, cpumask: &Cpumask) -> String {
1591 let mut buf = Vec::new();
1592 topo.format_cpumask_grid(&mut buf, cpumask, " ", 80)
1593 .unwrap();
1594 String::from_utf8(buf).unwrap()
1595 }
1596
1597 #[test]
1598 fn test_parse_schedstat_domains() {
1599 set_cpumask_test_width(8);
1600 let data = concat!(
1601 "version 17\n",
1602 "timestamp 42\n",
1603 "cpu0 0 0 0 0 0 0 0 0 0\n",
1604 "domain0 SMT 03 0 0\n",
1605 "domain1 MC 0f 0 0\n",
1606 "cpu1 0 0 0 0 0 0 0 0 0\n",
1607 "domain0 SMT 03 0 0\n",
1608 "domain1 MC ff 0 0\n",
1609 );
1610 let domains = parse_schedstat(data).unwrap();
1611 assert_eq!(
1612 domains[&0].iter().map(Cpumask::weight).collect::<Vec<_>>(),
1613 [2, 4]
1614 );
1615 assert_eq!(
1616 domains[&1].iter().map(Cpumask::weight).collect::<Vec<_>>(),
1617 [2, 8]
1618 );
1619 assert!(parse_schedstat(&data.replace("version 17", "version 18")).is_err());
1620 }
1621
1622 #[test]
1623 fn test_capacity_domain_selection() {
1624 set_cpumask_test_width(8);
1625 let domains = vec![
1626 mask_from_bits(8, &[0, 1]),
1627 mask_from_bits(8, &[0, 1, 2, 3]),
1628 mask_from_bits(8, &[0, 1, 2, 3, 4, 5, 6, 7]),
1629 ];
1630 let capacities = (0..8)
1631 .map(|cpu| (cpu, if cpu < 4 { 512 } else { 1024 }))
1632 .collect();
1633 assert_eq!(smallest_full_capacity_span(&domains, &capacities), 8);
1634
1635 let capacities = (0..8).map(|cpu| (cpu, 1024)).collect();
1636 assert_eq!(smallest_full_capacity_span(&domains, &capacities), 0);
1637 }
1638
1639 #[test]
1640 fn test_topology_sched_domain_fallback() {
1641 let (mut topo, _) = make_test_topo(2, 1, 2, 1);
1642 topo.sched_domains.set(None).unwrap();
1643 topo.nodes.get_mut(&0).unwrap().distance = vec![10, 100];
1644 topo.nodes.get_mut(&1).unwrap().distance = vec![100, 10];
1645
1646 let info = topo.sched_domain_info(0).unwrap();
1647 assert_eq!(info.source, SchedDomainSource::Topology);
1648 assert_eq!(info.fork_span, 2);
1649 assert_eq!(info.wake_affine_span, 2);
1650 assert_eq!(info.asym_capacity_span, 0);
1651 }
1652
1653 #[test]
1654 fn test_grid_2node_2llc_3core_2ht() {
1655 let (topo, total) = make_test_topo(2, 2, 3, 2);
1657 assert_eq!(total, 24);
1658
1659 let cpumask = mask_from_bits(total, &[1, 2, 3, 12]);
1667
1668 let output = grid_output(&topo, &cpumask);
1669 assert!(output.contains("N0 L00:"));
1672 assert!(output.contains("N1 L02:"));
1673 assert!(output.contains("▄█░|░░░"));
1675 assert!(output.contains("▀░░|░░░"));
1677
1678 assert_eq!(topo.cpumask_nr_cores(&cpumask), 3);
1680 }
1681
1682 #[test]
1683 fn test_grid_empty_cpumask() {
1684 let (topo, total) = make_test_topo(1, 2, 3, 2);
1685 let cpumask = mask_from_bits(total, &[]);
1686 let output = grid_output(&topo, &cpumask);
1687 assert!(!output.contains('█'));
1689 assert!(!output.contains('▀'));
1690 assert!(!output.contains('▄'));
1691 assert!(output.contains('░'));
1692 assert_eq!(topo.cpumask_nr_cores(&cpumask), 0);
1693 }
1694
1695 #[test]
1696 fn test_grid_full_cpumask() {
1697 let (topo, total) = make_test_topo(1, 2, 3, 2);
1698 let cpumask = mask_from_bits(total, &(0..total).collect::<Vec<_>>());
1699 let output = grid_output(&topo, &cpumask);
1700 assert!(!output.contains('░'));
1702 assert!(!output.contains('▀'));
1703 assert!(!output.contains('▄'));
1704 assert!(output.contains('█'));
1705 assert_eq!(topo.cpumask_nr_cores(&cpumask), 6);
1706 }
1707
1708 #[test]
1709 fn test_grid_mixed_ht() {
1710 let (topo, total) = make_test_topo(1, 1, 4, 2);
1712 let cpumask = mask_from_bits(total, &[0, 3, 4, 5]);
1715 let output = grid_output(&topo, &cpumask);
1716 assert!(output.contains('▀'));
1717 assert!(output.contains('▄'));
1718 assert!(output.contains('█'));
1719 assert!(output.contains('░'));
1720 }
1721
1722 #[test]
1723 fn test_grid_single_node() {
1724 let (topo, total) = make_test_topo(1, 1, 2, 2);
1725 let cpumask = mask_from_bits(total, &[0, 1]);
1726 let output = grid_output(&topo, &cpumask);
1727 assert!(output.contains("N0 L00:"));
1728 assert!(!output.contains("N1"));
1729 }
1730
1731 #[test]
1732 fn test_grid_overflow_wrap() {
1733 let (topo, total) = make_test_topo(1, 12, 4, 2);
1736 let cpumask = mask_from_bits(total, &[0]);
1737 let mut buf = Vec::new();
1738 topo.format_cpumask_grid(&mut buf, &cpumask, " ", 60)
1739 .unwrap();
1740 let output = String::from_utf8(buf).unwrap();
1741 let lines: Vec<&str> = output.lines().collect();
1743 assert!(
1744 lines.len() > 1,
1745 "Expected wrapping with narrow width, got {} lines",
1746 lines.len()
1747 );
1748 }
1749
1750 #[test]
1751 fn test_grid_smt_off() {
1752 let (topo, total) = make_test_topo(1, 1, 4, 1);
1754 let cpumask = mask_from_bits(total, &[0, 2]);
1756 let output = grid_output(&topo, &cpumask);
1757 assert!(output.contains('█'));
1759 assert!(output.contains('░'));
1760 assert!(!output.contains('▀'));
1761 assert!(!output.contains('▄'));
1762 }
1763
1764 #[test]
1765 fn test_grid_4way_smt() {
1766 let (topo, total) = make_test_topo(1, 1, 2, 4);
1768 let cpumask = mask_from_bits(total, &[0, 1, 2, 3, 4, 5]);
1771 let output = grid_output(&topo, &cpumask);
1772 assert!(output.contains('█')); assert!(output.contains('▄')); }
1775
1776 #[test]
1777 fn test_cpumask_header() {
1778 let (topo, total) = make_test_topo(1, 1, 4, 2);
1779 let cpumask = mask_from_bits(total, &[0, 1, 2]);
1781 let header = topo.format_cpumask_header(&cpumask, 5, 10);
1782 assert!(header.contains("cpus= 3( 2c)"));
1783 assert!(header.contains("[ 5, 10]"));
1784 }
1785}