1use anyhow::Result;
21use serde::{Deserialize, Serialize};
22use std::collections::{HashMap, VecDeque};
23use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
24use std::sync::Arc;
25use std::time::{Duration, Instant};
26use tokio::sync::RwLock;
27use tracing::info;
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct NumaConfig {
32 pub enabled: bool,
34 pub preferred_node: Option<usize>,
36 pub auto_detect_topology: bool,
38 pub local_memory_allocation: bool,
40 pub allocation_strategy: NumaAllocationStrategy,
42 pub affinity_mode: CpuAffinityMode,
44 pub buffer_pool_config: NumaBufferPoolConfig,
46 pub cross_socket_optimization: bool,
48 pub interleave_policy: MemoryInterleavePolicy,
50 pub worker_distribution: WorkerDistributionStrategy,
52 pub bandwidth_threshold_mbps: u64,
54 pub enable_memory_migration: bool,
56}
57
58impl Default for NumaConfig {
59 fn default() -> Self {
60 Self {
61 enabled: true,
62 preferred_node: None,
63 auto_detect_topology: true,
64 local_memory_allocation: true,
65 allocation_strategy: NumaAllocationStrategy::LocalFirst,
66 affinity_mode: CpuAffinityMode::Strict,
67 buffer_pool_config: NumaBufferPoolConfig::default(),
68 cross_socket_optimization: true,
69 interleave_policy: MemoryInterleavePolicy::None,
70 worker_distribution: WorkerDistributionStrategy::Balanced,
71 bandwidth_threshold_mbps: 10000,
72 enable_memory_migration: false,
73 }
74 }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
79pub enum NumaAllocationStrategy {
80 LocalFirst,
82 Interleave,
84 Preferred(usize),
86 RoundRobin,
88 BandwidthAware,
90 LatencyOptimized,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
96pub enum CpuAffinityMode {
97 Strict,
99 Soft,
101 None,
103 NumaLocal,
105 CacheAware,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
111pub enum MemoryInterleavePolicy {
112 None,
114 All,
116 Specific(Vec<usize>),
118 PageLevel,
120 CacheLineLevel,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
126pub enum WorkerDistributionStrategy {
127 Balanced,
129 Concentrated,
131 Dynamic,
133 BandwidthAware,
135 LatencyOptimized,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct NumaBufferPoolConfig {
142 pub buffer_size: usize,
144 pub buffers_per_node: usize,
146 pub enable_migration: bool,
148 pub max_in_flight: usize,
150 pub pre_allocate: bool,
152 pub use_huge_pages: bool,
154 pub huge_page_size: HugePageSize,
156}
157
158impl Default for NumaBufferPoolConfig {
159 fn default() -> Self {
160 Self {
161 buffer_size: 64 * 1024, buffers_per_node: 1024,
163 enable_migration: false,
164 max_in_flight: 4096,
165 pre_allocate: true,
166 use_huge_pages: false,
167 huge_page_size: HugePageSize::Size2MB,
168 }
169 }
170}
171
172#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
174pub enum HugePageSize {
175 Size2MB,
177 Size1GB,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct NumaNode {
184 pub id: usize,
186 pub cpus: Vec<usize>,
188 pub total_memory: u64,
190 pub free_memory: u64,
192 pub memory_bandwidth_mbps: u64,
194 pub distances: HashMap<usize, u32>,
196 pub online: bool,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct NumaTopology {
203 pub num_nodes: usize,
205 pub nodes: Vec<NumaNode>,
207 pub total_cpus: usize,
209 pub total_memory: u64,
211 pub distance_matrix: Vec<Vec<u32>>,
213 pub cpu_to_node: HashMap<usize, usize>,
215}
216
217#[derive(Debug)]
219pub struct NumaBuffer {
220 data: Vec<u8>,
222 node_id: usize,
224 id: u64,
226 allocated_at: Instant,
228 last_accessed: Instant,
230 access_count: AtomicU64,
232 in_use: AtomicBool,
234}
235
236impl NumaBuffer {
237 pub fn new(size: usize, node_id: usize, id: u64) -> Self {
239 Self {
240 data: vec![0u8; size],
241 node_id,
242 id,
243 allocated_at: Instant::now(),
244 last_accessed: Instant::now(),
245 access_count: AtomicU64::new(0),
246 in_use: AtomicBool::new(false),
247 }
248 }
249
250 pub fn data(&self) -> &[u8] {
252 self.access_count.fetch_add(1, Ordering::Relaxed);
253 &self.data
254 }
255
256 pub fn data_mut(&mut self) -> &mut [u8] {
258 self.access_count.fetch_add(1, Ordering::Relaxed);
259 &mut self.data
260 }
261
262 pub fn size(&self) -> usize {
264 self.data.len()
265 }
266
267 pub fn node_id(&self) -> usize {
269 self.node_id
270 }
271
272 pub fn id(&self) -> u64 {
274 self.id
275 }
276
277 pub fn acquire(&self) -> bool {
279 self.in_use
280 .compare_exchange(false, true, Ordering::SeqCst, Ordering::Relaxed)
281 .is_ok()
282 }
283
284 pub fn release(&self) {
286 self.in_use.store(false, Ordering::Release);
287 }
288
289 pub fn is_in_use(&self) -> bool {
291 self.in_use.load(Ordering::Acquire)
292 }
293}
294
295pub struct NumaBufferPool {
297 buffers: Arc<RwLock<HashMap<usize, VecDeque<NumaBuffer>>>>,
299 config: NumaBufferPoolConfig,
301 next_id: AtomicU64,
303 stats: Arc<RwLock<NumaBufferPoolStats>>,
305 topology: Arc<NumaTopology>,
307}
308
309#[derive(Debug, Clone, Default, Serialize, Deserialize)]
311pub struct NumaBufferPoolStats {
312 pub total_allocations: u64,
314 pub local_allocations: u64,
316 pub remote_allocations: u64,
318 pub buffer_hits: u64,
320 pub buffer_misses: u64,
322 pub current_buffers: u64,
324 pub buffers_in_use: u64,
326 pub total_memory_bytes: u64,
328 pub per_node_stats: HashMap<usize, NodeBufferStats>,
330}
331
332#[derive(Debug, Clone, Default, Serialize, Deserialize)]
334pub struct NodeBufferStats {
335 pub allocations: u64,
337 pub current_buffers: u64,
339 pub memory_bytes: u64,
341 pub avg_access_latency_ns: f64,
343}
344
345impl NumaBufferPool {
346 pub fn new(config: NumaBufferPoolConfig, topology: Arc<NumaTopology>) -> Self {
348 Self {
349 buffers: Arc::new(RwLock::new(HashMap::new())),
350 config,
351 next_id: AtomicU64::new(0),
352 stats: Arc::new(RwLock::new(NumaBufferPoolStats::default())),
353 topology,
354 }
355 }
356
357 pub async fn pre_allocate(&self) -> Result<()> {
359 if !self.config.pre_allocate {
360 return Ok(());
361 }
362
363 let mut buffers = self.buffers.write().await;
364 let mut stats = self.stats.write().await;
365
366 for node in &self.topology.nodes {
367 let node_buffers = buffers.entry(node.id).or_insert_with(VecDeque::new);
368
369 for _ in 0..self.config.buffers_per_node {
370 let id = self.next_id.fetch_add(1, Ordering::SeqCst);
371 let buffer = NumaBuffer::new(self.config.buffer_size, node.id, id);
372 node_buffers.push_back(buffer);
373
374 stats.total_allocations += 1;
375 stats.local_allocations += 1;
376 stats.current_buffers += 1;
377 stats.total_memory_bytes += self.config.buffer_size as u64;
378
379 let node_stats = stats.per_node_stats.entry(node.id).or_default();
380 node_stats.allocations += 1;
381 node_stats.current_buffers += 1;
382 node_stats.memory_bytes += self.config.buffer_size as u64;
383 }
384 }
385
386 info!(
387 "Pre-allocated {} buffers across {} nodes",
388 stats.current_buffers, self.topology.num_nodes
389 );
390
391 Ok(())
392 }
393
394 pub async fn acquire(&self, preferred_node: usize) -> Result<NumaBuffer> {
396 let mut buffers = self.buffers.write().await;
397 let mut stats = self.stats.write().await;
398
399 if let Some(node_buffers) = buffers.get_mut(&preferred_node) {
401 if let Some(buffer) = node_buffers.pop_front() {
402 stats.buffer_hits += 1;
403 stats.buffers_in_use += 1;
404 let node_stats = stats.per_node_stats.entry(preferred_node).or_default();
405 node_stats.current_buffers = node_stats.current_buffers.saturating_sub(1);
406 return Ok(buffer);
407 }
408 }
409
410 for node in &self.topology.nodes {
412 if node.id == preferred_node {
413 continue;
414 }
415 if let Some(node_buffers) = buffers.get_mut(&node.id) {
416 if let Some(buffer) = node_buffers.pop_front() {
417 stats.buffer_hits += 1;
418 stats.buffers_in_use += 1;
419 stats.remote_allocations += 1;
420 let node_stats = stats.per_node_stats.entry(node.id).or_default();
421 node_stats.current_buffers = node_stats.current_buffers.saturating_sub(1);
422 return Ok(buffer);
423 }
424 }
425 }
426
427 stats.buffer_misses += 1;
429 stats.total_allocations += 1;
430 stats.buffers_in_use += 1;
431 stats.total_memory_bytes += self.config.buffer_size as u64;
432
433 let node_stats = stats.per_node_stats.entry(preferred_node).or_default();
434 node_stats.allocations += 1;
435 node_stats.memory_bytes += self.config.buffer_size as u64;
436
437 let id = self.next_id.fetch_add(1, Ordering::SeqCst);
438 Ok(NumaBuffer::new(self.config.buffer_size, preferred_node, id))
439 }
440
441 pub async fn release(&self, buffer: NumaBuffer) {
443 let mut buffers = self.buffers.write().await;
444 let mut stats = self.stats.write().await;
445
446 stats.buffers_in_use = stats.buffers_in_use.saturating_sub(1);
447
448 let node_buffers = buffers.entry(buffer.node_id).or_insert_with(VecDeque::new);
449 let node_stats = stats.per_node_stats.entry(buffer.node_id).or_default();
450 node_stats.current_buffers += 1;
451 stats.current_buffers += 1;
452
453 node_buffers.push_back(buffer);
454 }
455
456 pub async fn get_stats(&self) -> NumaBufferPoolStats {
458 self.stats.read().await.clone()
459 }
460}
461
462pub struct NumaWorker {
464 id: usize,
466 node_id: usize,
468 cpu_affinity: Vec<usize>,
470 running: Arc<AtomicBool>,
472 stats: Arc<RwLock<NumaWorkerStats>>,
474}
475
476#[derive(Debug, Clone, Default, Serialize, Deserialize)]
478pub struct NumaWorkerStats {
479 pub tasks_processed: u64,
481 pub avg_task_latency_us: f64,
483 pub max_task_latency_us: u64,
485 pub cross_node_accesses: u64,
487 pub local_accesses: u64,
489 pub cpu_time_us: u64,
491}
492
493impl NumaWorker {
494 pub fn new(id: usize, node_id: usize, cpu_affinity: Vec<usize>) -> Self {
496 Self {
497 id,
498 node_id,
499 cpu_affinity,
500 running: Arc::new(AtomicBool::new(false)),
501 stats: Arc::new(RwLock::new(NumaWorkerStats::default())),
502 }
503 }
504
505 pub fn id(&self) -> usize {
507 self.id
508 }
509
510 pub fn node_id(&self) -> usize {
512 self.node_id
513 }
514
515 pub fn cpu_affinity(&self) -> &[usize] {
517 &self.cpu_affinity
518 }
519
520 pub fn is_running(&self) -> bool {
522 self.running.load(Ordering::Acquire)
523 }
524
525 pub async fn get_stats(&self) -> NumaWorkerStats {
527 self.stats.read().await.clone()
528 }
529
530 pub async fn record_task(&self, latency_us: u64, is_local: bool) {
532 let mut stats = self.stats.write().await;
533 stats.tasks_processed += 1;
534 stats.avg_task_latency_us =
535 (stats.avg_task_latency_us * (stats.tasks_processed - 1) as f64 + latency_us as f64)
536 / stats.tasks_processed as f64;
537 stats.max_task_latency_us = stats.max_task_latency_us.max(latency_us);
538
539 if is_local {
540 stats.local_accesses += 1;
541 } else {
542 stats.cross_node_accesses += 1;
543 }
544 }
545}
546
547pub struct NumaThreadPool {
549 workers: Arc<RwLock<HashMap<usize, Vec<NumaWorker>>>>,
551 config: NumaConfig,
553 topology: Arc<NumaTopology>,
555 running: Arc<AtomicBool>,
557 stats: Arc<RwLock<NumaThreadPoolStats>>,
559 round_robin_index: AtomicUsize,
561}
562
563#[derive(Debug, Clone, Default, Serialize, Deserialize)]
565pub struct NumaThreadPoolStats {
566 pub total_workers: usize,
568 pub workers_per_node: HashMap<usize, usize>,
570 pub tasks_submitted: u64,
572 pub tasks_completed: u64,
574 pub avg_queue_depth: f64,
576 pub load_imbalance_ratio: f64,
578}
579
580impl NumaThreadPool {
581 pub async fn new(config: NumaConfig, topology: Arc<NumaTopology>) -> Result<Self> {
583 let pool = Self {
584 workers: Arc::new(RwLock::new(HashMap::new())),
585 config,
586 topology,
587 running: Arc::new(AtomicBool::new(false)),
588 stats: Arc::new(RwLock::new(NumaThreadPoolStats::default())),
589 round_robin_index: AtomicUsize::new(0),
590 };
591
592 pool.initialize_workers().await?;
593
594 Ok(pool)
595 }
596
597 async fn initialize_workers(&self) -> Result<()> {
599 let mut workers = self.workers.write().await;
600 let mut stats = self.stats.write().await;
601
602 let workers_per_node = match &self.config.worker_distribution {
603 WorkerDistributionStrategy::Balanced => {
604 let _total_cpus: usize = self.topology.nodes.iter().map(|n| n.cpus.len()).sum();
606 let workers_per_cpu = 1;
607 self.topology
608 .nodes
609 .iter()
610 .map(|n| (n.id, n.cpus.len() * workers_per_cpu))
611 .collect::<HashMap<_, _>>()
612 }
613 WorkerDistributionStrategy::Concentrated => {
614 let preferred = self.config.preferred_node.unwrap_or(0);
616 self.topology
617 .nodes
618 .iter()
619 .map(|n| {
620 if n.id == preferred {
621 (n.id, n.cpus.len() * 2)
622 } else {
623 (n.id, 1)
624 }
625 })
626 .collect()
627 }
628 _ => {
629 self.topology
631 .nodes
632 .iter()
633 .map(|n| (n.id, n.cpus.len()))
634 .collect()
635 }
636 };
637
638 let mut worker_id = 0;
639 for node in &self.topology.nodes {
640 let count = workers_per_node.get(&node.id).copied().unwrap_or(1);
641 let node_workers = workers.entry(node.id).or_insert_with(Vec::new);
642
643 for i in 0..count {
644 let cpu = node.cpus.get(i % node.cpus.len()).copied().unwrap_or(0);
645 let worker = NumaWorker::new(worker_id, node.id, vec![cpu]);
646 node_workers.push(worker);
647 worker_id += 1;
648 }
649
650 stats.workers_per_node.insert(node.id, node_workers.len());
651 }
652
653 stats.total_workers = worker_id;
654
655 info!(
656 "Initialized NUMA thread pool with {} workers across {} nodes",
657 stats.total_workers, self.topology.num_nodes
658 );
659
660 Ok(())
661 }
662
663 pub async fn get_stats(&self) -> NumaThreadPoolStats {
665 self.stats.read().await.clone()
666 }
667
668 pub async fn start(&self) -> Result<()> {
670 self.running.store(true, Ordering::Release);
671 info!("NUMA thread pool started");
672 Ok(())
673 }
674
675 pub async fn stop(&self) -> Result<()> {
677 self.running.store(false, Ordering::Release);
678 info!("NUMA thread pool stopped");
679 Ok(())
680 }
681
682 pub fn is_running(&self) -> bool {
684 self.running.load(Ordering::Acquire)
685 }
686
687 pub async fn get_next_worker(&self) -> Option<usize> {
689 let workers = self.workers.read().await;
690 let total_workers: usize = workers.values().map(|v| v.len()).sum();
691
692 if total_workers == 0 {
693 return None;
694 }
695
696 let index = self.round_robin_index.fetch_add(1, Ordering::SeqCst) % total_workers;
697 Some(index)
698 }
699}
700
701pub struct NumaStreamProcessor {
703 config: NumaConfig,
705 topology: Arc<NumaTopology>,
707 buffer_pool: Arc<NumaBufferPool>,
709 thread_pool: Arc<NumaThreadPool>,
711 running: Arc<AtomicBool>,
713 stats: Arc<RwLock<NumaProcessorStats>>,
715}
716
717#[derive(Debug, Clone, Default, Serialize, Deserialize)]
719pub struct NumaProcessorStats {
720 pub events_processed: u64,
722 pub avg_processing_latency_us: f64,
724 pub max_processing_latency_us: u64,
726 pub memory_bandwidth_utilization: f64,
728 pub cross_node_transfers: u64,
730 pub local_node_hits: u64,
732 pub cache_miss_rate: f64,
734 pub per_node_stats: HashMap<usize, NodeProcessorStats>,
736}
737
738#[derive(Debug, Clone, Default, Serialize, Deserialize)]
740pub struct NodeProcessorStats {
741 pub events_processed: u64,
743 pub avg_latency_us: f64,
745 pub memory_usage_bytes: u64,
747 pub cpu_utilization: f64,
749}
750
751impl NumaStreamProcessor {
752 pub async fn new(config: NumaConfig) -> Result<Self> {
754 let topology = Arc::new(Self::detect_topology(&config).await?);
756
757 let buffer_pool = Arc::new(NumaBufferPool::new(
759 config.buffer_pool_config.clone(),
760 topology.clone(),
761 ));
762
763 buffer_pool.pre_allocate().await?;
765
766 let thread_pool = Arc::new(NumaThreadPool::new(config.clone(), topology.clone()).await?);
768
769 Ok(Self {
770 config,
771 topology,
772 buffer_pool,
773 thread_pool,
774 running: Arc::new(AtomicBool::new(false)),
775 stats: Arc::new(RwLock::new(NumaProcessorStats::default())),
776 })
777 }
778
779 async fn detect_topology(config: &NumaConfig) -> Result<NumaTopology> {
781 if !config.auto_detect_topology {
782 return Ok(NumaTopology {
784 num_nodes: 1,
785 nodes: vec![NumaNode {
786 id: 0,
787 cpus: (0..std::thread::available_parallelism()
788 .map(|n| n.get())
789 .unwrap_or(1))
790 .collect(),
791 total_memory: 8 * 1024 * 1024 * 1024, free_memory: 4 * 1024 * 1024 * 1024,
793 memory_bandwidth_mbps: 50000,
794 distances: HashMap::from([(0, 10)]),
795 online: true,
796 }],
797 total_cpus: std::thread::available_parallelism()
798 .map(|n| n.get())
799 .unwrap_or(1),
800 total_memory: 8 * 1024 * 1024 * 1024,
801 distance_matrix: vec![vec![10]],
802 cpu_to_node: (0..std::thread::available_parallelism()
803 .map(|n| n.get())
804 .unwrap_or(1))
805 .map(|cpu| (cpu, 0))
806 .collect(),
807 });
808 }
809
810 #[cfg(target_os = "linux")]
813 {
814 Self::detect_linux_numa_topology().await
815 }
816
817 #[cfg(not(target_os = "linux"))]
818 {
819 Self::detect_fallback_topology().await
821 }
822 }
823
824 #[cfg(target_os = "linux")]
825 async fn detect_linux_numa_topology() -> Result<NumaTopology> {
826 use std::fs;
827 use std::path::Path;
828
829 let numa_path = Path::new("/sys/devices/system/node");
830
831 if !numa_path.exists() {
832 return Self::detect_fallback_topology().await;
833 }
834
835 let mut nodes = Vec::new();
836 let mut cpu_to_node = HashMap::new();
837
838 for entry in fs::read_dir(numa_path)? {
840 let entry = entry?;
841 let name = entry.file_name().to_string_lossy().to_string();
842
843 if !name.starts_with("node") {
844 continue;
845 }
846
847 let node_id: usize = name[4..].parse().unwrap_or(0);
848 let node_path = entry.path();
849
850 let cpulist_path = node_path.join("cpulist");
852 let cpus = if cpulist_path.exists() {
853 let content = fs::read_to_string(cpulist_path)?;
854 Self::parse_cpu_list(&content)
855 } else {
856 vec![]
857 };
858
859 for &cpu in &cpus {
861 cpu_to_node.insert(cpu, node_id);
862 }
863
864 let meminfo_path = node_path.join("meminfo");
866 let (total_memory, free_memory) = if meminfo_path.exists() {
867 let content = fs::read_to_string(meminfo_path)?;
868 Self::parse_meminfo(&content)
869 } else {
870 (8 * 1024 * 1024 * 1024, 4 * 1024 * 1024 * 1024)
871 };
872
873 nodes.push(NumaNode {
874 id: node_id,
875 cpus,
876 total_memory,
877 free_memory,
878 memory_bandwidth_mbps: 50000, distances: HashMap::new(),
880 online: true,
881 });
882 }
883
884 if nodes.is_empty() {
885 return Self::detect_fallback_topology().await;
886 }
887
888 nodes.sort_by_key(|n| n.id);
890
891 let distance_path = numa_path.join("node0/distance");
893 let distance_matrix = if distance_path.exists() {
894 Self::read_distance_matrix(&nodes).await?
895 } else {
896 vec![vec![10; nodes.len()]; nodes.len()]
897 };
898
899 for (i, node) in nodes.iter_mut().enumerate() {
901 for (j, &dist) in distance_matrix[i].iter().enumerate() {
902 node.distances.insert(j, dist);
903 }
904 }
905
906 let total_cpus = nodes.iter().map(|n| n.cpus.len()).sum();
907 let total_memory = nodes.iter().map(|n| n.total_memory).sum();
908 let num_nodes = nodes.len();
909
910 info!(
911 "Detected NUMA topology: {} nodes, {} CPUs, {} MB total memory",
912 num_nodes,
913 total_cpus,
914 total_memory / (1024 * 1024)
915 );
916
917 Ok(NumaTopology {
918 num_nodes,
919 nodes,
920 total_cpus,
921 total_memory,
922 distance_matrix,
923 cpu_to_node,
924 })
925 }
926
927 #[cfg(target_os = "linux")]
928 fn parse_cpu_list(content: &str) -> Vec<usize> {
929 let mut cpus = Vec::new();
930
931 for part in content.trim().split(',') {
932 if part.contains('-') {
933 let range: Vec<&str> = part.split('-').collect();
934 if range.len() == 2 {
935 if let (Ok(start), Ok(end)) =
936 (range[0].parse::<usize>(), range[1].parse::<usize>())
937 {
938 cpus.extend(start..=end);
939 }
940 }
941 } else if let Ok(cpu) = part.parse::<usize>() {
942 cpus.push(cpu);
943 }
944 }
945
946 cpus
947 }
948
949 #[cfg(target_os = "linux")]
950 fn parse_meminfo(content: &str) -> (u64, u64) {
951 let mut total = 0u64;
952 let mut free = 0u64;
953
954 for line in content.lines() {
955 if line.contains("MemTotal:") {
956 if let Some(val) = line.split_whitespace().nth(3) {
957 total = val.parse().unwrap_or(0) * 1024; }
959 } else if line.contains("MemFree:") {
960 if let Some(val) = line.split_whitespace().nth(3) {
961 free = val.parse().unwrap_or(0) * 1024;
962 }
963 }
964 }
965
966 (total, free)
967 }
968
969 #[cfg(target_os = "linux")]
970 async fn read_distance_matrix(nodes: &[NumaNode]) -> Result<Vec<Vec<u32>>> {
971 use std::fs;
972
973 let mut matrix = vec![vec![10u32; nodes.len()]; nodes.len()];
974
975 for (i, node) in nodes.iter().enumerate() {
976 let path = format!("/sys/devices/system/node/node{}/distance", node.id);
977 if let Ok(content) = fs::read_to_string(&path) {
978 let distances: Vec<u32> = content
979 .split_whitespace()
980 .filter_map(|s| s.parse().ok())
981 .collect();
982
983 for (j, &dist) in distances.iter().enumerate() {
984 if j < nodes.len() {
985 matrix[i][j] = dist;
986 }
987 }
988 }
989 }
990
991 Ok(matrix)
992 }
993
994 async fn detect_fallback_topology() -> Result<NumaTopology> {
995 let num_cpus = std::thread::available_parallelism()
996 .map(|n| n.get())
997 .unwrap_or(1);
998
999 Ok(NumaTopology {
1000 num_nodes: 1,
1001 nodes: vec![NumaNode {
1002 id: 0,
1003 cpus: (0..num_cpus).collect(),
1004 total_memory: 8 * 1024 * 1024 * 1024,
1005 free_memory: 4 * 1024 * 1024 * 1024,
1006 memory_bandwidth_mbps: 50000,
1007 distances: HashMap::from([(0, 10)]),
1008 online: true,
1009 }],
1010 total_cpus: num_cpus,
1011 total_memory: 8 * 1024 * 1024 * 1024,
1012 distance_matrix: vec![vec![10]],
1013 cpu_to_node: (0..num_cpus).map(|cpu| (cpu, 0)).collect(),
1014 })
1015 }
1016
1017 pub async fn start(&self) -> Result<()> {
1019 self.running.store(true, Ordering::Release);
1020 self.thread_pool.start().await?;
1021 info!("NUMA stream processor started");
1022 Ok(())
1023 }
1024
1025 pub async fn stop(&self) -> Result<()> {
1027 self.running.store(false, Ordering::Release);
1028 self.thread_pool.stop().await?;
1029 info!("NUMA stream processor stopped");
1030 Ok(())
1031 }
1032
1033 pub async fn process_event(
1035 &self,
1036 data: &[u8],
1037 preferred_node: Option<usize>,
1038 ) -> Result<Vec<u8>> {
1039 let start_time = Instant::now();
1040 let node_id = preferred_node.unwrap_or(0);
1041
1042 let mut buffer = self.buffer_pool.acquire(node_id).await?;
1044
1045 let len = data.len().min(buffer.size());
1047 buffer.data_mut()[..len].copy_from_slice(&data[..len]);
1048
1049 let result = buffer.data()[..len].to_vec();
1051
1052 let latency_us = start_time.elapsed().as_micros() as u64;
1054 let is_local = buffer.node_id() == node_id;
1055
1056 let mut stats = self.stats.write().await;
1057 stats.events_processed += 1;
1058 stats.avg_processing_latency_us = (stats.avg_processing_latency_us
1059 * (stats.events_processed - 1) as f64
1060 + latency_us as f64)
1061 / stats.events_processed as f64;
1062 stats.max_processing_latency_us = stats.max_processing_latency_us.max(latency_us);
1063
1064 if is_local {
1065 stats.local_node_hits += 1;
1066 } else {
1067 stats.cross_node_transfers += 1;
1068 }
1069
1070 let node_stats = stats.per_node_stats.entry(node_id).or_default();
1071 node_stats.events_processed += 1;
1072 node_stats.avg_latency_us = (node_stats.avg_latency_us
1073 * (node_stats.events_processed - 1) as f64
1074 + latency_us as f64)
1075 / node_stats.events_processed as f64;
1076
1077 self.buffer_pool.release(buffer).await;
1079
1080 Ok(result)
1081 }
1082
1083 pub async fn process_batch(
1085 &self,
1086 events: Vec<Vec<u8>>,
1087 preferred_node: Option<usize>,
1088 ) -> Result<Vec<Vec<u8>>> {
1089 let mut results = Vec::with_capacity(events.len());
1090
1091 for event in events {
1092 let result = self.process_event(&event, preferred_node).await?;
1093 results.push(result);
1094 }
1095
1096 Ok(results)
1097 }
1098
1099 pub async fn get_stats(&self) -> NumaProcessorStats {
1101 self.stats.read().await.clone()
1102 }
1103
1104 pub async fn get_buffer_pool_stats(&self) -> NumaBufferPoolStats {
1106 self.buffer_pool.get_stats().await
1107 }
1108
1109 pub async fn get_thread_pool_stats(&self) -> NumaThreadPoolStats {
1111 self.thread_pool.get_stats().await
1112 }
1113
1114 pub fn get_topology(&self) -> &NumaTopology {
1116 &self.topology
1117 }
1118
1119 pub fn get_config(&self) -> &NumaConfig {
1121 &self.config
1122 }
1123
1124 pub fn get_node_for_cpu(&self, cpu: usize) -> Option<usize> {
1126 self.topology.cpu_to_node.get(&cpu).copied()
1127 }
1128
1129 pub fn get_node_distance(&self, from: usize, to: usize) -> u32 {
1131 if from < self.topology.distance_matrix.len()
1132 && to < self.topology.distance_matrix[from].len()
1133 {
1134 self.topology.distance_matrix[from][to]
1135 } else {
1136 10 }
1138 }
1139
1140 pub async fn find_closest_available_node(&self, from: usize) -> usize {
1142 let stats = self.buffer_pool.get_stats().await;
1143
1144 let mut best_node = from;
1145 let mut best_score = u32::MAX;
1146
1147 for node in &self.topology.nodes {
1148 if node.id == from {
1149 best_node = node.id;
1150 break;
1151 }
1152
1153 let distance = self.get_node_distance(from, node.id);
1154 let buffer_count = stats
1155 .per_node_stats
1156 .get(&node.id)
1157 .map(|s| s.current_buffers)
1158 .unwrap_or(0);
1159
1160 let score = distance.saturating_sub(buffer_count as u32 / 100);
1162
1163 if score < best_score {
1164 best_score = score;
1165 best_node = node.id;
1166 }
1167 }
1168
1169 best_node
1170 }
1171}
1172
1173type BandwidthSamples = Arc<RwLock<HashMap<usize, VecDeque<(Instant, u64)>>>>;
1175
1176pub struct MemoryBandwidthMonitor {
1178 samples: BandwidthSamples,
1180 window_size: Duration,
1182 max_samples: usize,
1184}
1185
1186impl MemoryBandwidthMonitor {
1187 pub fn new(window_size: Duration) -> Self {
1189 Self {
1190 samples: Arc::new(RwLock::new(HashMap::new())),
1191 window_size,
1192 max_samples: 1000,
1193 }
1194 }
1195
1196 pub async fn record_sample(&self, node_id: usize, bytes_transferred: u64) {
1198 let mut samples = self.samples.write().await;
1199 let node_samples = samples.entry(node_id).or_insert_with(VecDeque::new);
1200
1201 let now = Instant::now();
1202 node_samples.push_back((now, bytes_transferred));
1203
1204 while node_samples.len() > self.max_samples {
1206 node_samples.pop_front();
1207 }
1208
1209 let cutoff = now - self.window_size;
1211 while let Some((time, _)) = node_samples.front() {
1212 if *time < cutoff {
1213 node_samples.pop_front();
1214 } else {
1215 break;
1216 }
1217 }
1218 }
1219
1220 pub async fn get_bandwidth(&self, node_id: usize) -> f64 {
1222 let samples = self.samples.read().await;
1223
1224 if let Some(node_samples) = samples.get(&node_id) {
1225 if node_samples.len() < 2 {
1226 return 0.0;
1227 }
1228
1229 let first = node_samples
1230 .front()
1231 .expect("node_samples validated to have at least 2 elements");
1232 let last = node_samples
1233 .back()
1234 .expect("node_samples validated to have at least 2 elements");
1235
1236 let total_bytes: u64 = node_samples.iter().map(|(_, b)| b).sum();
1237 let duration = last.0.duration_since(first.0);
1238
1239 if duration.as_secs_f64() > 0.0 {
1240 (total_bytes as f64 / duration.as_secs_f64()) / (1024.0 * 1024.0)
1241 } else {
1242 0.0
1243 }
1244 } else {
1245 0.0
1246 }
1247 }
1248
1249 pub async fn get_all_bandwidth(&self) -> HashMap<usize, f64> {
1251 let samples = self.samples.read().await;
1252 let node_ids: Vec<usize> = samples.keys().copied().collect();
1253 drop(samples);
1254
1255 let mut result = HashMap::new();
1256 for node_id in node_ids {
1257 let bandwidth = self.get_bandwidth(node_id).await;
1258 result.insert(node_id, bandwidth);
1259 }
1260
1261 result
1262 }
1263}
1264
1265#[cfg(test)]
1266mod tests {
1267 use super::*;
1268
1269 #[tokio::test]
1270 async fn test_numa_config_default() {
1271 let config = NumaConfig::default();
1272 assert!(config.enabled);
1273 assert!(config.auto_detect_topology);
1274 assert!(config.local_memory_allocation);
1275 }
1276
1277 #[tokio::test]
1278 async fn test_numa_buffer() {
1279 let buffer = NumaBuffer::new(1024, 0, 1);
1280 assert_eq!(buffer.size(), 1024);
1281 assert_eq!(buffer.node_id(), 0);
1282 assert_eq!(buffer.id(), 1);
1283 assert!(!buffer.is_in_use());
1284
1285 assert!(buffer.acquire());
1286 assert!(buffer.is_in_use());
1287 assert!(!buffer.acquire()); buffer.release();
1290 assert!(!buffer.is_in_use());
1291 }
1292
1293 #[tokio::test]
1294 async fn test_numa_topology_detection() {
1295 let config = NumaConfig {
1296 auto_detect_topology: false, ..Default::default()
1298 };
1299
1300 let processor = NumaStreamProcessor::new(config).await.unwrap();
1301 let topology = processor.get_topology();
1302
1303 assert!(topology.num_nodes >= 1);
1304 assert!(topology.total_cpus >= 1);
1305 assert!(!topology.nodes.is_empty());
1306 }
1307
1308 #[tokio::test]
1309 async fn test_numa_buffer_pool() {
1310 let topology = Arc::new(NumaTopology {
1311 num_nodes: 1,
1312 nodes: vec![NumaNode {
1313 id: 0,
1314 cpus: vec![0, 1, 2, 3],
1315 total_memory: 8 * 1024 * 1024 * 1024,
1316 free_memory: 4 * 1024 * 1024 * 1024,
1317 memory_bandwidth_mbps: 50000,
1318 distances: HashMap::from([(0, 10)]),
1319 online: true,
1320 }],
1321 total_cpus: 4,
1322 total_memory: 8 * 1024 * 1024 * 1024,
1323 distance_matrix: vec![vec![10]],
1324 cpu_to_node: (0..4).map(|cpu| (cpu, 0)).collect(),
1325 });
1326
1327 let config = NumaBufferPoolConfig {
1328 buffer_size: 1024,
1329 buffers_per_node: 10,
1330 pre_allocate: true,
1331 ..Default::default()
1332 };
1333
1334 let pool = NumaBufferPool::new(config, topology);
1335 pool.pre_allocate().await.unwrap();
1336
1337 let stats = pool.get_stats().await;
1338 assert_eq!(stats.current_buffers, 10);
1339
1340 let buffer = pool.acquire(0).await.unwrap();
1342 assert_eq!(buffer.node_id(), 0);
1343
1344 pool.release(buffer).await;
1345 }
1346
1347 #[tokio::test]
1348 async fn test_numa_stream_processor() {
1349 let config = NumaConfig {
1350 auto_detect_topology: false,
1351 buffer_pool_config: NumaBufferPoolConfig {
1352 buffer_size: 1024,
1353 buffers_per_node: 10,
1354 pre_allocate: true,
1355 ..Default::default()
1356 },
1357 ..Default::default()
1358 };
1359
1360 let processor = NumaStreamProcessor::new(config).await.unwrap();
1361 processor.start().await.unwrap();
1362
1363 let data = vec![1u8, 2, 3, 4, 5];
1365 let result = processor.process_event(&data, Some(0)).await.unwrap();
1366 assert_eq!(result, data);
1367
1368 let stats = processor.get_stats().await;
1369 assert_eq!(stats.events_processed, 1);
1370
1371 processor.stop().await.unwrap();
1372 }
1373
1374 #[tokio::test]
1375 async fn test_numa_batch_processing() {
1376 let config = NumaConfig {
1377 auto_detect_topology: false,
1378 ..Default::default()
1379 };
1380
1381 let processor = NumaStreamProcessor::new(config).await.unwrap();
1382 processor.start().await.unwrap();
1383
1384 let events = vec![vec![1u8, 2, 3], vec![4u8, 5, 6], vec![7u8, 8, 9]];
1385
1386 let results = processor
1387 .process_batch(events.clone(), Some(0))
1388 .await
1389 .unwrap();
1390 assert_eq!(results.len(), 3);
1391 assert_eq!(results, events);
1392
1393 let stats = processor.get_stats().await;
1394 assert_eq!(stats.events_processed, 3);
1395
1396 processor.stop().await.unwrap();
1397 }
1398
1399 #[tokio::test]
1400 async fn test_memory_bandwidth_monitor() {
1401 let monitor = MemoryBandwidthMonitor::new(Duration::from_secs(10));
1402
1403 monitor.record_sample(0, 1024 * 1024).await;
1405 tokio::time::sleep(Duration::from_millis(10)).await;
1406 monitor.record_sample(0, 2 * 1024 * 1024).await;
1407 tokio::time::sleep(Duration::from_millis(10)).await;
1408 monitor.record_sample(0, 3 * 1024 * 1024).await;
1409
1410 let bandwidth = monitor.get_bandwidth(0).await;
1411 assert!(bandwidth >= 0.0);
1412 }
1413
1414 #[tokio::test]
1415 async fn test_numa_thread_pool() {
1416 let topology = Arc::new(NumaTopology {
1417 num_nodes: 2,
1418 nodes: vec![
1419 NumaNode {
1420 id: 0,
1421 cpus: vec![0, 1],
1422 total_memory: 4 * 1024 * 1024 * 1024,
1423 free_memory: 2 * 1024 * 1024 * 1024,
1424 memory_bandwidth_mbps: 50000,
1425 distances: HashMap::from([(0, 10), (1, 20)]),
1426 online: true,
1427 },
1428 NumaNode {
1429 id: 1,
1430 cpus: vec![2, 3],
1431 total_memory: 4 * 1024 * 1024 * 1024,
1432 free_memory: 2 * 1024 * 1024 * 1024,
1433 memory_bandwidth_mbps: 50000,
1434 distances: HashMap::from([(0, 20), (1, 10)]),
1435 online: true,
1436 },
1437 ],
1438 total_cpus: 4,
1439 total_memory: 8 * 1024 * 1024 * 1024,
1440 distance_matrix: vec![vec![10, 20], vec![20, 10]],
1441 cpu_to_node: HashMap::from([(0, 0), (1, 0), (2, 1), (3, 1)]),
1442 });
1443
1444 let config = NumaConfig {
1445 worker_distribution: WorkerDistributionStrategy::Balanced,
1446 ..Default::default()
1447 };
1448
1449 let pool = NumaThreadPool::new(config, topology).await.unwrap();
1450 pool.start().await.unwrap();
1451
1452 let stats = pool.get_stats().await;
1453 assert_eq!(stats.total_workers, 4);
1454 assert!(pool.is_running());
1455
1456 pool.stop().await.unwrap();
1457 assert!(!pool.is_running());
1458 }
1459
1460 #[tokio::test]
1461 async fn test_numa_worker() {
1462 let worker = NumaWorker::new(0, 0, vec![0, 1]);
1463 assert_eq!(worker.id(), 0);
1464 assert_eq!(worker.node_id(), 0);
1465 assert_eq!(worker.cpu_affinity(), &[0, 1]);
1466 assert!(!worker.is_running());
1467
1468 worker.record_task(100, true).await;
1469 let stats = worker.get_stats().await;
1470 assert_eq!(stats.tasks_processed, 1);
1471 assert_eq!(stats.local_accesses, 1);
1472 }
1473}