1use std::cmp::Reverse;
17use std::collections::{HashMap, VecDeque};
18
19#[derive(Debug, Clone)]
23pub struct PeConfig {
24 pub coaccess_window_ms: u64,
26 pub max_pair_history: usize,
28 pub min_coaccess_count: u32,
30 pub max_prefetch_hints: usize,
32 pub pattern_window: usize,
34}
35
36impl Default for PeConfig {
37 fn default() -> Self {
38 Self {
39 coaccess_window_ms: 5_000,
40 max_pair_history: 10_000,
41 min_coaccess_count: 2,
42 max_prefetch_hints: 20,
43 pattern_window: 20,
44 }
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Hash)]
52pub enum PeAccessType {
53 Read,
55 Write,
57 Delete,
59}
60
61#[derive(Debug, Clone)]
63pub struct PeAccessEvent {
64 pub cid: String,
66 pub timestamp: u64,
68 pub access_type: PeAccessType,
70}
71
72#[derive(Debug, Clone)]
76pub struct CoAccessPair {
77 pub cid_a: String,
79 pub cid_b: String,
81 pub coaccess_count: u32,
83 pub last_seen: u64,
85}
86
87#[derive(Debug, Clone)]
91pub struct PePrefetchHint {
92 pub cid: String,
94 pub priority: f64,
96 pub reason: String,
98}
99
100#[derive(Debug, Clone, PartialEq)]
104pub enum PeAccessPattern {
105 Sequential,
107 Random,
109 Strided { stride: usize },
111 Repeated,
113 Unknown,
115}
116
117#[derive(Debug, Clone)]
121pub struct PePrefetchStats {
122 pub total_events: u64,
124 pub total_pairs: usize,
126 pub avg_coaccess_count: f64,
128 pub top_pattern: String,
130 pub hints_generated: u64,
132}
133
134pub struct StoragePrefetchEngine {
140 pub config: PeConfig,
142 pub access_history: VecDeque<PeAccessEvent>,
144 pub coaccesses: HashMap<String, CoAccessPair>,
146 pub recent_cids: VecDeque<String>,
148 pub pattern_state: HashMap<String, Vec<u64>>,
150
151 total_events: u64,
153 hints_generated: u64,
154}
155
156impl StoragePrefetchEngine {
157 pub fn new(config: PeConfig) -> Self {
161 Self {
162 config,
163 access_history: VecDeque::new(),
164 coaccesses: HashMap::new(),
165 recent_cids: VecDeque::new(),
166 pattern_state: HashMap::new(),
167 total_events: 0,
168 hints_generated: 0,
169 }
170 }
171
172 pub fn record_access(&mut self, event: PeAccessEvent) -> Vec<PePrefetchHint> {
182 let now = event.timestamp;
183 let cid = event.cid.clone();
184
185 {
187 let ts_vec = self.pattern_state.entry(cid.clone()).or_default();
188 ts_vec.push(now);
189 let window = self.config.pattern_window;
190 if ts_vec.len() > window {
191 let drain_count = ts_vec.len() - window;
192 ts_vec.drain(..drain_count);
193 }
194 }
195
196 self.access_history.push_back(event);
198 self.total_events += 1;
199
200 self.update_coaccesses(now, &cid);
202
203 let cutoff = now.saturating_sub(self.config.coaccess_window_ms * 2);
207 while self
208 .access_history
209 .front()
210 .map(|e| e.timestamp < cutoff)
211 .unwrap_or(false)
212 {
213 self.access_history.pop_front();
214 }
215
216 self.recent_cids.push_back(cid.clone());
218 let pw = self.config.pattern_window;
219 while self.recent_cids.len() > pw {
220 self.recent_cids.pop_front();
221 }
222
223 if self.coaccesses.len() > self.config.max_pair_history {
225 self.evict_lowest_pairs();
226 }
227
228 let hints = self.generate_hints(&cid, now);
230 self.hints_generated += hints.len() as u64;
231 hints
232 }
233
234 pub fn generate_hints(&self, cid: &str, now: u64) -> Vec<PePrefetchHint> {
239 let max = self.config.max_prefetch_hints;
240 let min_count = self.config.min_coaccess_count;
241
242 let mut scored: Vec<(String, f64, u32)> = self
244 .coaccesses
245 .values()
246 .filter(|p| p.coaccess_count >= min_count && (p.cid_a == cid || p.cid_b == cid))
247 .map(|p| {
248 let partner = if p.cid_a == cid {
249 p.cid_b.clone()
250 } else {
251 p.cid_a.clone()
252 };
253 let age_ms = now.saturating_sub(p.last_seen) as f64;
254 let recency_weight = (-age_ms / 60_000.0_f64).exp();
255 let raw_score = p.coaccess_count as f64 * recency_weight;
256 (partner, raw_score, p.coaccess_count)
257 })
258 .collect();
259
260 scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
262 scored.truncate(max);
263
264 scored
265 .into_iter()
266 .map(|(partner, raw_score, count)| {
267 let priority = (raw_score / 10.0_f64).min(1.0_f64);
268 PePrefetchHint {
269 cid: partner,
270 priority,
271 reason: format!("co-accessed {} time(s) with {}", count, cid),
272 }
273 })
274 .collect()
275 }
276
277 pub fn detect_pattern(&self, cid: &str) -> PeAccessPattern {
287 let ts = match self.pattern_state.get(cid) {
288 Some(v) if !v.is_empty() => v,
289 _ => return PeAccessPattern::Unknown,
290 };
291
292 if ts.len() >= 3 {
293 if ts.len() >= 2 {
296 let intervals: Vec<f64> = ts
297 .windows(2)
298 .map(|w| (w[1].saturating_sub(w[0])) as f64)
299 .collect();
300
301 if !intervals.is_empty() {
302 let mean = intervals.iter().sum::<f64>() / intervals.len() as f64;
303 if mean > 0.0 {
304 let variance = intervals.iter().map(|x| (x - mean).powi(2)).sum::<f64>()
305 / intervals.len() as f64;
306 let std_dev = variance.sqrt();
307 if std_dev < 0.2 * mean {
308 return PeAccessPattern::Sequential;
309 }
310 }
311 }
312 }
313 return PeAccessPattern::Repeated;
314 }
315
316 PeAccessPattern::Unknown
317 }
318
319 pub fn top_coaccessed<'a>(&'a self, cid: &str, n: usize) -> Vec<&'a CoAccessPair> {
324 let mut pairs: Vec<&CoAccessPair> = self
325 .coaccesses
326 .values()
327 .filter(|p| p.cid_a == cid || p.cid_b == cid)
328 .collect();
329
330 pairs.sort_by_key(|p| Reverse(p.coaccess_count));
331 pairs.truncate(n);
332 pairs
333 }
334
335 pub fn evict_stale_pairs(&mut self, max_age_ms: u64, now: u64) -> usize {
339 let cutoff = now.saturating_sub(max_age_ms);
340 let before = self.coaccesses.len();
341 self.coaccesses.retain(|_, p| p.last_seen >= cutoff);
342 before - self.coaccesses.len()
343 }
344
345 pub fn total_pairs(&self) -> usize {
349 self.coaccesses.len()
350 }
351
352 pub fn history_len(&self) -> usize {
354 self.access_history.len()
355 }
356
357 pub fn most_accessed_cids(&self, n: usize) -> Vec<(String, usize)> {
360 let mut counts: HashMap<&str, usize> = HashMap::new();
361 for ev in &self.access_history {
362 *counts.entry(ev.cid.as_str()).or_insert(0) += 1;
363 }
364 let mut vec: Vec<(String, usize)> =
365 counts.into_iter().map(|(k, v)| (k.to_owned(), v)).collect();
366 vec.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
367 vec.truncate(n);
368 vec
369 }
370
371 pub fn stats(&self) -> PePrefetchStats {
373 let total_pairs = self.coaccesses.len();
374 let avg_coaccess_count = if total_pairs == 0 {
375 0.0
376 } else {
377 self.coaccesses
378 .values()
379 .map(|p| p.coaccess_count as f64)
380 .sum::<f64>()
381 / total_pairs as f64
382 };
383
384 let top_pattern = self.dominant_pattern();
386
387 PePrefetchStats {
388 total_events: self.total_events,
389 total_pairs,
390 avg_coaccess_count,
391 top_pattern,
392 hints_generated: self.hints_generated,
393 }
394 }
395
396 fn update_coaccesses(&mut self, now: u64, new_cid: &str) {
402 let window = self.config.coaccess_window_ms;
403 let cutoff = now.saturating_sub(window);
404
405 let history_len = self.access_history.len();
408 let slice_len = if history_len > 0 { history_len - 1 } else { 0 };
409
410 let partners: Vec<String> = self
411 .access_history
412 .iter()
413 .take(slice_len)
414 .filter(|e| e.timestamp >= cutoff && e.cid != new_cid)
415 .map(|e| e.cid.clone())
416 .collect();
417
418 for partner in partners {
419 let key = coacccess_key(&partner, new_cid);
420 let (cid_a, cid_b) = ordered_pair(&partner, new_cid);
421 let entry = self.coaccesses.entry(key).or_insert_with(|| CoAccessPair {
422 cid_a: cid_a.to_owned(),
423 cid_b: cid_b.to_owned(),
424 coaccess_count: 0,
425 last_seen: 0,
426 });
427 entry.coaccess_count = entry.coaccess_count.saturating_add(1);
428 if now > entry.last_seen {
429 entry.last_seen = now;
430 }
431 }
432 }
433
434 fn evict_lowest_pairs(&mut self) {
436 let max = self.config.max_pair_history;
437 if self.coaccesses.len() <= max {
438 return;
439 }
440 let excess = self.coaccesses.len() - max;
442 let mut keyed: Vec<(String, u32)> = self
443 .coaccesses
444 .iter()
445 .map(|(k, v)| (k.clone(), v.coaccess_count))
446 .collect();
447 keyed.sort_by_key(|(_, c)| *c);
448 for (key, _) in keyed.into_iter().take(excess) {
449 self.coaccesses.remove(&key);
450 }
451 }
452
453 fn dominant_pattern(&self) -> String {
455 let mut repeated = 0usize;
456 let mut sequential = 0usize;
457 let mut unknown = 0usize;
458
459 for cid in self.pattern_state.keys() {
460 match self.detect_pattern(cid) {
461 PeAccessPattern::Repeated => repeated += 1,
462 PeAccessPattern::Sequential => sequential += 1,
463 PeAccessPattern::Unknown
464 | PeAccessPattern::Random
465 | PeAccessPattern::Strided { .. } => unknown += 1,
466 }
467 }
468
469 if repeated >= sequential && repeated >= unknown {
470 "Repeated".to_owned()
471 } else if sequential >= unknown {
472 "Sequential".to_owned()
473 } else {
474 "Unknown".to_owned()
475 }
476 }
477}
478
479fn coacccess_key(a: &str, b: &str) -> String {
486 if a <= b {
487 format!("{}:{}", a, b)
488 } else {
489 format!("{}:{}", b, a)
490 }
491}
492
493fn ordered_pair<'a>(a: &'a str, b: &'a str) -> (&'a str, &'a str) {
495 if a <= b {
496 (a, b)
497 } else {
498 (b, a)
499 }
500}
501
502#[cfg(test)]
505mod tests {
506 use super::{
507 CoAccessPair, PeAccessEvent, PeAccessPattern, PeAccessType, PeConfig, PePrefetchHint,
508 PePrefetchStats, StoragePrefetchEngine,
509 };
510
511 fn default_engine() -> StoragePrefetchEngine {
514 StoragePrefetchEngine::new(PeConfig::default())
515 }
516
517 fn make_event(cid: &str, ts: u64) -> PeAccessEvent {
518 PeAccessEvent {
519 cid: cid.to_owned(),
520 timestamp: ts,
521 access_type: PeAccessType::Read,
522 }
523 }
524
525 fn make_write_event(cid: &str, ts: u64) -> PeAccessEvent {
526 PeAccessEvent {
527 cid: cid.to_owned(),
528 timestamp: ts,
529 access_type: PeAccessType::Write,
530 }
531 }
532
533 fn make_delete_event(cid: &str, ts: u64) -> PeAccessEvent {
534 PeAccessEvent {
535 cid: cid.to_owned(),
536 timestamp: ts,
537 access_type: PeAccessType::Delete,
538 }
539 }
540
541 #[test]
544 fn test_new_default_config() {
545 let engine = default_engine();
546 assert_eq!(engine.config.coaccess_window_ms, 5_000);
547 assert_eq!(engine.config.max_pair_history, 10_000);
548 assert_eq!(engine.config.min_coaccess_count, 2);
549 assert_eq!(engine.config.max_prefetch_hints, 20);
550 assert_eq!(engine.config.pattern_window, 20);
551 }
552
553 #[test]
554 fn test_new_custom_config() {
555 let cfg = PeConfig {
556 coaccess_window_ms: 1_000,
557 max_pair_history: 100,
558 min_coaccess_count: 3,
559 max_prefetch_hints: 5,
560 pattern_window: 10,
561 };
562 let engine = StoragePrefetchEngine::new(cfg.clone());
563 assert_eq!(engine.config.coaccess_window_ms, 1_000);
564 assert_eq!(engine.config.max_pair_history, 100);
565 assert_eq!(engine.config.min_coaccess_count, 3);
566 assert_eq!(engine.config.max_prefetch_hints, 5);
567 assert_eq!(engine.config.pattern_window, 10);
568 }
569
570 #[test]
571 fn test_initial_state() {
572 let engine = default_engine();
573 assert_eq!(engine.total_pairs(), 0);
574 assert_eq!(engine.history_len(), 0);
575 assert!(engine.most_accessed_cids(10).is_empty());
576 }
577
578 #[test]
581 fn test_record_single_event() {
582 let mut engine = default_engine();
583 engine.record_access(make_event("Qm1", 1_000));
584 assert_eq!(engine.history_len(), 1);
585 }
586
587 #[test]
588 fn test_history_len_grows() {
589 let mut engine = default_engine();
590 for i in 0u64..5 {
591 engine.record_access(make_event(&format!("Qm{}", i), i * 100));
592 }
593 assert_eq!(engine.history_len(), 5);
594 }
595
596 #[test]
597 fn test_record_returns_empty_hints_when_no_pairs() {
598 let mut engine = default_engine();
599 let hints = engine.record_access(make_event("QmA", 0));
600 assert!(hints.is_empty(), "no pairs yet → no hints");
601 }
602
603 #[test]
604 fn test_write_event_recorded() {
605 let mut engine = default_engine();
606 engine.record_access(make_write_event("QmW", 500));
607 assert_eq!(engine.history_len(), 1);
608 }
609
610 #[test]
611 fn test_delete_event_recorded() {
612 let mut engine = default_engine();
613 engine.record_access(make_delete_event("QmD", 600));
614 assert_eq!(engine.history_len(), 1);
615 }
616
617 #[test]
620 fn test_coaccess_pair_formed_within_window() {
621 let mut engine = default_engine();
622 engine.record_access(make_event("QmA", 1_000));
623 engine.record_access(make_event("QmB", 1_500)); assert_eq!(engine.total_pairs(), 1);
625 }
626
627 #[test]
628 fn test_coaccess_pair_not_formed_outside_window() {
629 let mut engine = default_engine();
630 engine.record_access(make_event("QmA", 1_000));
631 engine.record_access(make_event("QmB", 10_000)); assert_eq!(engine.total_pairs(), 0);
633 }
634
635 #[test]
636 fn test_coaccess_count_increments() {
637 let mut engine = default_engine();
638 engine.record_access(make_event("QmA", 1_000));
640 engine.record_access(make_event("QmB", 1_200));
641 engine.record_access(make_event("QmA", 2_000));
642 engine.record_access(make_event("QmB", 2_200));
643 let pairs = engine.top_coaccessed("QmA", 5);
644 assert!(!pairs.is_empty());
645 let pair = pairs[0];
646 assert!(pair.coaccess_count >= 2, "count={}", pair.coaccess_count);
647 }
648
649 #[test]
650 fn test_coaccess_key_is_sorted() {
651 let mut engine = default_engine();
653 engine.record_access(make_event("QmZ", 1_000));
654 engine.record_access(make_event("QmA", 1_100));
655 assert_eq!(engine.total_pairs(), 1);
656 let pairs: Vec<&CoAccessPair> = engine.coaccesses.values().collect();
657 assert_eq!(pairs[0].cid_a, "QmA");
658 assert_eq!(pairs[0].cid_b, "QmZ");
659 }
660
661 #[test]
662 fn test_top_coaccessed_returns_correct_cids() {
663 let mut engine = default_engine();
664 for ts_offset in 0u64..3 {
666 engine.record_access(make_event("QmA", 1_000 + ts_offset * 500));
667 engine.record_access(make_event(
668 &format!("QmX{}", ts_offset),
669 1_100 + ts_offset * 500,
670 ));
671 }
672 let top = engine.top_coaccessed("QmA", 10);
673 assert_eq!(top.len(), 3);
675 }
676
677 #[test]
678 fn test_top_coaccessed_sorted_desc() {
679 let mut engine = default_engine();
680 for i in 0u64..4 {
682 engine.record_access(make_event("QmA", 1_000 + i * 300));
683 engine.record_access(make_event("QmB", 1_050 + i * 300));
684 }
685 engine.record_access(make_event("QmA", 10_000));
686 engine.record_access(make_event("QmC", 10_050));
687 let top = engine.top_coaccessed("QmA", 10);
688 assert!(!top.is_empty());
689 for w in top.windows(2) {
691 assert!(w[0].coaccess_count >= w[1].coaccess_count);
692 }
693 }
694
695 #[test]
698 fn test_generate_hints_respects_min_count() {
699 let cfg = PeConfig {
700 min_coaccess_count: 3,
701 ..Default::default()
702 };
703 let mut engine = StoragePrefetchEngine::new(cfg);
704 engine.record_access(make_event("QmA", 1_000));
706 engine.record_access(make_event("QmB", 1_100));
707 let hints = engine.generate_hints("QmA", 2_000);
708 assert!(hints.is_empty(), "count=1 < min_coaccess_count=3");
709 }
710
711 #[test]
712 fn test_generate_hints_returns_after_sufficient_coaccesses() {
713 let cfg = PeConfig {
714 min_coaccess_count: 2,
715 ..Default::default()
716 };
717 let mut engine = StoragePrefetchEngine::new(cfg);
718 for i in 0u64..2 {
719 engine.record_access(make_event("QmA", 1_000 + i * 200));
720 engine.record_access(make_event("QmB", 1_050 + i * 200));
721 }
722 let hints = engine.generate_hints("QmA", 2_000);
723 assert!(!hints.is_empty());
724 assert_eq!(hints[0].cid, "QmB");
725 }
726
727 #[test]
728 fn test_generate_hints_priority_capped_at_one() {
729 let mut engine = default_engine();
730 for i in 0u64..20 {
732 engine.record_access(make_event("QmA", 1_000 + i * 10));
733 engine.record_access(make_event("QmB", 1_005 + i * 10));
734 }
735 let hints = engine.generate_hints("QmA", 1_500);
736 for h in &hints {
737 assert!(h.priority <= 1.0, "priority={} > 1.0", h.priority);
738 }
739 }
740
741 #[test]
742 fn test_generate_hints_max_hints_limit() {
743 let cfg = PeConfig {
744 max_prefetch_hints: 3,
745 min_coaccess_count: 1,
746 ..Default::default()
747 };
748 let mut engine = StoragePrefetchEngine::new(cfg);
749 for i in 0u64..10 {
750 engine.record_access(make_event("QmA", 1_000 + i * 5));
751 engine.record_access(make_event(&format!("QmP{}", i), 1_002 + i * 5));
752 }
753 let hints = engine.generate_hints("QmA", 2_000);
754 assert!(hints.len() <= 3, "len={}", hints.len());
755 }
756
757 #[test]
758 fn test_hint_reason_mentions_cid() {
759 let mut engine = default_engine();
760 for i in 0u64..2 {
761 engine.record_access(make_event("QmA", 1_000 + i * 100));
762 engine.record_access(make_event("QmB", 1_050 + i * 100));
763 }
764 let hints = engine.generate_hints("QmA", 2_000);
765 assert!(!hints.is_empty());
766 assert!(
767 hints[0].reason.contains("QmA"),
768 "reason missing trigger CID"
769 );
770 }
771
772 #[test]
775 fn test_detect_pattern_unknown_for_new_cid() {
776 let engine = default_engine();
777 assert_eq!(engine.detect_pattern("QmNone"), PeAccessPattern::Unknown);
778 }
779
780 #[test]
781 fn test_detect_pattern_unknown_for_two_events() {
782 let mut engine = default_engine();
783 engine.record_access(make_event("QmA", 1_000));
784 engine.record_access(make_event("QmA", 2_000));
785 assert_eq!(engine.detect_pattern("QmA"), PeAccessPattern::Unknown);
787 }
788
789 #[test]
790 fn test_detect_pattern_repeated_three_accesses() {
791 let mut engine = default_engine();
792 for ts in [1_000u64, 2_000, 3_000] {
793 engine.record_access(make_event("QmR", ts));
794 }
795 let pattern = engine.detect_pattern("QmR");
796 assert!(
798 matches!(
799 pattern,
800 PeAccessPattern::Sequential | PeAccessPattern::Repeated
801 ),
802 "pattern={:?}",
803 pattern
804 );
805 }
806
807 #[test]
808 fn test_detect_pattern_sequential_evenly_spaced() {
809 let mut engine = default_engine();
810 for i in 0u64..5 {
812 engine.record_access(make_event("QmS", i * 1_000));
813 }
814 let pattern = engine.detect_pattern("QmS");
815 assert_eq!(
816 pattern,
817 PeAccessPattern::Sequential,
818 "pattern={:?}",
819 pattern
820 );
821 }
822
823 #[test]
824 fn test_detect_pattern_repeated_uneven_intervals() {
825 let mut engine = default_engine();
826 for ts in [100u64, 200, 1_900] {
828 engine.record_access(make_event("QmU", ts));
829 }
830 let pattern = engine.detect_pattern("QmU");
831 assert!(
833 matches!(
834 pattern,
835 PeAccessPattern::Repeated | PeAccessPattern::Unknown
836 ),
837 "pattern={:?}",
838 pattern
839 );
840 }
841
842 #[test]
845 fn test_evict_stale_pairs_removes_old() {
846 let mut engine = default_engine();
847 engine.record_access(make_event("QmA", 1_000));
848 engine.record_access(make_event("QmB", 1_100));
849 assert_eq!(engine.total_pairs(), 1);
850
851 let removed = engine.evict_stale_pairs(500, 2_000);
852 assert_eq!(
853 removed, 1,
854 "pair at t=1100 is older than 500ms ago from t=2000"
855 );
856 assert_eq!(engine.total_pairs(), 0);
857 }
858
859 #[test]
860 fn test_evict_stale_pairs_keeps_fresh() {
861 let mut engine = default_engine();
862 engine.record_access(make_event("QmA", 1_000));
863 engine.record_access(make_event("QmB", 1_100));
864
865 let removed = engine.evict_stale_pairs(5_000, 1_500);
866 assert_eq!(removed, 0);
868 assert_eq!(engine.total_pairs(), 1);
869 }
870
871 #[test]
872 fn test_evict_stale_returns_count() {
873 let mut engine = default_engine();
874 for i in 0u64..3 {
875 engine.record_access(make_event("QmA", i * 100));
876 engine.record_access(make_event(&format!("QmX{}", i), i * 100 + 10));
877 }
878 let pairs_before = engine.total_pairs();
879 let removed = engine.evict_stale_pairs(0, 10_000);
880 assert_eq!(removed, pairs_before);
881 assert_eq!(engine.total_pairs(), 0);
882 }
883
884 #[test]
887 fn test_most_accessed_cids_single() {
888 let mut engine = default_engine();
889 engine.record_access(make_event("QmA", 1_000));
890 let top = engine.most_accessed_cids(5);
891 assert_eq!(top.len(), 1);
892 assert_eq!(top[0].0, "QmA");
893 assert_eq!(top[0].1, 1);
894 }
895
896 #[test]
897 fn test_most_accessed_cids_counts() {
898 let mut engine = default_engine();
899 for ts in [1_000u64, 2_000, 3_000] {
900 engine.record_access(make_event("QmHot", ts));
901 }
902 engine.record_access(make_event("QmCool", 4_000));
903 let top = engine.most_accessed_cids(2);
904 assert_eq!(top[0].0, "QmHot");
905 assert_eq!(top[0].1, 3);
906 }
907
908 #[test]
909 fn test_most_accessed_cids_n_limit() {
910 let mut engine = default_engine();
911 for i in 0u64..10 {
912 engine.record_access(make_event(&format!("Qm{}", i), i * 100));
913 }
914 let top = engine.most_accessed_cids(3);
915 assert_eq!(top.len(), 3);
916 }
917
918 #[test]
919 fn test_most_accessed_cids_sorted_desc() {
920 let mut engine = default_engine();
921 engine.record_access(make_event("QmA", 100));
922 for ts in [200u64, 300, 400] {
923 engine.record_access(make_event("QmB", ts));
924 }
925 for ts in [500u64, 600] {
926 engine.record_access(make_event("QmC", ts));
927 }
928 let top = engine.most_accessed_cids(10);
929 for w in top.windows(2) {
930 assert!(w[0].1 >= w[1].1);
931 }
932 }
933
934 #[test]
937 fn test_stats_initial() {
938 let engine = default_engine();
939 let s = engine.stats();
940 assert_eq!(s.total_events, 0);
941 assert_eq!(s.total_pairs, 0);
942 assert_eq!(s.avg_coaccess_count, 0.0);
943 assert_eq!(s.hints_generated, 0);
944 }
945
946 #[test]
947 fn test_stats_total_events() {
948 let mut engine = default_engine();
949 for i in 0u64..7 {
950 engine.record_access(make_event(&format!("Qm{}", i), i * 1_000));
951 }
952 assert_eq!(engine.stats().total_events, 7);
953 }
954
955 #[test]
956 fn test_stats_hints_generated_tracked() {
957 let cfg = PeConfig {
958 min_coaccess_count: 1,
959 ..Default::default()
960 };
961 let mut engine = StoragePrefetchEngine::new(cfg);
962 for i in 0u64..2 {
963 engine.record_access(make_event("QmA", 1_000 + i * 100));
964 engine.record_access(make_event("QmB", 1_050 + i * 100));
965 }
966 let s = engine.stats();
968 assert!(
969 s.hints_generated > 0,
970 "hints_generated={}",
971 s.hints_generated
972 );
973 }
974
975 #[test]
976 fn test_stats_avg_coaccess_count() {
977 let mut engine = default_engine();
978 for i in 0u64..3 {
979 engine.record_access(make_event("QmA", 1_000 + i * 100));
980 engine.record_access(make_event("QmB", 1_050 + i * 100));
981 }
982 let s = engine.stats();
983 assert!(s.avg_coaccess_count > 0.0);
984 }
985
986 #[test]
987 fn test_stats_top_pattern_string() {
988 let engine = default_engine();
989 let s = engine.stats();
990 assert!(!s.top_pattern.is_empty());
992 }
993
994 #[test]
997 fn test_hints_sorted_by_recency() {
998 let cfg = PeConfig {
1000 min_coaccess_count: 1,
1001 ..Default::default()
1002 };
1003 let mut engine = StoragePrefetchEngine::new(cfg);
1004
1005 engine.record_access(make_event("QmA", 0));
1007 engine.record_access(make_event("QmC", 50));
1008
1009 engine.record_access(make_event("QmA", 100_000));
1011 engine.record_access(make_event("QmB", 100_050));
1012
1013 let hints = engine.generate_hints("QmA", 100_100);
1014 assert!(!hints.is_empty());
1015 if hints.len() >= 2 {
1017 let pos_b = hints
1018 .iter()
1019 .position(|h| h.cid == "QmB")
1020 .unwrap_or(usize::MAX);
1021 let pos_c = hints
1022 .iter()
1023 .position(|h| h.cid == "QmC")
1024 .unwrap_or(usize::MAX);
1025 assert!(
1026 pos_b < pos_c,
1027 "QmB should precede QmC; hints={:?}",
1028 hints.iter().map(|h| &h.cid).collect::<Vec<_>>()
1029 );
1030 }
1031 }
1032
1033 #[test]
1034 fn test_priority_non_negative() {
1035 let cfg = PeConfig {
1036 min_coaccess_count: 1,
1037 ..Default::default()
1038 };
1039 let mut engine = StoragePrefetchEngine::new(cfg);
1040 for i in 0u64..2 {
1041 engine.record_access(make_event("QmA", i * 200));
1042 engine.record_access(make_event("QmB", i * 200 + 10));
1043 }
1044 let hints = engine.generate_hints("QmA", 500_000);
1045 for h in hints {
1046 assert!(h.priority >= 0.0);
1047 }
1048 }
1049
1050 #[test]
1053 fn test_total_pairs_after_eviction() {
1054 let mut engine = default_engine();
1055 engine.record_access(make_event("QmA", 1_000));
1056 engine.record_access(make_event("QmB", 1_100));
1057 assert_eq!(engine.total_pairs(), 1);
1058 engine.evict_stale_pairs(0, 100_000);
1059 assert_eq!(engine.total_pairs(), 0);
1060 }
1061
1062 #[test]
1063 fn test_access_type_variants_compile() {
1064 let _ = PeAccessType::Read;
1065 let _ = PeAccessType::Write;
1066 let _ = PeAccessType::Delete;
1067 }
1068
1069 #[test]
1070 fn test_pe_prefetch_hint_fields() {
1071 let hint = PePrefetchHint {
1072 cid: "QmX".to_owned(),
1073 priority: 0.75,
1074 reason: "test".to_owned(),
1075 };
1076 assert_eq!(hint.cid, "QmX");
1077 assert!((hint.priority - 0.75).abs() < 1e-9);
1078 }
1079
1080 #[test]
1081 fn test_pe_prefetch_stats_fields() {
1082 let s = PePrefetchStats {
1083 total_events: 5,
1084 total_pairs: 2,
1085 avg_coaccess_count: 1.5,
1086 top_pattern: "Repeated".to_owned(),
1087 hints_generated: 10,
1088 };
1089 assert_eq!(s.total_events, 5);
1090 assert_eq!(s.total_pairs, 2);
1091 }
1092
1093 #[test]
1096 fn test_same_cid_does_not_self_pair() {
1097 let mut engine = default_engine();
1098 engine.record_access(make_event("QmSelf", 1_000));
1099 engine.record_access(make_event("QmSelf", 1_100));
1100 assert_eq!(engine.total_pairs(), 0);
1102 }
1103
1104 #[test]
1105 fn test_multiple_pairs_single_anchor() {
1106 let mut engine = default_engine();
1107 engine.record_access(make_event("QmA", 1_000));
1109 for i in 1u64..=5 {
1110 engine.record_access(make_event(&format!("QmX{}", i), 1_000 + i * 50));
1111 }
1112 assert!(
1116 engine.total_pairs() >= 5,
1117 "expected >= 5 pairs, got {}",
1118 engine.total_pairs()
1119 );
1120 let qm_a_pairs = engine.top_coaccessed("QmA", 10);
1122 assert_eq!(
1123 qm_a_pairs.len(),
1124 5,
1125 "QmA should have 5 direct co-access partners"
1126 );
1127 }
1128
1129 #[test]
1130 fn test_pattern_window_limits_state() {
1131 let cfg = PeConfig {
1132 pattern_window: 3,
1133 ..Default::default()
1134 };
1135 let mut engine = StoragePrefetchEngine::new(cfg);
1136 for i in 0u64..10 {
1137 engine.record_access(make_event("QmPW", i * 100));
1138 }
1139 let ts = engine
1140 .pattern_state
1141 .get("QmPW")
1142 .cloned()
1143 .unwrap_or_default();
1144 assert!(ts.len() <= 3, "len={}", ts.len());
1145 }
1146
1147 #[test]
1148 fn test_generate_hints_no_match() {
1149 let engine = default_engine();
1150 let hints = engine.generate_hints("QmNone", 0);
1151 assert!(hints.is_empty());
1152 }
1153}