use lru::LruCache;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::num::NonZeroUsize;
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct CacheKey {
text: String,
language: String,
backend: String,
}
impl CacheKey {
fn new(
text: impl Into<String>,
language: impl Into<String>,
backend: impl Into<String>,
) -> Self {
Self {
text: text.into(),
language: language.into(),
backend: backend.into(),
}
}
fn fast_hash(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.hash(&mut hasher);
hasher.finish()
}
}
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub entries: usize,
pub capacity: usize,
pub hit_rate: f64,
pub memory_usage: usize,
}
impl CacheStats {
fn calculate_hit_rate(&mut self) {
let total = self.hits + self.misses;
self.hit_rate = if total > 0 {
self.hits as f64 / total as f64
} else {
0.0
};
}
}
pub struct PhonemeCache {
cache: Arc<Mutex<LruCache<CacheKey, Vec<String>>>>,
stats: Arc<Mutex<CacheStats>>,
capacity: usize,
}
impl PhonemeCache {
pub fn new(capacity: usize) -> Self {
let capacity = capacity.max(10); let cache_capacity = NonZeroUsize::new(capacity).expect("Capacity must be non-zero");
Self {
cache: Arc::new(Mutex::new(LruCache::new(cache_capacity))),
stats: Arc::new(Mutex::new(CacheStats {
capacity,
..Default::default()
})),
capacity,
}
}
pub fn get_or_compute<F>(
&self,
text: &str,
language: &str,
backend: &str,
compute_fn: F,
) -> Vec<String>
where
F: FnOnce() -> Vec<String>,
{
let key = CacheKey::new(text, language, backend);
{
let mut cache = self
.cache
.lock()
.expect("Phoneme cache mutex poisoned - unrecoverable error");
if let Some(phonemes) = cache.get(&key) {
let mut stats = self
.stats
.lock()
.expect("Phoneme cache stats mutex poisoned - unrecoverable error");
stats.hits += 1;
stats.calculate_hit_rate();
return phonemes.clone();
}
}
let phonemes = compute_fn();
{
let mut cache = self
.cache
.lock()
.expect("Phoneme cache mutex poisoned - unrecoverable error");
cache.put(key, phonemes.clone());
let mut stats = self
.stats
.lock()
.expect("Phoneme cache stats mutex poisoned - unrecoverable error");
stats.misses += 1;
stats.entries = cache.len();
stats.calculate_hit_rate();
stats.memory_usage = cache.len() * 256; }
phonemes
}
pub fn clear(&self) {
let mut cache = self
.cache
.lock()
.expect("Phoneme cache mutex poisoned - unrecoverable error");
cache.clear();
let mut stats = self
.stats
.lock()
.expect("Phoneme cache stats mutex poisoned - unrecoverable error");
stats.entries = 0;
stats.memory_usage = 0;
}
pub fn stats(&self) -> CacheStats {
let cache = self
.cache
.lock()
.expect("Phoneme cache mutex poisoned - unrecoverable error");
let mut stats = self
.stats
.lock()
.expect("Phoneme cache stats mutex poisoned - unrecoverable error");
stats.entries = cache.len();
stats.clone()
}
pub fn resize(&mut self, new_capacity: usize) {
let new_capacity = new_capacity.max(10);
let cache_capacity = NonZeroUsize::new(new_capacity).expect("Capacity must be non-zero");
let mut cache = self
.cache
.lock()
.expect("Phoneme cache mutex poisoned - unrecoverable error");
cache.resize(cache_capacity);
let mut stats = self
.stats
.lock()
.expect("Phoneme cache stats mutex poisoned - unrecoverable error");
stats.capacity = new_capacity;
self.capacity = new_capacity;
}
pub fn contains(&self, text: &str, language: &str, backend: &str) -> bool {
let key = CacheKey::new(text, language, backend);
let cache = self
.cache
.lock()
.expect("Phoneme cache mutex poisoned - unrecoverable error");
cache.contains(&key)
}
pub fn len(&self) -> usize {
let cache = self
.cache
.lock()
.expect("Phoneme cache mutex poisoned - unrecoverable error");
cache.len()
}
pub fn is_empty(&self) -> bool {
let cache = self
.cache
.lock()
.expect("Phoneme cache mutex poisoned - unrecoverable error");
cache.is_empty()
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn global(capacity: usize) -> Arc<Self> {
Arc::new(Self::new(capacity))
}
}
impl Default for PhonemeCache {
fn default() -> Self {
Self::new(1000)
}
}
impl Clone for PhonemeCache {
fn clone(&self) -> Self {
Self {
cache: Arc::clone(&self.cache),
stats: Arc::clone(&self.stats),
capacity: self.capacity,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_basic_operations() {
let cache = PhonemeCache::new(100);
let result1 = cache.get_or_compute("hello", "en", "test", || {
vec![
"h".to_string(),
"ɛ".to_string(),
"l".to_string(),
"oʊ".to_string(),
]
});
assert_eq!(result1.len(), 4);
assert_eq!(cache.len(), 1);
let result2 = cache.get_or_compute("hello", "en", "test", || {
panic!("Should not be called - should use cache");
});
assert_eq!(result1, result2);
}
#[test]
fn test_cache_stats() {
let cache = PhonemeCache::new(100);
cache.get_or_compute("hello", "en", "test", || vec!["h".to_string()]);
cache.get_or_compute("hello", "en", "test", || vec!["h".to_string()]);
cache.get_or_compute("world", "en", "test", || vec!["w".to_string()]);
let stats = cache.stats();
assert_eq!(stats.hits, 1); assert_eq!(stats.misses, 2); assert_eq!(stats.entries, 2);
assert_eq!(stats.hit_rate, 1.0 / 3.0);
}
#[test]
fn test_cache_clear() {
let cache = PhonemeCache::new(100);
cache.get_or_compute("hello", "en", "test", || vec!["h".to_string()]);
assert_eq!(cache.len(), 1);
cache.clear();
assert_eq!(cache.len(), 0);
assert!(cache.is_empty());
}
#[test]
fn test_cache_different_languages() {
let cache = PhonemeCache::new(100);
let en_result = cache.get_or_compute("hello", "en", "test", || {
vec![
"h".to_string(),
"ɛ".to_string(),
"l".to_string(),
"oʊ".to_string(),
]
});
let ja_result = cache.get_or_compute("hello", "ja", "test", || {
vec![
"h".to_string(),
"e".to_string(),
"r".to_string(),
"o".to_string(),
]
});
assert_ne!(en_result, ja_result);
assert_eq!(cache.len(), 2);
}
#[test]
fn test_cache_resize() {
let mut cache = PhonemeCache::new(10);
cache.get_or_compute("a", "en", "test", || vec!["a".to_string()]);
cache.get_or_compute("b", "en", "test", || vec!["b".to_string()]);
cache.get_or_compute("c", "en", "test", || vec!["c".to_string()]);
assert_eq!(cache.len(), 3);
assert_eq!(cache.capacity(), 10);
cache.resize(20);
assert_eq!(cache.capacity(), 20);
assert!(cache.contains("a", "en", "test"));
assert!(cache.contains("b", "en", "test"));
assert!(cache.contains("c", "en", "test"));
cache.get_or_compute("d", "en", "test", || vec!["d".to_string()]);
cache.get_or_compute("e", "en", "test", || vec!["e".to_string()]);
assert!(cache.len() >= 3); }
#[test]
fn test_cache_contains() {
let cache = PhonemeCache::new(100);
assert!(!cache.contains("hello", "en", "test"));
cache.get_or_compute("hello", "en", "test", || vec!["h".to_string()]);
assert!(cache.contains("hello", "en", "test"));
assert!(!cache.contains("hello", "ja", "test")); assert!(!cache.contains("world", "en", "test")); }
#[test]
fn test_cache_minimum_capacity() {
let cache = PhonemeCache::new(0); assert_eq!(cache.capacity(), 10);
let cache2 = PhonemeCache::new(5); assert_eq!(cache2.capacity(), 10);
}
#[test]
fn test_cache_clone() {
let cache1 = PhonemeCache::new(100);
cache1.get_or_compute("hello", "en", "test", || vec!["h".to_string()]);
let cache2 = cache1.clone();
assert_eq!(cache1.len(), cache2.len());
cache2.get_or_compute("world", "en", "test", || vec!["w".to_string()]);
assert_eq!(cache1.len(), 2);
assert_eq!(cache2.len(), 2);
}
}