1use lru::LruCache;
31use std::collections::hash_map::DefaultHasher;
32use std::hash::{Hash, Hasher};
33use std::num::NonZeroUsize;
34use std::sync::{Arc, Mutex};
35
36#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38struct CacheKey {
39 text: String,
41 language: String,
43 backend: String,
45}
46
47impl CacheKey {
48 fn new(
49 text: impl Into<String>,
50 language: impl Into<String>,
51 backend: impl Into<String>,
52 ) -> Self {
53 Self {
54 text: text.into(),
55 language: language.into(),
56 backend: backend.into(),
57 }
58 }
59
60 fn fast_hash(&self) -> u64 {
62 let mut hasher = DefaultHasher::new();
63 self.hash(&mut hasher);
64 hasher.finish()
65 }
66}
67
68#[derive(Debug, Clone, Default)]
70pub struct CacheStats {
71 pub hits: u64,
73 pub misses: u64,
75 pub entries: usize,
77 pub capacity: usize,
79 pub hit_rate: f64,
81 pub memory_usage: usize,
83}
84
85impl CacheStats {
86 fn calculate_hit_rate(&mut self) {
88 let total = self.hits + self.misses;
89 self.hit_rate = if total > 0 {
90 self.hits as f64 / total as f64
91 } else {
92 0.0
93 };
94 }
95}
96
97pub struct PhonemeCache {
99 cache: Arc<Mutex<LruCache<CacheKey, Vec<String>>>>,
100 stats: Arc<Mutex<CacheStats>>,
101 capacity: usize,
102}
103
104impl PhonemeCache {
105 pub fn new(capacity: usize) -> Self {
119 let capacity = capacity.max(10); let cache_capacity = NonZeroUsize::new(capacity).expect("Capacity must be non-zero");
121
122 Self {
123 cache: Arc::new(Mutex::new(LruCache::new(cache_capacity))),
124 stats: Arc::new(Mutex::new(CacheStats {
125 capacity,
126 ..Default::default()
127 })),
128 capacity,
129 }
130 }
131
132 pub fn get_or_compute<F>(
157 &self,
158 text: &str,
159 language: &str,
160 backend: &str,
161 compute_fn: F,
162 ) -> Vec<String>
163 where
164 F: FnOnce() -> Vec<String>,
165 {
166 let key = CacheKey::new(text, language, backend);
167
168 {
170 let mut cache = self
171 .cache
172 .lock()
173 .expect("Phoneme cache mutex poisoned - unrecoverable error");
174 if let Some(phonemes) = cache.get(&key) {
175 let mut stats = self
177 .stats
178 .lock()
179 .expect("Phoneme cache stats mutex poisoned - unrecoverable error");
180 stats.hits += 1;
181 stats.calculate_hit_rate();
182 return phonemes.clone();
183 }
184 }
185
186 let phonemes = compute_fn();
188
189 {
191 let mut cache = self
192 .cache
193 .lock()
194 .expect("Phoneme cache mutex poisoned - unrecoverable error");
195 cache.put(key, phonemes.clone());
196
197 let mut stats = self
198 .stats
199 .lock()
200 .expect("Phoneme cache stats mutex poisoned - unrecoverable error");
201 stats.misses += 1;
202 stats.entries = cache.len();
203 stats.calculate_hit_rate();
204
205 stats.memory_usage = cache.len() * 256; }
208
209 phonemes
210 }
211
212 pub fn clear(&self) {
222 let mut cache = self
223 .cache
224 .lock()
225 .expect("Phoneme cache mutex poisoned - unrecoverable error");
226 cache.clear();
227
228 let mut stats = self
229 .stats
230 .lock()
231 .expect("Phoneme cache stats mutex poisoned - unrecoverable error");
232 stats.entries = 0;
233 stats.memory_usage = 0;
234 }
235
236 pub fn stats(&self) -> CacheStats {
248 let cache = self
249 .cache
250 .lock()
251 .expect("Phoneme cache mutex poisoned - unrecoverable error");
252 let mut stats = self
253 .stats
254 .lock()
255 .expect("Phoneme cache stats mutex poisoned - unrecoverable error");
256
257 stats.entries = cache.len();
258 stats.clone()
259 }
260
261 pub fn resize(&mut self, new_capacity: usize) {
275 let new_capacity = new_capacity.max(10);
276 let cache_capacity = NonZeroUsize::new(new_capacity).expect("Capacity must be non-zero");
277
278 let mut cache = self
279 .cache
280 .lock()
281 .expect("Phoneme cache mutex poisoned - unrecoverable error");
282 cache.resize(cache_capacity);
283
284 let mut stats = self
285 .stats
286 .lock()
287 .expect("Phoneme cache stats mutex poisoned - unrecoverable error");
288 stats.capacity = new_capacity;
289 self.capacity = new_capacity;
290 }
291
292 pub fn contains(&self, text: &str, language: &str, backend: &str) -> bool {
304 let key = CacheKey::new(text, language, backend);
305 let cache = self
306 .cache
307 .lock()
308 .expect("Phoneme cache mutex poisoned - unrecoverable error");
309 cache.contains(&key)
310 }
311
312 pub fn len(&self) -> usize {
314 let cache = self
315 .cache
316 .lock()
317 .expect("Phoneme cache mutex poisoned - unrecoverable error");
318 cache.len()
319 }
320
321 pub fn is_empty(&self) -> bool {
323 let cache = self
324 .cache
325 .lock()
326 .expect("Phoneme cache mutex poisoned - unrecoverable error");
327 cache.is_empty()
328 }
329
330 pub fn capacity(&self) -> usize {
332 self.capacity
333 }
334
335 pub fn global(capacity: usize) -> Arc<Self> {
339 Arc::new(Self::new(capacity))
340 }
341}
342
343impl Default for PhonemeCache {
344 fn default() -> Self {
346 Self::new(1000)
347 }
348}
349
350impl Clone for PhonemeCache {
351 fn clone(&self) -> Self {
352 Self {
353 cache: Arc::clone(&self.cache),
354 stats: Arc::clone(&self.stats),
355 capacity: self.capacity,
356 }
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 #[test]
365 fn test_cache_basic_operations() {
366 let cache = PhonemeCache::new(100);
367
368 let result1 = cache.get_or_compute("hello", "en", "test", || {
370 vec![
371 "h".to_string(),
372 "ɛ".to_string(),
373 "l".to_string(),
374 "oʊ".to_string(),
375 ]
376 });
377
378 assert_eq!(result1.len(), 4);
379 assert_eq!(cache.len(), 1);
380
381 let result2 = cache.get_or_compute("hello", "en", "test", || {
383 panic!("Should not be called - should use cache");
384 });
385
386 assert_eq!(result1, result2);
387 }
388
389 #[test]
390 fn test_cache_stats() {
391 let cache = PhonemeCache::new(100);
392
393 cache.get_or_compute("hello", "en", "test", || vec!["h".to_string()]);
395 cache.get_or_compute("hello", "en", "test", || vec!["h".to_string()]);
396 cache.get_or_compute("world", "en", "test", || vec!["w".to_string()]);
397
398 let stats = cache.stats();
399 assert_eq!(stats.hits, 1); assert_eq!(stats.misses, 2); assert_eq!(stats.entries, 2);
402 assert_eq!(stats.hit_rate, 1.0 / 3.0);
403 }
404
405 #[test]
406 fn test_cache_clear() {
407 let cache = PhonemeCache::new(100);
408
409 cache.get_or_compute("hello", "en", "test", || vec!["h".to_string()]);
410 assert_eq!(cache.len(), 1);
411
412 cache.clear();
413 assert_eq!(cache.len(), 0);
414 assert!(cache.is_empty());
415 }
416
417 #[test]
418 fn test_cache_different_languages() {
419 let cache = PhonemeCache::new(100);
420
421 let en_result = cache.get_or_compute("hello", "en", "test", || {
422 vec![
423 "h".to_string(),
424 "ɛ".to_string(),
425 "l".to_string(),
426 "oʊ".to_string(),
427 ]
428 });
429
430 let ja_result = cache.get_or_compute("hello", "ja", "test", || {
431 vec![
432 "h".to_string(),
433 "e".to_string(),
434 "r".to_string(),
435 "o".to_string(),
436 ]
437 });
438
439 assert_ne!(en_result, ja_result);
441 assert_eq!(cache.len(), 2);
442 }
443
444 #[test]
445 fn test_cache_resize() {
446 let mut cache = PhonemeCache::new(10);
447
448 cache.get_or_compute("a", "en", "test", || vec!["a".to_string()]);
450 cache.get_or_compute("b", "en", "test", || vec!["b".to_string()]);
451 cache.get_or_compute("c", "en", "test", || vec!["c".to_string()]);
452
453 assert_eq!(cache.len(), 3);
454 assert_eq!(cache.capacity(), 10);
455
456 cache.resize(20);
458 assert_eq!(cache.capacity(), 20);
459
460 assert!(cache.contains("a", "en", "test"));
462 assert!(cache.contains("b", "en", "test"));
463 assert!(cache.contains("c", "en", "test"));
464
465 cache.get_or_compute("d", "en", "test", || vec!["d".to_string()]);
467 cache.get_or_compute("e", "en", "test", || vec!["e".to_string()]);
468
469 assert!(cache.len() >= 3); }
471
472 #[test]
473 fn test_cache_contains() {
474 let cache = PhonemeCache::new(100);
475
476 assert!(!cache.contains("hello", "en", "test"));
477
478 cache.get_or_compute("hello", "en", "test", || vec!["h".to_string()]);
479
480 assert!(cache.contains("hello", "en", "test"));
481 assert!(!cache.contains("hello", "ja", "test")); assert!(!cache.contains("world", "en", "test")); }
484
485 #[test]
486 fn test_cache_minimum_capacity() {
487 let cache = PhonemeCache::new(0); assert_eq!(cache.capacity(), 10);
489
490 let cache2 = PhonemeCache::new(5); assert_eq!(cache2.capacity(), 10);
492 }
493
494 #[test]
495 fn test_cache_clone() {
496 let cache1 = PhonemeCache::new(100);
497 cache1.get_or_compute("hello", "en", "test", || vec!["h".to_string()]);
498
499 let cache2 = cache1.clone();
500
501 assert_eq!(cache1.len(), cache2.len());
503
504 cache2.get_or_compute("world", "en", "test", || vec!["w".to_string()]);
505
506 assert_eq!(cache1.len(), 2);
508 assert_eq!(cache2.len(), 2);
509 }
510}