1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub struct Topology {
13 pub llcs: u32,
16 pub cores_per_llc: u32,
18 pub threads_per_core: u32,
20 pub numa_nodes: u32,
22 pub nodes: Option<&'static [NumaNode]>,
27 pub distances: Option<&'static NumaDistance>,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39pub struct NumaNode {
40 pub llcs: u32,
42 pub memory_mib: u32,
44 pub latency_ns: Option<u32>,
50 pub bandwidth_mbs: Option<u32>,
55 pub mem_side_cache: Option<MemSideCache>,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69pub struct MemSideCache {
70 pub size: u64,
72 pub associativity: u8,
75 pub write_policy: u8,
78 pub line_size: u16,
80}
81
82impl MemSideCache {
83 pub const fn new(size: u64, associativity: u8, write_policy: u8, line_size: u16) -> Self {
90 assert!(
91 associativity <= 15,
92 "MemSideCache: associativity must fit in 4 bits (0-15)"
93 );
94 assert!(
95 write_policy <= 15,
96 "MemSideCache: write_policy must fit in 4 bits (0-15)"
97 );
98 Self {
99 size,
100 associativity,
101 write_policy,
102 line_size,
103 }
104 }
105
106 pub fn validate(&self) -> Result<(), String> {
108 if self.associativity > 15 {
109 return Err(format!(
110 "associativity {} exceeds 4-bit maximum (15)",
111 self.associativity
112 ));
113 }
114 if self.write_policy > 15 {
115 return Err(format!(
116 "write_policy {} exceeds 4-bit maximum (15)",
117 self.write_policy
118 ));
119 }
120 Ok(())
121 }
122}
123
124impl NumaNode {
125 pub const fn new(llcs: u32, memory_mib: u32) -> Self {
127 Self {
128 llcs,
129 memory_mib,
130 latency_ns: None,
131 bandwidth_mbs: None,
132 mem_side_cache: None,
133 }
134 }
135
136 pub const fn with_hmat(
138 llcs: u32,
139 memory_mib: u32,
140 latency_ns: u32,
141 bandwidth_mbs: u32,
142 ) -> Self {
143 Self {
144 llcs,
145 memory_mib,
146 latency_ns: Some(latency_ns),
147 bandwidth_mbs: Some(bandwidth_mbs),
148 mem_side_cache: None,
149 }
150 }
151
152 pub const fn cache(mut self, cache: MemSideCache) -> Self {
154 self.mem_side_cache = Some(cache);
155 self
156 }
157
158 pub const fn is_memory_only(&self) -> bool {
160 self.llcs == 0
161 }
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
172pub struct NumaDistance {
173 n: u32,
175 entries: &'static [u8],
177}
178
179impl NumaDistance {
180 pub const fn new(n: u32, entries: &'static [u8]) -> Self {
191 assert!(n > 0, "NumaDistance: n must be > 0");
192 let expected = (n as usize) * (n as usize);
193 assert!(
194 entries.len() == expected,
195 "NumaDistance: entries.len() must equal n * n"
196 );
197 Self::validate_entries(n, entries);
198 Self { n, entries }
199 }
200
201 const fn validate_entries(n: u32, entries: &[u8]) {
202 let dim = n as usize;
203 let mut i = 0;
204 while i < dim {
205 let mut j = 0;
206 while j < dim {
207 let idx = i * dim + j;
208 if i == j {
209 assert!(
210 entries[idx] == 10,
211 "NumaDistance: diagonal entry must be 10"
212 );
213 } else {
214 assert!(
215 entries[idx] > 10,
216 "NumaDistance: off-diagonal entry must be > 10"
217 );
218 let sym_idx = j * dim + i;
219 assert!(
220 entries[idx] == entries[sym_idx],
221 "NumaDistance: matrix must be symmetric"
222 );
223 }
224 j += 1;
225 }
226 i += 1;
227 }
228 }
229
230 pub fn validate(&self) -> Result<(), String> {
232 if self.n == 0 {
233 return Err("n must be > 0".into());
234 }
235 let expected = (self.n as usize) * (self.n as usize);
236 if self.entries.len() != expected {
237 return Err(format!(
238 "entries.len() ({}) must equal n * n ({})",
239 self.entries.len(),
240 expected
241 ));
242 }
243 let dim = self.n as usize;
244 for i in 0..dim {
245 for j in 0..dim {
246 let v = self.entries[i * dim + j];
247 if i == j {
248 if v != 10 {
249 return Err(format!("diagonal entry [{i}][{j}] is {v}, must be 10"));
250 }
251 } else {
252 if v <= 10 {
253 return Err(format!(
254 "off-diagonal entry [{i}][{j}] is {v}, must be > 10"
255 ));
256 }
257 let sym = self.entries[j * dim + i];
258 if v != sym {
259 return Err(format!("asymmetric: [{i}][{j}]={v} != [{j}][{i}]={sym}"));
260 }
261 }
262 }
263 }
264 Ok(())
265 }
266
267 pub const fn dimension(&self) -> u32 {
269 self.n
270 }
271
272 pub const fn distance(&self, i: u32, j: u32) -> u8 {
274 self.entries[(i as usize) * (self.n as usize) + (j as usize)]
275 }
276
277 pub const fn entries(&self) -> &[u8] {
279 self.entries
280 }
281}
282
283impl std::fmt::Display for Topology {
286 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287 write!(
288 f,
289 "{}n{}l{}c{}t",
290 self.numa_nodes, self.llcs, self.cores_per_llc, self.threads_per_core,
291 )
292 }
293}
294
295#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
300pub enum TopologyParseError {
301 #[error(
304 "topology string {0:?} does not match \
305 {{numa}}n{{llcs}}l{{cores}}c{{threads}}t — e.g. \"1n2l4c2t\""
306 )]
307 BadFormat(String),
308 #[error("topology component {0:?} must be a positive integer")]
311 BadComponent(String),
312 #[error("llcs ({llcs}) must be a positive multiple of numa_nodes ({numa_nodes})")]
315 NotMultiple { llcs: u32, numa_nodes: u32 },
316 #[error("topology total CPU count overflows u32")]
320 Overflow,
321}
322
323impl std::str::FromStr for Topology {
338 type Err = TopologyParseError;
339
340 fn from_str(s: &str) -> Result<Self, Self::Err> {
341 let bad_format = || TopologyParseError::BadFormat(s.to_string());
342 let (numa_str, rest) = s.split_once('n').ok_or_else(bad_format)?;
343 let (llcs_str, rest) = rest.split_once('l').ok_or_else(bad_format)?;
344 let (cores_str, rest) = rest.split_once('c').ok_or_else(bad_format)?;
345 let (threads_str, tail) = rest.split_once('t').ok_or_else(bad_format)?;
346 if !tail.is_empty() {
347 return Err(bad_format());
348 }
349 let parse = |c: &str| -> Result<u32, TopologyParseError> {
350 let v: u32 = c
351 .parse()
352 .map_err(|_| TopologyParseError::BadComponent(c.to_string()))?;
353 if v == 0 {
354 return Err(TopologyParseError::BadComponent(c.to_string()));
355 }
356 Ok(v)
357 };
358 let numa_nodes = parse(numa_str)?;
359 let llcs = parse(llcs_str)?;
360 let cores_per_llc = parse(cores_str)?;
361 let threads_per_core = parse(threads_str)?;
362 if !llcs.is_multiple_of(numa_nodes) {
363 return Err(TopologyParseError::NotMultiple { llcs, numa_nodes });
364 }
365 let cpus_per_llc = cores_per_llc
366 .checked_mul(threads_per_core)
367 .ok_or(TopologyParseError::Overflow)?;
368 llcs.checked_mul(cpus_per_llc)
369 .ok_or(TopologyParseError::Overflow)?;
370 Ok(Topology {
371 llcs,
372 cores_per_llc,
373 threads_per_core,
374 numa_nodes,
375 nodes: None,
376 distances: None,
377 })
378 }
379}
380
381impl Topology {
382 pub const DEFAULT_FOR_PAYLOAD: Topology = Topology {
390 llcs: 1,
391 cores_per_llc: 2,
392 threads_per_core: 1,
393 numa_nodes: 1,
394 nodes: None,
395 distances: None,
396 };
397
398 pub const fn new(
414 numa_nodes: u32,
415 llcs: u32,
416 cores_per_llc: u32,
417 threads_per_core: u32,
418 ) -> Self {
419 assert!(llcs > 0, "invalid Topology: llcs must be > 0");
420 assert!(
421 cores_per_llc > 0,
422 "invalid Topology: cores_per_llc must be > 0"
423 );
424 assert!(
425 threads_per_core > 0,
426 "invalid Topology: threads_per_core must be > 0"
427 );
428 assert!(numa_nodes > 0, "invalid Topology: numa_nodes must be > 0");
429 assert!(
430 llcs.is_multiple_of(numa_nodes),
431 "invalid Topology: llcs must be divisible by numa_nodes"
432 );
433 let cpus_per_llc = match cores_per_llc.checked_mul(threads_per_core) {
434 Some(v) => v,
435 None => panic!("invalid Topology: total CPU count overflows u32"),
436 };
437 match llcs.checked_mul(cpus_per_llc) {
438 Some(_) => {}
439 None => panic!("invalid Topology: total CPU count overflows u32"),
440 };
441 Topology {
442 llcs,
443 cores_per_llc,
444 threads_per_core,
445 numa_nodes,
446 nodes: None,
447 distances: None,
448 }
449 }
450
451 pub const fn with_nodes(
467 cores_per_llc: u32,
468 threads_per_core: u32,
469 nodes: &'static [NumaNode],
470 ) -> Self {
471 assert!(
472 !nodes.is_empty(),
473 "invalid Topology: nodes must not be empty"
474 );
475 assert!(
476 cores_per_llc > 0,
477 "invalid Topology: cores_per_llc must be > 0"
478 );
479 assert!(
480 threads_per_core > 0,
481 "invalid Topology: threads_per_core must be > 0"
482 );
483
484 let mut llcs: u32 = 0;
485 let mut i = 0;
486 while i < nodes.len() {
487 llcs = match llcs.checked_add(nodes[i].llcs) {
488 Some(v) => v,
489 None => panic!("invalid Topology: node LLC sum overflows u32"),
490 };
491 assert!(
492 !(nodes[i].llcs > 0 && nodes[i].memory_mib == 0),
493 "invalid Topology: CPU-bearing node has zero memory"
494 );
495 i += 1;
496 }
497 assert!(llcs > 0, "invalid Topology: total LLCs must be > 0");
498
499 let cpus_per_llc = match cores_per_llc.checked_mul(threads_per_core) {
500 Some(v) => v,
501 None => panic!("invalid Topology: total CPU count overflows u32"),
502 };
503 match llcs.checked_mul(cpus_per_llc) {
504 Some(_) => {}
505 None => panic!("invalid Topology: total CPU count overflows u32"),
506 };
507
508 Topology {
509 llcs,
510 cores_per_llc,
511 threads_per_core,
512 numa_nodes: nodes.len() as u32,
513 nodes: Some(nodes),
514 distances: None,
515 }
516 }
517
518 pub const fn distances(mut self, distances: &'static NumaDistance) -> Self {
525 assert!(
526 distances.n == self.numa_nodes,
527 "invalid Topology: NumaDistance dimension must equal numa_nodes"
528 );
529 self.distances = Some(distances);
530 self
531 }
532
533 pub fn validate(&self) -> Result<(), String> {
538 if self.llcs == 0 {
539 return Err("llcs must be > 0".into());
540 }
541 if self.cores_per_llc == 0 {
542 return Err("cores_per_llc must be > 0".into());
543 }
544 if self.threads_per_core == 0 {
545 return Err("threads_per_core must be > 0".into());
546 }
547 if self.numa_nodes == 0 {
548 return Err("numa_nodes must be > 0".into());
549 }
550 if self
551 .cores_per_llc
552 .checked_mul(self.threads_per_core)
553 .and_then(|x| self.llcs.checked_mul(x))
554 .is_none()
555 {
556 return Err("total CPU count overflows u32".into());
557 }
558 match &self.nodes {
559 None => {
560 if !self.llcs.is_multiple_of(self.numa_nodes) {
561 return Err(format!(
562 "llcs ({}) must be divisible by numa_nodes ({})",
563 self.llcs, self.numa_nodes,
564 ));
565 }
566 }
567 Some(nodes) => {
568 if nodes.len() != self.numa_nodes as usize {
569 return Err(format!(
570 "nodes.len() ({}) must equal numa_nodes ({})",
571 nodes.len(),
572 self.numa_nodes,
573 ));
574 }
575 let llc_sum: u32 = nodes.iter().map(|n| n.llcs).sum();
576 if llc_sum != self.llcs {
577 return Err(format!(
578 "sum of node LLCs ({llc_sum}) must equal total llcs ({})",
579 self.llcs,
580 ));
581 }
582 for (i, node) in nodes.iter().enumerate() {
583 if node.llcs > 0 && node.memory_mib == 0 {
584 return Err(format!("node {i} has {} LLCs but zero memory", node.llcs,));
585 }
586 }
587 }
588 }
589 if let Some(d) = &self.distances {
590 if d.n != self.numa_nodes {
591 return Err(format!(
592 "NumaDistance dimension ({}) must equal numa_nodes ({})",
593 d.n, self.numa_nodes,
594 ));
595 }
596 d.validate()?;
597 }
598 Ok(())
599 }
600
601 pub fn total_cpus(&self) -> u32 {
603 self.llcs * self.cores_per_llc * self.threads_per_core
604 }
605
606 pub fn num_llcs(&self) -> u32 {
608 self.llcs
609 }
610
611 pub fn num_numa_nodes(&self) -> u32 {
613 self.numa_nodes
614 }
615
616 pub fn llcs_in_node(&self, node_id: u32) -> u32 {
621 match &self.nodes {
622 Some(nodes) => nodes[node_id as usize].llcs,
623 None => self.llcs / self.numa_nodes,
624 }
625 }
626
627 pub fn llcs_per_numa_node(&self) -> u32 {
636 assert!(
637 self.nodes.is_none(),
638 "llcs_per_numa_node() requires uniform topology; use llcs_in_node() instead"
639 );
640 assert!(self.numa_nodes > 0, "numa_nodes must be > 0");
641 assert!(
642 self.llcs.is_multiple_of(self.numa_nodes),
643 "llcs ({}) must be divisible by numa_nodes ({})",
644 self.llcs,
645 self.numa_nodes,
646 );
647 self.llcs / self.numa_nodes
648 }
649
650 pub fn numa_node_of(&self, llc_id: u32) -> u32 {
660 match &self.nodes {
661 Some(nodes) => {
662 let mut cumulative: u32 = 0;
663 for (i, node) in nodes.iter().enumerate() {
664 cumulative += node.llcs;
665 if llc_id < cumulative {
666 return i as u32;
667 }
668 }
669 (nodes.len() - 1) as u32
670 }
671 None => {
672 let per_node = self.llcs / self.numa_nodes;
673 llc_id / per_node
674 }
675 }
676 }
677
678 pub fn first_llc_in_node(&self, node_id: u32) -> u32 {
688 match &self.nodes {
689 Some(nodes) => {
690 let mut offset: u32 = 0;
691 for i in 0..node_id as usize {
692 offset += nodes[i].llcs;
693 }
694 offset
695 }
696 None => {
697 let per_node = self.llcs / self.numa_nodes;
698 node_id * per_node
699 }
700 }
701 }
702
703 pub fn node_memory_mib(&self, node_id: u32) -> Option<u32> {
709 self.nodes.map(|nodes| nodes[node_id as usize].memory_mib)
710 }
711
712 pub fn total_node_memory_mib(&self) -> Option<u32> {
714 self.nodes
715 .map(|nodes| nodes.iter().map(|n| n.memory_mib).sum())
716 }
717
718 pub fn distance(&self, i: u32, j: u32) -> u8 {
723 match &self.distances {
724 Some(d) => d.distance(i, j),
725 None => {
726 if i == j {
727 10
728 } else {
729 20
730 }
731 }
732 }
733 }
734
735 pub fn has_memory_only_nodes(&self) -> bool {
737 self.nodes
738 .is_some_and(|nodes| nodes.iter().any(|n| n.is_memory_only()))
739 }
740
741 pub fn cpu_bearing_nodes(&self) -> u32 {
743 match &self.nodes {
744 Some(nodes) => nodes.iter().filter(|n| !n.is_memory_only()).count() as u32,
745 None => self.numa_nodes,
746 }
747 }
748
749 pub fn decompose(&self, cpu_id: u32) -> (u32, u32, u32) {
751 let threads = self.threads_per_core;
752 let cores = self.cores_per_llc;
753 let thread_id = cpu_id % threads;
754 let core_id = (cpu_id / threads) % cores;
755 let llc_id = cpu_id / (threads * cores);
756 (llc_id, core_id, thread_id)
757 }
758}
759
760#[cfg(test)]
761mod tests {
762 use super::*;
763
764 #[test]
765 fn topology_total_cpus() {
766 let t = Topology {
767 llcs: 2,
768 cores_per_llc: 4,
769 threads_per_core: 2,
770 numa_nodes: 1,
771 nodes: None,
772 distances: None,
773 };
774 assert_eq!(t.total_cpus(), 16);
775 }
776
777 #[test]
778 fn topology_num_llcs() {
779 let t = Topology {
780 llcs: 3,
781 cores_per_llc: 4,
782 threads_per_core: 2,
783 numa_nodes: 1,
784 nodes: None,
785 distances: None,
786 };
787 assert_eq!(t.num_llcs(), 3);
788 }
789
790 #[test]
791 fn decompose_simple() {
792 let t = Topology {
793 llcs: 2,
794 cores_per_llc: 2,
795 threads_per_core: 2,
796 numa_nodes: 1,
797 nodes: None,
798 distances: None,
799 };
800 assert_eq!(t.decompose(0), (0, 0, 0));
801 assert_eq!(t.decompose(1), (0, 0, 1));
802 assert_eq!(t.decompose(2), (0, 1, 0));
803 assert_eq!(t.decompose(3), (0, 1, 1));
804 assert_eq!(t.decompose(4), (1, 0, 0));
805 assert_eq!(t.decompose(5), (1, 0, 1));
806 assert_eq!(t.decompose(6), (1, 1, 0));
807 assert_eq!(t.decompose(7), (1, 1, 1));
808 }
809
810 #[test]
811 fn decompose_no_smt() {
812 let t = Topology {
813 llcs: 2,
814 cores_per_llc: 4,
815 threads_per_core: 1,
816 numa_nodes: 1,
817 nodes: None,
818 distances: None,
819 };
820 assert_eq!(t.decompose(0), (0, 0, 0));
821 assert_eq!(t.decompose(3), (0, 3, 0));
822 assert_eq!(t.decompose(4), (1, 0, 0));
823 assert_eq!(t.decompose(7), (1, 3, 0));
824 }
825
826 #[test]
827 fn decompose_single_llc() {
828 let t = Topology {
829 llcs: 1,
830 cores_per_llc: 4,
831 threads_per_core: 1,
832 numa_nodes: 1,
833 nodes: None,
834 distances: None,
835 };
836 assert_eq!(t.decompose(0), (0, 0, 0));
837 assert_eq!(t.decompose(3), (0, 3, 0));
838 }
839
840 #[test]
841 fn numa_node_of_single_node() {
842 let t = Topology {
843 llcs: 4,
844 cores_per_llc: 2,
845 threads_per_core: 1,
846 numa_nodes: 1,
847 nodes: None,
848 distances: None,
849 };
850 for llc in 0..4 {
851 assert_eq!(t.numa_node_of(llc), 0);
852 }
853 }
854
855 #[test]
856 fn numa_node_of_two_nodes() {
857 let t = Topology {
858 llcs: 4,
859 cores_per_llc: 2,
860 threads_per_core: 1,
861 numa_nodes: 2,
862 nodes: None,
863 distances: None,
864 };
865 assert_eq!(t.llcs_per_numa_node(), 2);
866 assert_eq!(t.numa_node_of(0), 0);
867 assert_eq!(t.numa_node_of(1), 0);
868 assert_eq!(t.numa_node_of(2), 1);
869 assert_eq!(t.numa_node_of(3), 1);
870 }
871
872 #[test]
873 fn numa_node_of_out_of_bounds_explicit_saturates() {
874 static TWO: [NumaNode; 2] = [NumaNode::new(2, 512), NumaNode::new(2, 512)];
875 let t = Topology::with_nodes(2, 1, &TWO);
876 assert_eq!(t.numa_node_of(4), 1);
878 assert_eq!(t.numa_node_of(999), 1);
879 }
880
881 #[test]
882 fn numa_node_of_out_of_bounds_uniform_no_check() {
883 let t = Topology::new(2, 4, 2, 1);
886 assert_eq!(t.numa_node_of(100), 50);
887 assert!(t.numa_node_of(100) > t.numa_nodes - 1);
888 }
889
890 #[test]
891 fn num_numa_nodes() {
892 let t = Topology {
893 llcs: 6,
894 cores_per_llc: 4,
895 threads_per_core: 2,
896 numa_nodes: 3,
897 nodes: None,
898 distances: None,
899 };
900 assert_eq!(t.num_numa_nodes(), 3);
901 assert_eq!(t.llcs_per_numa_node(), 2);
902 }
903
904 #[test]
905 #[should_panic(expected = "llcs_per_numa_node() requires uniform topology")]
906 fn topology_llcs_per_numa_node_panics_on_explicit_nodes() {
907 static EXPLICIT: [NumaNode; 2] = [NumaNode::new(2, 512), NumaNode::new(2, 512)];
908 let t = Topology::with_nodes(2, 1, &EXPLICIT);
909 t.llcs_per_numa_node();
910 }
911
912 #[test]
913 #[should_panic(expected = "numa_nodes must be > 0")]
914 fn topology_llcs_per_numa_node_panics_on_zero_numa() {
915 let t = Topology {
917 llcs: 2,
918 cores_per_llc: 1,
919 threads_per_core: 1,
920 numa_nodes: 0,
921 nodes: None,
922 distances: None,
923 };
924 t.llcs_per_numa_node();
925 }
926
927 #[test]
928 #[should_panic(expected = "must be divisible by numa_nodes")]
929 fn topology_llcs_per_numa_node_panics_on_indivisible() {
930 let t = Topology {
932 llcs: 3,
933 cores_per_llc: 1,
934 threads_per_core: 1,
935 numa_nodes: 2,
936 nodes: None,
937 distances: None,
938 };
939 t.llcs_per_numa_node();
940 }
941
942 #[test]
943 fn new_valid() {
944 let t = Topology::new(2, 4, 2, 2);
945 assert_eq!(t.numa_nodes, 2);
946 assert_eq!(t.llcs, 4);
947 assert_eq!(t.cores_per_llc, 2);
948 assert_eq!(t.threads_per_core, 2);
949 assert!(t.nodes.is_none());
950 assert!(t.distances.is_none());
951 }
952
953 #[test]
954 fn new_single_everything() {
955 let t = Topology::new(1, 1, 1, 1);
956 assert_eq!(t.total_cpus(), 1);
957 }
958
959 #[test]
960 fn validate_valid() {
961 let t = Topology {
962 llcs: 4,
963 cores_per_llc: 2,
964 threads_per_core: 2,
965 numa_nodes: 2,
966 nodes: None,
967 distances: None,
968 };
969 assert!(t.validate().is_ok());
970 }
971
972 #[test]
973 fn validate_zero_llcs() {
974 let t = Topology {
975 llcs: 0,
976 cores_per_llc: 2,
977 threads_per_core: 1,
978 numa_nodes: 1,
979 nodes: None,
980 distances: None,
981 };
982 let err = t.validate().unwrap_err();
983 assert!(err.contains("llcs must be > 0"), "got: {err}");
984 }
985
986 #[test]
987 fn validate_zero_cores() {
988 let t = Topology {
989 llcs: 1,
990 cores_per_llc: 0,
991 threads_per_core: 1,
992 numa_nodes: 1,
993 nodes: None,
994 distances: None,
995 };
996 let err = t.validate().unwrap_err();
997 assert!(err.contains("cores_per_llc must be > 0"), "got: {err}");
998 }
999
1000 #[test]
1001 fn validate_zero_threads() {
1002 let t = Topology {
1003 llcs: 1,
1004 cores_per_llc: 2,
1005 threads_per_core: 0,
1006 numa_nodes: 1,
1007 nodes: None,
1008 distances: None,
1009 };
1010 let err = t.validate().unwrap_err();
1011 assert!(err.contains("threads_per_core must be > 0"), "got: {err}");
1012 }
1013
1014 #[test]
1015 fn validate_zero_numa_nodes() {
1016 let t = Topology {
1017 llcs: 1,
1018 cores_per_llc: 2,
1019 threads_per_core: 1,
1020 numa_nodes: 0,
1021 nodes: None,
1022 distances: None,
1023 };
1024 let err = t.validate().unwrap_err();
1025 assert!(err.contains("numa_nodes must be > 0"), "got: {err}");
1026 }
1027
1028 #[test]
1029 fn validate_llcs_not_divisible_by_numa() {
1030 let t = Topology {
1031 llcs: 3,
1032 cores_per_llc: 2,
1033 threads_per_core: 1,
1034 numa_nodes: 2,
1035 nodes: None,
1036 distances: None,
1037 };
1038 let err = t.validate().unwrap_err();
1039 assert!(err.contains("divisible"), "got: {err}");
1040 }
1041
1042 #[test]
1043 #[should_panic(expected = "invalid Topology: numa_nodes must be > 0")]
1044 fn topology_new_panics_on_zero_numa() {
1045 Topology::new(0, 2, 1, 1);
1046 }
1047
1048 #[test]
1049 #[should_panic(expected = "invalid Topology: llcs must be > 0")]
1050 fn topology_new_panics_on_zero_llcs() {
1051 Topology::new(1, 0, 2, 1);
1052 }
1053
1054 #[test]
1055 #[should_panic(expected = "invalid Topology: cores_per_llc must be > 0")]
1056 fn topology_new_panics_on_zero_cores() {
1057 Topology::new(1, 1, 0, 1);
1058 }
1059
1060 #[test]
1061 #[should_panic(expected = "invalid Topology: threads_per_core must be > 0")]
1062 fn topology_new_panics_on_zero_threads() {
1063 Topology::new(1, 1, 2, 0);
1064 }
1065
1066 #[test]
1067 #[should_panic(expected = "invalid Topology: llcs must be divisible by numa_nodes")]
1068 fn topology_new_panics_on_indivisible() {
1069 Topology::new(2, 3, 2, 1);
1070 }
1071
1072 #[test]
1073 #[should_panic(expected = "invalid Topology: total CPU count overflows u32")]
1074 fn topology_new_panics_on_llcs_times_cpus_overflow() {
1075 Topology::new(1, 65536, 65536, 2);
1077 }
1078
1079 #[test]
1080 #[should_panic(expected = "invalid Topology: total CPU count overflows u32")]
1081 fn topology_new_panics_on_cores_times_threads_overflow() {
1082 Topology::new(1, 1, 65_536, 65_536);
1085 }
1086
1087 #[test]
1088 fn validate_overflow() {
1089 let t = Topology {
1090 llcs: 65536,
1091 cores_per_llc: 65536,
1092 threads_per_core: 2,
1093 numa_nodes: 1,
1094 nodes: None,
1095 distances: None,
1096 };
1097 let err = t.validate().unwrap_err();
1098 assert!(err.contains("overflows"), "got: {err}");
1099 }
1100
1101 #[test]
1102 fn display_format() {
1103 let t = Topology {
1104 llcs: 2,
1105 cores_per_llc: 4,
1106 threads_per_core: 2,
1107 numa_nodes: 1,
1108 nodes: None,
1109 distances: None,
1110 };
1111 assert_eq!(t.to_string(), "1n2l4c2t");
1112 }
1113
1114 #[test]
1115 fn display_format_multi_numa() {
1116 let t = Topology {
1117 llcs: 4,
1118 cores_per_llc: 8,
1119 threads_per_core: 2,
1120 numa_nodes: 2,
1121 nodes: None,
1122 distances: None,
1123 };
1124 assert_eq!(t.to_string(), "2n4l8c2t");
1125 }
1126
1127 #[test]
1130 fn numa_node_memory_only() {
1131 let n = NumaNode::new(0, 1024);
1132 assert!(n.is_memory_only());
1133 }
1134
1135 #[test]
1136 fn numa_node_with_cpus() {
1137 let n = NumaNode::new(2, 512);
1138 assert!(!n.is_memory_only());
1139 }
1140
1141 #[test]
1144 fn numa_distance_single_node() {
1145 static D: NumaDistance = NumaDistance::new(1, &[10]);
1146 assert_eq!(D.dimension(), 1);
1147 assert_eq!(D.distance(0, 0), 10);
1148 }
1149
1150 #[test]
1151 fn numa_distance_two_nodes() {
1152 static D: NumaDistance = NumaDistance::new(2, &[10, 20, 20, 10]);
1153 assert_eq!(D.dimension(), 2);
1154 assert_eq!(D.distance(0, 0), 10);
1155 assert_eq!(D.distance(0, 1), 20);
1156 assert_eq!(D.distance(1, 0), 20);
1157 assert_eq!(D.distance(1, 1), 10);
1158 }
1159
1160 #[test]
1161 fn numa_distance_three_nodes_varied_weights() {
1162 static D: NumaDistance = NumaDistance::new(3, &[10, 20, 30, 20, 10, 40, 30, 40, 10]);
1163 assert_eq!(D.distance(0, 2), 30);
1164 assert_eq!(D.distance(1, 2), 40);
1165 }
1166
1167 #[test]
1168 fn numa_distance_validate_ok() {
1169 let d = NumaDistance {
1170 n: 2,
1171 entries: &[10, 20, 20, 10],
1172 };
1173 assert!(d.validate().is_ok());
1174 }
1175
1176 #[test]
1177 fn numa_distance_validate_bad_diagonal() {
1178 let d = NumaDistance {
1179 n: 2,
1180 entries: &[11, 20, 20, 10],
1181 };
1182 let err = d.validate().unwrap_err();
1183 assert!(err.contains("diagonal"), "got: {err}");
1184 }
1185
1186 #[test]
1187 fn numa_distance_validate_bad_offdiag() {
1188 let d = NumaDistance {
1189 n: 2,
1190 entries: &[10, 10, 10, 10],
1191 };
1192 let err = d.validate().unwrap_err();
1193 assert!(err.contains("off-diagonal"), "got: {err}");
1194 }
1195
1196 #[test]
1197 fn numa_distance_validate_asymmetric() {
1198 let d = NumaDistance {
1199 n: 2,
1200 entries: &[10, 20, 30, 10],
1201 };
1202 let err = d.validate().unwrap_err();
1203 assert!(err.contains("asymmetric"), "got: {err}");
1204 }
1205
1206 #[test]
1207 fn numa_distance_validate_wrong_size() {
1208 let d = NumaDistance {
1209 n: 2,
1210 entries: &[10, 20, 20],
1211 };
1212 let err = d.validate().unwrap_err();
1213 assert!(err.contains("n * n"), "got: {err}");
1214 }
1215
1216 static TWO_NODES: [NumaNode; 2] = [NumaNode::new(2, 512), NumaNode::new(2, 512)];
1219
1220 #[test]
1221 fn with_nodes_basic() {
1222 let t = Topology::with_nodes(4, 2, &TWO_NODES);
1223 assert_eq!(t.numa_nodes, 2);
1224 assert_eq!(t.llcs, 4);
1225 assert_eq!(t.total_cpus(), 32);
1226 assert!(t.nodes.is_some());
1227 }
1228
1229 #[test]
1230 fn with_nodes_numa_node_of() {
1231 let t = Topology::with_nodes(4, 2, &TWO_NODES);
1232 assert_eq!(t.numa_node_of(0), 0);
1233 assert_eq!(t.numa_node_of(1), 0);
1234 assert_eq!(t.numa_node_of(2), 1);
1235 assert_eq!(t.numa_node_of(3), 1);
1236 }
1237
1238 static ASYMMETRIC_NODES: [NumaNode; 2] = [NumaNode::new(1, 256), NumaNode::new(3, 768)];
1239
1240 #[test]
1241 fn with_nodes_asymmetric_llcs() {
1242 let t = Topology::with_nodes(2, 1, &ASYMMETRIC_NODES);
1243 assert_eq!(t.llcs_in_node(0), 1);
1244 assert_eq!(t.llcs_in_node(1), 3);
1245 assert_eq!(t.numa_node_of(0), 0);
1246 assert_eq!(t.numa_node_of(1), 1);
1247 assert_eq!(t.numa_node_of(2), 1);
1248 assert_eq!(t.numa_node_of(3), 1);
1249 assert_eq!(t.first_llc_in_node(0), 0);
1250 assert_eq!(t.first_llc_in_node(1), 1);
1251 }
1252
1253 #[test]
1254 fn with_nodes_memory() {
1255 let t = Topology::with_nodes(2, 1, &ASYMMETRIC_NODES);
1256 assert_eq!(t.node_memory_mib(0), Some(256));
1257 assert_eq!(t.node_memory_mib(1), Some(768));
1258 assert_eq!(t.total_node_memory_mib(), Some(1024));
1259 }
1260
1261 static CXL_NODES: [NumaNode; 3] = [
1264 NumaNode::new(2, 512),
1265 NumaNode::new(2, 512),
1266 NumaNode::new(0, 1024),
1267 ];
1268
1269 #[test]
1270 fn cxl_memory_only_node() {
1271 let t = Topology::with_nodes(4, 1, &CXL_NODES);
1272 assert_eq!(t.numa_nodes, 3);
1273 assert!(t.has_memory_only_nodes());
1274 assert_eq!(t.cpu_bearing_nodes(), 2);
1275 assert_eq!(t.llcs_in_node(2), 0);
1276 assert_eq!(t.node_memory_mib(2), Some(1024));
1277 }
1278
1279 #[test]
1280 fn cxl_first_llc_in_memory_only_node() {
1281 let t = Topology::with_nodes(4, 1, &CXL_NODES);
1282 assert_eq!(t.first_llc_in_node(0), 0);
1283 assert_eq!(t.first_llc_in_node(1), 2);
1284 assert_eq!(t.first_llc_in_node(2), 4);
1285 }
1286
1287 static CXL_MIDDLE: [NumaNode; 3] = [
1288 NumaNode::new(2, 512),
1289 NumaNode::new(0, 256),
1290 NumaNode::new(2, 512),
1291 ];
1292
1293 #[test]
1294 fn numa_node_of_cxl_middle_node() {
1295 let t = Topology::with_nodes(4, 1, &CXL_MIDDLE);
1296 assert_eq!(t.numa_nodes, 3);
1297 assert_eq!(t.numa_node_of(0), 0);
1298 assert_eq!(t.numa_node_of(1), 0);
1299 assert_eq!(t.numa_node_of(2), 2);
1300 assert_eq!(t.numa_node_of(3), 2);
1301 assert!(t.has_memory_only_nodes());
1302 assert_eq!(t.cpu_bearing_nodes(), 2);
1303 }
1304
1305 #[test]
1306 fn first_llc_in_cxl_middle_node() {
1307 let t = Topology::with_nodes(4, 1, &CXL_MIDDLE);
1308 assert_eq!(t.first_llc_in_node(0), 0);
1309 assert_eq!(t.first_llc_in_node(1), 2);
1310 assert_eq!(t.first_llc_in_node(2), 2);
1311 assert_eq!(t.llcs_in_node(1), 0);
1312 assert_eq!(t.llcs_in_node(2), 2);
1313 }
1314
1315 static CXL_FIRST: [NumaNode; 3] = [
1316 NumaNode::new(0, 256),
1317 NumaNode::new(2, 512),
1318 NumaNode::new(2, 512),
1319 ];
1320
1321 #[test]
1322 fn cxl_first_node_numa_node_of() {
1323 let t = Topology::with_nodes(4, 1, &CXL_FIRST);
1324 assert_eq!(t.numa_node_of(0), 1);
1325 assert_eq!(t.numa_node_of(1), 1);
1326 assert_eq!(t.numa_node_of(2), 2);
1327 assert_eq!(t.numa_node_of(3), 2);
1328 assert_eq!(t.first_llc_in_node(0), 0);
1329 assert_eq!(t.first_llc_in_node(1), 0);
1330 assert_eq!(t.first_llc_in_node(2), 2);
1331 }
1332
1333 static MULTI_CXL: [NumaNode; 4] = [
1334 NumaNode::new(2, 512),
1335 NumaNode::new(0, 256),
1336 NumaNode::new(0, 256),
1337 NumaNode::new(2, 512),
1338 ];
1339
1340 #[test]
1341 fn multiple_consecutive_cxl_nodes() {
1342 let t = Topology::with_nodes(4, 1, &MULTI_CXL);
1343 assert_eq!(t.numa_nodes, 4);
1344 assert_eq!(t.cpu_bearing_nodes(), 2);
1345 assert_eq!(t.numa_node_of(0), 0);
1346 assert_eq!(t.numa_node_of(1), 0);
1347 assert_eq!(t.numa_node_of(2), 3);
1348 assert_eq!(t.numa_node_of(3), 3);
1349 }
1350
1351 static ASYMMETRIC_HEAVY: [NumaNode; 2] = [NumaNode::new(1, 256), NumaNode::new(7, 1792)];
1352
1353 #[test]
1354 fn highly_asymmetric_llcs() {
1355 let t = Topology::with_nodes(2, 1, &ASYMMETRIC_HEAVY);
1356 assert_eq!(t.numa_node_of(0), 0);
1357 assert_eq!(t.numa_node_of(1), 1);
1358 assert_eq!(t.numa_node_of(7), 1);
1359 assert_eq!(t.llcs_in_node(0), 1);
1360 assert_eq!(t.llcs_in_node(1), 7);
1361 }
1362
1363 static CXL_DIST: NumaDistance = NumaDistance::new(3, &[10, 20, 30, 20, 10, 25, 30, 25, 10]);
1364
1365 #[test]
1366 fn distance_with_cxl_middle() {
1367 let t = Topology::with_nodes(4, 1, &CXL_MIDDLE).distances(&CXL_DIST);
1368 assert_eq!(t.distance(1, 2), 25);
1369 assert_eq!(t.distance(0, 1), 20);
1370 assert!(t.validate().is_ok());
1371 }
1372
1373 static DIST_2: NumaDistance = NumaDistance::new(2, &[10, 20, 20, 10]);
1376
1377 #[test]
1378 fn distances() {
1379 let t = Topology::new(2, 4, 2, 1).distances(&DIST_2);
1380 assert_eq!(t.distance(0, 0), 10);
1381 assert_eq!(t.distance(0, 1), 20);
1382 assert_eq!(t.distance(1, 0), 20);
1383 assert_eq!(t.distance(1, 1), 10);
1384 }
1385
1386 #[test]
1387 fn default_distances() {
1388 let t = Topology::new(2, 4, 2, 1);
1389 assert_eq!(t.distance(0, 0), 10);
1390 assert_eq!(t.distance(0, 1), 20);
1391 }
1392
1393 static DIST_3: NumaDistance = NumaDistance::new(3, &[10, 20, 30, 20, 10, 25, 30, 25, 10]);
1394
1395 #[test]
1396 fn with_nodes_then_distances() {
1397 let t = Topology::with_nodes(4, 1, &CXL_NODES).distances(&DIST_3);
1398 assert_eq!(t.distance(0, 2), 30);
1399 assert_eq!(t.distance(1, 2), 25);
1400 assert!(t.validate().is_ok());
1401 }
1402
1403 #[test]
1406 fn validate_with_nodes_ok() {
1407 let t = Topology::with_nodes(4, 2, &TWO_NODES);
1408 assert!(t.validate().is_ok());
1409 }
1410
1411 #[test]
1412 fn validate_with_nodes_llc_mismatch() {
1413 static BAD: [NumaNode; 2] = [NumaNode::new(1, 256), NumaNode::new(1, 256)];
1414 let t = Topology {
1415 llcs: 4,
1416 cores_per_llc: 2,
1417 threads_per_core: 1,
1418 numa_nodes: 2,
1419 nodes: Some(&BAD),
1420 distances: None,
1421 };
1422 let err = t.validate().unwrap_err();
1423 assert!(err.contains("sum of node LLCs"), "got: {err}");
1424 }
1425
1426 #[test]
1427 fn validate_with_nodes_count_mismatch() {
1428 let t = Topology {
1429 llcs: 4,
1430 cores_per_llc: 2,
1431 threads_per_core: 1,
1432 numa_nodes: 3,
1433 nodes: Some(&TWO_NODES),
1434 distances: None,
1435 };
1436 let err = t.validate().unwrap_err();
1437 assert!(err.contains("nodes.len()"), "got: {err}");
1438 }
1439
1440 #[test]
1441 fn validate_distance_dimension_mismatch() {
1442 static BAD_DIST: NumaDistance = NumaDistance::new(1, &[10]);
1443 let t = Topology {
1444 llcs: 4,
1445 cores_per_llc: 2,
1446 threads_per_core: 1,
1447 numa_nodes: 2,
1448 nodes: None,
1449 distances: Some(&BAD_DIST),
1450 };
1451 let err = t.validate().unwrap_err();
1452 assert!(err.contains("dimension"), "got: {err}");
1453 }
1454
1455 #[test]
1456 fn validate_cpu_node_zero_memory() {
1457 static BAD: [NumaNode; 2] = [NumaNode::new(2, 0), NumaNode::new(2, 512)];
1458 let t = Topology {
1459 llcs: 4,
1460 cores_per_llc: 2,
1461 threads_per_core: 1,
1462 numa_nodes: 2,
1463 nodes: Some(&BAD),
1464 distances: None,
1465 };
1466 let err = t.validate().unwrap_err();
1467 assert!(err.contains("zero memory"), "got: {err}");
1468 }
1469
1470 #[test]
1471 fn uniform_no_node_memory() {
1472 let t = Topology::new(2, 4, 2, 1);
1473 assert!(t.node_memory_mib(0).is_none());
1474 assert!(t.total_node_memory_mib().is_none());
1475 }
1476
1477 const _CONST_TOPO: Topology = Topology::new(1, 2, 4, 2);
1480
1481 static _CONST_NODES: [NumaNode; 2] = [NumaNode::new(1, 256), NumaNode::new(1, 256)];
1482 const _CONST_WITH_NODES: Topology = Topology::with_nodes(4, 2, &_CONST_NODES);
1483
1484 static _CONST_DIST: NumaDistance = NumaDistance::new(2, &[10, 20, 20, 10]);
1485 const _CONST_WITH_DISTANCES: Topology = Topology::new(2, 2, 4, 2).distances(&_CONST_DIST);
1486
1487 #[test]
1488 fn const_construction_valid() {
1489 assert!(_CONST_TOPO.validate().is_ok());
1490 assert!(_CONST_WITH_NODES.validate().is_ok());
1491 assert!(_CONST_WITH_DISTANCES.validate().is_ok());
1492 }
1493
1494 #[test]
1497 fn single_node_topology() {
1498 let t = Topology::new(1, 1, 1, 1);
1499 assert_eq!(t.total_cpus(), 1);
1500 assert_eq!(t.numa_node_of(0), 0);
1501 assert_eq!(t.distance(0, 0), 10);
1502 assert!(!t.has_memory_only_nodes());
1503 assert_eq!(t.cpu_bearing_nodes(), 1);
1504 }
1505
1506 #[test]
1507 fn first_llc_in_node_uniform() {
1508 let t = Topology::new(2, 4, 2, 1);
1509 assert_eq!(t.first_llc_in_node(0), 0);
1510 assert_eq!(t.first_llc_in_node(1), 2);
1511 }
1512
1513 #[test]
1514 fn first_llc_in_node_at_numa_nodes_returns_total() {
1515 let t = Topology::with_nodes(2, 1, &TWO_NODES);
1518 assert_eq!(t.first_llc_in_node(2), 4);
1519 }
1520
1521 #[test]
1522 #[should_panic(expected = "index out of bounds")]
1523 fn topology_first_llc_in_node_panics_on_index_above_numa_nodes() {
1524 let t = Topology::with_nodes(2, 1, &TWO_NODES);
1527 let _ = t.first_llc_in_node(3);
1528 }
1529
1530 #[test]
1533 fn topology_fromstr_display_roundtrip_uniform() {
1534 let t = Topology::new(2, 4, 8, 2);
1535 let s = t.to_string();
1536 assert_eq!(s, "2n4l8c2t");
1537 let parsed: Topology = s.parse().unwrap();
1538 assert_eq!(parsed, t);
1539 }
1540
1541 #[test]
1542 fn topology_fromstr_minimal() {
1543 let parsed: Topology = "1n1l1c1t".parse().unwrap();
1544 assert_eq!(parsed, Topology::new(1, 1, 1, 1));
1545 }
1546
1547 #[test]
1548 fn topology_fromstr_default_for_payload_roundtrip() {
1549 let s = Topology::DEFAULT_FOR_PAYLOAD.to_string();
1551 let parsed: Topology = s.parse().unwrap();
1552 assert_eq!(parsed, Topology::DEFAULT_FOR_PAYLOAD);
1553 }
1554
1555 #[test]
1556 fn topology_fromstr_rejects_empty() {
1557 let err = "".parse::<Topology>().unwrap_err();
1558 assert!(matches!(err, TopologyParseError::BadFormat(s) if s.is_empty()));
1559 }
1560
1561 #[test]
1562 fn topology_fromstr_rejects_missing_separator() {
1563 assert!(matches!(
1564 "1n2l4c".parse::<Topology>(),
1565 Err(TopologyParseError::BadFormat(_))
1566 ));
1567 assert!(matches!(
1568 "1n2l4c2".parse::<Topology>(),
1569 Err(TopologyParseError::BadFormat(_))
1570 ));
1571 }
1572
1573 #[test]
1574 fn topology_fromstr_rejects_trailing_garbage() {
1575 assert!(matches!(
1576 "1n2l4c2tjunk".parse::<Topology>(),
1577 Err(TopologyParseError::BadFormat(_))
1578 ));
1579 }
1580
1581 #[test]
1582 fn topology_fromstr_rejects_zero_component() {
1583 for s in ["0n1l1c1t", "1n0l1c1t", "1n1l0c1t", "1n1l1c0t"] {
1584 assert!(
1585 matches!(
1586 s.parse::<Topology>(),
1587 Err(TopologyParseError::BadComponent(_))
1588 ),
1589 "expected BadComponent for {s}",
1590 );
1591 }
1592 }
1593
1594 #[test]
1595 fn topology_fromstr_rejects_non_numeric() {
1596 assert!(matches!(
1597 "Xn1l1c1t".parse::<Topology>(),
1598 Err(TopologyParseError::BadComponent(_))
1599 ));
1600 assert!(matches!(
1601 "1n-1l1c1t".parse::<Topology>(),
1602 Err(TopologyParseError::BadComponent(_))
1603 ));
1604 }
1605
1606 #[test]
1607 fn topology_fromstr_rejects_llcs_not_multiple_of_numa_nodes() {
1608 let err = "2n3l1c1t".parse::<Topology>().unwrap_err();
1610 assert!(matches!(
1611 err,
1612 TopologyParseError::NotMultiple {
1613 llcs: 3,
1614 numa_nodes: 2
1615 }
1616 ));
1617 }
1618
1619 #[test]
1620 fn topology_fromstr_rejects_total_cpu_overflow() {
1621 let err = "1n1l65536c65537t".parse::<Topology>().unwrap_err();
1624 assert!(matches!(err, TopologyParseError::Overflow));
1625 }
1626
1627 #[test]
1628 fn topology_hash_consistent_with_eq() {
1629 use std::collections::HashSet;
1630 let t1 = Topology::new(2, 4, 8, 2);
1631 let t2 = Topology::new(2, 4, 8, 2);
1632 let mut set: HashSet<Topology> = HashSet::new();
1633 set.insert(t1);
1634 assert!(set.contains(&t2));
1635 }
1636
1637 #[test]
1638 fn numa_node_hash_consistent_with_eq() {
1639 use std::collections::HashSet;
1640 let n1 = NumaNode {
1641 llcs: 2,
1642 memory_mib: 4096,
1643 latency_ns: Some(100),
1644 bandwidth_mbs: Some(51200),
1645 mem_side_cache: None,
1646 };
1647 let n2 = NumaNode {
1648 llcs: 2,
1649 memory_mib: 4096,
1650 latency_ns: Some(100),
1651 bandwidth_mbs: Some(51200),
1652 mem_side_cache: None,
1653 };
1654 let mut set: HashSet<NumaNode> = HashSet::new();
1655 set.insert(n1);
1656 assert!(set.contains(&n2));
1657 }
1658
1659 #[test]
1660 fn topology_fromstr_per_node_topology_is_lossy() {
1661 let original = Topology::with_nodes(2, 1, &TWO_NODES);
1667 let s = original.to_string();
1668 let parsed: Topology = s.parse().unwrap();
1669 assert!(parsed.nodes.is_none(), "FromStr must produce nodes = None");
1670 assert!(
1671 parsed.distances.is_none(),
1672 "FromStr must produce distances = None"
1673 );
1674 assert_eq!(parsed.numa_nodes, original.numa_nodes);
1676 assert_eq!(parsed.llcs, original.llcs);
1677 assert_eq!(parsed.cores_per_llc, original.cores_per_llc);
1678 assert_eq!(parsed.threads_per_core, original.threads_per_core);
1679 }
1680
1681 #[test]
1684 #[should_panic(expected = "MemSideCache: associativity must fit in 4 bits")]
1685 fn mem_side_cache_new_panics_on_associativity_above_15() {
1686 MemSideCache::new(4096, 16, 1, 64);
1687 }
1688
1689 #[test]
1690 #[should_panic(expected = "MemSideCache: write_policy must fit in 4 bits")]
1691 fn mem_side_cache_new_panics_on_write_policy_above_15() {
1692 MemSideCache::new(4096, 8, 16, 64);
1693 }
1694
1695 #[test]
1698 #[should_panic(expected = "NumaDistance: n must be > 0")]
1699 fn numa_distance_new_panics_on_zero_n() {
1700 static EMPTY: [u8; 0] = [];
1701 NumaDistance::new(0, &EMPTY);
1702 }
1703
1704 #[test]
1705 #[should_panic(expected = "NumaDistance: entries.len() must equal n * n")]
1706 fn numa_distance_new_panics_on_size_mismatch() {
1707 static THREE_ENTRIES: [u8; 3] = [10, 20, 10];
1708 NumaDistance::new(2, &THREE_ENTRIES);
1710 }
1711
1712 #[test]
1713 #[should_panic(expected = "NumaDistance: diagonal entry must be 10")]
1714 fn numa_distance_new_panics_on_nonten_diagonal() {
1715 static BAD_DIAG: [u8; 4] = [9, 20, 20, 10];
1717 NumaDistance::new(2, &BAD_DIAG);
1718 }
1719
1720 #[test]
1721 #[should_panic(expected = "NumaDistance: off-diagonal entry must be > 10")]
1722 fn numa_distance_new_panics_on_offdiagonal_at_or_below_ten() {
1723 static OFF_DIAG_TEN: [u8; 4] = [10, 10, 10, 10];
1725 NumaDistance::new(2, &OFF_DIAG_TEN);
1726 }
1727
1728 #[test]
1729 #[should_panic(expected = "NumaDistance: matrix must be symmetric")]
1730 fn numa_distance_new_panics_on_asymmetric_matrix() {
1731 static ASYMMETRIC: [u8; 4] = [10, 20, 30, 10];
1733 NumaDistance::new(2, &ASYMMETRIC);
1734 }
1735
1736 #[test]
1739 #[should_panic(expected = "invalid Topology: nodes must not be empty")]
1740 fn topology_with_nodes_panics_on_empty_nodes() {
1741 static EMPTY: [NumaNode; 0] = [];
1742 Topology::with_nodes(2, 1, &EMPTY);
1743 }
1744
1745 #[test]
1746 #[should_panic(expected = "invalid Topology: cores_per_llc must be > 0")]
1747 fn topology_with_nodes_panics_on_zero_cores() {
1748 static NODES: [NumaNode; 1] = [NumaNode::new(2, 512)];
1749 Topology::with_nodes(0, 1, &NODES);
1750 }
1751
1752 #[test]
1753 #[should_panic(expected = "invalid Topology: threads_per_core must be > 0")]
1754 fn topology_with_nodes_panics_on_zero_threads() {
1755 static NODES: [NumaNode; 1] = [NumaNode::new(2, 512)];
1756 Topology::with_nodes(2, 0, &NODES);
1757 }
1758
1759 #[test]
1760 #[should_panic(expected = "invalid Topology: node LLC sum overflows u32")]
1761 fn topology_with_nodes_panics_on_llc_sum_overflow() {
1762 static NODES: [NumaNode; 2] = [NumaNode::new(u32::MAX, 512), NumaNode::new(1, 512)];
1766 Topology::with_nodes(1, 1, &NODES);
1767 }
1768
1769 #[test]
1770 #[should_panic(expected = "invalid Topology: CPU-bearing node has zero memory")]
1771 fn topology_with_nodes_panics_on_cpu_node_zero_memory() {
1772 static BAD_MEM: [NumaNode; 1] = [NumaNode::new(2, 0)];
1774 Topology::with_nodes(2, 1, &BAD_MEM);
1775 }
1776
1777 #[test]
1778 #[should_panic(expected = "invalid Topology: total LLCs must be > 0")]
1779 fn topology_with_nodes_panics_on_no_llcs() {
1780 static MEMORY_ONLY: [NumaNode; 1] = [NumaNode::new(0, 512)];
1782 Topology::with_nodes(2, 1, &MEMORY_ONLY);
1783 }
1784
1785 #[test]
1786 #[should_panic(expected = "invalid Topology: total CPU count overflows u32")]
1787 fn topology_with_nodes_panics_on_cpu_count_overflow() {
1788 static NODES: [NumaNode; 1] = [NumaNode::new(1, 512)];
1790 Topology::with_nodes(65_536, 65_536, &NODES);
1791 }
1792
1793 #[test]
1796 #[should_panic(expected = "invalid Topology: NumaDistance dimension must equal numa_nodes")]
1797 fn topology_distances_panics_on_dimension_mismatch() {
1798 static D3X3: [u8; 9] = [10, 20, 20, 20, 10, 20, 20, 20, 10];
1801 static DIST: NumaDistance = NumaDistance::new(3, &D3X3);
1802 Topology::new(2, 2, 1, 1).distances(&DIST);
1803 }
1804}