use crate::util::hash128;
use kurbo::Affine;
use pdf_syntax::object::{Array, Dict, MaybeRef, Name, Null, ObjRef, Object, Stream};
use std::any::Any;
use std::collections::hash_map::Entry;
use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Mutex};
type CacheMap = HashMap<u128, Option<Box<dyn Any + Send + Sync>>>;
const IMAGE_CACHE_CAPACITY: usize = 32;
const IMAGE_CACHE_MAX_BYTES: usize = 256 * 1024 * 1024;
struct ImageCache {
map: HashMap<u128, (Box<dyn Any + Send + Sync>, usize)>,
order: VecDeque<u128>,
total_bytes: usize,
}
#[derive(Clone)]
pub struct Cache {
generic: Arc<Mutex<CacheMap>>,
images: Arc<Mutex<ImageCache>>,
}
impl Default for Cache {
fn default() -> Self {
Self::new()
}
}
impl Cache {
pub fn new() -> Self {
Self {
generic: Arc::new(Mutex::new(HashMap::new())),
images: Arc::new(Mutex::new(ImageCache {
map: HashMap::new(),
order: VecDeque::new(),
total_bytes: 0,
})),
}
}
pub(crate) fn get_or_insert_with<T: Clone + Send + Sync + 'static>(
&self,
id: u128,
f: impl FnOnce() -> Option<T>,
) -> Option<T> {
let mut locked = self.generic.lock().unwrap();
match locked.entry(id) {
Entry::Occupied(o) => o
.get()
.as_ref()
.and_then(|val| val.downcast_ref::<T>().cloned()),
Entry::Vacant(_) => {
drop(locked);
let val = f();
self.generic.lock().unwrap().insert(
id,
val.clone()
.map(|val| Box::new(val) as Box<dyn Any + Send + Sync>),
);
val
}
}
}
pub(crate) fn get_or_insert_image<T: Clone + Send + Sync + 'static>(
&self,
id: u128,
f: impl FnOnce() -> Option<T>,
weight: impl FnOnce(&T) -> usize,
) -> Option<T> {
let mut locked = self.images.lock().unwrap();
if locked.map.contains_key(&id) {
if let Some(pos) = locked.order.iter().position(|&x| x == id) {
locked.order.remove(pos);
}
locked.order.push_back(id);
let (val, _) = locked.map.get(&id).unwrap();
return val.downcast_ref::<T>().cloned();
}
drop(locked);
let val = f();
if let Some(ref v) = val {
let w = weight(v);
let mut locked = self.images.lock().unwrap();
if let Some((_, old_w)) = locked
.map
.insert(id, (Box::new(v.clone()) as Box<dyn Any + Send + Sync>, w))
{
locked.total_bytes = locked.total_bytes.saturating_sub(old_w);
if let Some(pos) = locked.order.iter().position(|&x| x == id) {
locked.order.remove(pos);
}
}
locked.total_bytes += w;
locked.order.push_back(id);
while !locked.order.is_empty()
&& (locked.order.len() > IMAGE_CACHE_CAPACITY
|| locked.total_bytes > IMAGE_CACHE_MAX_BYTES)
{
if let Some(oldest_id) = locked.order.pop_front()
&& let Some((_, evicted_w)) = locked.map.remove(&oldest_id)
{
locked.total_bytes = locked.total_bytes.saturating_sub(evicted_w);
}
}
}
val
}
#[cfg(test)]
pub(crate) fn image_cache_stats(&self) -> (usize, usize) {
let locked = self.images.lock().unwrap();
(locked.map.len(), locked.total_bytes)
}
}
pub trait CacheKey {
fn cache_key(&self) -> u128;
}
impl<T: CacheKey, U: CacheKey> CacheKey for (T, U) {
fn cache_key(&self) -> u128 {
hash128(&(self.0.cache_key(), self.1.cache_key()))
}
}
impl CacheKey for Dict<'_> {
fn cache_key(&self) -> u128 {
hash128(self.data())
}
}
impl CacheKey for Stream<'_> {
fn cache_key(&self) -> u128 {
self.dict().cache_key()
}
}
impl CacheKey for Null {
fn cache_key(&self) -> u128 {
hash128(self)
}
}
impl CacheKey for bool {
fn cache_key(&self) -> u128 {
hash128(self)
}
}
impl CacheKey for pdf_syntax::object::Number {
fn cache_key(&self) -> u128 {
hash128(&self.as_f64().to_bits())
}
}
impl CacheKey for pdf_syntax::object::String {
fn cache_key(&self) -> u128 {
hash128(self.as_ref())
}
}
impl CacheKey for Name {
fn cache_key(&self) -> u128 {
hash128(self)
}
}
impl CacheKey for Array<'_> {
fn cache_key(&self) -> u128 {
hash128(self.data())
}
}
impl CacheKey for Object<'_> {
fn cache_key(&self) -> u128 {
match self {
Object::Null(n) => n.cache_key(),
Object::Boolean(b) => b.cache_key(),
Object::Number(n) => n.cache_key(),
Object::String(s) => s.cache_key(),
Object::Name(n) => n.cache_key(),
Object::Dict(d) => d.cache_key(),
Object::Array(a) => a.cache_key(),
Object::Stream(s) => s.cache_key(),
}
}
}
impl CacheKey for ObjRef {
fn cache_key(&self) -> u128 {
hash128(self)
}
}
impl<T: CacheKey> CacheKey for MaybeRef<T> {
fn cache_key(&self) -> u128 {
match self {
Self::Ref(r) => r.cache_key(),
Self::NotRef(o) => o.cache_key(),
}
}
}
impl CacheKey for Affine {
fn cache_key(&self) -> u128 {
let c = self.as_coeffs();
hash128(&[
c[0].to_bits(),
c[1].to_bits(),
c[2].to_bits(),
c[3].to_bits(),
c[4].to_bits(),
c[5].to_bits(),
])
}
}
impl CacheKey for u128 {
fn cache_key(&self) -> u128 {
hash128(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generic_cache() {
let cache = Cache::new();
let val = cache.get_or_insert_with(12345, || Some("hello".to_string()));
assert_eq!(val, Some("hello".to_string()));
let cached = cache.get_or_insert_with(12345, || Some("world".to_string()));
assert_eq!(cached, Some("hello".to_string()));
}
fn tiny(_: &i32) -> usize {
4
}
#[test]
fn test_image_cache_lru_eviction() {
let cache = Cache::new();
for i in 0..32 {
let val = cache.get_or_insert_image(i as u128, || Some(i), tiny);
assert_eq!(val, Some(i));
}
for i in 0..32 {
let cached = cache.get_or_insert_image::<i32>(i as u128, || None, tiny);
assert_eq!(cached, Some(i));
}
let first_accessed = cache.get_or_insert_image::<i32>(0, || None, tiny);
assert_eq!(first_accessed, Some(0));
let val32 = cache.get_or_insert_image(32, || Some(32), tiny);
assert_eq!(val32, Some(32));
let evicted1 = cache.get_or_insert_image::<i32>(1, || None, tiny);
assert_eq!(evicted1, None);
let kept0 = cache.get_or_insert_image::<i32>(0, || None, tiny);
assert_eq!(kept0, Some(0));
}
#[test]
fn test_image_cache_evicts_by_byte_budget() {
let cache = Cache::new();
let big = 100 * 1024 * 1024;
cache.get_or_insert_image(1u128, || Some(1i32), |_| big);
cache.get_or_insert_image(2u128, || Some(2i32), |_| big);
cache.get_or_insert_image(3u128, || Some(3i32), |_| big);
let (count, bytes) = cache.image_cache_stats();
assert_eq!(count, 2, "byte budget should drop the cache to two entries");
assert!(
bytes <= IMAGE_CACHE_MAX_BYTES,
"total bytes must stay within budget"
);
assert_eq!(
cache.get_or_insert_image::<i32>(1, || None, |_| 0),
None,
"oldest entry evicted by byte budget"
);
assert_eq!(cache.get_or_insert_image::<i32>(2, || None, |_| 0), Some(2));
assert_eq!(cache.get_or_insert_image::<i32>(3, || None, |_| 0), Some(3));
}
#[test]
fn test_image_cache_byte_budget_respects_lru_touch() {
let cache = Cache::new();
let big = 100 * 1024 * 1024;
cache.get_or_insert_image(1u128, || Some(1i32), |_| big);
cache.get_or_insert_image(2u128, || Some(2i32), |_| big);
assert_eq!(cache.get_or_insert_image::<i32>(1, || None, |_| 0), Some(1));
cache.get_or_insert_image(3u128, || Some(3i32), |_| big);
assert_eq!(
cache.get_or_insert_image::<i32>(2, || None, |_| 0),
None,
"least-recently-used key 2 evicted, not the touched key 1"
);
assert_eq!(cache.get_or_insert_image::<i32>(1, || None, |_| 0), Some(1));
assert_eq!(cache.get_or_insert_image::<i32>(3, || None, |_| 0), Some(3));
}
#[test]
fn test_image_cache_oversized_item_not_retained() {
let cache = Cache::new();
let huge = IMAGE_CACHE_MAX_BYTES + 1;
assert_eq!(
cache.get_or_insert_image(1u128, || Some(1i32), |_| huge),
Some(1)
);
let (count, bytes) = cache.image_cache_stats();
assert_eq!(count, 0, "an item larger than the budget is not retained");
assert_eq!(bytes, 0);
assert_eq!(cache.get_or_insert_image::<i32>(1, || None, |_| 0), None);
}
#[test]
fn test_image_cache_stats_stay_within_both_bounds() {
let cache = Cache::new();
for i in 0..100u128 {
cache.get_or_insert_image(i, || Some(i as i32), |_| 1024);
}
let (count, bytes) = cache.image_cache_stats();
assert_eq!(count, IMAGE_CACHE_CAPACITY, "count cap holds at 32 entries");
assert_eq!(
bytes,
IMAGE_CACHE_CAPACITY * 1024,
"32 retained * 1 KiB each"
);
assert!(bytes <= IMAGE_CACHE_MAX_BYTES);
}
}