Skip to main content

numaperf_core/
capability.rs

1//! System capability detection for NUMA operations.
2
3use std::fs;
4use std::path::Path;
5
6/// Detected system capabilities for NUMA operations.
7///
8/// This struct provides information about what NUMA features are available
9/// on the current system, including kernel capabilities, system settings,
10/// and hardware topology.
11///
12/// # Example
13///
14/// ```
15/// use numaperf_core::Capabilities;
16///
17/// let caps = Capabilities::detect();
18/// println!("System capabilities: {}", caps.summary());
19///
20/// if caps.supports_hard_mode() {
21///     println!("Hard mode is fully supported");
22/// } else {
23///     println!("Missing capabilities:");
24///     for cap in caps.missing_for_hard_mode() {
25///         println!("  - {}", cap);
26///     }
27/// }
28/// ```
29#[derive(Debug, Clone)]
30pub struct Capabilities {
31    /// Has CAP_SYS_ADMIN (for strict memory binding with MPOL_BIND).
32    pub strict_memory_binding: bool,
33    /// Has CAP_SYS_NICE (for strict CPU affinity with realtime scheduling).
34    pub strict_cpu_affinity: bool,
35    /// Has CAP_IPC_LOCK (for memory locking with mlock).
36    pub memory_locking: bool,
37    /// Kernel NUMA balancing is disabled (kernel.numa_balancing=0).
38    pub numa_balancing_disabled: bool,
39    /// Number of NUMA nodes detected on the system.
40    pub numa_node_count: usize,
41}
42
43impl Capabilities {
44    /// Detect current system capabilities.
45    ///
46    /// This reads from `/proc` and `/sys` filesystems to determine
47    /// what NUMA features are available.
48    pub fn detect() -> Self {
49        Self {
50            strict_memory_binding: has_capability(CAP_SYS_ADMIN),
51            strict_cpu_affinity: has_capability(CAP_SYS_NICE),
52            memory_locking: has_capability(CAP_IPC_LOCK),
53            numa_balancing_disabled: check_numa_balancing_disabled(),
54            numa_node_count: count_numa_nodes(),
55        }
56    }
57
58    /// Check if all hard mode requirements are met.
59    ///
60    /// Hard mode requires:
61    /// - CAP_SYS_ADMIN for strict memory binding
62    /// - CAP_SYS_NICE for strict CPU affinity
63    /// - NUMA balancing disabled to prevent kernel migration
64    pub fn supports_hard_mode(&self) -> bool {
65        self.strict_memory_binding
66            && self.strict_cpu_affinity
67            && self.numa_balancing_disabled
68    }
69
70    /// Get a list of missing capabilities required for hard mode.
71    ///
72    /// Returns an empty list if `supports_hard_mode()` returns true.
73    pub fn missing_for_hard_mode(&self) -> Vec<&'static str> {
74        let mut missing = Vec::new();
75        if !self.strict_memory_binding {
76            missing.push("CAP_SYS_ADMIN (for strict memory binding)");
77        }
78        if !self.strict_cpu_affinity {
79            missing.push("CAP_SYS_NICE (for strict CPU affinity)");
80        }
81        if !self.numa_balancing_disabled {
82            missing.push("kernel.numa_balancing=0 (to prevent automatic migration)");
83        }
84        missing
85    }
86
87    /// Check if this is a NUMA system (more than one node).
88    pub fn is_numa_system(&self) -> bool {
89        self.numa_node_count > 1
90    }
91
92    /// Generate a human-readable summary of detected capabilities.
93    pub fn summary(&self) -> String {
94        let mut s = String::new();
95        s.push_str("NUMA System Capabilities\n");
96        s.push_str("========================\n");
97        s.push_str(&format!("NUMA nodes detected: {}\n", self.numa_node_count));
98        s.push_str(&format!(
99            "CAP_SYS_ADMIN (strict memory binding): {}\n",
100            if self.strict_memory_binding { "yes" } else { "no" }
101        ));
102        s.push_str(&format!(
103            "CAP_SYS_NICE (strict CPU affinity): {}\n",
104            if self.strict_cpu_affinity { "yes" } else { "no" }
105        ));
106        s.push_str(&format!(
107            "CAP_IPC_LOCK (memory locking): {}\n",
108            if self.memory_locking { "yes" } else { "no" }
109        ));
110        s.push_str(&format!(
111            "NUMA balancing disabled: {}\n",
112            if self.numa_balancing_disabled { "yes" } else { "no" }
113        ));
114        s.push_str(&format!(
115            "\nHard mode supported: {}\n",
116            if self.supports_hard_mode() { "YES" } else { "NO" }
117        ));
118
119        if !self.supports_hard_mode() {
120            s.push_str("\nMissing for hard mode:\n");
121            for cap in self.missing_for_hard_mode() {
122                s.push_str(&format!("  - {}\n", cap));
123            }
124        }
125        s
126    }
127}
128
129impl std::fmt::Display for Capabilities {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        write!(
132            f,
133            "Capabilities(nodes={}, hard_mode={})",
134            self.numa_node_count,
135            if self.supports_hard_mode() { "supported" } else { "unavailable" }
136        )
137    }
138}
139
140// Linux capability numbers from include/uapi/linux/capability.h
141const CAP_IPC_LOCK: u8 = 14;
142const CAP_SYS_ADMIN: u8 = 21;
143const CAP_SYS_NICE: u8 = 23;
144
145/// Check if the current process has a specific capability.
146///
147/// Reads from /proc/self/status and checks the CapEff bitmask.
148fn has_capability(cap: u8) -> bool {
149    let status = match fs::read_to_string("/proc/self/status") {
150        Ok(s) => s,
151        Err(_) => return false,
152    };
153
154    for line in status.lines() {
155        if let Some(hex) = line.strip_prefix("CapEff:\t") {
156            return check_capability_bit(hex.trim(), cap);
157        }
158    }
159    false
160}
161
162/// Check if a capability bit is set in a hex string.
163fn check_capability_bit(hex: &str, cap: u8) -> bool {
164    // CapEff is a hex string like "0000000000000001"
165    // Each hex digit represents 4 bits
166    let cap_bit = cap as u64;
167
168    match u64::from_str_radix(hex, 16) {
169        Ok(caps) => (caps & (1 << cap_bit)) != 0,
170        Err(_) => false,
171    }
172}
173
174/// Check if kernel NUMA balancing is disabled.
175fn check_numa_balancing_disabled() -> bool {
176    let path = Path::new("/proc/sys/kernel/numa_balancing");
177    if !path.exists() {
178        // If the file doesn't exist, NUMA balancing is likely disabled
179        // or not supported by this kernel.
180        return true;
181    }
182
183    match fs::read_to_string(path) {
184        Ok(content) => {
185            // File contains "0" if disabled, "1" if enabled
186            content.trim() == "0"
187        }
188        Err(_) => false,
189    }
190}
191
192/// Count the number of NUMA nodes on the system.
193fn count_numa_nodes() -> usize {
194    let node_dir = Path::new("/sys/devices/system/node");
195    if !node_dir.exists() {
196        // Not a NUMA system or sysfs not mounted
197        return 1;
198    }
199
200    match fs::read_dir(node_dir) {
201        Ok(entries) => {
202            let count = entries
203                .filter_map(|e| e.ok())
204                .filter(|e| {
205                    e.file_name()
206                        .to_string_lossy()
207                        .starts_with("node")
208                })
209                .count();
210            // Ensure at least 1 node
211            std::cmp::max(count, 1)
212        }
213        Err(_) => 1,
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn test_capability_detection() {
223        let caps = Capabilities::detect();
224        // Should at least detect one node
225        assert!(caps.numa_node_count >= 1);
226    }
227
228    #[test]
229    fn test_missing_capabilities() {
230        let caps = Capabilities::detect();
231        let missing = caps.missing_for_hard_mode();
232
233        // If hard mode is supported, missing should be empty
234        if caps.supports_hard_mode() {
235            assert!(missing.is_empty());
236        } else {
237            // If not supported, there should be at least one missing capability
238            assert!(!missing.is_empty());
239        }
240    }
241
242    #[test]
243    fn test_capability_bit_check() {
244        // Test hex parsing
245        assert!(check_capability_bit("0000000000200000", CAP_SYS_ADMIN)); // bit 21
246        assert!(check_capability_bit("0000000000800000", CAP_SYS_NICE));  // bit 23
247        assert!(check_capability_bit("0000000000004000", CAP_IPC_LOCK));  // bit 14
248
249        // Full caps (all bits set)
250        assert!(check_capability_bit("ffffffffffffffff", CAP_SYS_ADMIN));
251        assert!(check_capability_bit("ffffffffffffffff", CAP_SYS_NICE));
252        assert!(check_capability_bit("ffffffffffffffff", CAP_IPC_LOCK));
253
254        // No caps
255        assert!(!check_capability_bit("0000000000000000", CAP_SYS_ADMIN));
256        assert!(!check_capability_bit("0000000000000000", CAP_SYS_NICE));
257        assert!(!check_capability_bit("0000000000000000", CAP_IPC_LOCK));
258    }
259
260    #[test]
261    fn test_summary_format() {
262        let caps = Capabilities::detect();
263        let summary = caps.summary();
264
265        assert!(summary.contains("NUMA System Capabilities"));
266        assert!(summary.contains("NUMA nodes detected:"));
267        assert!(summary.contains("CAP_SYS_ADMIN"));
268        assert!(summary.contains("CAP_SYS_NICE"));
269        assert!(summary.contains("Hard mode supported:"));
270    }
271
272    #[test]
273    fn test_display() {
274        let caps = Capabilities::detect();
275        let display = format!("{}", caps);
276
277        assert!(display.contains("Capabilities"));
278        assert!(display.contains("nodes="));
279    }
280
281    #[test]
282    fn test_is_numa_system() {
283        let caps = Capabilities::detect();
284
285        if caps.numa_node_count > 1 {
286            assert!(caps.is_numa_system());
287        } else {
288            assert!(!caps.is_numa_system());
289        }
290    }
291
292    #[test]
293    fn test_capability_struct_fields() {
294        // Create a mock capabilities struct
295        let caps = Capabilities {
296            strict_memory_binding: true,
297            strict_cpu_affinity: true,
298            memory_locking: true,
299            numa_balancing_disabled: true,
300            numa_node_count: 2,
301        };
302
303        assert!(caps.supports_hard_mode());
304        assert!(caps.missing_for_hard_mode().is_empty());
305        assert!(caps.is_numa_system());
306    }
307
308    #[test]
309    fn test_missing_some_capabilities() {
310        let caps = Capabilities {
311            strict_memory_binding: false,
312            strict_cpu_affinity: true,
313            memory_locking: true,
314            numa_balancing_disabled: false,
315            numa_node_count: 1,
316        };
317
318        assert!(!caps.supports_hard_mode());
319        let missing = caps.missing_for_hard_mode();
320        assert_eq!(missing.len(), 2);
321        assert!(missing.iter().any(|s| s.contains("CAP_SYS_ADMIN")));
322        assert!(missing.iter().any(|s| s.contains("numa_balancing")));
323    }
324}