| 1 | //! Decoded-image LRU with a byte budget. |
| 2 | |
| 3 | use std::collections::HashMap; |
| 4 | use std::path::{Path, PathBuf}; |
| 5 | use std::sync::Arc; |
| 6 | |
| 7 | use crate::decode::DecodedImage; |
| 8 | |
| 9 | struct Slot { |
| 10 | img: Arc<DecodedImage>, |
| 11 | used: u64, |
| 12 | } |
| 13 | |
| 14 | pub struct ImageCache { |
| 15 | map: HashMap<PathBuf, Slot>, |
| 16 | budget: usize, |
| 17 | bytes: usize, |
| 18 | clock: u64, |
| 19 | } |
| 20 | |
| 21 | impl ImageCache { |
| 22 | pub fn new(budget_bytes: usize) -> Self { |
| 23 | Self { |
| 24 | map: HashMap::new(), |
| 25 | budget: budget_bytes, |
| 26 | bytes: 0, |
| 27 | clock: 0, |
| 28 | } |
| 29 | } |
| 30 | |
| 31 | pub fn get(&mut self, path: &Path) -> Option<Arc<DecodedImage>> { |
| 32 | self.clock += 1; |
| 33 | let slot = self.map.get_mut(path)?; |
| 34 | slot.used = self.clock; |
| 35 | Some(slot.img.clone()) |
| 36 | } |
| 37 | |
| 38 | #[cfg(test)] |
| 39 | pub fn contains(&self, path: &Path) -> bool { |
| 40 | self.map.contains_key(path) |
| 41 | } |
| 42 | |
| 43 | /// Insert, then evict least-recently-used entries over budget, sparing `keep`. |
| 44 | pub fn insert(&mut self, path: PathBuf, img: Arc<DecodedImage>, keep: &[&Path]) { |
| 45 | self.clock += 1; |
| 46 | self.bytes += img.bytes(); |
| 47 | if let Some(old) = self.map.insert( |
| 48 | path, |
| 49 | Slot { |
| 50 | img, |
| 51 | used: self.clock, |
| 52 | }, |
| 53 | ) { |
| 54 | self.bytes -= old.img.bytes(); |
| 55 | } |
| 56 | while self.bytes > self.budget { |
| 57 | let victim = self |
| 58 | .map |
| 59 | .iter() |
| 60 | .filter(|(p, _)| !keep.contains(&p.as_path())) |
| 61 | .min_by_key(|(_, s)| s.used) |
| 62 | .map(|(p, _)| p.clone()); |
| 63 | let Some(victim) = victim else { break }; |
| 64 | self.remove(&victim); |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | pub fn remove(&mut self, path: &Path) { |
| 69 | if let Some(old) = self.map.remove(path) { |
| 70 | self.bytes -= old.img.bytes(); |
| 71 | } |
| 72 | } |
| 73 | } |
| 74 | |
| 75 | #[cfg(test)] |
| 76 | mod tests { |
| 77 | use super::*; |
| 78 | |
| 79 | fn img(bytes: usize) -> Arc<DecodedImage> { |
| 80 | Arc::new(DecodedImage { |
| 81 | size: [1, 1], |
| 82 | pixels: vec![0; bytes], |
| 83 | format: "PNG", |
| 84 | source_size: [1, 1], |
| 85 | }) |
| 86 | } |
| 87 | |
| 88 | #[test] |
| 89 | fn evicts_least_recent_but_spares_kept() { |
| 90 | let mut c = ImageCache::new(250); |
| 91 | c.insert("a".into(), img(100), &[]); |
| 92 | c.insert("b".into(), img(100), &[]); |
| 93 | c.get(Path::new("a")); |
| 94 | c.insert("c".into(), img(100), &[]); |
| 95 | assert!(c.contains(Path::new("a")) && !c.contains(Path::new("b"))); |
| 96 | c.insert("d".into(), img(100), &[Path::new("a")]); |
| 97 | assert!(c.contains(Path::new("a")) && !c.contains(Path::new("c"))); |
| 98 | } |
| 99 | } |