Skip to main content

scirs2_core/memory_efficient/
numa_topology.rs

1//! NUMA topology detection and management.
2//!
3//! This module provides utilities for detecting and working with NUMA (Non-Uniform Memory Access)
4//! topologies on systems that support it. NUMA awareness can significantly improve performance
5//! for memory-intensive operations by reducing cross-node memory access latency.
6//!
7//! # Supported Platforms
8//!
9//! - **Linux**: Full support via `/sys/devices/system/node` interface
10//! - **Windows**: Full support via Windows API (requires `windows-sys` crate)
11//! - **macOS/BSD**: Graceful fallback (returns None - these systems don't typically have NUMA)
12//!
13//! # Optional Features
14//!
15//! - `numa`: Enable libnuma integration for advanced NUMA management on Linux
16
17use crate::error::{CoreError, CoreResult, ErrorContext, ErrorLocation};
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20
21/// NUMA node information
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
23pub struct NumaNode {
24    /// Node ID
25    pub node_id: usize,
26
27    /// CPU cores associated with this node
28    pub cpu_list: Vec<usize>,
29
30    /// Memory available on this node (in bytes)
31    pub memory_bytes: u64,
32
33    /// Memory free on this node (in bytes)
34    pub memory_free_bytes: u64,
35}
36
37impl NumaNode {
38    /// Create a new NUMA node
39    pub fn new(node_id: usize, cpu_list: Vec<usize>, memory_bytes: u64) -> Self {
40        Self {
41            node_id,
42            cpu_list,
43            memory_bytes,
44            memory_free_bytes: memory_bytes,
45        }
46    }
47
48    /// Get the number of CPUs in this node
49    pub fn num_cpus(&self) -> usize {
50        self.cpu_list.len()
51    }
52
53    /// Check if a CPU belongs to this node
54    pub fn contains_cpu(&self, cpu_id: usize) -> bool {
55        self.cpu_list.contains(&cpu_id)
56    }
57
58    /// Get memory utilization percentage
59    pub fn memory_utilization(&self) -> f64 {
60        if self.memory_bytes == 0 {
61            0.0
62        } else {
63            (self.memory_bytes - self.memory_free_bytes) as f64 / self.memory_bytes as f64
64        }
65    }
66}
67
68/// NUMA topology information for the system
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct NumaTopology {
71    /// All NUMA nodes in the system
72    pub nodes: Vec<NumaNode>,
73
74    /// Whether the system is NUMA-aware
75    pub is_numa: bool,
76}
77
78impl NumaTopology {
79    /// Create a new NUMA topology
80    pub fn new(nodes: Vec<NumaNode>, is_numa: bool) -> Self {
81        Self { nodes, is_numa }
82    }
83
84    /// Get the number of NUMA nodes
85    pub fn num_nodes(&self) -> usize {
86        self.nodes.len()
87    }
88
89    /// Get a specific NUMA node by ID
90    pub fn get_node(&self, node_id: usize) -> Option<&NumaNode> {
91        self.nodes.iter().find(|node| node.node_id == node_id)
92    }
93
94    /// Find which NUMA node contains a specific CPU
95    pub fn find_node_for_cpu(&self, cpu_id: usize) -> Option<&NumaNode> {
96        self.nodes.iter().find(|node| node.contains_cpu(cpu_id))
97    }
98
99    /// Get total memory across all NUMA nodes
100    pub fn total_memory(&self) -> u64 {
101        self.nodes.iter().map(|node| node.memory_bytes).sum()
102    }
103
104    /// Get total free memory across all NUMA nodes
105    pub fn total_free_memory(&self) -> u64 {
106        self.nodes.iter().map(|node| node.memory_free_bytes).sum()
107    }
108
109    /// Detect NUMA topology on the current system
110    ///
111    /// Returns `None` if NUMA is not supported or detection fails
112    pub fn detect() -> Option<Self> {
113        #[cfg(target_os = "linux")]
114        {
115            Self::detect_linux().ok()
116        }
117
118        #[cfg(target_os = "windows")]
119        {
120            Self::detect_windows().ok()
121        }
122
123        #[cfg(not(any(target_os = "linux", target_os = "windows")))]
124        {
125            // macOS, BSD, and other systems - no NUMA support
126            None
127        }
128    }
129
130    /// Detect NUMA topology on Linux using sysfs
131    #[cfg(target_os = "linux")]
132    fn detect_linux() -> CoreResult<Self> {
133        use std::fs;
134        use std::path::Path;
135
136        let node_path = Path::new("/sys/devices/system/node");
137
138        if !node_path.exists() {
139            // No NUMA support - return single node with all CPUs
140            return Self::detect_non_numa();
141        }
142
143        let mut nodes = Vec::new();
144
145        // Iterate through node directories
146        let entries = fs::read_dir(node_path).map_err(|e| {
147            CoreError::IoError(
148                ErrorContext::new(format!("Failed to read NUMA node directory: {e}"))
149                    .with_location(ErrorLocation::new(file!(), line!())),
150            )
151        })?;
152
153        for entry in entries {
154            let entry = entry.map_err(|e| {
155                CoreError::IoError(
156                    ErrorContext::new(format!("Failed to read NUMA directory entry: {e}"))
157                        .with_location(ErrorLocation::new(file!(), line!())),
158                )
159            })?;
160
161            let path = entry.path();
162            let filename = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
163
164            // Check if this is a node directory (e.g., node0, node1)
165            if let Some(node_id_str) = filename.strip_prefix("node") {
166                if let Ok(node_id) = node_id_str.parse::<usize>() {
167                    // Read CPU list
168                    let cpulist_path = path.join("cpulist");
169                    let cpu_list = if cpulist_path.exists() {
170                        let cpulist_str = fs::read_to_string(&cpulist_path).map_err(|e| {
171                            CoreError::IoError(
172                                ErrorContext::new(format!("Failed to read cpulist: {e}"))
173                                    .with_location(ErrorLocation::new(file!(), line!())),
174                            )
175                        })?;
176
177                        Self::parse_cpu_list(&cpulist_str.trim())?
178                    } else {
179                        Vec::new()
180                    };
181
182                    // Read memory info
183                    let meminfo_path = path.join("meminfo");
184                    let (memory_bytes, memory_free_bytes) = if meminfo_path.exists() {
185                        Self::parse_node_meminfo(&meminfo_path)?
186                    } else {
187                        (0, 0)
188                    };
189
190                    let mut node = NumaNode::new(node_id, cpu_list, memory_bytes);
191                    node.memory_free_bytes = memory_free_bytes;
192
193                    nodes.push(node);
194                }
195            }
196        }
197
198        // Sort nodes by ID
199        nodes.sort_by_key(|node| node.node_id);
200
201        if nodes.is_empty() {
202            return Self::detect_non_numa();
203        }
204
205        Ok(Self::new(nodes, true))
206    }
207
208    /// Parse Linux CPU list format (e.g., "0-3,5,7-9")
209    #[cfg(target_os = "linux")]
210    fn parse_cpu_list(cpulist: &str) -> CoreResult<Vec<usize>> {
211        let mut cpus = Vec::new();
212
213        for range in cpulist.split(',') {
214            let range = range.trim();
215            if range.is_empty() {
216                continue;
217            }
218
219            if range.contains('-') {
220                // Range format (e.g., "0-3")
221                let parts: Vec<&str> = range.split('-').collect();
222                if parts.len() == 2 {
223                    let start = parts[0].parse::<usize>().map_err(|e| {
224                        CoreError::InvalidArgument(
225                            ErrorContext::new(format!("Invalid CPU range start: {e}"))
226                                .with_location(ErrorLocation::new(file!(), line!())),
227                        )
228                    })?;
229                    let end = parts[1].parse::<usize>().map_err(|e| {
230                        CoreError::InvalidArgument(
231                            ErrorContext::new(format!("Invalid CPU range end: {e}"))
232                                .with_location(ErrorLocation::new(file!(), line!())),
233                        )
234                    })?;
235
236                    cpus.extend(start..=end);
237                }
238            } else {
239                // Single CPU
240                let cpu = range.parse::<usize>().map_err(|e| {
241                    CoreError::InvalidArgument(
242                        ErrorContext::new(format!("Invalid CPU ID: {e}"))
243                            .with_location(ErrorLocation::new(file!(), line!())),
244                    )
245                })?;
246                cpus.push(cpu);
247            }
248        }
249
250        Ok(cpus)
251    }
252
253    /// Parse NUMA node meminfo file
254    #[cfg(target_os = "linux")]
255    fn parse_node_meminfo(meminfo_path: &std::path::Path) -> CoreResult<(u64, u64)> {
256        use std::fs;
257
258        let contents = fs::read_to_string(meminfo_path).map_err(|e| {
259            CoreError::IoError(
260                ErrorContext::new(format!("Failed to read meminfo: {e}"))
261                    .with_location(ErrorLocation::new(file!(), line!())),
262            )
263        })?;
264
265        let mut total_kb = 0u64;
266        let mut free_kb = 0u64;
267
268        for line in contents.lines() {
269            let parts: Vec<&str> = line.split_whitespace().collect();
270            if parts.len() >= 4 {
271                if parts[2] == "MemTotal:" {
272                    total_kb = parts[3].parse::<u64>().unwrap_or(0);
273                } else if parts[2] == "MemFree:" {
274                    free_kb = parts[3].parse::<u64>().unwrap_or(0);
275                }
276            }
277        }
278
279        // Convert KB to bytes
280        Ok((total_kb * 1024, free_kb * 1024))
281    }
282
283    /// Detect NUMA topology on Windows
284    #[cfg(target_os = "windows")]
285    fn detect_windows() -> CoreResult<Self> {
286        // Windows NUMA detection would use GetNumaHighestNodeNumber and GetNumaNodeProcessorMask
287        // For now, return non-NUMA fallback
288        // TODO: Implement Windows NUMA detection when windows-sys feature is added
289        Self::detect_non_numa()
290    }
291
292    /// Fallback for non-NUMA systems
293    fn detect_non_numa() -> CoreResult<Self> {
294        use crate::memory_efficient::platform_memory::PlatformMemoryInfo;
295
296        // Create a single node with all available CPUs and memory
297        let num_cpus = std::thread::available_parallelism()
298            .map(|n| n.get())
299            .unwrap_or(1);
300
301        let cpu_list: Vec<usize> = (0..num_cpus).collect();
302
303        // Get total system memory
304        let memory_info = PlatformMemoryInfo::detect();
305        let (memory_bytes, memory_free_bytes) = if let Some(info) = memory_info {
306            (info.total_memory as u64, info.available_memory as u64)
307        } else {
308            (0, 0)
309        };
310
311        let mut node = NumaNode::new(0, cpu_list, memory_bytes);
312        node.memory_free_bytes = memory_free_bytes;
313
314        Ok(Self::new(vec![node], false))
315    }
316}
317
318/// NUMA-aware memory allocator hint
319#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
320pub enum NumaPolicy {
321    /// Default system policy
322    Default,
323    /// Bind to specific node
324    Bind(usize),
325    /// Interleave across all nodes
326    Interleave,
327    /// Prefer specific node but allow fallback
328    Preferred(usize),
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334
335    #[test]
336    fn test_numa_node_creation() {
337        let node = NumaNode::new(0, vec![0, 1, 2, 3], 8 * 1024 * 1024 * 1024);
338
339        assert_eq!(node.node_id, 0);
340        assert_eq!(node.num_cpus(), 4);
341        assert!(node.contains_cpu(2));
342        assert!(!node.contains_cpu(4));
343    }
344
345    #[test]
346    fn test_numa_topology_creation() {
347        let node0 = NumaNode::new(0, vec![0, 1], 4 * 1024 * 1024 * 1024);
348        let node1 = NumaNode::new(1, vec![2, 3], 4 * 1024 * 1024 * 1024);
349
350        let topology = NumaTopology::new(vec![node0, node1], true);
351
352        assert_eq!(topology.num_nodes(), 2);
353        assert!(topology.is_numa);
354        assert_eq!(topology.total_memory(), 8 * 1024 * 1024 * 1024);
355    }
356
357    #[test]
358    fn test_find_node_for_cpu() {
359        let node0 = NumaNode::new(0, vec![0, 1], 4 * 1024 * 1024 * 1024);
360        let node1 = NumaNode::new(1, vec![2, 3], 4 * 1024 * 1024 * 1024);
361
362        let topology = NumaTopology::new(vec![node0, node1], true);
363
364        let node = topology.find_node_for_cpu(2);
365        assert!(node.is_some());
366        assert_eq!(node.expect("Node not found").node_id, 1);
367    }
368
369    #[test]
370    #[cfg(target_os = "linux")]
371    fn test_parse_cpu_list() {
372        // Test single CPU
373        let cpus = NumaTopology::parse_cpu_list("0").expect("Parse failed");
374        assert_eq!(cpus, vec![0]);
375
376        // Test range
377        let cpus = NumaTopology::parse_cpu_list("0-3").expect("Parse failed");
378        assert_eq!(cpus, vec![0, 1, 2, 3]);
379
380        // Test complex list
381        let cpus = NumaTopology::parse_cpu_list("0-2,5,7-9").expect("Parse failed");
382        assert_eq!(cpus, vec![0, 1, 2, 5, 7, 8, 9]);
383
384        // Test with whitespace
385        let cpus = NumaTopology::parse_cpu_list(" 0-1, 3 ").expect("Parse failed");
386        assert_eq!(cpus, vec![0, 1, 3]);
387    }
388
389    #[test]
390    fn test_numa_detection() {
391        // This test will work differently on different platforms
392        let topology = NumaTopology::detect();
393
394        // Should always return something (even if non-NUMA fallback)
395        // We can't assert much more since it depends on the system
396        if let Some(topo) = topology {
397            assert!(topo.num_nodes() > 0);
398            assert!(topo.total_memory() > 0 || topo.total_memory() == 0); // Allow 0 for test environments
399        }
400    }
401
402    #[test]
403    fn test_memory_utilization() {
404        let mut node = NumaNode::new(0, vec![0, 1], 1000);
405        node.memory_free_bytes = 600;
406
407        let utilization = node.memory_utilization();
408        assert!((utilization - 0.4).abs() < 1e-10);
409    }
410}