1use std::collections::HashMap;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum EvictionPolicy {
15 Lru,
17 Lfu,
19 ArcApprox,
21}
22
23#[derive(Clone, Debug)]
25pub struct AccessEvent {
26 pub cid: String,
28 pub timestamp_tick: u64,
30}
31
32#[derive(Clone, Debug)]
34pub struct SimulationResult {
35 pub policy: EvictionPolicy,
36 pub hits: u64,
37 pub misses: u64,
38 pub evictions: u64,
39}
40
41impl SimulationResult {
42 pub fn hit_rate(&self) -> f64 {
44 let total = self.hits + self.misses;
45 if total == 0 {
46 0.0
47 } else {
48 self.hits as f64 / total as f64
49 }
50 }
51
52 pub fn miss_rate(&self) -> f64 {
54 1.0 - self.hit_rate()
55 }
56}
57
58struct CacheState {
64 capacity: usize,
65 items: Vec<String>,
67 freq: HashMap<String, u64>,
69 t1: Vec<String>,
71 t2: Vec<String>,
73}
74
75impl CacheState {
76 fn new(capacity: usize) -> Self {
77 Self {
78 capacity,
79 items: Vec::new(),
80 freq: HashMap::new(),
81 t1: Vec::new(),
82 t2: Vec::new(),
83 }
84 }
85
86 #[cfg(test)]
91 #[allow(dead_code)]
92 fn lru_contains(&self, cid: &str) -> bool {
93 self.items.iter().any(|x| x == cid)
94 }
95
96 fn lru_access(&mut self, cid: &str) -> (bool, u64) {
98 let mut evictions = 0u64;
99 if let Some(pos) = self.items.iter().position(|x| x == cid) {
100 self.items.remove(pos);
102 self.items.push(cid.to_owned());
103 (true, evictions)
104 } else {
105 if self.items.len() >= self.capacity {
107 self.items.remove(0);
108 evictions += 1;
109 }
110 self.items.push(cid.to_owned());
111 (false, evictions)
112 }
113 }
114
115 fn lfu_contains(&self, cid: &str) -> bool {
120 self.items.iter().any(|x| x == cid)
121 }
122
123 fn lfu_access(&mut self, cid: &str) -> (bool, u64) {
125 let mut evictions = 0u64;
126 if self.lfu_contains(cid) {
127 *self.freq.entry(cid.to_owned()).or_insert(0) += 1;
129 (true, evictions)
130 } else {
131 if self.items.len() >= self.capacity {
133 let evict_pos = self.lfu_evict_pos();
136 let evicted = self.items.remove(evict_pos);
137 self.freq.remove(&evicted);
138 evictions += 1;
139 }
140 self.freq.insert(cid.to_owned(), 1);
141 self.items.push(cid.to_owned());
142 (false, evictions)
143 }
144 }
145
146 fn lfu_evict_pos(&self) -> usize {
148 let mut best_pos = 0usize;
149 let mut best_freq = u64::MAX;
150 for (pos, item) in self.items.iter().enumerate() {
151 let f = *self.freq.get(item).unwrap_or(&0);
152 if f < best_freq {
153 best_freq = f;
154 best_pos = pos;
155 }
156 }
160 best_pos
161 }
162
163 #[cfg(test)]
168 #[allow(dead_code)]
169 fn arc_contains(&self, cid: &str) -> bool {
170 self.t1.iter().any(|x| x == cid) || self.t2.iter().any(|x| x == cid)
171 }
172
173 fn arc_access(&mut self, cid: &str) -> (bool, u64) {
175 let mut evictions = 0u64;
176
177 if let Some(pos) = self.t2.iter().position(|x| x == cid) {
179 self.t2.remove(pos);
181 self.t2.push(cid.to_owned());
182 return (true, evictions);
183 }
184
185 if let Some(pos) = self.t1.iter().position(|x| x == cid) {
187 self.t1.remove(pos);
189 let total = self.t1.len() + self.t2.len();
191 if total >= self.capacity {
192 evictions += self.arc_evict();
193 }
194 self.t2.push(cid.to_owned());
195 return (true, evictions);
196 }
197
198 let total = self.t1.len() + self.t2.len();
200 if total >= self.capacity {
201 evictions += self.arc_evict();
202 }
203 self.t1.push(cid.to_owned());
204 (false, evictions)
205 }
206
207 fn arc_evict(&mut self) -> u64 {
210 let half = self.capacity / 2;
211 if self.t1.len() > half && !self.t1.is_empty() {
212 self.t1.remove(0);
213 return 1;
214 }
215 if !self.t2.is_empty() {
216 self.t2.remove(0);
217 return 1;
218 }
219 if !self.t1.is_empty() {
221 self.t1.remove(0);
222 return 1;
223 }
224 0
225 }
226}
227
228pub struct CacheEvictionSimulator {
234 pub capacity: usize,
235}
236
237impl CacheEvictionSimulator {
238 pub fn new(capacity: usize) -> Self {
240 Self { capacity }
241 }
242
243 pub fn simulate(&self, policy: EvictionPolicy, trace: &[AccessEvent]) -> SimulationResult {
245 let mut state = CacheState::new(self.capacity);
246 let mut hits = 0u64;
247 let mut misses = 0u64;
248 let mut evictions = 0u64;
249
250 for event in trace {
251 let (hit, ev) = match policy {
252 EvictionPolicy::Lru => state.lru_access(&event.cid),
253 EvictionPolicy::Lfu => state.lfu_access(&event.cid),
254 EvictionPolicy::ArcApprox => state.arc_access(&event.cid),
255 };
256 if hit {
257 hits += 1;
258 } else {
259 misses += 1;
260 }
261 evictions += ev;
262 }
263
264 SimulationResult {
265 policy,
266 hits,
267 misses,
268 evictions,
269 }
270 }
271
272 pub fn compare_policies(&self, trace: &[AccessEvent]) -> Vec<SimulationResult> {
274 let mut results = vec![
275 self.simulate(EvictionPolicy::Lru, trace),
276 self.simulate(EvictionPolicy::Lfu, trace),
277 self.simulate(EvictionPolicy::ArcApprox, trace),
278 ];
279 results.sort_by(|a, b| {
280 b.hit_rate()
281 .partial_cmp(&a.hit_rate())
282 .unwrap_or(std::cmp::Ordering::Equal)
283 });
284 results
285 }
286}
287
288#[cfg(test)]
293mod tests {
294 use super::*;
295
296 fn make_trace(pairs: &[(&str, u64)]) -> Vec<AccessEvent> {
298 pairs
299 .iter()
300 .map(|(cid, tick)| AccessEvent {
301 cid: cid.to_string(),
302 timestamp_tick: *tick,
303 })
304 .collect()
305 }
306
307 #[test]
312 fn lru_hit_basic() {
313 let sim = CacheEvictionSimulator::new(3);
314 let trace = make_trace(&[("a", 1), ("b", 2), ("c", 3), ("a", 4)]);
316 let res = sim.simulate(EvictionPolicy::Lru, &trace);
317 assert_eq!(res.hits, 1);
318 assert_eq!(res.misses, 3);
319 }
320
321 #[test]
322 fn lru_miss_and_eviction() {
323 let sim = CacheEvictionSimulator::new(2);
324 let trace = make_trace(&[("a", 1), ("b", 2), ("c", 3), ("a", 4)]);
326 let res = sim.simulate(EvictionPolicy::Lru, &trace);
327 assert_eq!(res.hits, 0);
328 assert_eq!(res.evictions, 2); assert_eq!(res.misses, 4);
330 }
331
332 #[test]
333 fn lru_order_after_access() {
334 let sim = CacheEvictionSimulator::new(2);
336 let trace = make_trace(&[("a", 1), ("b", 2), ("a", 3), ("c", 4), ("b", 5)]);
337 let res = sim.simulate(EvictionPolicy::Lru, &trace);
338 assert_eq!(res.hits, 1); assert_eq!(res.evictions, 2); }
342
343 #[test]
344 fn lru_hit_rate_and_miss_rate() {
345 let sim = CacheEvictionSimulator::new(3);
346 let trace = make_trace(&[("a", 1), ("b", 2), ("c", 3), ("a", 4), ("b", 5)]);
347 let res = sim.simulate(EvictionPolicy::Lru, &trace);
348 assert_eq!(res.hits, 2);
349 assert_eq!(res.misses, 3);
350 let hr = res.hit_rate();
351 let mr = res.miss_rate();
352 assert!((hr - 0.4).abs() < 1e-9);
353 assert!((mr - 0.6).abs() < 1e-9);
354 assert!((hr + mr - 1.0).abs() < 1e-9);
355 }
356
357 #[test]
358 fn lru_evictions_count() {
359 let sim = CacheEvictionSimulator::new(2);
360 let trace = make_trace(&[("a", 1), ("b", 2), ("c", 3), ("d", 4), ("e", 5)]);
362 let res = sim.simulate(EvictionPolicy::Lru, &trace);
363 assert_eq!(res.evictions, 3); }
365
366 #[test]
367 fn lru_capacity_one() {
368 let sim = CacheEvictionSimulator::new(1);
369 let trace = make_trace(&[("a", 1), ("a", 2), ("b", 3), ("b", 4)]);
370 let res = sim.simulate(EvictionPolicy::Lru, &trace);
371 assert_eq!(res.hits, 2);
373 assert_eq!(res.misses, 2);
374 assert_eq!(res.evictions, 1);
375 }
376
377 #[test]
382 fn lfu_selects_min_freq() {
383 let sim = CacheEvictionSimulator::new(2);
384 let trace = make_trace(&[("a", 1), ("b", 2), ("a", 3), ("a", 4), ("c", 5), ("b", 6)]);
388 let res = sim.simulate(EvictionPolicy::Lfu, &trace);
389 assert_eq!(res.hits, 2); assert_eq!(res.misses, 4); assert_eq!(res.evictions, 2); }
393
394 #[test]
395 fn lfu_tie_break_lru_order() {
396 let sim = CacheEvictionSimulator::new(2);
399 let trace = make_trace(&[("a", 1), ("b", 2), ("c", 3), ("b", 4)]);
400 let res = sim.simulate(EvictionPolicy::Lfu, &trace);
401 assert_eq!(res.hits, 1); assert_eq!(res.evictions, 1);
404 }
405
406 #[test]
407 fn lfu_hit_rate() {
408 let sim = CacheEvictionSimulator::new(3);
409 let trace = make_trace(&[("x", 1), ("y", 2), ("z", 3), ("x", 4), ("x", 5), ("y", 6)]);
410 let res = sim.simulate(EvictionPolicy::Lfu, &trace);
411 assert_eq!(res.hits, 3);
412 assert_eq!(res.misses, 3);
413 assert!((res.hit_rate() - 0.5).abs() < 1e-9);
414 }
415
416 #[test]
417 fn lfu_capacity_one() {
418 let sim = CacheEvictionSimulator::new(1);
419 let trace = make_trace(&[("a", 1), ("a", 2), ("b", 3), ("b", 4)]);
420 let res = sim.simulate(EvictionPolicy::Lfu, &trace);
421 assert_eq!(res.hits, 2); assert_eq!(res.evictions, 1);
426 }
427
428 #[test]
433 fn arc_t1_to_t2_promotion() {
434 let sim = CacheEvictionSimulator::new(4);
436 let trace = make_trace(&[("a", 1), ("b", 2), ("a", 3)]);
437 let res = sim.simulate(EvictionPolicy::ArcApprox, &trace);
438 assert_eq!(res.hits, 1);
440 assert_eq!(res.misses, 2);
441 assert_eq!(res.evictions, 0);
442 }
443
444 #[test]
445 fn arc_evicts_t1_when_over_half() {
446 let sim = CacheEvictionSimulator::new(3);
452 let trace = make_trace(&[("x", 1), ("z", 2), ("z", 3), ("y", 4), ("w", 5)]);
453 let res = sim.simulate(EvictionPolicy::ArcApprox, &trace);
454 assert_eq!(res.hits, 1); assert_eq!(res.misses, 4); assert_eq!(res.evictions, 1); }
458
459 #[test]
460 fn arc_t2_hit_refreshes() {
461 let sim = CacheEvictionSimulator::new(4);
462 let trace = make_trace(&[("a", 1), ("a", 2), ("a", 3)]);
464 let res = sim.simulate(EvictionPolicy::ArcApprox, &trace);
465 assert_eq!(res.hits, 2); assert_eq!(res.misses, 1);
467 assert_eq!(res.evictions, 0);
468 }
469
470 #[test]
471 fn arc_capacity_one() {
472 let sim = CacheEvictionSimulator::new(1);
473 let trace = make_trace(&[("a", 1), ("a", 2), ("b", 3), ("b", 4)]);
474 let res = sim.simulate(EvictionPolicy::ArcApprox, &trace);
475 assert_eq!(res.hits, 2);
477 assert_eq!(res.misses, 2);
478 assert_eq!(res.evictions, 1);
479 }
480
481 #[test]
486 fn compare_policies_returns_three_results() {
487 let sim = CacheEvictionSimulator::new(3);
488 let trace = make_trace(&[("a", 1), ("b", 2), ("a", 3)]);
489 let results = sim.compare_policies(&trace);
490 assert_eq!(results.len(), 3);
491 }
492
493 #[test]
494 fn compare_policies_sorted_by_hit_rate_desc() {
495 let sim = CacheEvictionSimulator::new(3);
496 let trace = make_trace(&[("a", 1), ("b", 2), ("c", 3), ("a", 4), ("b", 5), ("c", 6)]);
497 let results = sim.compare_policies(&trace);
498 for pair in results.windows(2) {
499 assert!(pair[0].hit_rate() >= pair[1].hit_rate());
500 }
501 }
502
503 #[test]
504 fn compare_policies_all_three_policies_present() {
505 let sim = CacheEvictionSimulator::new(2);
506 let trace = make_trace(&[("a", 1), ("b", 2), ("a", 3)]);
507 let results = sim.compare_policies(&trace);
508 let policies: Vec<EvictionPolicy> = results.iter().map(|r| r.policy).collect();
509 assert!(policies.contains(&EvictionPolicy::Lru));
510 assert!(policies.contains(&EvictionPolicy::Lfu));
511 assert!(policies.contains(&EvictionPolicy::ArcApprox));
512 }
513
514 #[test]
519 fn empty_trace_all_policies() {
520 let sim = CacheEvictionSimulator::new(4);
521 for policy in [
522 EvictionPolicy::Lru,
523 EvictionPolicy::Lfu,
524 EvictionPolicy::ArcApprox,
525 ] {
526 let res = sim.simulate(policy, &[]);
527 assert_eq!(res.hits, 0);
528 assert_eq!(res.misses, 0);
529 assert_eq!(res.evictions, 0);
530 assert!((res.hit_rate() - 0.0).abs() < 1e-9);
531 assert!((res.miss_rate() - 1.0).abs() < 1e-9);
532 }
533 }
534
535 #[test]
536 fn hit_rate_miss_rate_sum_to_one() {
537 let sim = CacheEvictionSimulator::new(2);
538 let trace = make_trace(&[("a", 1), ("b", 2), ("c", 3), ("a", 4)]);
539 for policy in [
540 EvictionPolicy::Lru,
541 EvictionPolicy::Lfu,
542 EvictionPolicy::ArcApprox,
543 ] {
544 let res = sim.simulate(policy, &trace);
545 assert!((res.hit_rate() + res.miss_rate() - 1.0).abs() < 1e-9);
546 }
547 }
548
549 #[test]
550 fn eviction_policy_derive_traits() {
551 let p = EvictionPolicy::Lru;
552 let q = p; let r = p; assert_eq!(p, q);
555 assert_eq!(p, r);
556 let _ = format!("{p:?}"); }
558
559 #[test]
560 fn large_trace_lru_no_panic() {
561 let sim = CacheEvictionSimulator::new(10);
562 let trace: Vec<AccessEvent> = (0u64..200)
563 .map(|i| AccessEvent {
564 cid: format!("item-{}", i % 15),
565 timestamp_tick: i,
566 })
567 .collect();
568 let res = sim.simulate(EvictionPolicy::Lru, &trace);
569 assert!(res.hits + res.misses == 200);
570 }
571
572 #[test]
573 fn lru_no_eviction_under_capacity() {
574 let sim = CacheEvictionSimulator::new(10);
575 let trace = make_trace(&[("a", 1), ("b", 2), ("c", 3), ("a", 4), ("b", 5), ("c", 6)]);
576 let res = sim.simulate(EvictionPolicy::Lru, &trace);
577 assert_eq!(res.evictions, 0);
578 assert_eq!(res.hits, 3);
579 }
580}