metrale_storage/
eviction.rs1use std::collections::HashSet;
11
12#[derive(Debug)]
13pub struct EvictionPolicy {
14 last_access: Vec<u64>,
16 last_score: Vec<f32>,
19 epoch: u64,
21}
22
23impl EvictionPolicy {
24 pub fn new(num_slots: u32) -> Self {
25 Self {
26 last_access: vec![0; num_slots as usize],
27 last_score: vec![f32::NEG_INFINITY; num_slots as usize],
28 epoch: 1,
29 }
30 }
31
32 pub fn capacity(&self) -> u32 {
33 self.last_access.len() as u32
34 }
35
36 pub fn touch(&mut self, slot: u32) {
38 self.last_access[slot as usize] = self.epoch;
39 self.epoch = self.epoch.wrapping_add(1);
40 }
41
42 pub fn record_score(&mut self, slot: u32, score: f32) {
43 self.last_score[slot as usize] = score;
44 }
45
46 pub fn reset(&mut self) {
48 for v in self.last_access.iter_mut() {
49 *v = 0;
50 }
51 for v in self.last_score.iter_mut() {
52 *v = f32::NEG_INFINITY;
53 }
54 self.epoch = 1;
55 }
56
57 pub fn rank(&self, pinned: &[u32]) -> Vec<u32> {
60 let pinned_set: HashSet<u32> = pinned.iter().copied().collect();
61 let mut candidates: Vec<u32> = (0..self.capacity())
62 .filter(|s| !pinned_set.contains(s))
63 .collect();
64 candidates.sort_by(|a, b| {
65 let ai = *a as usize;
66 let bi = *b as usize;
67 self.last_score[ai]
68 .partial_cmp(&self.last_score[bi])
69 .unwrap_or(std::cmp::Ordering::Equal)
70 .then(self.last_access[ai].cmp(&self.last_access[bi]))
71 });
72 candidates
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 #[test]
81 fn lowest_score_evicts_first() {
82 let mut p = EvictionPolicy::new(4);
83 p.record_score(0, 10.0);
84 p.record_score(1, 1.0);
85 p.record_score(2, 5.0);
86 p.record_score(3, 100.0);
87 let order = p.rank(&[]);
88 assert_eq!(order[0], 1, "lowest score should evict first: {order:?}");
89 assert_eq!(order.last(), Some(&3));
90 }
91
92 #[test]
93 fn ties_break_to_oldest_access() {
94 let mut p = EvictionPolicy::new(3);
95 p.record_score(0, 5.0);
96 p.record_score(1, 5.0);
97 p.record_score(2, 5.0);
98 p.touch(2);
99 p.touch(0);
100 let order = p.rank(&[]);
101 assert_eq!(order[0], 1, "oldest should win the tie: {order:?}");
102 assert_eq!(order[1], 2);
103 assert_eq!(order[2], 0);
104 }
105
106 #[test]
107 fn pinned_slots_excluded() {
108 let mut p = EvictionPolicy::new(4);
109 p.record_score(0, 1.0);
110 p.record_score(1, 2.0);
111 p.record_score(2, 3.0);
112 p.record_score(3, 4.0);
113 let order = p.rank(&[0, 1]);
114 assert_eq!(order, vec![2, 3]);
115 }
116
117 #[test]
118 fn reset_clears_state() {
119 let mut p = EvictionPolicy::new(2);
120 p.record_score(0, 1.0);
121 p.record_score(1, 2.0);
122 p.touch(0);
123 p.touch(1);
124 p.reset();
125 assert_eq!(p.last_score, vec![f32::NEG_INFINITY; 2]);
126 assert_eq!(p.last_access, vec![0; 2]);
127 assert_eq!(p.epoch, 1);
128 }
129}