1use std::collections::{HashMap, VecDeque};
16use std::time::{Duration, Instant};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum ReadAheadPattern {
25 Repeated,
27 Sequential,
29 Strided {
31 stride: u64,
33 },
34 Random,
36}
37
38impl ReadAheadPattern {
39 pub fn detect(history: &[u64]) -> Self {
44 if history.len() < 2 {
45 return Self::Random;
46 }
47
48 let deltas: Vec<u64> = history
50 .windows(2)
51 .map(|w| w[1].wrapping_sub(w[0]))
52 .collect();
53
54 let first = deltas[0];
55
56 if deltas.iter().all(|&d| d == first) {
58 match first {
59 0 => Self::Repeated,
60 1 => Self::Sequential,
61 s => Self::Strided { stride: s },
62 }
63 } else {
64 Self::Random
65 }
66 }
67}
68
69pub type AccessPattern = ReadAheadPattern;
73
74#[derive(Debug, Clone)]
80pub struct PrefetchHint {
81 pub block_offsets: Vec<u64>,
83 pub pattern: ReadAheadPattern,
85 pub confidence: f32,
87}
88
89#[derive(Debug, Clone, Default)]
97pub struct ReadAheadStats {
98 pub total_accesses: u64,
100 pub total_hints_issued: u64,
102 pub total_deduped: u64,
104}
105
106const HISTORY_CAP: usize = 64;
112
113pub struct ReadAheadScheduler {
128 history: VecDeque<u64>,
130 lookahead: usize,
132 prefetch_cache: HashMap<u64, Instant>,
134 cache_ttl: Duration,
136 stats: ReadAheadStats,
138}
139
140impl Default for ReadAheadScheduler {
141 fn default() -> Self {
142 Self::new()
143 }
144}
145
146impl ReadAheadScheduler {
147 pub fn new() -> Self {
149 Self {
150 history: VecDeque::with_capacity(HISTORY_CAP),
151 lookahead: 8,
152 prefetch_cache: HashMap::new(),
153 cache_ttl: Duration::from_secs(30),
154 stats: ReadAheadStats::default(),
155 }
156 }
157
158 pub fn with_config(lookahead: usize, cache_ttl: Duration) -> Self {
160 Self {
161 history: VecDeque::with_capacity(HISTORY_CAP),
162 lookahead,
163 prefetch_cache: HashMap::new(),
164 cache_ttl,
165 stats: ReadAheadStats::default(),
166 }
167 }
168
169 pub fn record_access(&mut self, offset: u64) {
177 if self.history.len() == HISTORY_CAP {
178 self.history.pop_front();
179 }
180 self.history.push_back(offset);
181 self.stats.total_accesses += 1;
182 }
183
184 pub fn next_hints(&mut self) -> Option<PrefetchHint> {
190 if self.history.len() < 2 {
192 return None;
193 }
194
195 let history_slice: Vec<u64> = self.history.iter().copied().collect();
196 let pattern = ReadAheadPattern::detect(&history_slice);
197
198 let last = *self.history.back()?;
199
200 let (raw_offsets, confidence) = match pattern {
201 ReadAheadPattern::Sequential => {
202 let offsets: Vec<u64> = (1..=(self.lookahead as u64))
203 .map(|i| last.wrapping_add(i))
204 .collect();
205 (offsets, 0.95_f32)
206 }
207 ReadAheadPattern::Strided { stride } => {
208 let offsets: Vec<u64> = (1..=(self.lookahead as u64))
209 .map(|i| last.wrapping_add(stride.wrapping_mul(i)))
210 .collect();
211 (offsets, 0.85_f32)
212 }
213 ReadAheadPattern::Repeated => {
214 (vec![last], 0.5_f32)
216 }
217 ReadAheadPattern::Random => {
218 return None;
219 }
220 };
221
222 let now = Instant::now();
224 let mut deduped_count: u64 = 0;
225 let mut block_offsets = Vec::with_capacity(raw_offsets.len());
226
227 for offset in raw_offsets {
228 let cached = self
229 .prefetch_cache
230 .get(&offset)
231 .is_some_and(|&issued| now.duration_since(issued) < self.cache_ttl);
232 if cached {
233 deduped_count += 1;
234 } else {
235 block_offsets.push(offset);
236 self.prefetch_cache.insert(offset, now);
237 }
238 }
239
240 self.stats.total_deduped += deduped_count;
241
242 let hint = PrefetchHint {
245 block_offsets,
246 pattern,
247 confidence,
248 };
249 self.stats.total_hints_issued += 1;
250 Some(hint)
251 }
252
253 pub fn evict_stale_cache(&mut self) {
255 let ttl = self.cache_ttl;
256 let now = Instant::now();
257 self.prefetch_cache
258 .retain(|_offset, &mut issued| now.duration_since(issued) < ttl);
259 }
260
261 pub fn pattern(&self) -> ReadAheadPattern {
265 if self.history.len() < 2 {
266 return ReadAheadPattern::Random;
267 }
268 let slice: Vec<u64> = self.history.iter().copied().collect();
269 ReadAheadPattern::detect(&slice)
270 }
271
272 pub fn stats(&self) -> &ReadAheadStats {
274 &self.stats
275 }
276
277 pub fn stats_mut(&mut self) -> &mut ReadAheadStats {
279 &mut self.stats
280 }
281
282 pub fn prefetch_cache_len(&self) -> usize {
284 self.prefetch_cache.len()
285 }
286}
287
288#[cfg(test)]
293mod tests {
294 use super::*;
295 use std::thread;
296 use std::time::Duration;
297
298 #[test]
301 fn test_detect_sequential() {
302 let history = vec![10, 11, 12, 13, 14];
303 assert_eq!(
304 ReadAheadPattern::detect(&history),
305 ReadAheadPattern::Sequential
306 );
307 }
308
309 #[test]
310 fn test_detect_strided() {
311 let history = vec![0, 4, 8, 12, 16];
312 assert_eq!(
313 ReadAheadPattern::detect(&history),
314 ReadAheadPattern::Strided { stride: 4 }
315 );
316 }
317
318 #[test]
319 fn test_detect_repeated() {
320 let history = vec![7, 7, 7, 7, 7];
321 assert_eq!(
322 ReadAheadPattern::detect(&history),
323 ReadAheadPattern::Repeated
324 );
325 }
326
327 #[test]
328 fn test_detect_random() {
329 let history = vec![1, 5, 2, 9, 3];
330 assert_eq!(ReadAheadPattern::detect(&history), ReadAheadPattern::Random);
331 }
332
333 #[test]
334 fn test_detect_single_entry_is_random() {
335 let history = vec![42];
336 assert_eq!(ReadAheadPattern::detect(&history), ReadAheadPattern::Random);
337 }
338
339 #[test]
340 fn test_detect_empty_is_random() {
341 assert_eq!(ReadAheadPattern::detect(&[]), ReadAheadPattern::Random);
342 }
343
344 #[test]
345 fn test_detect_two_entry_sequential() {
346 let history = vec![100, 101];
347 assert_eq!(
348 ReadAheadPattern::detect(&history),
349 ReadAheadPattern::Sequential
350 );
351 }
352
353 #[test]
356 fn test_sequential_prefetch_offsets() {
357 let mut sched = ReadAheadScheduler::new();
358 for i in 0u64..4 {
359 sched.record_access(i);
360 }
361 let hint = sched.next_hints().expect("expected a hint");
362 assert_eq!(hint.pattern, ReadAheadPattern::Sequential);
363 let expected: Vec<u64> = (4..=11).collect();
365 assert_eq!(hint.block_offsets, expected);
366 }
367
368 #[test]
369 fn test_sequential_confidence_high() {
370 let mut sched = ReadAheadScheduler::new();
371 for i in 0u64..4 {
372 sched.record_access(i);
373 }
374 let hint = sched.next_hints().expect("expected a hint");
375 assert!(
376 hint.confidence >= 0.9,
377 "sequential confidence should be high"
378 );
379 }
380
381 #[test]
384 fn test_strided_prefetch_offsets() {
385 let mut sched = ReadAheadScheduler::new();
386 for i in 0u64..4 {
387 sched.record_access(i * 8);
388 }
389 let hint = sched.next_hints().expect("expected a hint");
390 assert_eq!(hint.pattern, ReadAheadPattern::Strided { stride: 8 });
391 let expected: Vec<u64> = (1..=8).map(|i| 24 + i * 8).collect();
393 assert_eq!(hint.block_offsets, expected);
394 }
395
396 #[test]
399 fn test_random_returns_none() {
400 let mut sched = ReadAheadScheduler::new();
401 for &offset in &[1u64, 100, 5, 77, 42] {
402 sched.record_access(offset);
403 }
404 assert!(
405 sched.next_hints().is_none(),
406 "random pattern should produce no hint"
407 );
408 }
409
410 #[test]
413 fn test_repeated_returns_same_offset_hint() {
414 let mut sched = ReadAheadScheduler::new();
415 for _ in 0..5 {
416 sched.record_access(99);
417 }
418 let hint = sched
419 .next_hints()
420 .expect("expected a hint for repeated pattern");
421 assert_eq!(hint.pattern, ReadAheadPattern::Repeated);
422 assert_eq!(hint.block_offsets, vec![99]);
423 assert!(
424 (hint.confidence - 0.5).abs() < f32::EPSILON,
425 "repeated confidence should be 0.5"
426 );
427 }
428
429 #[test]
432 fn test_dedup_skips_recently_prefetched() {
433 let mut sched = ReadAheadScheduler::new();
434 for i in 0u64..4 {
436 sched.record_access(i);
437 }
438
439 let hint1 = sched.next_hints().expect("expected hint");
441 assert_eq!(hint1.block_offsets.len(), 8);
442 assert_eq!(sched.stats().total_deduped, 0);
443
444 sched.record_access(4);
446
447 let hint2 = sched.next_hints().expect("expected hint");
450 assert_eq!(
452 hint2.block_offsets.len(),
453 1,
454 "only offset 12 should be fresh"
455 );
456 assert_eq!(sched.stats().total_deduped, 7);
457 }
458
459 #[test]
462 fn test_evict_stale_cache_removes_expired() {
463 let mut sched = ReadAheadScheduler::with_config(4, Duration::from_millis(50));
465
466 for i in 0u64..4 {
468 sched.record_access(i);
469 }
470 let _hint = sched.next_hints();
471 assert!(sched.prefetch_cache_len() > 0, "cache should be populated");
472
473 thread::sleep(Duration::from_millis(80));
475
476 sched.evict_stale_cache();
477 assert_eq!(
478 sched.prefetch_cache_len(),
479 0,
480 "all entries should be evicted"
481 );
482 }
483
484 #[test]
487 fn test_history_capped_at_64() {
488 let mut sched = ReadAheadScheduler::new();
489 for i in 0u64..200 {
490 sched.record_access(i);
491 }
492 assert_eq!(sched.history.len(), 64);
494 assert_eq!(*sched.history.back().expect("non-empty"), 199);
496 assert_eq!(*sched.history.front().expect("non-empty"), 136);
498 }
499
500 #[test]
503 fn test_stats_accumulate() {
504 let mut sched = ReadAheadScheduler::new();
505
506 for i in 0u64..10 {
508 sched.record_access(i);
509 }
510 assert_eq!(sched.stats().total_accesses, 10);
511
512 let h1 = sched.next_hints();
514 let h2 = sched.next_hints();
515
516 assert!(h1.is_some());
517 assert!(h2.is_some());
518 assert_eq!(sched.stats().total_hints_issued, 2);
519 }
520
521 #[test]
524 fn test_pattern_accessor_sequential() {
525 let mut sched = ReadAheadScheduler::new();
526 for i in 0u64..6 {
527 sched.record_access(i);
528 }
529 assert_eq!(sched.pattern(), ReadAheadPattern::Sequential);
530 }
531
532 #[test]
533 fn test_pattern_accessor_insufficient_history() {
534 let mut sched = ReadAheadScheduler::new();
535 sched.record_access(5);
536 assert_eq!(sched.pattern(), ReadAheadPattern::Random);
537 }
538}