Skip to main content

laminar_core/detect/
io.rs

1//! # I/O Capabilities Detection
2//!
3//! Detects I/O subsystem capabilities including `io_uring`, XDP, and storage type.
4//!
5//! ## Usage
6//!
7//! ```rust,ignore
8//! use laminar_core::detect::{IoUringCapabilities, XdpCapabilities, StorageInfo};
9//!
10//! let io_uring = IoUringCapabilities::detect();
11//! if io_uring.sqpoll_supported {
12//!     println!("SQPOLL is available!");
13//! }
14//!
15//! let storage = StorageInfo::detect("/var/lib/laminardb");
16//! match storage.device_type {
17//!     StorageType::NVMe => println!("Fast NVMe storage!"),
18//!     _ => {}
19//! }
20//! ```
21
22use std::path::Path;
23
24use super::KernelVersion;
25
26/// `io_uring` capabilities.
27#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
28#[allow(clippy::struct_excessive_bools)]
29pub struct IoUringCapabilities {
30    /// Whether `io_uring` is available at all.
31    pub available: bool,
32    /// Whether SQPOLL mode is supported (kernel polling thread).
33    pub sqpoll_supported: bool,
34    /// Whether IOPOLL mode is supported (polling completions from device).
35    pub iopoll_supported: bool,
36    /// Whether registered buffers are supported.
37    pub registered_buffers: bool,
38    /// Whether multishot operations are supported.
39    pub multishot_supported: bool,
40    /// Whether `COOP_TASKRUN` is supported.
41    pub coop_taskrun: bool,
42    /// Whether `SINGLE_ISSUER` optimization is supported.
43    pub single_issuer: bool,
44    /// Whether the `io_uring` Cargo feature is enabled.
45    pub feature_enabled: bool,
46}
47
48impl IoUringCapabilities {
49    /// Detect `io_uring` capabilities.
50    #[must_use]
51    pub fn detect() -> Self {
52        let kernel = KernelVersion::detect();
53        Self::from_kernel_version(kernel.as_ref())
54    }
55
56    /// Determine capabilities from kernel version.
57    #[must_use]
58    pub fn from_kernel_version(kernel: Option<&KernelVersion>) -> Self {
59        // io_uring is only available on Linux
60        #[cfg(not(target_os = "linux"))]
61        {
62            let _ = kernel;
63            Self {
64                feature_enabled: false,
65                ..Default::default()
66            }
67        }
68
69        #[cfg(target_os = "linux")]
70        {
71            let feature_enabled = cfg!(feature = "io-uring");
72
73            let Some(kv) = kernel else {
74                return Self {
75                    feature_enabled,
76                    ..Default::default()
77                };
78            };
79
80            let available = kv.supports_io_uring() && feature_enabled;
81
82            Self {
83                available,
84                sqpoll_supported: available && kv.supports_io_uring_sqpoll(),
85                iopoll_supported: available && kv.supports_io_uring_iopoll(),
86                registered_buffers: available && kv.supports_io_uring_registered_buffers(),
87                multishot_supported: available && kv.supports_io_uring_multishot(),
88                coop_taskrun: available && kv.supports_io_uring_coop_taskrun(),
89                single_issuer: available && kv.supports_io_uring_single_issuer(),
90                feature_enabled,
91            }
92        }
93    }
94
95    /// Check if `io_uring` is usable for file I/O.
96    #[must_use]
97    pub fn is_usable(&self) -> bool {
98        self.available && self.feature_enabled
99    }
100
101    /// Check if advanced features are available.
102    #[must_use]
103    pub fn has_advanced_features(&self) -> bool {
104        self.sqpoll_supported && self.iopoll_supported
105    }
106
107    /// Get a summary string.
108    #[must_use]
109    pub fn summary(&self) -> String {
110        if !self.feature_enabled {
111            return "io_uring feature not enabled".to_string();
112        }
113
114        if !self.available {
115            return "io_uring not available".to_string();
116        }
117
118        let mut features = vec!["basic"];
119
120        if self.sqpoll_supported {
121            features.push("SQPOLL");
122        }
123        if self.iopoll_supported {
124            features.push("IOPOLL");
125        }
126        if self.registered_buffers {
127            features.push("registered_buffers");
128        }
129        if self.multishot_supported {
130            features.push("multishot");
131        }
132        if self.coop_taskrun {
133            features.push("coop_taskrun");
134        }
135        if self.single_issuer {
136            features.push("single_issuer");
137        }
138
139        format!("io_uring: {}", features.join(", "))
140    }
141}
142
143/// XDP/eBPF capabilities.
144#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
145#[allow(clippy::struct_excessive_bools)]
146pub struct XdpCapabilities {
147    /// Whether XDP is available.
148    pub available: bool,
149    /// Whether XDP generic mode (SKB) is supported.
150    pub generic_supported: bool,
151    /// Whether XDP native mode is widely supported.
152    pub native_supported: bool,
153    /// Whether XDP CPU map redirect is supported.
154    pub cpumap_supported: bool,
155    /// Whether the XDP Cargo feature is enabled.
156    pub feature_enabled: bool,
157}
158
159impl XdpCapabilities {
160    /// Detect XDP capabilities.
161    #[must_use]
162    pub fn detect() -> Self {
163        let kernel = KernelVersion::detect();
164        Self::from_kernel_version(kernel.as_ref())
165    }
166
167    /// Determine capabilities from kernel version.
168    #[must_use]
169    pub fn from_kernel_version(kernel: Option<&KernelVersion>) -> Self {
170        // XDP is only available on Linux
171        #[cfg(not(target_os = "linux"))]
172        {
173            let _ = kernel;
174            Self {
175                feature_enabled: false,
176                ..Default::default()
177            }
178        }
179
180        #[cfg(target_os = "linux")]
181        {
182            let feature_enabled = cfg!(feature = "xdp");
183
184            let Some(kv) = kernel else {
185                return Self {
186                    feature_enabled,
187                    ..Default::default()
188                };
189            };
190
191            let available = kv.supports_xdp() && feature_enabled;
192
193            Self {
194                available,
195                generic_supported: available && kv.supports_xdp_generic(),
196                native_supported: available && kv.supports_xdp_native(),
197                cpumap_supported: available && kv.supports_xdp_cpumap(),
198                feature_enabled,
199            }
200        }
201    }
202
203    /// Check if XDP is usable.
204    #[must_use]
205    pub fn is_usable(&self) -> bool {
206        self.available && self.feature_enabled
207    }
208
209    /// Get a summary string.
210    #[must_use]
211    pub fn summary(&self) -> String {
212        if !self.feature_enabled {
213            return "XDP feature not enabled".to_string();
214        }
215
216        if !self.available {
217            return "XDP not available".to_string();
218        }
219
220        let mut modes = Vec::new();
221
222        if self.generic_supported {
223            modes.push("generic");
224        }
225        if self.native_supported {
226            modes.push("native");
227        }
228        if self.cpumap_supported {
229            modes.push("cpumap");
230        }
231
232        format!("XDP: {}", modes.join(", "))
233    }
234}
235
236/// Storage device type.
237#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
238pub enum StorageType {
239    /// `NVMe` SSD (fastest, supports IOPOLL)
240    NVMe,
241    /// SATA/SAS SSD
242    Ssd,
243    /// Spinning hard drive
244    Hdd,
245    /// Network-attached storage
246    Network,
247    /// RAM disk / tmpfs
248    RamDisk,
249    /// Unknown or undetected
250    #[default]
251    Unknown,
252}
253
254impl StorageType {
255    /// Check if this storage type supports `io_uring` IOPOLL.
256    #[must_use]
257    pub fn supports_iopoll(&self) -> bool {
258        matches!(self, Self::NVMe)
259    }
260
261    /// Check if this is fast storage (SSD or `NVMe`).
262    #[must_use]
263    pub fn is_fast(&self) -> bool {
264        matches!(self, Self::NVMe | Self::Ssd | Self::RamDisk)
265    }
266
267    /// Get a description of the storage type.
268    #[must_use]
269    pub fn description(&self) -> &'static str {
270        match self {
271            Self::NVMe => "NVMe SSD",
272            Self::Ssd => "SSD",
273            Self::Hdd => "HDD",
274            Self::Network => "Network storage",
275            Self::RamDisk => "RAM disk",
276            Self::Unknown => "Unknown",
277        }
278    }
279}
280
281impl std::fmt::Display for StorageType {
282    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283        write!(f, "{}", self.description())
284    }
285}
286
287/// Storage device information.
288#[derive(Debug, Clone, Default)]
289pub struct StorageInfo {
290    /// Detected storage type.
291    pub device_type: StorageType,
292    /// Whether `O_DIRECT` is supported.
293    pub supports_direct_io: bool,
294    /// Detected block device name (e.g., "nvme0n1").
295    pub device_name: Option<String>,
296    /// Detected filesystem type (e.g., "ext4", "xfs").
297    pub filesystem: Option<String>,
298}
299
300impl StorageInfo {
301    /// Detect storage information for a path.
302    #[must_use]
303    pub fn detect<P: AsRef<Path>>(path: P) -> Self {
304        #[cfg(target_os = "linux")]
305        {
306            Self::detect_linux(path.as_ref())
307        }
308
309        #[cfg(not(target_os = "linux"))]
310        {
311            let _ = path;
312            Self::default()
313        }
314    }
315
316    /// Detect storage info on Linux.
317    #[cfg(target_os = "linux")]
318    fn detect_linux(path: &Path) -> Self {
319        let mut info = Self::default();
320
321        // Get the device from the path
322        let Some(device) = Self::get_device_for_path(path) else {
323            return info;
324        };
325
326        info.device_name = Some(device.clone());
327
328        // Detect storage type from sysfs
329        info.device_type = Self::detect_storage_type_sysfs(&device);
330
331        // Detect filesystem type
332        info.filesystem = Self::detect_filesystem_type(path);
333
334        // Check O_DIRECT support
335        info.supports_direct_io = Self::check_direct_io_support(path);
336
337        info
338    }
339
340    /// Get the block device for a path.
341    #[cfg(target_os = "linux")]
342    fn get_device_for_path(path: &Path) -> Option<String> {
343        use std::fs;
344        use std::os::unix::fs::MetadataExt;
345
346        // Get the device ID from the path
347        let metadata = fs::metadata(path).ok()?;
348        let dev = metadata.dev();
349
350        // Major/minor device numbers
351        let major = (dev >> 8) & 0xFF;
352        let minor = dev & 0xFF;
353
354        // Read /proc/diskstats or /sys/dev/block to find the device name
355        let _block_path = format!("/sys/dev/block/{major}:{minor}/device/../");
356
357        if let Ok(entries) = fs::read_dir("/sys/block") {
358            for entry in entries.flatten() {
359                let name = entry.file_name();
360                let dev_path = format!("/sys/block/{}/dev", name.to_string_lossy());
361                if let Ok(content) = fs::read_to_string(&dev_path) {
362                    let content = content.trim();
363                    if content == format!("{major}:{minor}") {
364                        return Some(name.to_string_lossy().to_string());
365                    }
366                    // Check for partitions
367                    if content.starts_with(&format!("{major}:")) {
368                        let base_name = name.to_string_lossy();
369                        // Strip partition number for the base device
370                        let base = base_name.trim_end_matches(|c: char| c.is_ascii_digit());
371                        if !base.is_empty() {
372                            return Some(base.to_string());
373                        }
374                    }
375                }
376            }
377        }
378
379        // Fallback: try to find from /proc/mounts
380        if let Ok(mounts) = fs::read_to_string("/proc/mounts") {
381            let path_str = path.to_string_lossy();
382            for line in mounts.lines() {
383                let parts: Vec<&str> = line.split_whitespace().collect();
384                if parts.len() >= 2 && path_str.starts_with(parts[1]) {
385                    if let Some(device) = parts[0].strip_prefix("/dev/") {
386                        let base = device.trim_end_matches(|c: char| c.is_ascii_digit());
387                        return Some(base.to_string());
388                    }
389                }
390            }
391        }
392
393        None
394    }
395
396    /// Detect storage type from sysfs.
397    #[cfg(target_os = "linux")]
398    fn detect_storage_type_sysfs(device: &str) -> StorageType {
399        use std::fs;
400
401        // Check for NVMe
402        if device.starts_with("nvme") {
403            return StorageType::NVMe;
404        }
405
406        // Check rotational flag
407        let rotational_path = format!("/sys/block/{device}/queue/rotational");
408        if let Ok(content) = fs::read_to_string(&rotational_path) {
409            if content.trim() == "0" {
410                return StorageType::Ssd;
411            } else if content.trim() == "1" {
412                return StorageType::Hdd;
413            }
414        }
415
416        // Check for RAM disk
417        if device.starts_with("ram") || device.starts_with("zram") {
418            return StorageType::RamDisk;
419        }
420
421        // Check for network devices
422        if device.starts_with("nbd") || device.starts_with("rbd") {
423            return StorageType::Network;
424        }
425
426        StorageType::Unknown
427    }
428
429    /// Detect filesystem type.
430    #[cfg(target_os = "linux")]
431    fn detect_filesystem_type(path: &Path) -> Option<String> {
432        use std::fs;
433
434        let path_str = path.to_string_lossy();
435
436        if let Ok(mounts) = fs::read_to_string("/proc/mounts") {
437            // Find the most specific mount point for this path
438            let mut best_match: Option<(&str, &str)> = None;
439
440            for line in mounts.lines() {
441                let parts: Vec<&str> = line.split_whitespace().collect();
442                if parts.len() >= 3 {
443                    let mount_point = parts[1];
444                    let fs_type = parts[2];
445
446                    if path_str.starts_with(mount_point) {
447                        match best_match {
448                            None => best_match = Some((mount_point, fs_type)),
449                            Some((prev_mount, _)) => {
450                                if mount_point.len() > prev_mount.len() {
451                                    best_match = Some((mount_point, fs_type));
452                                }
453                            }
454                        }
455                    }
456                }
457            }
458
459            return best_match.map(|(_, fs)| fs.to_string());
460        }
461
462        None
463    }
464
465    /// Check if `O_DIRECT` is supported.
466    #[cfg(target_os = "linux")]
467    fn check_direct_io_support(path: &Path) -> bool {
468        use std::fs::OpenOptions;
469        use std::os::unix::fs::OpenOptionsExt;
470
471        // Try to open a test file with O_DIRECT
472        let test_path = if path.is_dir() {
473            path.join(".laminardb_direct_test")
474        } else {
475            path.parent()
476                .map_or_else(|| path.to_path_buf(), |p| p.join(".laminardb_direct_test"))
477        };
478
479        let result = OpenOptions::new()
480            .read(true)
481            .write(true)
482            .create(true)
483            .custom_flags(libc::O_DIRECT)
484            .open(&test_path);
485
486        // Clean up test file
487        let _ = std::fs::remove_file(&test_path);
488
489        result.is_ok()
490    }
491
492    /// Get a summary string.
493    #[must_use]
494    pub fn summary(&self) -> String {
495        let mut parts = vec![self.device_type.description().to_string()];
496
497        if let Some(ref device) = self.device_name {
498            parts.push(format!("({device})"));
499        }
500
501        if let Some(ref fs) = self.filesystem {
502            parts.push(format!("[{fs}]"));
503        }
504
505        if self.supports_direct_io {
506            parts.push("O_DIRECT".to_string());
507        }
508
509        parts.join(" ")
510    }
511}
512
513/// Memory information.
514#[derive(Debug, Clone, Copy, Default)]
515pub struct MemoryInfo {
516    /// Total system memory in bytes.
517    pub total_memory: u64,
518    /// Available memory in bytes.
519    pub available_memory: u64,
520    /// Whether huge pages are available.
521    pub huge_pages_available: bool,
522    /// Huge page size in bytes (usually 2MB or 1GB).
523    pub huge_page_size: usize,
524    /// Number of free huge pages.
525    pub huge_pages_free: usize,
526    /// Whether transparent huge pages are enabled.
527    pub thp_enabled: bool,
528}
529
530impl MemoryInfo {
531    /// Detect memory information.
532    #[must_use]
533    pub fn detect() -> Self {
534        #[cfg(target_os = "linux")]
535        {
536            Self::detect_linux()
537        }
538
539        #[cfg(not(target_os = "linux"))]
540        {
541            Self::detect_fallback()
542        }
543    }
544
545    /// Detect memory info on Linux.
546    #[cfg(target_os = "linux")]
547    fn detect_linux() -> Self {
548        use std::fs;
549
550        let mut info = Self::default();
551
552        // Parse /proc/meminfo
553        if let Ok(meminfo) = fs::read_to_string("/proc/meminfo") {
554            for line in meminfo.lines() {
555                let parts: Vec<&str> = line.split_whitespace().collect();
556                if parts.len() >= 2 {
557                    let value: u64 = parts[1].parse().unwrap_or(0);
558                    match parts[0].trim_end_matches(':') {
559                        "MemTotal" => info.total_memory = value * 1024,
560                        "MemAvailable" => info.available_memory = value * 1024,
561                        #[allow(clippy::cast_possible_truncation)]
562                        "Hugepagesize" => info.huge_page_size = (value * 1024) as usize,
563                        #[allow(clippy::cast_possible_truncation)]
564                        "HugePages_Free" => info.huge_pages_free = value as usize,
565                        _ => {}
566                    }
567                }
568            }
569        }
570
571        // Check if huge pages are available
572        info.huge_pages_available = info.huge_page_size > 0;
573
574        // Check THP status
575        if let Ok(thp_enabled) = fs::read_to_string("/sys/kernel/mm/transparent_hugepage/enabled") {
576            // Format is: "[always] madvise never" where brackets indicate current setting
577            info.thp_enabled =
578                thp_enabled.contains("[always]") || thp_enabled.contains("[madvise]");
579        }
580
581        info
582    }
583
584    /// Fallback detection for non-Linux platforms.
585    #[cfg(not(target_os = "linux"))]
586    fn detect_fallback() -> Self {
587        // Estimate 16GB as a reasonable default
588        Self {
589            total_memory: 16 * 1024 * 1024 * 1024,
590            available_memory: 8 * 1024 * 1024 * 1024,
591            huge_pages_available: false,
592            huge_page_size: 0,
593            huge_pages_free: 0,
594            thp_enabled: false,
595        }
596    }
597
598    /// Get total memory in gigabytes.
599    #[must_use]
600    #[allow(clippy::cast_precision_loss)]
601    pub fn total_memory_gb(&self) -> f64 {
602        self.total_memory as f64 / (1024.0 * 1024.0 * 1024.0)
603    }
604
605    /// Get available memory in gigabytes.
606    #[must_use]
607    #[allow(clippy::cast_precision_loss)]
608    pub fn available_memory_gb(&self) -> f64 {
609        self.available_memory as f64 / (1024.0 * 1024.0 * 1024.0)
610    }
611
612    /// Get a summary string.
613    #[must_use]
614    pub fn summary(&self) -> String {
615        let mut parts = vec![format!("{:.1} GB total", self.total_memory_gb())];
616
617        if self.huge_pages_available {
618            parts.push(format!(
619                "{} huge pages ({} KB each)",
620                self.huge_pages_free,
621                self.huge_page_size / 1024
622            ));
623        }
624
625        if self.thp_enabled {
626            parts.push("THP enabled".to_string());
627        }
628
629        parts.join(", ")
630    }
631}
632
633#[cfg(test)]
634mod tests {
635    use super::*;
636
637    #[test]
638    fn test_io_uring_capabilities_default() {
639        let caps = IoUringCapabilities::default();
640        assert!(!caps.available);
641        assert!(!caps.sqpoll_supported);
642        assert!(!caps.iopoll_supported);
643    }
644
645    #[test]
646    fn test_io_uring_capabilities_from_kernel() {
647        // Test with kernel 5.1 (basic io_uring)
648        let kv = KernelVersion::new(5, 1, 0);
649        let caps = IoUringCapabilities::from_kernel_version(Some(&kv));
650
651        // Feature enabled depends on build flags
652        #[cfg(all(target_os = "linux", feature = "io-uring"))]
653        {
654            assert!(caps.available);
655            assert!(caps.registered_buffers);
656            assert!(!caps.sqpoll_supported); // Needs 5.11+
657        }
658
659        #[cfg(not(all(target_os = "linux", feature = "io-uring")))]
660        {
661            assert!(!caps.available);
662        }
663    }
664
665    #[test]
666    fn test_io_uring_capabilities_advanced() {
667        let kv = KernelVersion::new(6, 0, 0);
668        let caps = IoUringCapabilities::from_kernel_version(Some(&kv));
669
670        #[cfg(all(target_os = "linux", feature = "io-uring"))]
671        {
672            assert!(caps.sqpoll_supported);
673            assert!(caps.iopoll_supported);
674            assert!(caps.coop_taskrun);
675            assert!(caps.single_issuer);
676        }
677
678        // Suppress warning when feature is disabled
679        let _ = caps;
680    }
681
682    #[test]
683    fn test_io_uring_summary() {
684        let caps = IoUringCapabilities::default();
685        let summary = caps.summary();
686        assert!(!summary.is_empty());
687    }
688
689    #[test]
690    fn test_xdp_capabilities_default() {
691        let caps = XdpCapabilities::default();
692        assert!(!caps.available);
693        assert!(!caps.generic_supported);
694        assert!(!caps.native_supported);
695    }
696
697    #[test]
698    fn test_xdp_capabilities_from_kernel() {
699        let kv = KernelVersion::new(5, 3, 0);
700        let caps = XdpCapabilities::from_kernel_version(Some(&kv));
701
702        #[cfg(all(target_os = "linux", feature = "xdp"))]
703        {
704            assert!(caps.available);
705            assert!(caps.generic_supported);
706            assert!(caps.native_supported);
707            assert!(caps.cpumap_supported);
708        }
709
710        // Suppress warning when feature is disabled
711        let _ = caps;
712    }
713
714    #[test]
715    fn test_xdp_summary() {
716        let caps = XdpCapabilities::default();
717        let summary = caps.summary();
718        assert!(!summary.is_empty());
719    }
720
721    #[test]
722    fn test_storage_type_properties() {
723        assert!(StorageType::NVMe.supports_iopoll());
724        assert!(!StorageType::Ssd.supports_iopoll());
725        assert!(!StorageType::Hdd.supports_iopoll());
726
727        assert!(StorageType::NVMe.is_fast());
728        assert!(StorageType::Ssd.is_fast());
729        assert!(!StorageType::Hdd.is_fast());
730        assert!(StorageType::RamDisk.is_fast());
731    }
732
733    #[test]
734    fn test_storage_type_display() {
735        assert_eq!(format!("{}", StorageType::NVMe), "NVMe SSD");
736        assert_eq!(format!("{}", StorageType::Ssd), "SSD");
737        assert_eq!(format!("{}", StorageType::Hdd), "HDD");
738    }
739
740    #[test]
741    fn test_storage_info_default() {
742        let info = StorageInfo::default();
743        assert_eq!(info.device_type, StorageType::Unknown);
744        assert!(!info.supports_direct_io);
745    }
746
747    #[test]
748    fn test_storage_info_detect() {
749        // This should not panic on any platform
750        let info = StorageInfo::detect("/");
751
752        // Summary should work
753        let summary = info.summary();
754        assert!(!summary.is_empty());
755    }
756
757    #[test]
758    fn test_memory_info_detect() {
759        let info = MemoryInfo::detect();
760
761        // Should have some memory
762        assert!(info.total_memory > 0);
763
764        // Summary should work
765        let summary = info.summary();
766        assert!(!summary.is_empty());
767    }
768
769    #[test]
770    fn test_memory_info_gb_conversion() {
771        let info = MemoryInfo {
772            total_memory: 16 * 1024 * 1024 * 1024,    // 16 GB
773            available_memory: 8 * 1024 * 1024 * 1024, // 8 GB
774            ..Default::default()
775        };
776
777        assert!((info.total_memory_gb() - 16.0).abs() < 0.01);
778        assert!((info.available_memory_gb() - 8.0).abs() < 0.01);
779    }
780}