Skip to main content

voirs_g2p/performance/
cache.rs

1//! Cache functionality for G2P performance optimization
2
3use 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/// Cache statistics for monitoring performance
13#[derive(Debug, Clone, Default, Serialize)]
14pub struct CacheStats {
15    /// Number of successful cache hits
16    pub hits: u64,
17    /// Number of cache misses (entry not found)
18    pub misses: u64,
19    /// Number of entries evicted from cache
20    pub evictions: u64,
21    /// Current total number of entries in cache
22    pub total_size: usize,
23}
24
25impl CacheStats {
26    /// Get cache hit rate (0.0 to 1.0)
27    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    /// Get cache miss rate (0.0 to 1.0)
37    pub fn miss_rate(&self) -> f64 {
38        1.0 - self.hit_rate()
39    }
40}
41
42/// Cache entry with timestamp for LRU eviction
43#[derive(Debug, Clone)]
44struct CacheEntry<T> {
45    value: T,
46    timestamp: Instant,
47    access_count: u64,
48}
49
50/// Advanced cache eviction strategies
51#[derive(Debug, Clone, Copy)]
52pub enum EvictionStrategy {
53    /// Least Recently Used
54    LRU,
55    /// Least Frequently Used
56    LFU,
57    /// Time-based eviction (TTL)
58    TTL,
59    /// Adaptive cache based on access patterns
60    Adaptive,
61}
62
63/// Advanced cache configuration
64#[derive(Debug, Clone)]
65pub struct AdvancedCacheConfig {
66    /// Maximum number of entries allowed in cache
67    pub max_size: usize,
68    /// Optional time-to-live for cache entries
69    pub ttl: Option<Duration>,
70    /// Strategy for evicting entries when cache is full
71    pub eviction_strategy: EvictionStrategy,
72    /// Preload popular entries on initialization
73    pub preload_popular: bool,
74    /// Automatically adjust cache size based on usage patterns
75    pub adaptive_sizing: bool,
76    /// Collect detailed cache statistics
77    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)), // 1 hour
85            eviction_strategy: EvictionStrategy::Adaptive,
86            preload_popular: true,
87            adaptive_sizing: true,
88            stats_collection: true,
89        }
90    }
91}
92
93/// Enhanced cache entry with access frequency tracking
94#[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        // Update access frequency using exponential moving average
122        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                // Combine recency, frequency, and size
134                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
144/// High-performance advanced cache for G2P pronunciations
145pub 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    /// Create a new advanced cache with configuration
158    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    /// Get value from cache with advanced access tracking
168    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    /// Insert value into cache with intelligent eviction
183    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        // Check if we need to evict
188        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        // Update popular keys if enabled
196        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); // Keep only recent popular keys
205                }
206            }
207        }
208
209        stats.total_size = cache.len();
210    }
211
212    /// Intelligent eviction based on configured strategy
213    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        // Find the best candidate for eviction
223        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    /// Get cache statistics
241    pub fn get_stats(&self) -> CacheStats {
242        self.stats
243            .lock()
244            .expect("lock should not be poisoned")
245            .clone()
246    }
247
248    /// Clear cache
249    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    /// Get current cache size
258    pub fn size(&self) -> usize {
259        self.cache
260            .lock()
261            .expect("lock should not be poisoned")
262            .len()
263    }
264
265    /// Preload popular pronunciations
266    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    /// Optimize cache based on access patterns
273    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 hit rate is low, consider adjusting strategy or size
282        if stats.hit_rate() < 0.5 && cache.len() > 1000 {
283            // Could implement dynamic strategy switching here
284            // For now, just log the optimization opportunity
285            tracing::info!(
286                "Cache optimization opportunity detected: hit_rate={:.2}, size={}",
287                stats.hit_rate(),
288                cache.len()
289            );
290        }
291    }
292}
293
294/// High-performance LRU cache for G2P pronunciations (legacy interface)
295pub 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    /// Create new cache with specified capacity
308    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    /// Create cache with TTL (time-to-live)
318    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    /// Get value from cache
325    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            // Check TTL expiration
331            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            // Update access timestamp and count
340            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    /// Insert value into cache
352    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        // Evict oldest entries if cache is full
357        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    /// Evict least recently used entries
372    fn evict_lru(&self, cache: &mut HashMap<K, CacheEntry<V>>, stats: &mut CacheStats) {
373        // Find the least recently used entry
374        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    /// Clear all cache entries
391    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    /// Get cache statistics
400    pub fn stats(&self) -> CacheStats {
401        self.stats
402            .lock()
403            .expect("lock should not be poisoned")
404            .clone()
405    }
406
407    /// Get current cache size
408    pub fn size(&self) -> usize {
409        self.cache
410            .lock()
411            .expect("lock should not be poisoned")
412            .len()
413    }
414
415    /// Get cache capacity
416    pub fn capacity(&self) -> usize {
417        self.max_size
418    }
419
420    /// Batch insert multiple key-value pairs efficiently
421    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            // Evict oldest entries if cache is full
427            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
444/// Cached G2P wrapper for any G2P backend
445pub struct CachedG2p<T> {
446    backend: T,
447    cache: G2pCache<String, Vec<Phoneme>>,
448    cache_by_language: bool,
449}
450
451impl<T> CachedG2p<T> {
452    /// Create new cached G2P wrapper
453    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    /// Create cached G2P with TTL
462    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    /// Enable/disable language-specific caching
471    pub fn set_cache_by_language(&mut self, enabled: bool) {
472        self.cache_by_language = enabled;
473    }
474
475    /// Get cache statistics
476    pub fn cache_stats(&self) -> CacheStats {
477        self.cache.stats()
478    }
479
480    /// Clear cache
481    pub fn clear_cache(&self) {
482        self.cache.clear();
483    }
484
485    /// Generate cache key for text and language
486    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        // Check cache first
505        if let Some(phonemes) = self.cache.get(&cache_key) {
506            return Ok(phonemes);
507        }
508
509        // Cache miss - get from backend
510        let phonemes = self.backend.to_phonemes(text, lang).await?;
511
512        // Store in cache
513        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        // Test insertion and retrieval
540        cache.insert("key1".to_string(), "value1".to_string());
541        assert_eq!(cache.get(&"key1".to_string()), Some("value1".to_string()));
542
543        // Test miss
544        assert_eq!(cache.get(&"nonexistent".to_string()), None);
545
546        // Test stats
547        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        // Fill cache
557        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        // Trigger eviction
562        cache.insert("key3".to_string(), "value3".to_string());
563        assert_eq!(cache.size(), 2);
564
565        // Check eviction stats
566        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        // Wait for TTL expiration
578        std::thread::sleep(Duration::from_millis(2));
579
580        // Should be expired
581        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}