use alloc::collections::BTreeMap;
use alloc::collections::btree_map::Entry;
use alloc::sync::Arc;
use spin::RwLock;
#[derive(Debug)]
pub(crate) struct TwiddleCache<K, V: ?Sized> {
entries: RwLock<BTreeMap<K, Arc<V>>>,
}
impl<K, V: ?Sized> Default for TwiddleCache<K, V> {
fn default() -> Self {
Self {
entries: RwLock::new(BTreeMap::new()),
}
}
}
impl<K: Ord, V: ?Sized> TwiddleCache<K, V> {
pub(crate) fn get_or_compute(&self, key: K, compute: impl FnOnce() -> Arc<V>) -> Arc<V> {
if let Some(value) = self.entries.read().get(&key) {
return value.clone();
}
let value = compute();
let mut entries = self.entries.write();
match entries.entry(key) {
Entry::Occupied(entry) => entry.get().clone(),
Entry::Vacant(entry) => entry.insert(value).clone(),
}
}
}
#[cfg(test)]
mod tests {
use alloc::sync::Weak;
use core::sync::atomic::{AtomicBool, Ordering};
use super::*;
#[test]
fn hit_reuses_table_without_computing() {
let cache = TwiddleCache::default();
let expected: Arc<[u64]> = alloc::vec![1, 2, 3].into();
let first = cache.get_or_compute(4, || expected.clone());
let second = cache.get_or_compute(4, || panic!("a cached table must not be recomputed"));
assert!(Arc::ptr_eq(&first, &expected));
assert!(Arc::ptr_eq(&second, &expected));
}
#[test]
fn miss_computes_without_holding_the_lock() {
let cache = TwiddleCache::<usize, u64>::default();
let value = cache.get_or_compute(4, || {
assert!(
cache.entries.try_write().is_some(),
"table construction must not hold a cache lock"
);
Arc::new(23)
});
assert_eq!(*value, 23);
}
#[test]
fn miss_reuses_entry_published_during_computation() {
let cache = TwiddleCache::default();
let inserted = Arc::new(17_u64);
let result = cache.get_or_compute(4, || {
assert!(
cache.entries.try_write().is_some(),
"table construction must not hold a cache lock"
);
let nested = cache.get_or_compute(4, || inserted.clone());
assert!(Arc::ptr_eq(&nested, &inserted));
Arc::new(23_u64)
});
assert!(Arc::ptr_eq(&result, &inserted));
assert!(Arc::ptr_eq(
cache.entries.read().get(&4).unwrap(),
&inserted
));
}
#[test]
fn miss_drops_unused_table_after_unlocking() {
type Cache = TwiddleCache<usize, DropProbe>;
#[derive(Default)]
struct DropProbe {
cache: Weak<Cache>,
dropped: Arc<AtomicBool>,
}
impl Drop for DropProbe {
fn drop(&mut self) {
if let Some(cache) = self.cache.upgrade() {
assert!(
cache.entries.try_write().is_some(),
"discarded table must be dropped after unlocking the cache"
);
}
self.dropped.store(true, Ordering::Relaxed);
}
}
let cache = Arc::new(Cache::default());
let inserted = Arc::new(DropProbe::default());
let dropped = Arc::new(AtomicBool::new(false));
let result = cache.get_or_compute(4, || {
assert!(
cache.entries.try_write().is_some(),
"table construction must not hold a cache lock"
);
cache.get_or_compute(4, || inserted.clone());
Arc::new(DropProbe {
cache: Arc::downgrade(&cache),
dropped: dropped.clone(),
})
});
assert!(Arc::ptr_eq(&result, &inserted));
assert!(dropped.load(Ordering::Relaxed));
}
}