1use std::collections::HashMap;
7
8#[derive(Clone, Debug, PartialEq)]
14pub enum AccessPattern {
15 Sequential,
17 Repeated { interval_ticks: u64 },
19 Bursty { burst_size: usize },
21 Random,
23 Cooling,
25}
26
27#[derive(Clone, Debug)]
29pub struct AccessEvent {
30 pub cid: String,
32 pub tick: u64,
34 pub size_bytes: u64,
36}
37
38#[derive(Clone, Debug)]
40pub struct PredictionResult {
41 pub cid: String,
43 pub pattern: AccessPattern,
45 pub next_access_tick: Option<u64>,
47 pub confidence: f32,
49}
50
51#[derive(Clone, Debug, Default)]
53pub struct PredictorStats {
54 pub total_events: u64,
56 pub unique_cids: usize,
58 pub sequential_count: usize,
60 pub repeated_count: usize,
62 pub random_count: usize,
64}
65
66const MAX_HISTORY: usize = 20;
72
73pub struct StorageAccessPredictor {
79 pub history: HashMap<String, Vec<AccessEvent>>,
81}
82
83impl StorageAccessPredictor {
84 pub fn new() -> Self {
86 Self {
87 history: HashMap::new(),
88 }
89 }
90
91 pub fn record(&mut self, event: AccessEvent) {
96 let entries = self.history.entry(event.cid.clone()).or_default();
97 entries.push(event);
98 if entries.len() > MAX_HISTORY {
99 entries.remove(0);
100 }
101 }
102
103 pub fn predict(&self, cid: &str) -> PredictionResult {
109 let no_history = || PredictionResult {
110 cid: cid.to_owned(),
111 pattern: AccessPattern::Random,
112 next_access_tick: None,
113 confidence: 0.0,
114 };
115
116 let entries = match self.history.get(cid) {
117 Some(v) if !v.is_empty() => v,
118 _ => return no_history(),
119 };
120
121 if entries.len() == 1 {
122 return PredictionResult {
123 cid: cid.to_owned(),
124 pattern: AccessPattern::Random,
125 next_access_tick: None,
126 confidence: 0.1,
127 };
128 }
129
130 let intervals: Vec<u64> = entries
132 .windows(2)
133 .map(|w| w[1].tick.saturating_sub(w[0].tick))
134 .collect();
135
136 let last_tick = entries.last().map(|e| e.tick).unwrap_or(0);
137
138 if intervals.iter().all(|&i| i == intervals[0]) {
140 let interval = intervals[0];
141 return PredictionResult {
142 cid: cid.to_owned(),
143 pattern: AccessPattern::Repeated {
144 interval_ticks: interval,
145 },
146 next_access_tick: Some(last_tick + interval),
147 confidence: 0.9,
148 };
149 }
150
151 let strictly_decreasing = intervals.windows(2).all(|w| w[0] > w[1]);
153 if strictly_decreasing {
154 return PredictionResult {
155 cid: cid.to_owned(),
156 pattern: AccessPattern::Cooling,
157 next_access_tick: None,
158 confidence: 0.7,
159 };
160 }
161
162 if intervals.iter().all(|&i| i <= 5) {
164 return PredictionResult {
165 cid: cid.to_owned(),
166 pattern: AccessPattern::Bursty {
167 burst_size: entries.len(),
168 },
169 next_access_tick: None,
170 confidence: 0.6,
171 };
172 }
173
174 let non_decreasing = intervals.windows(2).all(|w| w[1] >= w[0]);
176 if non_decreasing {
177 let avg = intervals.iter().sum::<u64>() as f64 / intervals.len() as f64;
178 let max_interval = *intervals.iter().max().unwrap_or(&0);
179 let min_interval = *intervals.iter().min().unwrap_or(&0);
180 let variation = if avg > 0.0 {
182 (max_interval - min_interval) as f64 / avg
183 } else {
184 0.0
185 };
186 if variation <= 0.10 {
187 let avg_interval = avg.round() as u64;
188 return PredictionResult {
189 cid: cid.to_owned(),
190 pattern: AccessPattern::Sequential,
191 next_access_tick: Some(last_tick + avg_interval),
192 confidence: 0.75,
193 };
194 }
195 }
196
197 PredictionResult {
199 cid: cid.to_owned(),
200 pattern: AccessPattern::Random,
201 next_access_tick: None,
202 confidence: 0.2,
203 }
204 }
205
206 pub fn top_predicted(&self) -> Vec<PredictionResult> {
209 let mut results: Vec<PredictionResult> = self
210 .history
211 .keys()
212 .map(|cid| self.predict(cid))
213 .filter(|r| {
214 matches!(
215 r.pattern,
216 AccessPattern::Sequential | AccessPattern::Repeated { .. }
217 )
218 })
219 .collect();
220
221 results.sort_by(|a, b| {
222 b.confidence
223 .partial_cmp(&a.confidence)
224 .unwrap_or(std::cmp::Ordering::Equal)
225 });
226 results
227 }
228
229 pub fn stats(&self) -> PredictorStats {
231 let total_events: u64 = self.history.values().map(|v| v.len() as u64).sum();
232 let unique_cids = self.history.len();
233
234 let mut sequential_count = 0usize;
235 let mut repeated_count = 0usize;
236 let mut random_count = 0usize;
237
238 for cid in self.history.keys() {
239 match self.predict(cid).pattern {
240 AccessPattern::Sequential => sequential_count += 1,
241 AccessPattern::Repeated { .. } => repeated_count += 1,
242 AccessPattern::Random => random_count += 1,
243 _ => {}
244 }
245 }
246
247 PredictorStats {
248 total_events,
249 unique_cids,
250 sequential_count,
251 repeated_count,
252 random_count,
253 }
254 }
255}
256
257impl Default for StorageAccessPredictor {
258 fn default() -> Self {
259 Self::new()
260 }
261}
262
263#[cfg(test)]
268mod tests {
269 use super::*;
270
271 fn make_event(cid: &str, tick: u64) -> AccessEvent {
272 AccessEvent {
273 cid: cid.to_owned(),
274 tick,
275 size_bytes: 512,
276 }
277 }
278
279 #[test]
281 fn test_new_starts_empty() {
282 let predictor = StorageAccessPredictor::new();
283 assert!(predictor.history.is_empty());
284 }
285
286 #[test]
288 fn test_record_stores_event() {
289 let mut p = StorageAccessPredictor::new();
290 p.record(make_event("cid-a", 1));
291 assert_eq!(p.history["cid-a"].len(), 1);
292 assert_eq!(p.history["cid-a"][0].tick, 1);
293 }
294
295 #[test]
297 fn test_record_caps_at_20() {
298 let mut p = StorageAccessPredictor::new();
299 for i in 0..25u64 {
300 p.record(make_event("cid-a", i));
301 }
302 let entries = &p.history["cid-a"];
303 assert_eq!(entries.len(), 20);
304 assert_eq!(entries[0].tick, 5);
306 }
307
308 #[test]
310 fn test_predict_unknown_cid() {
311 let p = StorageAccessPredictor::new();
312 let result = p.predict("unknown");
313 assert_eq!(result.pattern, AccessPattern::Random);
314 assert!((result.confidence - 0.0).abs() < f32::EPSILON);
315 assert!(result.next_access_tick.is_none());
316 }
317
318 #[test]
320 fn test_predict_single_event() {
321 let mut p = StorageAccessPredictor::new();
322 p.record(make_event("cid-a", 10));
323 let result = p.predict("cid-a");
324 assert_eq!(result.pattern, AccessPattern::Random);
325 assert!((result.confidence - 0.1).abs() < f32::EPSILON);
326 }
327
328 #[test]
330 fn test_predict_equal_intervals_repeated() {
331 let mut p = StorageAccessPredictor::new();
332 for i in 0u64..5 {
333 p.record(make_event("cid-r", i * 10));
334 }
335 let result = p.predict("cid-r");
336 assert_eq!(
337 result.pattern,
338 AccessPattern::Repeated { interval_ticks: 10 }
339 );
340 }
341
342 #[test]
344 fn test_predict_repeated_next_access() {
345 let mut p = StorageAccessPredictor::new();
346 for i in 0u64..4 {
347 p.record(make_event("cid-r", i * 5));
348 }
349 let result = p.predict("cid-r");
350 assert_eq!(result.next_access_tick, Some(20));
352 }
353
354 #[test]
356 fn test_predict_repeated_confidence() {
357 let mut p = StorageAccessPredictor::new();
358 for i in 0u64..3 {
359 p.record(make_event("cid-r", i * 7));
360 }
361 let result = p.predict("cid-r");
362 assert!((result.confidence - 0.9).abs() < f32::EPSILON);
363 }
364
365 #[test]
367 fn test_predict_decreasing_intervals_cooling() {
368 let mut p = StorageAccessPredictor::new();
369 let ticks = [0u64, 100, 190, 270, 340];
371 for &t in &ticks {
372 p.record(make_event("cid-c", t));
373 }
374 let result = p.predict("cid-c");
375 assert_eq!(result.pattern, AccessPattern::Cooling);
376 }
377
378 #[test]
380 fn test_predict_cooling_next_access_none() {
381 let mut p = StorageAccessPredictor::new();
382 let ticks = [0u64, 100, 190, 270, 340];
383 for &t in &ticks {
384 p.record(make_event("cid-c", t));
385 }
386 let result = p.predict("cid-c");
387 assert!(result.next_access_tick.is_none());
388 }
389
390 #[test]
392 fn test_predict_small_intervals_bursty() {
393 let mut p = StorageAccessPredictor::new();
394 for &t in &[0u64, 1, 3, 5, 7] {
397 p.record(make_event("cid-b", t));
398 }
399 let result = p.predict("cid-b");
400 assert_eq!(result.pattern, AccessPattern::Bursty { burst_size: 5 });
401 }
402
403 #[test]
405 fn test_predict_bursty_burst_size() {
406 let mut p = StorageAccessPredictor::new();
407 for &t in &[0u64, 1, 3, 5, 7, 9] {
408 p.record(make_event("cid-b", t));
409 }
410 let result = p.predict("cid-b");
411 if let AccessPattern::Bursty { burst_size } = result.pattern {
412 assert_eq!(burst_size, 6);
413 } else {
414 panic!("Expected Bursty, got {:?}", result.pattern);
415 }
416 }
417
418 #[test]
420 fn test_predict_random_confidence() {
421 let mut p = StorageAccessPredictor::new();
422 for &t in &[0u64, 10, 15, 100, 102, 200] {
424 p.record(make_event("cid-x", t));
425 }
426 let result = p.predict("cid-x");
427 assert_eq!(result.pattern, AccessPattern::Random);
428 assert!((result.confidence - 0.2).abs() < f32::EPSILON);
429 }
430
431 #[test]
433 fn test_top_predicted_only_sequential_repeated() {
434 let mut p = StorageAccessPredictor::new();
435
436 for i in 0u64..4 {
438 p.record(make_event("cid-rep", i * 10));
439 }
440 for &t in &[0u64, 10, 15, 100, 102, 200] {
442 p.record(make_event("cid-rand", t));
443 }
444
445 let top = p.top_predicted();
446 for r in &top {
447 assert!(
448 matches!(
449 r.pattern,
450 AccessPattern::Sequential | AccessPattern::Repeated { .. }
451 ),
452 "Unexpected pattern {:?} for {}",
453 r.pattern,
454 r.cid
455 );
456 }
457 assert!(!top.is_empty());
458 }
459
460 #[test]
462 fn test_top_predicted_sorted_by_confidence_desc() {
463 let mut p = StorageAccessPredictor::new();
464
465 for i in 0u64..4 {
467 p.record(make_event("cid-rep", i * 10));
468 }
469 for &t in &[0u64, 10, 20, 31, 42] {
472 p.record(make_event("cid-seq", t));
473 }
474
475 let top = p.top_predicted();
476 for pair in top.windows(2) {
477 assert!(pair[0].confidence >= pair[1].confidence);
478 }
479 }
480
481 #[test]
483 fn test_stats_total_events() {
484 let mut p = StorageAccessPredictor::new();
485 assert_eq!(p.stats().total_events, 0);
486 p.record(make_event("cid-a", 1));
487 assert_eq!(p.stats().total_events, 1);
488 p.record(make_event("cid-a", 2));
489 assert_eq!(p.stats().total_events, 2);
490 p.record(make_event("cid-b", 5));
491 assert_eq!(p.stats().total_events, 3);
492 }
493
494 #[test]
496 fn test_stats_unique_cids() {
497 let mut p = StorageAccessPredictor::new();
498 assert_eq!(p.stats().unique_cids, 0);
499 p.record(make_event("a", 1));
500 assert_eq!(p.stats().unique_cids, 1);
501 p.record(make_event("b", 2));
502 assert_eq!(p.stats().unique_cids, 2);
503 p.record(make_event("a", 3));
504 assert_eq!(p.stats().unique_cids, 2); }
506
507 #[test]
509 fn test_multiple_cids_independent() {
510 let mut p = StorageAccessPredictor::new();
511 for i in 0u64..3 {
512 p.record(make_event("alpha", i * 10));
513 p.record(make_event("beta", i * 7));
514 }
515 let ra = p.predict("alpha");
516 let rb = p.predict("beta");
517 assert_eq!(ra.pattern, AccessPattern::Repeated { interval_ticks: 10 });
518 assert_eq!(rb.pattern, AccessPattern::Repeated { interval_ticks: 7 });
519 }
520
521 #[test]
523 fn test_stats_pattern_counts() {
524 let mut p = StorageAccessPredictor::new();
525
526 for i in 0u64..4 {
528 p.record(make_event("rep", i * 5));
529 }
530 for &t in &[0u64, 10, 15, 100, 102, 200] {
532 p.record(make_event("rnd", t));
533 }
534
535 let s = p.stats();
536 assert_eq!(s.repeated_count, 1);
537 assert_eq!(s.random_count, 1);
538 }
539
540 #[test]
542 fn test_history_eviction_preserves_order() {
543 let mut p = StorageAccessPredictor::new();
544 for i in 0..25u64 {
545 p.record(make_event("cid-ord", i));
546 }
547 let entries = &p.history["cid-ord"];
548 assert_eq!(entries.len(), 20);
550 for (idx, entry) in entries.iter().enumerate() {
551 assert_eq!(entry.tick, (idx as u64) + 5);
552 }
553 }
554
555 #[test]
557 fn test_predict_bursty_confidence() {
558 let mut p = StorageAccessPredictor::new();
559 for &t in &[0u64, 1, 3, 5, 7] {
560 p.record(make_event("cid-b2", t));
561 }
562 let result = p.predict("cid-b2");
563 assert!((result.confidence - 0.6).abs() < f32::EPSILON);
564 }
565
566 #[test]
568 fn test_predict_cooling_confidence() {
569 let mut p = StorageAccessPredictor::new();
570 let ticks = [0u64, 100, 190, 270, 340];
571 for &t in &ticks {
572 p.record(make_event("cid-cool2", t));
573 }
574 let result = p.predict("cid-cool2");
575 assert!((result.confidence - 0.7).abs() < f32::EPSILON);
576 }
577
578 #[test]
580 fn test_top_predicted_empty_when_no_patterns() {
581 let mut p = StorageAccessPredictor::new();
582 for &t in &[0u64, 10, 15, 100, 102, 200] {
584 p.record(make_event("rnd1", t));
585 }
586 for &t in &[0u64, 50, 55, 300, 305, 700] {
587 p.record(make_event("rnd2", t));
588 }
589 assert!(p.top_predicted().is_empty());
590 }
591
592 #[test]
594 fn test_default_is_empty() {
595 let p = StorageAccessPredictor::default();
596 assert!(p.history.is_empty());
597 assert_eq!(p.stats().total_events, 0);
598 }
599}