use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use crate::primitives::image::ImageSource;
pub const DEFAULT_CAPACITY: usize = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct ImageCacheKey {
source_hash: u64,
width: u32,
height: u32,
scale_millis: u32,
}
impl ImageCacheKey {
fn new(source: &ImageSource, width: u32, height: u32, scale: f32) -> Self {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
source.hash(&mut hasher);
Self {
source_hash: hasher.finish(),
width,
height,
scale_millis: (scale.max(0.0) * 1000.0).round() as u32,
}
}
}
pub struct ImageCache<T> {
capacity: usize,
order: Vec<ImageCacheKey>,
entries: HashMap<ImageCacheKey, T>,
}
impl<T> ImageCache<T> {
pub fn with_capacity(capacity: usize) -> Self {
Self {
capacity: capacity.max(1),
order: Vec::new(),
entries: HashMap::new(),
}
}
pub fn get_or_decode(
&mut self,
source: &ImageSource,
width: u32,
height: u32,
scale: f32,
decode: impl FnOnce() -> Option<T>,
) -> Option<&T> {
let key = ImageCacheKey::new(source, width, height, scale);
if self.entries.contains_key(&key) {
self.touch(key);
return self.entries.get(&key);
}
let value = decode()?;
self.insert(key, value);
self.entries.get(&key)
}
fn touch(&mut self, key: ImageCacheKey) {
self.order.retain(|k| *k != key);
self.order.push(key);
}
fn insert(&mut self, key: ImageCacheKey, value: T) {
if self.entries.len() >= self.capacity {
if !self.order.is_empty() {
let oldest = self.order.remove(0);
self.entries.remove(&oldest);
}
}
self.entries.insert(key, value);
self.touch(key);
}
}
impl<T> Default for ImageCache<T> {
fn default() -> Self {
Self::with_capacity(DEFAULT_CAPACITY)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
fn src(bytes: &[u8]) -> ImageSource {
ImageSource::Bytes(bytes.to_vec())
}
#[test]
fn repeated_lookup_for_same_key_decodes_once() {
let mut cache: ImageCache<u32> = ImageCache::with_capacity(4);
let calls = Cell::new(0);
let s = src(b"hello");
for _ in 0..3 {
let v = cache.get_or_decode(&s, 10, 10, 1.0, || {
calls.set(calls.get() + 1);
Some(42)
});
assert_eq!(v, Some(&42));
}
assert_eq!(calls.get(), 1, "decode should only run on the first miss");
}
#[test]
fn different_size_is_a_distinct_key() {
let mut cache: ImageCache<u32> = ImageCache::with_capacity(4);
let calls = Cell::new(0);
let s = src(b"hello");
cache.get_or_decode(&s, 10, 10, 1.0, || {
calls.set(calls.get() + 1);
Some(1)
});
cache.get_or_decode(&s, 20, 20, 1.0, || {
calls.set(calls.get() + 1);
Some(2)
});
assert_eq!(
calls.get(),
2,
"different target size must not share a cache entry"
);
}
#[test]
fn different_scale_is_a_distinct_key() {
let mut cache: ImageCache<u32> = ImageCache::with_capacity(4);
let calls = Cell::new(0);
let s = src(b"hello");
cache.get_or_decode(&s, 10, 10, 1.0, || {
calls.set(calls.get() + 1);
Some(1)
});
cache.get_or_decode(&s, 10, 10, 2.0, || {
calls.set(calls.get() + 1);
Some(2)
});
assert_eq!(
calls.get(),
2,
"different scale must not share a cache entry"
);
}
#[test]
fn different_source_is_a_distinct_key() {
let mut cache: ImageCache<u32> = ImageCache::with_capacity(4);
let calls = Cell::new(0);
cache.get_or_decode(&src(b"a"), 10, 10, 1.0, || {
calls.set(calls.get() + 1);
Some(1)
});
cache.get_or_decode(&src(b"b"), 10, 10, 1.0, || {
calls.set(calls.get() + 1);
Some(2)
});
assert_eq!(
calls.get(),
2,
"different source bytes must not share a cache entry"
);
}
#[test]
fn failed_decode_is_not_cached_and_is_retried() {
let mut cache: ImageCache<u32> = ImageCache::with_capacity(4);
let calls = Cell::new(0);
let s = src(b"hello");
let v = cache.get_or_decode(&s, 10, 10, 1.0, || {
calls.set(calls.get() + 1);
None
});
assert_eq!(v, None);
let v = cache.get_or_decode(&s, 10, 10, 1.0, || {
calls.set(calls.get() + 1);
Some(7)
});
assert_eq!(v, Some(&7));
assert_eq!(calls.get(), 2, "a failed decode must not be cached");
}
#[test]
fn eviction_drops_least_recently_used_entry_once_over_capacity() {
let mut cache: ImageCache<u32> = ImageCache::with_capacity(2);
let calls = Cell::new(0);
let mut decode_for = |n: u8| {
let s = src(&[n]);
cache.get_or_decode(&s, 1, 1, 1.0, || {
calls.set(calls.get() + 1);
Some(n as u32)
});
};
decode_for(1);
decode_for(2);
let before = calls.get();
decode_for(1);
decode_for(2);
assert_eq!(calls.get(), before, "both entries should still be cached");
decode_for(3);
let calls_before_recheck = calls.get();
decode_for(2); assert_eq!(
calls.get(),
calls_before_recheck,
"2 should still be cached"
);
decode_for(1); assert_eq!(
calls.get(),
calls_before_recheck + 1,
"1 should have been evicted"
);
}
}