1use std::path::Path;
23
24use super::KernelVersion;
25
26#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
28#[allow(clippy::struct_excessive_bools)]
29pub struct IoUringCapabilities {
30 pub available: bool,
32 pub sqpoll_supported: bool,
34 pub iopoll_supported: bool,
36 pub registered_buffers: bool,
38 pub multishot_supported: bool,
40 pub coop_taskrun: bool,
42 pub single_issuer: bool,
44 pub feature_enabled: bool,
46}
47
48impl IoUringCapabilities {
49 #[must_use]
51 pub fn detect() -> Self {
52 let kernel = KernelVersion::detect();
53 Self::from_kernel_version(kernel.as_ref())
54 }
55
56 #[must_use]
58 pub fn from_kernel_version(kernel: Option<&KernelVersion>) -> Self {
59 #[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 #[must_use]
97 pub fn is_usable(&self) -> bool {
98 self.available && self.feature_enabled
99 }
100
101 #[must_use]
103 pub fn has_advanced_features(&self) -> bool {
104 self.sqpoll_supported && self.iopoll_supported
105 }
106
107 #[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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
145#[allow(clippy::struct_excessive_bools)]
146pub struct XdpCapabilities {
147 pub available: bool,
149 pub generic_supported: bool,
151 pub native_supported: bool,
153 pub cpumap_supported: bool,
155 pub feature_enabled: bool,
157}
158
159impl XdpCapabilities {
160 #[must_use]
162 pub fn detect() -> Self {
163 let kernel = KernelVersion::detect();
164 Self::from_kernel_version(kernel.as_ref())
165 }
166
167 #[must_use]
169 pub fn from_kernel_version(kernel: Option<&KernelVersion>) -> Self {
170 #[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 #[must_use]
205 pub fn is_usable(&self) -> bool {
206 self.available && self.feature_enabled
207 }
208
209 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
238pub enum StorageType {
239 NVMe,
241 Ssd,
243 Hdd,
245 Network,
247 RamDisk,
249 #[default]
251 Unknown,
252}
253
254impl StorageType {
255 #[must_use]
257 pub fn supports_iopoll(&self) -> bool {
258 matches!(self, Self::NVMe)
259 }
260
261 #[must_use]
263 pub fn is_fast(&self) -> bool {
264 matches!(self, Self::NVMe | Self::Ssd | Self::RamDisk)
265 }
266
267 #[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#[derive(Debug, Clone, Default)]
289pub struct StorageInfo {
290 pub device_type: StorageType,
292 pub supports_direct_io: bool,
294 pub device_name: Option<String>,
296 pub filesystem: Option<String>,
298}
299
300impl StorageInfo {
301 #[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 #[cfg(target_os = "linux")]
318 fn detect_linux(path: &Path) -> Self {
319 let mut info = Self::default();
320
321 let Some(device) = Self::get_device_for_path(path) else {
323 return info;
324 };
325
326 info.device_name = Some(device.clone());
327
328 info.device_type = Self::detect_storage_type_sysfs(&device);
330
331 info.filesystem = Self::detect_filesystem_type(path);
333
334 info.supports_direct_io = Self::check_direct_io_support(path);
336
337 info
338 }
339
340 #[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 let metadata = fs::metadata(path).ok()?;
348 let dev = metadata.dev();
349
350 let major = (dev >> 8) & 0xFF;
352 let minor = dev & 0xFF;
353
354 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 if content.starts_with(&format!("{major}:")) {
368 let base_name = name.to_string_lossy();
369 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 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 #[cfg(target_os = "linux")]
398 fn detect_storage_type_sysfs(device: &str) -> StorageType {
399 use std::fs;
400
401 if device.starts_with("nvme") {
403 return StorageType::NVMe;
404 }
405
406 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 if device.starts_with("ram") || device.starts_with("zram") {
418 return StorageType::RamDisk;
419 }
420
421 if device.starts_with("nbd") || device.starts_with("rbd") {
423 return StorageType::Network;
424 }
425
426 StorageType::Unknown
427 }
428
429 #[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 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 #[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 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 let _ = std::fs::remove_file(&test_path);
488
489 result.is_ok()
490 }
491
492 #[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#[derive(Debug, Clone, Copy, Default)]
515pub struct MemoryInfo {
516 pub total_memory: u64,
518 pub available_memory: u64,
520 pub huge_pages_available: bool,
522 pub huge_page_size: usize,
524 pub huge_pages_free: usize,
526 pub thp_enabled: bool,
528}
529
530impl MemoryInfo {
531 #[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 #[cfg(target_os = "linux")]
547 fn detect_linux() -> Self {
548 use std::fs;
549
550 let mut info = Self::default();
551
552 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 info.huge_pages_available = info.huge_page_size > 0;
573
574 if let Ok(thp_enabled) = fs::read_to_string("/sys/kernel/mm/transparent_hugepage/enabled") {
576 info.thp_enabled =
578 thp_enabled.contains("[always]") || thp_enabled.contains("[madvise]");
579 }
580
581 info
582 }
583
584 #[cfg(not(target_os = "linux"))]
586 fn detect_fallback() -> Self {
587 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 #[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 #[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 #[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 let kv = KernelVersion::new(5, 1, 0);
649 let caps = IoUringCapabilities::from_kernel_version(Some(&kv));
650
651 #[cfg(all(target_os = "linux", feature = "io-uring"))]
653 {
654 assert!(caps.available);
655 assert!(caps.registered_buffers);
656 assert!(!caps.sqpoll_supported); }
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 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 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 let info = StorageInfo::detect("/");
751
752 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 assert!(info.total_memory > 0);
763
764 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, available_memory: 8 * 1024 * 1024 * 1024, ..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}