scirs2_core/memory_efficient/
numa_topology.rs1use crate::error::{CoreError, CoreResult, ErrorContext, ErrorLocation};
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
23pub struct NumaNode {
24 pub node_id: usize,
26
27 pub cpu_list: Vec<usize>,
29
30 pub memory_bytes: u64,
32
33 pub memory_free_bytes: u64,
35}
36
37impl NumaNode {
38 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 pub fn num_cpus(&self) -> usize {
50 self.cpu_list.len()
51 }
52
53 pub fn contains_cpu(&self, cpu_id: usize) -> bool {
55 self.cpu_list.contains(&cpu_id)
56 }
57
58 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#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct NumaTopology {
71 pub nodes: Vec<NumaNode>,
73
74 pub is_numa: bool,
76}
77
78impl NumaTopology {
79 pub fn new(nodes: Vec<NumaNode>, is_numa: bool) -> Self {
81 Self { nodes, is_numa }
82 }
83
84 pub fn num_nodes(&self) -> usize {
86 self.nodes.len()
87 }
88
89 pub fn get_node(&self, node_id: usize) -> Option<&NumaNode> {
91 self.nodes.iter().find(|node| node.node_id == node_id)
92 }
93
94 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 pub fn total_memory(&self) -> u64 {
101 self.nodes.iter().map(|node| node.memory_bytes).sum()
102 }
103
104 pub fn total_free_memory(&self) -> u64 {
106 self.nodes.iter().map(|node| node.memory_free_bytes).sum()
107 }
108
109 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 None
127 }
128 }
129
130 #[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 return Self::detect_non_numa();
141 }
142
143 let mut nodes = Vec::new();
144
145 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 if let Some(node_id_str) = filename.strip_prefix("node") {
166 if let Ok(node_id) = node_id_str.parse::<usize>() {
167 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 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 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 #[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 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 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 #[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 Ok((total_kb * 1024, free_kb * 1024))
281 }
282
283 #[cfg(target_os = "windows")]
285 fn detect_windows() -> CoreResult<Self> {
286 Self::detect_non_numa()
290 }
291
292 fn detect_non_numa() -> CoreResult<Self> {
294 use crate::memory_efficient::platform_memory::PlatformMemoryInfo;
295
296 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
320pub enum NumaPolicy {
321 Default,
323 Bind(usize),
325 Interleave,
327 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 let cpus = NumaTopology::parse_cpu_list("0").expect("Parse failed");
374 assert_eq!(cpus, vec![0]);
375
376 let cpus = NumaTopology::parse_cpu_list("0-3").expect("Parse failed");
378 assert_eq!(cpus, vec![0, 1, 2, 3]);
379
380 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 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 let topology = NumaTopology::detect();
393
394 if let Some(topo) = topology {
397 assert!(topo.num_nodes() > 0);
398 assert!(topo.total_memory() > 0 || topo.total_memory() == 0); }
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}