1#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
36pub struct BlockKey(pub u64);
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
40#[repr(u8)]
41pub enum Tier {
42 Tier0 = 0,
44 Tier1 = 1,
46 Tier2 = 2,
48 Tier3 = 3,
50}
51
52#[derive(Clone, Debug)]
54pub struct BlockMeta {
55 pub ema_rate: f32,
57 pub access_window: u64,
60 pub last_access: u64,
62 pub access_count: u64,
64 pub current_tier: Tier,
66 pub tier_since: u64,
68}
69
70impl BlockMeta {
71 pub fn new(now: u64) -> Self {
73 Self {
74 ema_rate: 0.0,
75 access_window: 0,
76 last_access: now,
77 access_count: 0,
78 current_tier: Tier::Tier1,
79 tier_since: now,
80 }
81 }
82}
83
84#[derive(Clone, Debug)]
92pub struct TierConfig {
93 pub alpha: f32,
95 pub tau: f32,
97 pub w_ema: f32,
99 pub w_pop: f32,
101 pub w_rec: f32,
103 pub t1: f32,
105 pub t2: f32,
107 pub t3: f32,
109 pub hysteresis: f32,
112 pub min_residency: u32,
114 pub max_delta_chain: u8,
116 pub block_bytes: usize,
118 pub tier1_byte_cap: Option<usize>,
120 pub warm_aggressive_threshold: Option<usize>,
122}
123
124impl Default for TierConfig {
125 fn default() -> Self {
126 Self {
127 alpha: 0.3,
128 tau: 100.0,
129 w_ema: 0.4,
130 w_pop: 0.3,
131 w_rec: 0.3,
132 t1: 0.7,
133 t2: 0.3,
134 t3: 0.1,
135 hysteresis: 0.05,
136 min_residency: 5,
137 max_delta_chain: 8,
138 block_bytes: 16384,
139 tier1_byte_cap: None,
140 warm_aggressive_threshold: None,
141 }
142 }
143}
144
145#[inline]
157#[allow(dead_code)]
158fn fast_exp_neg(x: f32) -> f32 {
159 if x < 0.0 {
160 return 1.0;
161 }
162 1.0 / (1.0 + x)
163}
164
165const LUT_SIZE: usize = 64;
167const LUT_X_MAX: f32 = 8.0;
169
170const EXP_LUT: [f32; LUT_SIZE + 1] = {
172 let mut table = [0.0f32; LUT_SIZE + 1];
173 let mut i = 0;
174 while i <= LUT_SIZE {
175 let x = -(i as f64) * (LUT_X_MAX as f64) / (LUT_SIZE as f64);
177 let v = const_exp(x);
179 table[i] = v as f32;
180 i += 1;
181 }
182 table
183};
184
185const fn const_exp(x: f64) -> f64 {
190 if x < 0.0 {
192 let pos = const_exp_pos(-x);
193 return 1.0 / pos;
194 }
195 const_exp_pos(x)
196}
197
198const fn const_exp_pos(x: f64) -> f64 {
201 let mut sum = 1.0f64;
202 let mut term = 1.0f64;
203 let mut k = 1u32;
204 while k <= 35 {
205 term *= x / (k as f64);
206 sum += term;
207 k += 1;
208 }
209 sum
210}
211
212#[inline]
217fn fast_exp_neg_lut(x: f32) -> f32 {
218 if x <= 0.0 {
219 return 1.0;
220 }
221 if x >= LUT_X_MAX {
222 return EXP_LUT[LUT_SIZE];
223 }
224 let scaled = x * (LUT_SIZE as f32) / LUT_X_MAX;
225 let idx = scaled as usize; let frac = scaled - (idx as f32);
227 let lo = EXP_LUT[idx];
229 let hi = EXP_LUT[idx + 1];
230 lo + frac * (hi - lo)
231}
232
233pub fn compute_score(config: &TierConfig, now: u64, meta: &BlockMeta) -> f32 {
248 let ema_component = config.w_ema * meta.ema_rate.clamp(0.0, 1.0);
249
250 let pop = meta.access_window.count_ones() as f32 / 64.0;
251 let pop_component = config.w_pop * pop;
252
253 let dt = now.saturating_sub(meta.last_access) as f32;
254 let recency = fast_exp_neg_lut(dt / config.tau);
255 let rec_component = config.w_rec * recency;
256
257 ema_component + pop_component + rec_component
258}
259
260pub fn choose_tier(config: &TierConfig, now: u64, meta: &BlockMeta) -> Option<Tier> {
270 let ticks_in_tier = now.saturating_sub(meta.tier_since);
272 if ticks_in_tier < config.min_residency as u64 {
273 return None;
274 }
275
276 let score = compute_score(config, now, meta);
277 let current = meta.current_tier;
278
279 let raw_target = if score >= config.t1 {
281 Tier::Tier1
282 } else if score >= config.t2 {
283 Tier::Tier2
284 } else if score >= config.t3 {
285 Tier::Tier3
286 } else {
287 Tier::Tier3 };
289
290 if raw_target == current {
291 return None;
292 }
293
294 let h = config.hysteresis;
297
298 let transition_allowed = if raw_target < current {
299 let threshold = match raw_target {
302 Tier::Tier0 => return None, Tier::Tier1 => config.t1,
304 Tier::Tier2 => config.t2,
305 Tier::Tier3 => config.t3,
306 };
307 score > threshold + h
308 } else {
309 let threshold = match current {
312 Tier::Tier0 => return None,
313 Tier::Tier1 => config.t1,
314 Tier::Tier2 => config.t2,
315 Tier::Tier3 => return None, };
317 score < threshold - h
318 };
319
320 if transition_allowed {
321 Some(raw_target)
322 } else {
323 None
324 }
325}
326
327pub fn touch(config: &TierConfig, now: u64, meta: &mut BlockMeta) {
339 meta.ema_rate = config.alpha + (1.0 - config.alpha) * meta.ema_rate;
341
342 let elapsed = now.saturating_sub(meta.last_access);
344 if elapsed > 0 {
345 if elapsed >= 64 {
346 meta.access_window = 1;
347 } else {
348 meta.access_window = (meta.access_window << elapsed) | 1;
349 }
350 } else {
351 meta.access_window |= 1;
353 }
354
355 meta.last_access = now;
356 meta.access_count = meta.access_count.saturating_add(1);
357}
358
359pub fn tick_decay(config: &TierConfig, meta: &mut BlockMeta) {
369 meta.ema_rate *= 1.0 - config.alpha;
370 meta.access_window <<= 1;
371}
372
373#[derive(Debug, Default)]
379pub struct MaintenanceResult {
380 pub upgrades: u32,
381 pub downgrades: u32,
382 pub evictions: u32,
383 pub bytes_freed: usize,
384 pub ops_used: u32,
385}
386
387#[derive(Debug)]
389pub struct MigrationCandidate {
390 pub key: BlockKey,
391 pub current_tier: Tier,
392 pub target_tier: Tier,
393 pub score: f32,
394}
395
396pub fn select_candidates(
402 config: &TierConfig,
403 now: u64,
404 blocks: &[(BlockKey, &BlockMeta)],
405) -> Vec<MigrationCandidate> {
406 let mut upgrades: Vec<MigrationCandidate> = Vec::new();
407 let mut downgrades: Vec<MigrationCandidate> = Vec::new();
408
409 for &(key, meta) in blocks {
410 if let Some(target) = choose_tier(config, now, meta) {
411 let score = compute_score(config, now, meta);
412 let candidate = MigrationCandidate {
413 key,
414 current_tier: meta.current_tier,
415 target_tier: target,
416 score,
417 };
418 if target < meta.current_tier {
419 upgrades.push(candidate);
420 } else {
421 downgrades.push(candidate);
422 }
423 }
424 }
425
426 upgrades.sort_by(|a, b| {
428 b.score
429 .partial_cmp(&a.score)
430 .unwrap_or(core::cmp::Ordering::Equal)
431 });
432 downgrades.sort_by(|a, b| {
434 a.score
435 .partial_cmp(&b.score)
436 .unwrap_or(core::cmp::Ordering::Equal)
437 });
438
439 upgrades.extend(downgrades);
440 upgrades
441}
442
443#[derive(Clone, Debug)]
449pub struct ScoredPartition {
450 pub hot: Vec<usize>,
452 pub warm: Vec<usize>,
454 pub cold: Vec<usize>,
456 pub evict: Vec<usize>,
458 pub scores: Vec<f32>,
460}
461
462pub fn compute_scores_batch(config: &TierConfig, now: u64, metas: &[BlockMeta]) -> Vec<f32> {
467 metas
468 .iter()
469 .map(|m| compute_score(config, now, m))
470 .collect()
471}
472
473pub fn choose_tiers_batch(config: &TierConfig, now: u64, metas: &[BlockMeta]) -> Vec<Option<Tier>> {
478 metas.iter().map(|m| choose_tier(config, now, m)).collect()
479}
480
481pub fn score_and_partition(config: &TierConfig, now: u64, metas: &[BlockMeta]) -> ScoredPartition {
488 let scores = compute_scores_batch(config, now, metas);
489 let mut hot = Vec::new();
490 let mut warm = Vec::new();
491 let mut cold = Vec::new();
492 let mut evict = Vec::new();
493 for (i, &score) in scores.iter().enumerate() {
494 if score >= config.t1 {
495 hot.push(i);
496 } else if score >= config.t2 {
497 warm.push(i);
498 } else if score >= config.t3 {
499 cold.push(i);
500 } else {
501 evict.push(i);
502 }
503 }
504 ScoredPartition {
505 hot,
506 warm,
507 cold,
508 evict,
509 scores,
510 }
511}
512
513pub fn top_k_coldest(
519 config: &TierConfig,
520 now: u64,
521 metas: &[BlockMeta],
522 k: usize,
523) -> Vec<(usize, f32)> {
524 let scores = compute_scores_batch(config, now, metas);
525 let mut indexed: Vec<(usize, f32)> = scores.into_iter().enumerate().collect();
526 if k < indexed.len() {
528 indexed.select_nth_unstable_by(k, |a, b| {
529 a.1.partial_cmp(&b.1).unwrap_or(core::cmp::Ordering::Equal)
530 });
531 indexed.truncate(k);
532 }
533 indexed.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(core::cmp::Ordering::Equal));
534 indexed
535}
536
537pub fn bits_for_tier(config: &TierConfig, tier: Tier, warm_bytes: usize) -> u8 {
550 match tier {
551 Tier::Tier0 => 0,
552 Tier::Tier1 => 8,
553 Tier::Tier2 => {
554 if let Some(threshold) = config.warm_aggressive_threshold {
555 if warm_bytes > threshold {
556 return 5;
557 }
558 }
559 7
560 }
561 Tier::Tier3 => 3,
562 }
563}
564
565#[cfg(test)]
570mod tests {
571 use super::*;
572
573 fn default_config() -> TierConfig {
574 TierConfig::default()
575 }
576
577 fn make_meta(
578 ema_rate: f32,
579 access_window: u64,
580 last_access: u64,
581 current_tier: Tier,
582 tier_since: u64,
583 ) -> BlockMeta {
584 BlockMeta {
585 ema_rate,
586 access_window,
587 last_access,
588 access_count: 0,
589 current_tier,
590 tier_since,
591 }
592 }
593
594 #[test]
599 fn score_all_components_at_max() {
600 let cfg = default_config();
601 let meta = make_meta(1.0, u64::MAX, 100, Tier::Tier1, 0);
603 let score = compute_score(&cfg, 100, &meta);
604 assert!((score - 1.0).abs() < 1e-4, "score={score}");
606 }
607
608 #[test]
609 fn score_all_components_at_zero() {
610 let cfg = default_config();
611 let meta = make_meta(0.0, 0, 0, Tier::Tier3, 0);
613 let score = compute_score(&cfg, 10_000, &meta);
614 assert!(score < 0.01, "score={score}");
616 }
617
618 #[test]
619 fn score_only_ema_contributes() {
620 let cfg = TierConfig {
621 w_ema: 1.0,
622 w_pop: 0.0,
623 w_rec: 0.0,
624 ..default_config()
625 };
626 let meta = make_meta(0.75, 0, 0, Tier::Tier2, 0);
627 let score = compute_score(&cfg, 1000, &meta);
628 assert!((score - 0.75).abs() < 1e-6, "score={score}");
629 }
630
631 #[test]
632 fn score_only_popcount_contributes() {
633 let cfg = TierConfig {
634 w_ema: 0.0,
635 w_pop: 1.0,
636 w_rec: 0.0,
637 ..default_config()
638 };
639 let meta = make_meta(0.0, 0x0000_FFFF_FFFF_0000, 0, Tier::Tier2, 0);
641 let pop = 0x0000_FFFF_FFFF_0000u64.count_ones() as f32 / 64.0;
642 let score = compute_score(&cfg, 1000, &meta);
643 assert!(
644 (score - pop).abs() < 1e-6,
645 "score={score}, expected pop={pop}"
646 );
647 }
648
649 #[test]
654 fn fast_exp_neg_monotonic() {
655 let mut prev = fast_exp_neg(0.0);
656 for i in 1..100 {
657 let x = i as f32 * 0.1;
658 let val = fast_exp_neg(x);
659 assert!(val <= prev, "not monotonic at x={x}");
660 assert!(val >= 0.0);
661 prev = val;
662 }
663 }
664
665 #[test]
666 fn fast_exp_neg_at_zero() {
667 assert!((fast_exp_neg(0.0) - 1.0).abs() < 1e-6);
668 }
669
670 #[test]
671 fn fast_exp_neg_negative_input() {
672 assert!((fast_exp_neg(-5.0) - 1.0).abs() < 1e-6);
674 }
675
676 #[test]
677 fn fast_exp_neg_vs_stdlib() {
678 for i in 0..50 {
680 let x = i as f32 * 0.2;
681 let approx = fast_exp_neg(x);
682 let exact = (-x).exp();
683 assert!(
684 approx >= exact - 1e-6,
685 "approx={approx} < exact={exact} at x={x}"
686 );
687 }
688 }
689
690 #[test]
695 fn lut_exp_at_zero() {
696 assert!((fast_exp_neg_lut(0.0) - 1.0).abs() < 1e-4);
697 }
698
699 #[test]
700 fn lut_exp_accuracy() {
701 for i in 0..80 {
703 let x = i as f32 * 0.1;
704 let approx = fast_exp_neg_lut(x);
705 let exact = (-x).exp();
706 let rel_err = if exact > 1e-10 {
707 (approx - exact).abs() / exact
708 } else {
709 (approx - exact).abs()
710 };
711 assert!(
712 rel_err < 0.01,
713 "x={x} approx={approx} exact={exact} rel_err={rel_err}"
714 );
715 }
716 }
717
718 #[test]
719 fn lut_exp_beyond_domain() {
720 let val = fast_exp_neg_lut(100.0);
722 assert!(val < 0.001, "val={val}");
723 assert!(val >= 0.0);
724 }
725
726 #[test]
727 fn lut_exp_monotonic() {
728 let mut prev = fast_exp_neg_lut(0.0);
729 for i in 1..160 {
730 let x = i as f32 * 0.05;
731 let val = fast_exp_neg_lut(x);
732 assert!(val <= prev + 1e-7, "not monotonic at x={x}");
733 prev = val;
734 }
735 }
736
737 #[test]
742 fn tier_selection_clear_hot() {
743 let cfg = default_config();
744 let meta = make_meta(1.0, u64::MAX, 100, Tier::Tier3, 0);
746 let target = choose_tier(&cfg, 100, &meta);
747 assert_eq!(target, Some(Tier::Tier1));
748 }
749
750 #[test]
751 fn tier_selection_clear_cold() {
752 let cfg = default_config();
753 let meta = make_meta(0.0, 0, 0, Tier::Tier1, 0);
755 let target = choose_tier(&cfg, 10_000, &meta);
756 assert_eq!(target, Some(Tier::Tier3));
757 }
758
759 #[test]
760 fn tier_selection_hysteresis_prevents_upgrade() {
761 let cfg = TierConfig {
763 hysteresis: 0.10,
764 ..default_config()
765 };
766 let meta = make_meta(0.4, u64::MAX, 50, Tier::Tier2, 0);
774 let score = compute_score(&cfg, 50, &meta);
775 assert!(score > cfg.t1, "score={score}");
776 assert!(score < cfg.t1 + cfg.hysteresis, "score={score}");
777 let target = choose_tier(&cfg, 50, &meta);
778 assert_eq!(
780 target, None,
781 "score={score} should be within hysteresis band"
782 );
783 }
784
785 #[test]
786 fn tier_selection_hysteresis_prevents_downgrade() {
787 let cfg = TierConfig {
788 hysteresis: 0.10,
789 ..default_config()
790 };
791 let meta = make_meta(0.5, 0x0000_0000_FFFF_FFFF, 90, Tier::Tier1, 0);
797 let score = compute_score(&cfg, 100, &meta);
798 assert!(
799 score < cfg.t1 && score > cfg.t1 - cfg.hysteresis,
800 "score={score}, expected in ({}, {})",
801 cfg.t1 - cfg.hysteresis,
802 cfg.t1
803 );
804 let target = choose_tier(&cfg, 100, &meta);
805 assert_eq!(
806 target, None,
807 "hysteresis should prevent downgrade, score={score}"
808 );
809 }
810
811 #[test]
816 fn touch_increments_count() {
817 let cfg = default_config();
818 let mut meta = BlockMeta::new(0);
819 assert_eq!(meta.access_count, 0);
820 touch(&cfg, 1, &mut meta);
821 assert_eq!(meta.access_count, 1);
822 touch(&cfg, 2, &mut meta);
823 assert_eq!(meta.access_count, 2);
824 }
825
826 #[test]
827 fn touch_updates_ema() {
828 let cfg = default_config();
829 let mut meta = BlockMeta::new(0);
830 assert_eq!(meta.ema_rate, 0.0);
831 touch(&cfg, 1, &mut meta);
832 assert!((meta.ema_rate - 0.3).abs() < 1e-6);
834 touch(&cfg, 2, &mut meta);
835 assert!((meta.ema_rate - 0.51).abs() < 1e-6);
837 }
838
839 #[test]
840 fn touch_updates_window() {
841 let cfg = default_config();
842 let mut meta = BlockMeta::new(0);
843 meta.access_window = 0;
844 touch(&cfg, 1, &mut meta);
845 assert_eq!(meta.access_window, 1);
846 touch(&cfg, 3, &mut meta);
847 assert_eq!(meta.access_window, 0b101);
849 }
850
851 #[test]
852 fn touch_same_tick() {
853 let cfg = default_config();
854 let mut meta = BlockMeta::new(5);
855 meta.access_window = 0b1010;
856 touch(&cfg, 5, &mut meta);
857 assert_eq!(meta.access_window, 0b1011);
859 }
860
861 #[test]
862 fn touch_large_gap_clears_window() {
863 let cfg = default_config();
864 let mut meta = BlockMeta::new(0);
865 meta.access_window = u64::MAX;
866 touch(&cfg, 100, &mut meta);
867 assert_eq!(meta.access_window, 1);
869 }
870
871 #[test]
876 fn min_residency_blocks_migration() {
877 let cfg = TierConfig {
878 min_residency: 10,
879 ..default_config()
880 };
881 let meta = make_meta(1.0, u64::MAX, 100, Tier::Tier3, 95);
883 let target = choose_tier(&cfg, 100, &meta);
884 assert_eq!(target, None);
885 }
886
887 #[test]
888 fn min_residency_allows_after_enough_ticks() {
889 let cfg = TierConfig {
890 min_residency: 10,
891 ..default_config()
892 };
893 let meta = make_meta(1.0, u64::MAX, 100, Tier::Tier3, 90);
895 let target = choose_tier(&cfg, 100, &meta);
896 assert_eq!(target, Some(Tier::Tier1));
897 }
898
899 #[test]
904 fn candidates_upgrades_before_downgrades() {
905 let cfg = default_config();
906
907 let hot_meta = make_meta(1.0, u64::MAX, 50, Tier::Tier3, 0);
908 let cold_meta = make_meta(0.0, 0, 0, Tier::Tier1, 0);
909
910 let blocks = vec![(BlockKey(1), &cold_meta), (BlockKey(2), &hot_meta)];
911
912 let candidates = select_candidates(&cfg, 50, &blocks);
913 assert!(candidates.len() >= 2, "expected at least 2 candidates");
914 assert_eq!(candidates[0].key, BlockKey(2));
916 assert_eq!(candidates[0].target_tier, Tier::Tier1);
917 assert_eq!(candidates[1].key, BlockKey(1));
919 assert_eq!(candidates[1].target_tier, Tier::Tier3);
920 }
921
922 #[test]
923 fn candidates_upgrades_sorted_by_highest_score() {
924 let cfg = default_config();
925
926 let meta_a = make_meta(0.9, u64::MAX, 50, Tier::Tier3, 0);
927 let meta_b = make_meta(1.0, u64::MAX, 50, Tier::Tier3, 0);
928
929 let blocks = vec![(BlockKey(1), &meta_a), (BlockKey(2), &meta_b)];
930
931 let candidates = select_candidates(&cfg, 50, &blocks);
932 assert!(candidates.len() >= 2);
934 assert_eq!(candidates[0].key, BlockKey(2));
935 assert_eq!(candidates[1].key, BlockKey(1));
936 }
937
938 #[test]
939 fn candidates_empty_when_all_stable() {
940 let cfg = default_config();
941 let meta = make_meta(0.5, 0x0000_0000_FFFF_FFFF, 50, Tier::Tier2, 0);
943 let blocks = vec![(BlockKey(1), &meta)];
944 let candidates = select_candidates(&cfg, 50, &blocks);
945 let _ = candidates;
947 }
948
949 #[test]
954 fn bits_tier0() {
955 assert_eq!(bits_for_tier(&default_config(), Tier::Tier0, 0), 0);
956 }
957
958 #[test]
959 fn bits_tier1() {
960 assert_eq!(bits_for_tier(&default_config(), Tier::Tier1, 0), 8);
961 }
962
963 #[test]
964 fn bits_tier2_normal() {
965 assert_eq!(bits_for_tier(&default_config(), Tier::Tier2, 0), 7);
966 }
967
968 #[test]
969 fn bits_tier3() {
970 assert_eq!(bits_for_tier(&default_config(), Tier::Tier3, 0), 3);
971 }
972
973 #[test]
978 fn bits_tier2_aggressive() {
979 let cfg = TierConfig {
980 warm_aggressive_threshold: Some(1024),
981 ..default_config()
982 };
983 assert_eq!(bits_for_tier(&cfg, Tier::Tier2, 512), 7);
984 assert_eq!(bits_for_tier(&cfg, Tier::Tier2, 1024), 7); assert_eq!(bits_for_tier(&cfg, Tier::Tier2, 1025), 5);
986 }
987
988 #[test]
993 fn edge_zero_access_count() {
994 let cfg = default_config();
995 let meta = BlockMeta::new(0);
996 let score = compute_score(&cfg, 0, &meta);
997 assert!((score - cfg.w_rec).abs() < 1e-4, "score={score}");
999 }
1000
1001 #[test]
1002 fn edge_max_timestamp() {
1003 let cfg = default_config();
1004 let meta = make_meta(0.5, 0xAAAA_AAAA_AAAA_AAAA, u64::MAX - 1, Tier::Tier2, 0);
1005 let score = compute_score(&cfg, u64::MAX, &meta);
1006 assert!(score.is_finite(), "score={score}");
1008 }
1009
1010 #[test]
1011 fn edge_touch_at_u64_max() {
1012 let cfg = default_config();
1013 let mut meta = BlockMeta::new(u64::MAX - 1);
1014 touch(&cfg, u64::MAX, &mut meta);
1015 assert_eq!(meta.last_access, u64::MAX);
1016 assert_eq!(meta.access_count, 1);
1017 }
1018
1019 #[test]
1020 fn edge_access_count_saturates() {
1021 let cfg = default_config();
1022 let mut meta = BlockMeta::new(0);
1023 meta.access_count = u64::MAX;
1024 touch(&cfg, 1, &mut meta);
1025 assert_eq!(meta.access_count, u64::MAX);
1026 }
1027
1028 #[test]
1029 fn tick_decay_reduces_ema() {
1030 let cfg = default_config();
1031 let mut meta = BlockMeta::new(0);
1032 meta.ema_rate = 1.0;
1033 meta.access_window = 0b1111;
1034 tick_decay(&cfg, &mut meta);
1035 assert!((meta.ema_rate - 0.7).abs() < 1e-6, "ema={}", meta.ema_rate);
1036 assert_eq!(meta.access_window, 0b1111_0);
1037 }
1038
1039 #[test]
1040 fn tick_decay_converges_to_zero() {
1041 let cfg = default_config();
1042 let mut meta = BlockMeta::new(0);
1043 meta.ema_rate = 1.0;
1044 for _ in 0..200 {
1045 tick_decay(&cfg, &mut meta);
1046 }
1047 assert!(meta.ema_rate < 1e-10, "ema={}", meta.ema_rate);
1048 }
1049
1050 #[test]
1051 fn tier_config_default_weights_sum_to_one() {
1052 let cfg = default_config();
1053 let sum = cfg.w_ema + cfg.w_pop + cfg.w_rec;
1054 assert!((sum - 1.0).abs() < 1e-6, "sum={sum}");
1055 }
1056
1057 #[test]
1058 fn block_meta_new_defaults() {
1059 let meta = BlockMeta::new(42);
1060 assert_eq!(meta.ema_rate, 0.0);
1061 assert_eq!(meta.access_window, 0);
1062 assert_eq!(meta.last_access, 42);
1063 assert_eq!(meta.access_count, 0);
1064 assert_eq!(meta.current_tier, Tier::Tier1);
1065 assert_eq!(meta.tier_since, 42);
1066 }
1067
1068 #[test]
1069 fn tier_ordering() {
1070 assert!(Tier::Tier0 < Tier::Tier1);
1071 assert!(Tier::Tier1 < Tier::Tier2);
1072 assert!(Tier::Tier2 < Tier::Tier3);
1073 }
1074
1075 #[test]
1080 fn batch_scores_match_individual() {
1081 let cfg = default_config();
1082 let metas: Vec<BlockMeta> = vec![
1083 make_meta(1.0, u64::MAX, 100, Tier::Tier1, 0),
1084 make_meta(0.0, 0, 0, Tier::Tier3, 0),
1085 make_meta(0.5, 0x0000_0000_FFFF_FFFF, 50, Tier::Tier2, 0),
1086 ];
1087 let batch = compute_scores_batch(&cfg, 100, &metas);
1088 for (i, meta) in metas.iter().enumerate() {
1089 let single = compute_score(&cfg, 100, meta);
1090 assert!((batch[i] - single).abs() < 1e-6, "index {i}");
1091 }
1092 }
1093
1094 #[test]
1095 fn batch_tiers_match_individual() {
1096 let cfg = default_config();
1097 let metas: Vec<BlockMeta> = vec![
1098 make_meta(1.0, u64::MAX, 100, Tier::Tier1, 0),
1099 make_meta(0.0, 0, 0, Tier::Tier3, 0),
1100 ];
1101 let batch = choose_tiers_batch(&cfg, 100, &metas);
1102 for (i, meta) in metas.iter().enumerate() {
1103 let single = choose_tier(&cfg, 100, meta);
1104 assert_eq!(batch[i], single, "index {i}");
1105 }
1106 }
1107
1108 #[test]
1109 fn score_and_partition_distributes_correctly() {
1110 let cfg = default_config();
1111 let metas: Vec<BlockMeta> = vec![
1112 make_meta(1.0, u64::MAX, 100, Tier::Tier1, 0), make_meta(0.5, 0x0000_0000_FFFF_FFFF, 90, Tier::Tier2, 0), make_meta(0.0, 0, 0, Tier::Tier3, 0), ];
1116 let part = score_and_partition(&cfg, 100, &metas);
1117 assert!(!part.hot.is_empty(), "should have hot blocks");
1118 assert_eq!(part.scores.len(), 3);
1119 }
1120
1121 #[test]
1122 fn top_k_coldest_returns_lowest() {
1123 let cfg = default_config();
1124 let metas: Vec<BlockMeta> = vec![
1125 make_meta(1.0, u64::MAX, 100, Tier::Tier1, 0),
1126 make_meta(0.0, 0, 0, Tier::Tier3, 0),
1127 make_meta(0.5, 0x0000_0000_FFFF_FFFF, 50, Tier::Tier2, 0),
1128 ];
1129 let coldest = top_k_coldest(&cfg, 100, &metas, 2);
1130 assert_eq!(coldest.len(), 2);
1131 assert_eq!(coldest[0].0, 1);
1133 assert!(coldest[0].1 <= coldest[1].1);
1134 }
1135
1136 #[test]
1137 fn top_k_coldest_k_exceeds_len() {
1138 let cfg = default_config();
1139 let metas: Vec<BlockMeta> = vec![make_meta(1.0, u64::MAX, 100, Tier::Tier1, 0)];
1140 let coldest = top_k_coldest(&cfg, 100, &metas, 10);
1141 assert_eq!(coldest.len(), 1);
1142 }
1143
1144 #[test]
1145 fn batch_empty_input() {
1146 let cfg = default_config();
1147 let empty: Vec<BlockMeta> = vec![];
1148 assert!(compute_scores_batch(&cfg, 100, &empty).is_empty());
1149 assert!(choose_tiers_batch(&cfg, 100, &empty).is_empty());
1150 let part = score_and_partition(&cfg, 100, &empty);
1151 assert!(
1152 part.hot.is_empty()
1153 && part.warm.is_empty()
1154 && part.cold.is_empty()
1155 && part.evict.is_empty()
1156 );
1157 assert!(top_k_coldest(&cfg, 100, &empty, 5).is_empty());
1158 }
1159}