use std::cell::RefCell;
const CAPACITY: usize = 1024;
const MASK: usize = CAPACITY - 1;
const EMPTY_KEY: u64 = u64::MAX;
#[derive(Clone, Copy)]
struct Slot {
key: u64,
value: u64,
}
pub struct TimecodeCache {
slots: Box<[Slot; CAPACITY]>,
pub total_queries: u64,
pub cache_hits: u64,
}
impl TimecodeCache {
#[must_use]
pub fn new() -> Self {
Self {
slots: Box::new(
[Slot {
key: EMPTY_KEY,
value: 0,
}; CAPACITY],
),
total_queries: 0,
cache_hits: 0,
}
}
#[inline]
fn pack_key(
hours: u8,
minutes: u8,
seconds: u8,
frames: u8,
fps: u32,
drop_frame: bool,
) -> u64 {
let fps_byte = (fps & 0xFF) as u64;
let df_bit = if drop_frame { 1u64 } else { 0u64 };
(hours as u64)
| ((minutes as u64) << 8)
| ((seconds as u64) << 16)
| ((frames as u64) << 24)
| (fps_byte << 32)
| (df_bit << 40)
}
#[inline]
fn hash_slot(key: u64) -> usize {
let h = key.wrapping_mul(0x9e37_79b9_7f4a_7c15);
(h >> (64 - 10)) as usize & MASK }
pub fn get_or_compute<F: FnOnce() -> u64>(
&mut self,
hours: u8,
minutes: u8,
seconds: u8,
frames: u8,
fps: u32,
drop_frame: bool,
compute_fn: F,
) -> u64 {
self.total_queries += 1;
let key = Self::pack_key(hours, minutes, seconds, frames, fps, drop_frame);
let base = Self::hash_slot(key);
let mut i = 0usize;
loop {
let slot_idx = (base + i * i) & MASK;
let slot = &self.slots[slot_idx];
if slot.key == key {
self.cache_hits += 1;
return slot.value;
}
if slot.key == EMPTY_KEY {
let value = compute_fn();
self.slots[slot_idx] = Slot { key, value };
return value;
}
i += 1;
if i > CAPACITY / 2 {
let value = compute_fn();
self.slots[base] = Slot { key, value };
return value;
}
}
}
pub fn clear(&mut self) {
for slot in self.slots.iter_mut() {
slot.key = EMPTY_KEY;
}
self.total_queries = 0;
self.cache_hits = 0;
}
#[must_use]
pub fn hit_rate(&self) -> f64 {
if self.total_queries == 0 {
0.0
} else {
self.cache_hits as f64 / self.total_queries as f64
}
}
#[must_use]
pub fn occupied_count(&self) -> usize {
self.slots.iter().filter(|s| s.key != EMPTY_KEY).count()
}
}
impl Default for TimecodeCache {
fn default() -> Self {
Self::new()
}
}
thread_local! {
static TC_CACHE: RefCell<TimecodeCache> = RefCell::new(TimecodeCache::new());
}
pub fn with_tc_cache<F, R>(f: F) -> R
where
F: FnOnce(&mut TimecodeCache) -> R,
{
TC_CACHE.with(|cell| f(&mut cell.borrow_mut()))
}
pub fn clear_tc_cache() {
TC_CACHE.with(|cell| cell.borrow_mut().clear());
}
#[must_use]
pub fn tc_cache_hit_rate() -> f64 {
TC_CACHE.with(|cell| cell.borrow().hit_rate())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_cache_hit() {
let mut cache = TimecodeCache::new();
let v1 = cache.get_or_compute(1, 0, 0, 0, 25, false, || 90_000);
assert_eq!(v1, 90_000);
assert_eq!(cache.cache_hits, 0);
assert_eq!(cache.total_queries, 1);
let called = std::cell::Cell::new(false);
let v2 = cache.get_or_compute(1, 0, 0, 0, 25, false, || {
called.set(true);
99
});
assert_eq!(v2, 90_000);
assert!(!called.get(), "compute_fn must not be called on cache hit");
assert_eq!(cache.cache_hits, 1);
assert_eq!(cache.total_queries, 2);
}
#[test]
fn test_different_keys_no_collision() {
let mut cache = TimecodeCache::new();
let a = cache.get_or_compute(0, 0, 0, 0, 25, false, || 0);
let b = cache.get_or_compute(0, 0, 0, 1, 25, false, || 1);
let c = cache.get_or_compute(0, 0, 1, 0, 25, false, || 25);
assert_eq!(a, 0);
assert_eq!(b, 1);
assert_eq!(c, 25);
}
#[test]
fn test_drop_frame_vs_ndf_distinct_keys() {
let mut cache = TimecodeCache::new();
let ndf = cache.get_or_compute(1, 0, 0, 0, 30, false, || 108_000);
let df = cache.get_or_compute(1, 0, 0, 0, 30, true, || 107_892);
assert_ne!(ndf, df);
assert_eq!(ndf, 108_000);
assert_eq!(df, 107_892);
}
#[test]
fn test_hit_rate_computation() {
let mut cache = TimecodeCache::new();
cache.get_or_compute(0, 0, 0, 0, 25, false, || 0);
cache.get_or_compute(0, 0, 0, 0, 25, false, || 0);
cache.get_or_compute(0, 0, 0, 0, 25, false, || 0);
assert!((cache.hit_rate() - 2.0 / 3.0).abs() < 1e-9);
}
#[test]
fn test_clear_resets_cache() {
let mut cache = TimecodeCache::new();
cache.get_or_compute(0, 0, 1, 0, 25, false, || 25);
assert_eq!(cache.occupied_count(), 1);
cache.clear();
assert_eq!(cache.occupied_count(), 0);
assert_eq!(cache.total_queries, 0);
}
#[test]
fn test_thread_local_cache() {
clear_tc_cache();
let v = with_tc_cache(|c| c.get_or_compute(2, 30, 0, 0, 25, false, || 9_000_000));
assert_eq!(v, 9_000_000);
let hit = with_tc_cache(|c| {
c.get_or_compute(2, 30, 0, 0, 25, false, || panic!("should be cached"))
});
assert_eq!(hit, 9_000_000);
}
#[test]
fn test_fps60_high_fps_key() {
let mut cache = TimecodeCache::new();
let v = cache.get_or_compute(0, 0, 1, 0, 60, false, || 60);
let v2 = cache.get_or_compute(0, 0, 1, 0, 25, false, || 25);
assert_eq!(v, 60);
assert_eq!(v2, 25);
}
#[test]
fn test_many_entries_no_panic() {
let mut cache = TimecodeCache::new();
for frame in 0..100u8 {
cache.get_or_compute(0, 0, 0, frame, 25, false, || frame as u64);
}
for frame in 0..100u8 {
let v = cache.get_or_compute(0, 0, 0, frame, 25, false, || panic!("miss on {frame}"));
assert_eq!(v, frame as u64);
}
}
}