1use std::collections::HashMap;
4use std::hash::Hash;
5use std::sync::{Arc, Mutex};
6use std::time::{Duration, Instant};
7
8use crate::{G2p, G2pMetadata, LanguageCode, Phoneme, Result};
9use async_trait::async_trait;
10use serde::Serialize;
11
12#[derive(Debug, Clone, Default, Serialize)]
14pub struct CacheStats {
15 pub hits: u64,
17 pub misses: u64,
19 pub evictions: u64,
21 pub total_size: usize,
23}
24
25impl CacheStats {
26 pub fn hit_rate(&self) -> f64 {
28 let total = self.hits + self.misses;
29 if total == 0 {
30 0.0
31 } else {
32 self.hits as f64 / total as f64
33 }
34 }
35
36 pub fn miss_rate(&self) -> f64 {
38 1.0 - self.hit_rate()
39 }
40}
41
42#[derive(Debug, Clone)]
44struct CacheEntry<T> {
45 value: T,
46 timestamp: Instant,
47 access_count: u64,
48}
49
50#[derive(Debug, Clone, Copy)]
52pub enum EvictionStrategy {
53 LRU,
55 LFU,
57 TTL,
59 Adaptive,
61}
62
63#[derive(Debug, Clone)]
65pub struct AdvancedCacheConfig {
66 pub max_size: usize,
68 pub ttl: Option<Duration>,
70 pub eviction_strategy: EvictionStrategy,
72 pub preload_popular: bool,
74 pub adaptive_sizing: bool,
76 pub stats_collection: bool,
78}
79
80impl Default for AdvancedCacheConfig {
81 fn default() -> Self {
82 Self {
83 max_size: 10000,
84 ttl: Some(Duration::from_secs(3600)), eviction_strategy: EvictionStrategy::Adaptive,
86 preload_popular: true,
87 adaptive_sizing: true,
88 stats_collection: true,
89 }
90 }
91}
92
93#[derive(Debug, Clone)]
95struct AdvancedCacheEntry<T> {
96 value: T,
97 timestamp: Instant,
98 last_access: Instant,
99 access_count: u64,
100 access_frequency: f64,
101 size_estimate: usize,
102}
103
104impl<T> AdvancedCacheEntry<T> {
105 fn new(value: T, size_estimate: usize) -> Self {
106 let now = Instant::now();
107 Self {
108 value,
109 timestamp: now,
110 last_access: now,
111 access_count: 1,
112 access_frequency: 1.0,
113 size_estimate,
114 }
115 }
116
117 fn access(&mut self) {
118 let now = Instant::now();
119 let time_since_last = now.duration_since(self.last_access).as_secs_f64();
120
121 self.access_frequency = 0.8 * self.access_frequency + 0.2 * (1.0 / (time_since_last + 1.0));
123 self.access_count += 1;
124 self.last_access = now;
125 }
126
127 fn score(&self, strategy: EvictionStrategy) -> f64 {
128 match strategy {
129 EvictionStrategy::LRU => self.last_access.elapsed().as_secs_f64(),
130 EvictionStrategy::LFU => -(self.access_count as f64),
131 EvictionStrategy::TTL => self.timestamp.elapsed().as_secs_f64(),
132 EvictionStrategy::Adaptive => {
133 let recency_score = self.last_access.elapsed().as_secs_f64();
135 let frequency_score = -(self.access_frequency);
136 let size_penalty = (self.size_estimate as f64).sqrt();
137
138 recency_score + frequency_score + size_penalty * 0.1
139 }
140 }
141 }
142}
143
144pub struct AdvancedG2pCache<K, V> {
146 cache: Arc<Mutex<HashMap<K, AdvancedCacheEntry<V>>>>,
147 config: AdvancedCacheConfig,
148 stats: Arc<Mutex<CacheStats>>,
149 popular_keys: Arc<Mutex<Vec<K>>>,
150}
151
152impl<K, V> AdvancedG2pCache<K, V>
153where
154 K: Hash + Eq + Clone,
155 V: Clone,
156{
157 pub fn new(config: AdvancedCacheConfig) -> Self {
159 Self {
160 cache: Arc::new(Mutex::new(HashMap::new())),
161 config,
162 stats: Arc::new(Mutex::new(CacheStats::default())),
163 popular_keys: Arc::new(Mutex::new(Vec::new())),
164 }
165 }
166
167 pub fn get(&self, key: &K) -> Option<V> {
169 let mut cache = self.cache.lock().expect("lock should not be poisoned");
170 let mut stats = self.stats.lock().expect("lock should not be poisoned");
171
172 if let Some(entry) = cache.get_mut(key) {
173 entry.access();
174 stats.hits += 1;
175 Some(entry.value.clone())
176 } else {
177 stats.misses += 1;
178 None
179 }
180 }
181
182 pub fn insert(&self, key: K, value: V, size_estimate: usize) {
184 let mut cache = self.cache.lock().expect("lock should not be poisoned");
185 let mut stats = self.stats.lock().expect("lock should not be poisoned");
186
187 if cache.len() >= self.config.max_size {
189 self.evict_intelligently(&mut cache, &mut stats);
190 }
191
192 let entry = AdvancedCacheEntry::new(value, size_estimate);
193 cache.insert(key.clone(), entry);
194
195 if self.config.preload_popular {
197 let mut popular = self
198 .popular_keys
199 .lock()
200 .expect("lock should not be poisoned");
201 if !popular.contains(&key) {
202 popular.push(key);
203 if popular.len() > 100 {
204 popular.remove(0); }
206 }
207 }
208
209 stats.total_size = cache.len();
210 }
211
212 fn evict_intelligently(
214 &self,
215 cache: &mut HashMap<K, AdvancedCacheEntry<V>>,
216 stats: &mut CacheStats,
217 ) {
218 if cache.is_empty() {
219 return;
220 }
221
222 let mut best_key = None;
224 let mut best_score = f64::NEG_INFINITY;
225
226 for (key, entry) in cache.iter() {
227 let score = entry.score(self.config.eviction_strategy);
228 if score > best_score {
229 best_score = score;
230 best_key = Some(key.clone());
231 }
232 }
233
234 if let Some(key) = best_key {
235 cache.remove(&key);
236 stats.evictions += 1;
237 }
238 }
239
240 pub fn get_stats(&self) -> CacheStats {
242 self.stats
243 .lock()
244 .expect("lock should not be poisoned")
245 .clone()
246 }
247
248 pub fn clear(&self) {
250 let mut cache = self.cache.lock().expect("lock should not be poisoned");
251 cache.clear();
252
253 let mut stats = self.stats.lock().expect("lock should not be poisoned");
254 *stats = CacheStats::default();
255 }
256
257 pub fn size(&self) -> usize {
259 self.cache
260 .lock()
261 .expect("lock should not be poisoned")
262 .len()
263 }
264
265 pub fn preload(&self, popular_items: Vec<(K, V, usize)>) {
267 for (key, value, size) in popular_items {
268 self.insert(key, value, size);
269 }
270 }
271
272 pub fn optimize(&self) {
274 if !self.config.adaptive_sizing {
275 return;
276 }
277
278 let cache = self.cache.lock().expect("lock should not be poisoned");
279 let stats = self.stats.lock().expect("lock should not be poisoned");
280
281 if stats.hit_rate() < 0.5 && cache.len() > 1000 {
283 tracing::info!(
286 "Cache optimization opportunity detected: hit_rate={:.2}, size={}",
287 stats.hit_rate(),
288 cache.len()
289 );
290 }
291 }
292}
293
294pub struct G2pCache<K, V> {
296 cache: Arc<Mutex<HashMap<K, CacheEntry<V>>>>,
297 max_size: usize,
298 ttl: Option<Duration>,
299 stats: Arc<Mutex<CacheStats>>,
300}
301
302impl<K, V> G2pCache<K, V>
303where
304 K: Hash + Eq + Clone,
305 V: Clone,
306{
307 pub fn new(max_size: usize) -> Self {
309 Self {
310 cache: Arc::new(Mutex::new(HashMap::with_capacity(max_size))),
311 max_size,
312 ttl: None,
313 stats: Arc::new(Mutex::new(CacheStats::default())),
314 }
315 }
316
317 pub fn with_ttl(max_size: usize, ttl: Duration) -> Self {
319 let mut cache = Self::new(max_size);
320 cache.ttl = Some(ttl);
321 cache
322 }
323
324 pub fn get(&self, key: &K) -> Option<V> {
326 let mut cache = self.cache.lock().expect("lock should not be poisoned");
327 let mut stats = self.stats.lock().expect("lock should not be poisoned");
328
329 if let Some(entry) = cache.get_mut(key) {
330 if let Some(ttl) = self.ttl {
332 if entry.timestamp.elapsed() > ttl {
333 cache.remove(key);
334 stats.misses += 1;
335 return None;
336 }
337 }
338
339 entry.timestamp = Instant::now();
341 entry.access_count += 1;
342
343 stats.hits += 1;
344 Some(entry.value.clone())
345 } else {
346 stats.misses += 1;
347 None
348 }
349 }
350
351 pub fn insert(&self, key: K, value: V) {
353 let mut cache = self.cache.lock().expect("lock should not be poisoned");
354 let mut stats = self.stats.lock().expect("lock should not be poisoned");
355
356 if cache.len() >= self.max_size {
358 self.evict_lru(&mut cache, &mut stats);
359 }
360
361 let entry = CacheEntry {
362 value,
363 timestamp: Instant::now(),
364 access_count: 1,
365 };
366
367 cache.insert(key, entry);
368 stats.total_size = cache.len();
369 }
370
371 fn evict_lru(&self, cache: &mut HashMap<K, CacheEntry<V>>, stats: &mut CacheStats) {
373 let mut oldest_key = None;
375 let mut oldest_time = Instant::now();
376
377 for (key, entry) in cache.iter() {
378 if entry.timestamp < oldest_time {
379 oldest_time = entry.timestamp;
380 oldest_key = Some(key.clone());
381 }
382 }
383
384 if let Some(key) = oldest_key {
385 cache.remove(&key);
386 stats.evictions += 1;
387 }
388 }
389
390 pub fn clear(&self) {
392 let mut cache = self.cache.lock().expect("lock should not be poisoned");
393 let mut stats = self.stats.lock().expect("lock should not be poisoned");
394
395 cache.clear();
396 stats.total_size = 0;
397 }
398
399 pub fn stats(&self) -> CacheStats {
401 self.stats
402 .lock()
403 .expect("lock should not be poisoned")
404 .clone()
405 }
406
407 pub fn size(&self) -> usize {
409 self.cache
410 .lock()
411 .expect("lock should not be poisoned")
412 .len()
413 }
414
415 pub fn capacity(&self) -> usize {
417 self.max_size
418 }
419
420 pub fn batch_insert(&self, items: Vec<(K, V)>) {
422 let mut cache = self.cache.lock().expect("lock should not be poisoned");
423 let mut stats = self.stats.lock().expect("lock should not be poisoned");
424
425 for (key, value) in items {
426 if cache.len() >= self.max_size {
428 self.evict_lru(&mut cache, &mut stats);
429 }
430
431 let entry = CacheEntry {
432 value,
433 timestamp: Instant::now(),
434 access_count: 1,
435 };
436
437 cache.insert(key, entry);
438 }
439
440 stats.total_size = cache.len();
441 }
442}
443
444pub struct CachedG2p<T> {
446 backend: T,
447 cache: G2pCache<String, Vec<Phoneme>>,
448 cache_by_language: bool,
449}
450
451impl<T> CachedG2p<T> {
452 pub fn new(backend: T, cache_size: usize) -> Self {
454 Self {
455 backend,
456 cache: G2pCache::new(cache_size),
457 cache_by_language: true,
458 }
459 }
460
461 pub fn with_ttl(backend: T, cache_size: usize, ttl: Duration) -> Self {
463 Self {
464 backend,
465 cache: G2pCache::with_ttl(cache_size, ttl),
466 cache_by_language: true,
467 }
468 }
469
470 pub fn set_cache_by_language(&mut self, enabled: bool) {
472 self.cache_by_language = enabled;
473 }
474
475 pub fn cache_stats(&self) -> CacheStats {
477 self.cache.stats()
478 }
479
480 pub fn clear_cache(&self) {
482 self.cache.clear();
483 }
484
485 fn make_cache_key(&self, text: &str, lang: Option<LanguageCode>) -> String {
487 if self.cache_by_language {
488 if let Some(lang) = lang {
489 format!("{}:{text}", lang.as_str())
490 } else {
491 format!("default:{text}")
492 }
493 } else {
494 text.to_string()
495 }
496 }
497}
498
499#[async_trait]
500impl<T: G2p + Send + Sync> G2p for CachedG2p<T> {
501 async fn to_phonemes(&self, text: &str, lang: Option<LanguageCode>) -> Result<Vec<Phoneme>> {
502 let cache_key = self.make_cache_key(text, lang);
503
504 if let Some(phonemes) = self.cache.get(&cache_key) {
506 return Ok(phonemes);
507 }
508
509 let phonemes = self.backend.to_phonemes(text, lang).await?;
511
512 self.cache.insert(cache_key, phonemes.clone());
514
515 Ok(phonemes)
516 }
517
518 fn supported_languages(&self) -> Vec<LanguageCode> {
519 self.backend.supported_languages()
520 }
521
522 fn metadata(&self) -> G2pMetadata {
523 let mut metadata = self.backend.metadata();
524 metadata.name = format!("Cached {}", metadata.name);
525 metadata.description = format!("Cached wrapper for {}", metadata.description);
526 metadata
527 }
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533 use std::time::Duration;
534
535 #[test]
536 fn test_cache_basic_operations() {
537 let cache = G2pCache::new(10);
538
539 cache.insert("key1".to_string(), "value1".to_string());
541 assert_eq!(cache.get(&"key1".to_string()), Some("value1".to_string()));
542
543 assert_eq!(cache.get(&"nonexistent".to_string()), None);
545
546 let stats = cache.stats();
548 assert_eq!(stats.hits, 1);
549 assert_eq!(stats.misses, 1);
550 }
551
552 #[test]
553 fn test_cache_eviction() {
554 let cache = G2pCache::new(2);
555
556 cache.insert("key1".to_string(), "value1".to_string());
558 cache.insert("key2".to_string(), "value2".to_string());
559 assert_eq!(cache.size(), 2);
560
561 cache.insert("key3".to_string(), "value3".to_string());
563 assert_eq!(cache.size(), 2);
564
565 let stats = cache.stats();
567 assert_eq!(stats.evictions, 1);
568 }
569
570 #[test]
571 fn test_cache_ttl() {
572 let cache = G2pCache::with_ttl(10, Duration::from_millis(1));
573
574 cache.insert("key1".to_string(), "value1".to_string());
575 assert_eq!(cache.get(&"key1".to_string()), Some("value1".to_string()));
576
577 std::thread::sleep(Duration::from_millis(2));
579
580 assert_eq!(cache.get(&"key1".to_string()), None);
582
583 let stats = cache.stats();
584 assert_eq!(stats.hits, 1);
585 assert_eq!(stats.misses, 1);
586 }
587}