1use std::collections::VecDeque;
18use std::str::FromStr;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub enum StorageMedia {
22 Nvme,
23 Ssd,
24 Hdd,
25 Unknown,
26}
27
28impl StorageMedia {
29 pub fn as_str(&self) -> &'static str {
30 match self {
31 Self::Nvme => "nvme",
32 Self::Ssd => "ssd",
33 Self::Hdd => "hdd",
34 Self::Unknown => "unknown",
35 }
36 }
37}
38
39impl FromStr for StorageMedia {
40 type Err = ();
41
42 fn from_str(value: &str) -> Result<Self, Self::Err> {
43 match value.trim().to_ascii_lowercase().as_str() {
44 "nvme" => Ok(Self::Nvme),
45 "ssd" => Ok(Self::Ssd),
46 "hdd" => Ok(Self::Hdd),
47 "unknown" => Ok(Self::Unknown),
48 _ => Err(()),
49 }
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
54pub enum AccessPattern {
55 Sequential,
56 Random,
57 Mixed,
58 Unknown,
59}
60
61impl AccessPattern {
62 pub fn as_str(&self) -> &'static str {
63 match self {
64 Self::Sequential => "sequential",
65 Self::Random => "random",
66 Self::Mixed => "mixed",
67 Self::Unknown => "unknown",
68 }
69 }
70
71 pub fn is_sequential(&self) -> bool {
73 matches!(self, Self::Sequential)
74 }
75
76 pub fn is_random(&self) -> bool {
78 matches!(self, Self::Random)
79 }
80
81 pub fn is_mixed(&self) -> bool {
83 matches!(self, Self::Mixed)
84 }
85
86 pub fn is_unknown(&self) -> bool {
88 matches!(self, Self::Unknown)
89 }
90}
91
92#[derive(Debug, Clone, Copy, PartialEq)]
93pub struct StorageProfile {
94 pub media: StorageMedia,
95 pub buffer_cap: usize,
96 pub sequential_boost_multiplier: f64,
97 pub random_penalty_multiplier: f64,
98 pub prefers_readahead: bool,
99}
100
101impl StorageProfile {
102 pub fn for_media(media: StorageMedia, nvme_buffer_cap: usize, ssd_buffer_cap: usize, hdd_buffer_cap: usize) -> Self {
103 match media {
104 StorageMedia::Nvme => Self {
105 media,
106 buffer_cap: nvme_buffer_cap,
107 sequential_boost_multiplier: 1.35,
108 random_penalty_multiplier: 0.9,
109 prefers_readahead: true,
110 },
111 StorageMedia::Ssd => Self {
112 media,
113 buffer_cap: ssd_buffer_cap,
114 sequential_boost_multiplier: 1.2,
115 random_penalty_multiplier: 0.8,
116 prefers_readahead: true,
117 },
118 StorageMedia::Hdd => Self {
119 media,
120 buffer_cap: hdd_buffer_cap,
121 sequential_boost_multiplier: 1.1,
122 random_penalty_multiplier: 0.65,
123 prefers_readahead: false,
124 },
125 StorageMedia::Unknown => Self {
126 media,
127 buffer_cap: ssd_buffer_cap,
128 sequential_boost_multiplier: 1.0,
129 random_penalty_multiplier: 0.8,
130 prefers_readahead: true,
131 },
132 }
133 }
134}
135
136#[derive(Debug, Clone)]
137pub struct IoPatternDetector {
138 history_size: usize,
139 sequential_step_tolerance_bytes: u64,
140 history: VecDeque<(u64, u64)>,
141}
142
143impl IoPatternDetector {
144 pub fn new(history_size: usize, sequential_step_tolerance_bytes: u64) -> Self {
145 Self {
146 history_size: history_size.max(2),
147 sequential_step_tolerance_bytes,
148 history: VecDeque::with_capacity(history_size.max(2)),
149 }
150 }
151
152 pub fn record(&mut self, offset: u64, len: u64) {
153 if self.history.len() == self.history_size {
154 self.history.pop_front();
155 }
156 self.history.push_back((offset, len));
157 }
158
159 pub fn current_pattern(&self) -> AccessPattern {
160 if self.history.len() < 2 {
161 return AccessPattern::Unknown;
162 }
163
164 let history = self.history.iter().copied().collect::<Vec<_>>();
165 let mut sequential = 0usize;
166 let mut random = 0usize;
167
168 for window in history.windows(2) {
169 let (prev_offset, prev_len) = window[0];
170 let (curr_offset, _) = window[1];
171 let prev_end = prev_offset.saturating_add(prev_len);
172 if curr_offset.abs_diff(prev_end) <= self.sequential_step_tolerance_bytes {
173 sequential += 1;
174 } else {
175 random += 1;
176 }
177 }
178
179 match (sequential, random) {
180 (0, 0) => AccessPattern::Unknown,
181 (_, 0) => AccessPattern::Sequential,
182 (0, _) => AccessPattern::Random,
183 _ => AccessPattern::Mixed,
184 }
185 }
186}
187
188pub fn detect_storage_media(storage_detection_enabled: bool, storage_media_override: &str) -> StorageMedia {
189 if let Ok(media) = StorageMedia::from_str(storage_media_override) {
190 return media;
191 }
192
193 if !storage_detection_enabled {
194 return StorageMedia::Unknown;
195 }
196
197 #[cfg(target_os = "linux")]
199 {
200 if let Ok(media) = detect_linux_storage_media()
201 && media != StorageMedia::Unknown
202 {
203 return media;
204 }
205 }
206
207 #[cfg(target_os = "macos")]
208 {
209 if let Ok(media) = detect_macos_storage_media()
210 && media != StorageMedia::Unknown
211 {
212 return media;
213 }
214 }
215
216 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
217 {
218 if let Ok(media) = detect_platform_storage_media()
219 && media != StorageMedia::Unknown
220 {
221 return media;
222 }
223 }
224
225 StorageMedia::Unknown
226}
227
228#[cfg(target_os = "linux")]
229fn detect_linux_storage_media() -> Result<StorageMedia, std::io::Error> {
230 use std::path::Path;
231
232 if Path::new("/sys/class/nvme").exists() {
234 if let Ok(entries) = std::fs::read_dir("/sys/class/nvme") {
236 for entry in entries.flatten() {
237 let name = entry.file_name();
238 let name_str = name.to_string_lossy();
239 if name_str.starts_with("nvme") {
240 return Ok(StorageMedia::Nvme);
241 }
242 }
243 }
244 }
245
246 for device in &["sda", "sdb", "nvme0n1", "vda"] {
248 let rotational_path = format!("/sys/block/{}/queue/rotational", device);
249 if let Ok(content) = std::fs::read_to_string(&rotational_path) {
250 let rotational = content.trim().parse::<u32>().unwrap_or(1);
251 if rotational == 0 {
252 if device.starts_with("nvme") {
255 return Ok(StorageMedia::Nvme);
256 }
257 return Ok(StorageMedia::Ssd);
258 } else {
259 return Ok(StorageMedia::Hdd);
261 }
262 }
263 }
264
265 Ok(StorageMedia::Unknown)
266}
267
268#[cfg(target_os = "macos")]
269fn detect_macos_storage_media() -> Result<StorageMedia, std::io::Error> {
270 use std::process::Command;
271
272 let output = Command::new("diskutil").args(["info", "/"]).output()?;
274
275 if !output.status.success() {
276 return Ok(StorageMedia::Unknown);
277 }
278
279 let info = String::from_utf8_lossy(&output.stdout);
280
281 if info.contains("NVMe") || info.contains("nvme") {
283 return Ok(StorageMedia::Nvme);
284 }
285
286 if info.contains("Solid State") || info.contains("SSD") || info.contains("Solid-State") {
288 return Ok(StorageMedia::Ssd);
289 }
290
291 if info.contains("Rotational") || info.contains("HDD") {
295 return Ok(StorageMedia::Hdd);
296 }
297
298 Ok(StorageMedia::Ssd)
301}
302
303#[cfg(not(any(target_os = "linux", target_os = "macos")))]
304fn detect_platform_storage_media() -> Result<StorageMedia, std::io::Error> {
305 Ok(StorageMedia::Unknown)
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn test_storage_media_override() {
314 assert_eq!(detect_storage_media(true, "nvme"), StorageMedia::Nvme);
316 assert_eq!(detect_storage_media(false, "ssd"), StorageMedia::Ssd);
317 assert_eq!(detect_storage_media(true, "hdd"), StorageMedia::Hdd);
318 assert_eq!(detect_storage_media(false, "unknown"), StorageMedia::Unknown);
319 }
320
321 #[test]
322 fn test_storage_media_from_str() {
323 assert_eq!(StorageMedia::from_str("nvme"), Ok(StorageMedia::Nvme));
324 assert_eq!(StorageMedia::from_str("NVMe"), Ok(StorageMedia::Nvme));
325 assert_eq!(StorageMedia::from_str("ssd"), Ok(StorageMedia::Ssd));
326 assert_eq!(StorageMedia::from_str("SSD"), Ok(StorageMedia::Ssd));
327 assert_eq!(StorageMedia::from_str("hdd"), Ok(StorageMedia::Hdd));
328 assert_eq!(StorageMedia::from_str("HDD"), Ok(StorageMedia::Hdd));
329 assert_eq!(StorageMedia::from_str("unknown"), Ok(StorageMedia::Unknown));
330 assert_eq!(StorageMedia::from_str("invalid"), Err(()));
331 assert_eq!(StorageMedia::from_str(""), Err(()));
332 }
333
334 #[test]
335 fn test_storage_media_as_str() {
336 assert_eq!(StorageMedia::Nvme.as_str(), "nvme");
337 assert_eq!(StorageMedia::Ssd.as_str(), "ssd");
338 assert_eq!(StorageMedia::Hdd.as_str(), "hdd");
339 assert_eq!(StorageMedia::Unknown.as_str(), "unknown");
340 }
341
342 #[test]
343 fn test_storage_detection_disabled() {
344 assert_eq!(detect_storage_media(false, ""), StorageMedia::Unknown);
346 }
347
348 #[test]
349 fn test_pattern_detector_sequential() {
350 let mut detector = IoPatternDetector::new(4, 1024);
351 detector.record(0, 4096);
352 detector.record(4096, 4096);
353 detector.record(8192, 4096);
354 assert_eq!(detector.current_pattern(), AccessPattern::Sequential);
355 }
356
357 #[test]
358 fn test_pattern_detector_random() {
359 let mut detector = IoPatternDetector::new(4, 1024);
360 detector.record(0, 4096);
361 detector.record(65536, 4096);
362 detector.record(4096, 4096);
363 assert_eq!(detector.current_pattern(), AccessPattern::Random);
364 }
365
366 #[test]
367 fn test_pattern_detector_mixed() {
368 let mut detector = IoPatternDetector::new(10, 1024);
369 detector.record(0, 4096); detector.record(4096, 4096); detector.record(65536, 4096); detector.record(98304, 4096); assert_eq!(detector.current_pattern(), AccessPattern::Mixed);
374 }
375
376 #[test]
377 fn test_pattern_detector_insufficient_history() {
378 let detector = IoPatternDetector::new(10, 1024);
379 assert_eq!(detector.current_pattern(), AccessPattern::Unknown);
381
382 let mut detector = IoPatternDetector::new(10, 1024);
384 detector.record(0, 4096);
385 assert_eq!(detector.current_pattern(), AccessPattern::Unknown);
386 }
387
388 #[test]
389 fn test_access_pattern_helpers() {
390 assert!(AccessPattern::Sequential.is_sequential());
391 assert!(!AccessPattern::Sequential.is_random());
392 assert!(!AccessPattern::Sequential.is_mixed());
393 assert!(!AccessPattern::Sequential.is_unknown());
394
395 assert!(AccessPattern::Random.is_random());
396 assert!(!AccessPattern::Random.is_sequential());
397
398 assert!(AccessPattern::Mixed.is_mixed());
399 assert!(!AccessPattern::Mixed.is_sequential());
400 assert!(!AccessPattern::Mixed.is_random());
401
402 assert!(AccessPattern::Unknown.is_unknown());
403 assert!(!AccessPattern::Unknown.is_sequential());
404 }
405
406 #[test]
407 fn test_storage_profile_for_media() {
408 let nvme_cap = 2 * 1024 * 1024;
409 let ssd_cap = 1024 * 1024;
410 let hdd_cap = 512 * 1024;
411
412 let nvme_profile = StorageProfile::for_media(StorageMedia::Nvme, nvme_cap, ssd_cap, hdd_cap);
413 assert_eq!(nvme_profile.media, StorageMedia::Nvme);
414 assert_eq!(nvme_profile.buffer_cap, nvme_cap);
415 assert_eq!(nvme_profile.sequential_boost_multiplier, 1.35);
416 assert_eq!(nvme_profile.random_penalty_multiplier, 0.9);
417 assert!(nvme_profile.prefers_readahead);
418
419 let ssd_profile = StorageProfile::for_media(StorageMedia::Ssd, nvme_cap, ssd_cap, hdd_cap);
420 assert_eq!(ssd_profile.media, StorageMedia::Ssd);
421 assert_eq!(ssd_profile.buffer_cap, ssd_cap);
422 assert_eq!(ssd_profile.sequential_boost_multiplier, 1.2);
423 assert_eq!(ssd_profile.random_penalty_multiplier, 0.8);
424
425 let hdd_profile = StorageProfile::for_media(StorageMedia::Hdd, nvme_cap, ssd_cap, hdd_cap);
426 assert_eq!(hdd_profile.media, StorageMedia::Hdd);
427 assert_eq!(hdd_profile.buffer_cap, hdd_cap);
428 assert_eq!(hdd_profile.sequential_boost_multiplier, 1.1);
429 assert_eq!(hdd_profile.random_penalty_multiplier, 0.65);
430 assert!(!hdd_profile.prefers_readahead);
431
432 let unknown_profile = StorageProfile::for_media(StorageMedia::Unknown, nvme_cap, ssd_cap, hdd_cap);
433 assert_eq!(unknown_profile.media, StorageMedia::Unknown);
434 assert_eq!(unknown_profile.buffer_cap, ssd_cap);
436 assert_eq!(unknown_profile.sequential_boost_multiplier, 1.0);
437 }
438
439 #[test]
443 fn storage_media_override_wins_over_platform_detection() {
444 for (override_value, expected) in [
445 ("nvme", StorageMedia::Nvme),
446 ("ssd", StorageMedia::Ssd),
447 ("hdd", StorageMedia::Hdd),
448 ] {
449 assert_eq!(detect_storage_media(true, override_value), expected);
450 assert_eq!(
451 detect_storage_media(false, override_value),
452 expected,
453 "an override must be honoured even with detection disabled"
454 );
455 }
456 }
457
458 #[test]
459 fn disabled_detection_reports_unknown_instead_of_guessing() {
460 assert_eq!(detect_storage_media(false, ""), StorageMedia::Unknown);
461 assert_eq!(
462 detect_storage_media(false, "not-a-medium"),
463 StorageMedia::Unknown,
464 "an unparseable override falls through to the disabled path"
465 );
466 }
467
468 #[test]
469 fn enabled_detection_returns_a_medium_for_this_platform() {
470 let first = detect_storage_media(true, "");
474 assert!(matches!(
475 first,
476 StorageMedia::Nvme | StorageMedia::Ssd | StorageMedia::Hdd | StorageMedia::Unknown
477 ));
478 assert_eq!(detect_storage_media(true, ""), first);
479 }
480}