1use std::cmp::Reverse;
5use std::collections::{HashMap, VecDeque};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct AccessEvent {
14 pub cid: String,
16 pub timestamp_ms: u64,
18 pub latency_ms: u32,
20 pub cache_hit: bool,
22}
23
24#[derive(Debug, Clone)]
26pub struct CoAccessPair {
27 pub cid_a: String,
28 pub cid_b: String,
29 pub co_access_count: u64,
31 pub avg_interval_ms: u64,
33}
34
35#[derive(Debug, Clone)]
37pub struct PrefetchRecommendation {
38 pub trigger_cid: String,
40 pub prefetch_cids: Vec<String>,
42 pub confidence: f64,
44 pub estimated_benefit_ms: u32,
46}
47
48#[derive(Debug, Clone)]
50pub struct AccessPattern {
51 pub cid: String,
52 pub total_accesses: u64,
54 pub avg_interval_ms: u64,
56 pub last_access_ms: u64,
58 pub predicted_next_ms: u64,
60}
61
62#[derive(Debug, Clone)]
64pub struct OptimizerConfig {
65 pub window_size: usize,
67 pub min_co_access_count: u64,
69 pub max_prefetch_candidates: usize,
71 pub confidence_threshold: f64,
73 pub pattern_decay_factor: f64,
75}
76
77impl Default for OptimizerConfig {
78 fn default() -> Self {
79 Self {
80 window_size: 1000,
81 min_co_access_count: 3,
82 max_prefetch_candidates: 5,
83 confidence_threshold: 0.6,
84 pattern_decay_factor: 0.95,
85 }
86 }
87}
88
89#[derive(Debug, Clone)]
91pub struct OptimizerStats {
92 pub total_events: u64,
93 pub unique_blocks: usize,
94 pub co_access_pairs: usize,
95 pub cache_hit_rate: f64,
96 pub window_size: usize,
97}
98
99pub struct BlockAccessOptimizer {
106 pub config: OptimizerConfig,
108 pub recent_window: VecDeque<AccessEvent>,
110 pub patterns: HashMap<String, AccessPattern>,
112 pub co_access: HashMap<String, HashMap<String, CoAccessPair>>,
114 pub total_events: u64,
116 pub cache_hit_count: u64,
118}
119
120impl BlockAccessOptimizer {
121 pub fn new(config: OptimizerConfig) -> Self {
123 Self {
124 config,
125 recent_window: VecDeque::new(),
126 patterns: HashMap::new(),
127 co_access: HashMap::new(),
128 total_events: 0,
129 cache_hit_count: 0,
130 }
131 }
132
133 pub fn with_defaults() -> Self {
135 Self::new(OptimizerConfig::default())
136 }
137
138 pub fn record_access(&mut self, event: AccessEvent) {
149 self.total_events += 1;
151 if event.cache_hit {
152 self.cache_hit_count += 1;
153 }
154
155 let cid = event.cid.clone();
156 let ts = event.timestamp_ms;
157
158 match self.patterns.get_mut(&cid) {
160 Some(pat) => {
161 let interval = ts.saturating_sub(pat.last_access_ms);
163 const ALPHA: f64 = 0.3;
165 let new_avg = if pat.total_accesses == 1 {
166 interval as f64
168 } else {
169 ALPHA * interval as f64 + (1.0 - ALPHA) * pat.avg_interval_ms as f64
170 };
171 pat.avg_interval_ms = new_avg.round() as u64;
172 pat.last_access_ms = ts;
173 pat.predicted_next_ms = ts + pat.avg_interval_ms;
174 pat.total_accesses += 1;
175 }
176 None => {
177 self.patterns.insert(
178 cid.clone(),
179 AccessPattern {
180 cid: cid.clone(),
181 total_accesses: 1,
182 avg_interval_ms: 0,
183 last_access_ms: ts,
184 predicted_next_ms: ts,
185 },
186 );
187 }
188 }
189
190 let look_back = self.recent_window.len().min(5);
194 let start = self.recent_window.len().saturating_sub(look_back);
195 let prev_events: Vec<(String, u64)> = self
196 .recent_window
197 .iter()
198 .skip(start)
199 .filter(|e| e.cid != cid)
200 .map(|e| (e.cid.clone(), e.timestamp_ms))
201 .collect();
202
203 for (prev_cid, prev_ts) in prev_events {
204 let interval = ts.saturating_sub(prev_ts);
205 self.update_co_access_pair(cid.clone(), prev_cid.clone(), interval);
206 self.update_co_access_pair(prev_cid, cid.clone(), interval);
207 }
208
209 if self.recent_window.len() >= self.config.window_size {
211 self.recent_window.pop_front();
212 }
213 self.recent_window.push_back(event);
214 }
215
216 fn update_co_access_pair(&mut self, cid_a: String, cid_b: String, interval_ms: u64) {
218 let inner = self.co_access.entry(cid_a.clone()).or_default();
219 match inner.get_mut(&cid_b) {
220 Some(pair) => {
221 pair.co_access_count += 1;
222 const ALPHA: f64 = 0.3;
224 let new_avg =
225 ALPHA * interval_ms as f64 + (1.0 - ALPHA) * pair.avg_interval_ms as f64;
226 pair.avg_interval_ms = new_avg.round() as u64;
227 }
228 None => {
229 inner.insert(
230 cid_b.clone(),
231 CoAccessPair {
232 cid_a,
233 cid_b,
234 co_access_count: 1,
235 avg_interval_ms: interval_ms,
236 },
237 );
238 }
239 }
240 }
241
242 pub fn recommend_prefetch(&self, trigger_cid: &str) -> PrefetchRecommendation {
251 let candidates = match self.co_access.get(trigger_cid) {
252 Some(inner) => inner,
253 None => {
254 return PrefetchRecommendation {
255 trigger_cid: trigger_cid.to_owned(),
256 prefetch_cids: vec![],
257 confidence: 0.0,
258 estimated_benefit_ms: 0,
259 };
260 }
261 };
262
263 let mut pairs: Vec<&CoAccessPair> = candidates
265 .values()
266 .filter(|p| p.co_access_count >= self.config.min_co_access_count)
267 .collect();
268
269 pairs.sort_by_key(|b| Reverse(b.co_access_count));
271
272 let mut prefetch_cids: Vec<String> = Vec::new();
274 let mut total_confidence = 0.0_f64;
275 let mut count = 0usize;
276
277 for pair in pairs.into_iter().take(self.config.max_prefetch_candidates) {
278 let n = pair.co_access_count as f64;
279 let confidence = (n / (n + 1.0)).min(1.0);
280 if confidence >= self.config.confidence_threshold {
281 prefetch_cids.push(pair.cid_b.clone());
282 total_confidence += confidence;
283 count += 1;
284 }
285 }
286
287 let aggregate_confidence = if count > 0 {
288 (total_confidence / count as f64).min(1.0)
289 } else {
290 0.0
291 };
292
293 let estimated_benefit_ms = (count as u32) * 50;
295
296 PrefetchRecommendation {
297 trigger_cid: trigger_cid.to_owned(),
298 prefetch_cids,
299 confidence: aggregate_confidence,
300 estimated_benefit_ms,
301 }
302 }
303
304 pub fn predict_next_access(&self, cid: &str) -> Option<u64> {
306 self.patterns.get(cid).map(|p| p.predicted_next_ms)
307 }
308
309 pub fn hot_blocks(&self, k: usize) -> Vec<&AccessPattern> {
315 let mut patterns: Vec<&AccessPattern> = self.patterns.values().collect();
316 patterns.sort_by_key(|b| Reverse(b.total_accesses));
317 patterns.into_iter().take(k).collect()
318 }
319
320 pub fn top_co_access_pairs(&self, k: usize) -> Vec<&CoAccessPair> {
322 let mut all_pairs: Vec<&CoAccessPair> = self
323 .co_access
324 .values()
325 .flat_map(|inner| inner.values())
326 .collect();
327 all_pairs.sort_by_key(|b| Reverse(b.co_access_count));
328 all_pairs.into_iter().take(k).collect()
329 }
330
331 pub fn apply_decay(&mut self) {
340 let decay = self.config.pattern_decay_factor;
341 for inner in self.co_access.values_mut() {
342 inner.retain(|_, pair| {
343 let decayed = (pair.co_access_count as f64 * decay).round() as u64;
344 pair.co_access_count = decayed;
345 decayed >= 1
346 });
347 }
348 self.co_access.retain(|_, inner| !inner.is_empty());
350 }
351
352 pub fn cache_hit_rate(&self) -> f64 {
358 if self.total_events == 0 {
359 0.0
360 } else {
361 self.cache_hit_count as f64 / self.total_events as f64
362 }
363 }
364
365 pub fn optimizer_stats(&self) -> OptimizerStats {
367 let co_access_pairs = self
368 .co_access
369 .values()
370 .map(|inner| inner.len())
371 .sum::<usize>();
372
373 OptimizerStats {
374 total_events: self.total_events,
375 unique_blocks: self.patterns.len(),
376 co_access_pairs,
377 cache_hit_rate: self.cache_hit_rate(),
378 window_size: self.recent_window.len(),
379 }
380 }
381}
382
383#[cfg(test)]
388mod tests {
389 use crate::block_access_optimizer::{AccessEvent, BlockAccessOptimizer, OptimizerConfig};
390
391 fn event(cid: &str, ts: u64, latency: u32, hit: bool) -> AccessEvent {
396 AccessEvent {
397 cid: cid.to_owned(),
398 timestamp_ms: ts,
399 latency_ms: latency,
400 cache_hit: hit,
401 }
402 }
403
404 fn default_optimizer() -> BlockAccessOptimizer {
405 BlockAccessOptimizer::with_defaults()
406 }
407
408 #[test]
413 fn test_new_empty() {
414 let opt = default_optimizer();
415 assert_eq!(opt.total_events, 0);
416 assert_eq!(opt.cache_hit_count, 0);
417 assert!(opt.patterns.is_empty());
418 assert!(opt.co_access.is_empty());
419 assert!(opt.recent_window.is_empty());
420 }
421
422 #[test]
423 fn test_new_with_config() {
424 let cfg = OptimizerConfig {
425 window_size: 50,
426 min_co_access_count: 2,
427 max_prefetch_candidates: 3,
428 confidence_threshold: 0.5,
429 pattern_decay_factor: 0.9,
430 };
431 let opt = BlockAccessOptimizer::new(cfg.clone());
432 assert_eq!(opt.config.window_size, 50);
433 assert_eq!(opt.config.min_co_access_count, 2);
434 assert_eq!(opt.config.max_prefetch_candidates, 3);
435 assert!((opt.config.confidence_threshold - 0.5).abs() < 1e-9);
436 assert!((opt.config.pattern_decay_factor - 0.9).abs() < 1e-9);
437 }
438
439 #[test]
444 fn test_record_increments_total_events() {
445 let mut opt = default_optimizer();
446 opt.record_access(event("cid1", 1000, 10, false));
447 assert_eq!(opt.total_events, 1);
448 opt.record_access(event("cid2", 2000, 5, true));
449 assert_eq!(opt.total_events, 2);
450 }
451
452 #[test]
453 fn test_record_increments_cache_hit_count() {
454 let mut opt = default_optimizer();
455 opt.record_access(event("cid1", 1000, 10, true));
456 opt.record_access(event("cid2", 2000, 5, false));
457 opt.record_access(event("cid3", 3000, 8, true));
458 assert_eq!(opt.cache_hit_count, 2);
459 }
460
461 #[test]
462 fn test_record_creates_pattern() {
463 let mut opt = default_optimizer();
464 opt.record_access(event("cid1", 1000, 10, false));
465 assert!(opt.patterns.contains_key("cid1"));
466 let pat = opt.patterns.get("cid1").expect("pattern should exist");
467 assert_eq!(pat.cid, "cid1");
468 assert_eq!(pat.total_accesses, 1);
469 assert_eq!(pat.last_access_ms, 1000);
470 }
471
472 #[test]
473 fn test_record_increments_total_accesses() {
474 let mut opt = default_optimizer();
475 opt.record_access(event("cid1", 1000, 10, false));
476 opt.record_access(event("cid1", 2000, 5, false));
477 let pat = opt.patterns.get("cid1").expect("pattern should exist");
478 assert_eq!(pat.total_accesses, 2);
479 }
480
481 #[test]
482 fn test_record_updates_last_access() {
483 let mut opt = default_optimizer();
484 opt.record_access(event("cid1", 1000, 10, false));
485 opt.record_access(event("cid1", 3000, 5, false));
486 let pat = opt.patterns.get("cid1").expect("pattern should exist");
487 assert_eq!(pat.last_access_ms, 3000);
488 }
489
490 #[test]
491 fn test_predicted_next_equals_last_plus_avg() {
492 let mut opt = default_optimizer();
493 opt.record_access(event("cid1", 1000, 10, false));
494 opt.record_access(event("cid1", 3000, 5, false));
495 let pat = opt.patterns.get("cid1").expect("pattern should exist");
496 assert_eq!(
497 pat.predicted_next_ms,
498 pat.last_access_ms + pat.avg_interval_ms
499 );
500 }
501
502 #[test]
507 fn test_avg_interval_bootstrap_on_second_access() {
508 let mut opt = default_optimizer();
509 opt.record_access(event("cid1", 0, 10, false));
510 opt.record_access(event("cid1", 1000, 5, false));
511 let pat = opt.patterns.get("cid1").expect("pattern should exist");
513 assert_eq!(pat.avg_interval_ms, 1000);
514 }
515
516 #[test]
517 fn test_avg_interval_ewma_third_access() {
518 let mut opt = default_optimizer();
519 opt.record_access(event("cid1", 0, 10, false));
520 opt.record_access(event("cid1", 1000, 5, false)); opt.record_access(event("cid1", 3000, 5, false)); let pat = opt.patterns.get("cid1").expect("pattern should exist");
524 assert_eq!(pat.avg_interval_ms, 1300);
525 }
526
527 #[test]
532 fn test_window_respects_size() {
533 let cfg = OptimizerConfig {
534 window_size: 5,
535 ..OptimizerConfig::default()
536 };
537 let mut opt = BlockAccessOptimizer::new(cfg);
538 for i in 0..10u64 {
539 opt.record_access(event(&format!("c{}", i), i * 100, 10, false));
540 }
541 assert_eq!(opt.recent_window.len(), 5);
542 }
543
544 #[test]
545 fn test_window_evicts_oldest() {
546 let cfg = OptimizerConfig {
547 window_size: 3,
548 ..OptimizerConfig::default()
549 };
550 let mut opt = BlockAccessOptimizer::new(cfg);
551 opt.record_access(event("old", 100, 10, false));
552 opt.record_access(event("mid", 200, 10, false));
553 opt.record_access(event("new1", 300, 10, false));
554 opt.record_access(event("new2", 400, 10, false));
555 assert!(opt.recent_window.iter().all(|e| e.cid != "old"));
557 }
558
559 #[test]
564 fn test_co_access_created_for_pair() {
565 let mut opt = default_optimizer();
566 opt.record_access(event("A", 100, 5, false));
567 opt.record_access(event("B", 200, 5, false));
568 assert!(opt.co_access.get("B").is_some_and(|m| m.contains_key("A")));
570 assert!(opt.co_access.get("A").is_some_and(|m| m.contains_key("B")));
571 }
572
573 #[test]
574 fn test_co_access_count_increments() {
575 let mut opt = default_optimizer();
576 opt.record_access(event("A", 100, 5, false));
577 opt.record_access(event("B", 200, 5, false));
578 opt.record_access(event("A", 300, 5, false));
579 opt.record_access(event("B", 400, 5, false));
580 let count = opt
581 .co_access
582 .get("B")
583 .and_then(|m| m.get("A"))
584 .map(|p| p.co_access_count)
585 .unwrap_or(0);
586 assert!(count >= 2, "expected at least 2 co-accesses, got {count}");
587 }
588
589 #[test]
590 fn test_co_access_same_cid_skipped() {
591 let mut opt = default_optimizer();
592 opt.record_access(event("A", 100, 5, false));
593 opt.record_access(event("A", 200, 5, false));
594 let has_self = opt.co_access.get("A").is_some_and(|m| m.contains_key("A"));
596 assert!(!has_self);
597 }
598
599 #[test]
600 fn test_co_access_look_back_limit() {
601 let mut opt = default_optimizer();
602 for i in 0..8u64 {
604 opt.record_access(event(&format!("c{}", i), i * 100, 5, false));
605 }
606 let inner = opt.co_access.get("c7").cloned().unwrap_or_default();
608 assert!(!inner.contains_key("c0"), "c0 should be outside look-back");
610 assert!(!inner.contains_key("c1"), "c1 should be outside look-back");
611 }
612
613 #[test]
618 fn test_predict_next_returns_none_for_unknown() {
619 let opt = default_optimizer();
620 assert!(opt.predict_next_access("nonexistent").is_none());
621 }
622
623 #[test]
624 fn test_predict_next_returns_some_after_access() {
625 let mut opt = default_optimizer();
626 opt.record_access(event("A", 1000, 5, false));
627 assert!(opt.predict_next_access("A").is_some());
628 }
629
630 #[test]
631 fn test_predict_next_matches_pattern() {
632 let mut opt = default_optimizer();
633 opt.record_access(event("A", 0, 5, false));
634 opt.record_access(event("A", 2000, 5, false));
635 let predicted = opt.predict_next_access("A").unwrap_or(0);
636 let pat = opt.patterns.get("A").expect("pattern");
637 assert_eq!(predicted, pat.predicted_next_ms);
638 }
639
640 #[test]
645 fn test_hot_blocks_ordering() {
646 let mut opt = default_optimizer();
647 for i in 0..5u64 {
649 opt.record_access(event("hot", i * 100, 5, false));
650 }
651 opt.record_access(event("cold", 600, 5, false));
652 let top = opt.hot_blocks(1);
653 assert_eq!(top.len(), 1);
654 assert_eq!(top[0].cid, "hot");
655 }
656
657 #[test]
658 fn test_hot_blocks_k_limit() {
659 let mut opt = default_optimizer();
660 for i in 0..10u64 {
661 opt.record_access(event(&format!("c{}", i), i * 100, 5, false));
662 }
663 let top = opt.hot_blocks(3);
664 assert_eq!(top.len(), 3);
665 }
666
667 #[test]
668 fn test_hot_blocks_empty() {
669 let opt = default_optimizer();
670 assert!(opt.hot_blocks(5).is_empty());
671 }
672
673 #[test]
678 fn test_top_co_access_pairs_ordering() {
679 let mut opt = default_optimizer();
680 for i in 0..10u64 {
682 opt.record_access(event("A", i * 100, 5, false));
683 opt.record_access(event("B", i * 100 + 50, 5, false));
684 }
685 opt.record_access(event("C", 5000, 5, false));
687 opt.record_access(event("D", 5050, 5, false));
688 let top = opt.top_co_access_pairs(1);
689 assert_eq!(top.len(), 1);
690 assert!(
691 (top[0].cid_a == "A" && top[0].cid_b == "B")
692 || (top[0].cid_a == "B" && top[0].cid_b == "A"),
693 "Expected A-B pair at top, got {}-{}",
694 top[0].cid_a,
695 top[0].cid_b,
696 );
697 }
698
699 #[test]
700 fn test_top_co_access_pairs_empty() {
701 let opt = default_optimizer();
702 assert!(opt.top_co_access_pairs(5).is_empty());
703 }
704
705 #[test]
710 fn test_recommend_prefetch_empty_for_unknown_cid() {
711 let opt = default_optimizer();
712 let rec = opt.recommend_prefetch("nope");
713 assert!(rec.prefetch_cids.is_empty());
714 assert_eq!(rec.trigger_cid, "nope");
715 assert!((rec.confidence - 0.0).abs() < 1e-9);
716 }
717
718 #[test]
719 fn test_recommend_prefetch_filters_below_threshold() {
720 let cfg = OptimizerConfig {
721 min_co_access_count: 10, ..OptimizerConfig::default()
723 };
724 let mut opt = BlockAccessOptimizer::new(cfg);
725 opt.record_access(event("A", 100, 5, false));
726 opt.record_access(event("B", 200, 5, false));
727 let rec = opt.recommend_prefetch("A");
728 assert!(rec.prefetch_cids.is_empty());
730 }
731
732 #[test]
733 fn test_recommend_prefetch_returns_candidates() {
734 let cfg = OptimizerConfig {
735 min_co_access_count: 2,
736 confidence_threshold: 0.5,
737 ..OptimizerConfig::default()
738 };
739 let mut opt = BlockAccessOptimizer::new(cfg);
740 for i in 0..10u64 {
742 opt.record_access(event("trigger", i * 200, 5, false));
743 opt.record_access(event("follower", i * 200 + 100, 5, false));
744 }
745 let rec = opt.recommend_prefetch("trigger");
746 assert!(!rec.prefetch_cids.is_empty());
747 assert!(rec.confidence >= 0.5);
748 }
749
750 #[test]
751 fn test_recommend_prefetch_respects_max_candidates() {
752 let cfg = OptimizerConfig {
753 min_co_access_count: 1,
754 max_prefetch_candidates: 2,
755 confidence_threshold: 0.0,
756 ..OptimizerConfig::default()
757 };
758 let mut opt = BlockAccessOptimizer::new(cfg);
759 opt.record_access(event("trigger", 100, 5, false));
760 for i in 1..=6u64 {
761 opt.record_access(event(&format!("follow{}", i), 100 + i * 10, 5, false));
762 }
763 opt.record_access(event("trigger", 200, 5, false));
765 let rec = opt.recommend_prefetch("trigger");
766 assert!(rec.prefetch_cids.len() <= 2);
767 }
768
769 #[test]
770 fn test_recommend_prefetch_trigger_cid_in_result() {
771 let opt = default_optimizer();
772 let rec = opt.recommend_prefetch("xyz");
773 assert_eq!(rec.trigger_cid, "xyz");
774 }
775
776 #[test]
777 fn test_recommend_prefetch_estimated_benefit_nonzero() {
778 let cfg = OptimizerConfig {
779 min_co_access_count: 2,
780 confidence_threshold: 0.5,
781 ..OptimizerConfig::default()
782 };
783 let mut opt = BlockAccessOptimizer::new(cfg);
784 for i in 0..10u64 {
785 opt.record_access(event("A", i * 200, 5, false));
786 opt.record_access(event("B", i * 200 + 100, 5, false));
787 }
788 let rec = opt.recommend_prefetch("A");
789 if !rec.prefetch_cids.is_empty() {
790 assert!(rec.estimated_benefit_ms > 0);
791 }
792 }
793
794 #[test]
799 fn test_cache_hit_rate_zero_events() {
800 let opt = default_optimizer();
801 assert!((opt.cache_hit_rate() - 0.0).abs() < 1e-9);
802 }
803
804 #[test]
805 fn test_cache_hit_rate_all_hits() {
806 let mut opt = default_optimizer();
807 opt.record_access(event("A", 100, 5, true));
808 opt.record_access(event("B", 200, 5, true));
809 assert!((opt.cache_hit_rate() - 1.0).abs() < 1e-9);
810 }
811
812 #[test]
813 fn test_cache_hit_rate_no_hits() {
814 let mut opt = default_optimizer();
815 opt.record_access(event("A", 100, 5, false));
816 opt.record_access(event("B", 200, 5, false));
817 assert!((opt.cache_hit_rate() - 0.0).abs() < 1e-9);
818 }
819
820 #[test]
821 fn test_cache_hit_rate_partial() {
822 let mut opt = default_optimizer();
823 opt.record_access(event("A", 100, 5, true));
824 opt.record_access(event("B", 200, 5, false));
825 let rate = opt.cache_hit_rate();
826 assert!((rate - 0.5).abs() < 1e-9);
827 }
828
829 #[test]
834 fn test_apply_decay_reduces_counts() {
835 let cfg = OptimizerConfig {
836 pattern_decay_factor: 0.5,
837 ..OptimizerConfig::default()
838 };
839 let mut opt = BlockAccessOptimizer::new(cfg);
840 for i in 0..10u64 {
842 opt.record_access(event("A", i * 100, 5, false));
843 opt.record_access(event("B", i * 100 + 50, 5, false));
844 }
845 let before = opt
846 .co_access
847 .get("A")
848 .and_then(|m| m.get("B"))
849 .map(|p| p.co_access_count)
850 .unwrap_or(0);
851 opt.apply_decay();
852 let after = opt
853 .co_access
854 .get("A")
855 .and_then(|m| m.get("B"))
856 .map(|p| p.co_access_count)
857 .unwrap_or(0);
858 assert!(after < before, "after={after} should be < before={before}");
859 }
860
861 #[test]
862 fn test_apply_decay_removes_low_count_pairs() {
863 let cfg = OptimizerConfig {
864 pattern_decay_factor: 0.01, ..OptimizerConfig::default()
866 };
867 let mut opt = BlockAccessOptimizer::new(cfg);
868 opt.record_access(event("A", 100, 5, false));
869 opt.record_access(event("B", 200, 5, false));
870 opt.apply_decay();
871 let exists = opt.co_access.get("A").is_some_and(|m| m.contains_key("B"));
873 assert!(!exists, "pair should have been removed after extreme decay");
874 }
875
876 #[test]
877 fn test_apply_decay_cleans_empty_outer_map() {
878 let cfg = OptimizerConfig {
879 pattern_decay_factor: 0.01,
880 ..OptimizerConfig::default()
881 };
882 let mut opt = BlockAccessOptimizer::new(cfg);
883 opt.record_access(event("A", 100, 5, false));
884 opt.record_access(event("B", 200, 5, false));
885 opt.apply_decay();
886 let outer_empty = !opt.co_access.contains_key("A")
888 || opt.co_access.get("A").is_some_and(|m| m.is_empty());
889 assert!(outer_empty);
890 }
891
892 #[test]
897 fn test_optimizer_stats_initial() {
898 let opt = default_optimizer();
899 let stats = opt.optimizer_stats();
900 assert_eq!(stats.total_events, 0);
901 assert_eq!(stats.unique_blocks, 0);
902 assert_eq!(stats.co_access_pairs, 0);
903 assert_eq!(stats.window_size, 0);
904 }
905
906 #[test]
907 fn test_optimizer_stats_after_events() {
908 let mut opt = default_optimizer();
909 opt.record_access(event("A", 100, 5, true));
910 opt.record_access(event("B", 200, 5, false));
911 let stats = opt.optimizer_stats();
912 assert_eq!(stats.total_events, 2);
913 assert_eq!(stats.unique_blocks, 2);
914 assert_eq!(stats.window_size, 2);
915 assert!((stats.cache_hit_rate - 0.5).abs() < 1e-9);
916 }
917
918 #[test]
919 fn test_optimizer_stats_co_access_count() {
920 let mut opt = default_optimizer();
921 opt.record_access(event("A", 100, 5, false));
922 opt.record_access(event("B", 200, 5, false));
923 let stats = opt.optimizer_stats();
924 assert_eq!(stats.co_access_pairs, 2);
926 }
927
928 #[test]
933 fn test_single_event_no_co_access() {
934 let mut opt = default_optimizer();
935 opt.record_access(event("only", 100, 5, false));
936 assert!(opt.co_access.is_empty());
937 }
938
939 #[test]
940 fn test_same_cid_repeated_no_co_access_with_itself() {
941 let mut opt = default_optimizer();
942 for i in 0..5u64 {
943 opt.record_access(event("X", i * 100, 5, false));
944 }
945 let has_self_pair = opt.co_access.get("X").is_some_and(|m| m.contains_key("X"));
946 assert!(!has_self_pair);
947 }
948
949 #[test]
950 fn test_hot_blocks_k_larger_than_available() {
951 let mut opt = default_optimizer();
952 opt.record_access(event("only", 100, 5, false));
953 let top = opt.hot_blocks(100);
954 assert_eq!(top.len(), 1);
955 }
956
957 #[test]
958 fn test_top_co_access_pairs_k_larger_than_available() {
959 let mut opt = default_optimizer();
960 opt.record_access(event("A", 100, 5, false));
961 opt.record_access(event("B", 200, 5, false));
962 let pairs = opt.top_co_access_pairs(100);
963 assert_eq!(pairs.len(), 2); }
965
966 #[test]
967 fn test_decay_on_empty_optimizer() {
968 let mut opt = default_optimizer();
969 opt.apply_decay();
971 assert!(opt.co_access.is_empty());
972 }
973
974 #[test]
975 fn test_multiple_decays_converge_to_zero() {
976 let cfg = OptimizerConfig {
979 pattern_decay_factor: 0.01,
980 ..OptimizerConfig::default()
981 };
982 let mut opt = BlockAccessOptimizer::new(cfg);
983 opt.record_access(event("A", 100, 5, false));
985 opt.record_access(event("B", 200, 5, false));
986 opt.apply_decay();
988 assert!(
989 opt.co_access.is_empty(),
990 "all pairs should be pruned after one aggressive decay"
991 );
992 }
993}