Skip to main content

ferrous_di/
performance.rs

1//! Performance optimization components for ferrous-di.
2//!
3//! This module provides advanced performance features including:
4//! - Service resolution caching
5//! - Memory pool management
6//! - Lazy initialization
7//! - Dependency graph optimization
8
9use std::collections::HashMap;
10use std::sync::{Arc, Mutex, RwLock};
11// use std::any::TypeId;
12use std::time::{Duration, Instant};
13use crate::{Key, DiResult, DiError};
14use crate::registration::AnyArc;
15
16/// Service resolution cache for frequently accessed services
17///
18/// Caches resolved service instances to avoid repeated resolution overhead.
19/// Particularly beneficial for complex dependency graphs with deep nesting.
20#[derive(Debug)]
21pub struct ResolutionCache {
22    /// Cached service instances with expiration times
23    cache: RwLock<HashMap<Key, CacheEntry>>,
24    /// Cache configuration settings
25    config: CacheConfig,
26    /// Cache hit/miss statistics
27    stats: Mutex<CacheStats>,
28}
29
30#[derive(Debug, Clone)]
31struct CacheEntry {
32    /// The cached service instance
33    service: AnyArc,
34    /// When this entry was created
35    created_at: Instant,
36    /// How many times this entry has been accessed
37    access_count: u64,
38    /// Last access time for LRU eviction
39    last_accessed: Instant,
40}
41
42#[derive(Debug, Clone)]
43pub struct CacheConfig {
44    /// Maximum number of cached entries
45    pub max_entries: usize,
46    /// Time-to-live for cache entries (None = no expiration)
47    pub ttl: Option<Duration>,
48    /// Enable LRU eviction when cache is full
49    pub enable_lru: bool,
50    /// Enable access count tracking
51    pub track_access_count: bool,
52}
53
54impl Default for CacheConfig {
55    fn default() -> Self {
56        Self {
57            max_entries: 1000,
58            ttl: Some(Duration::from_secs(300)), // 5 minutes
59            enable_lru: true,
60            track_access_count: true,
61        }
62    }
63}
64
65#[derive(Debug, Default)]
66pub struct CacheStats {
67    /// Total cache hit count
68    pub hits: u64,
69    /// Total cache miss count
70    pub misses: u64,
71    /// Total evictions due to TTL expiration
72    pub ttl_evictions: u64,
73    /// Total evictions due to LRU
74    pub lru_evictions: u64,
75}
76
77impl CacheStats {
78    /// Calculate hit ratio as a percentage
79    pub fn hit_ratio(&self) -> f64 {
80        let total = self.hits + self.misses;
81        if total == 0 {
82            0.0
83        } else {
84            (self.hits as f64 / total as f64) * 100.0
85        }
86    }
87}
88
89impl ResolutionCache {
90    /// Create a new resolution cache with default configuration
91    pub fn new() -> Self {
92        Self::with_config(CacheConfig::default())
93    }
94
95    /// Create a new resolution cache with custom configuration
96    pub fn with_config(config: CacheConfig) -> Self {
97        Self {
98            cache: RwLock::new(HashMap::new()),
99            config,
100            stats: Mutex::new(CacheStats::default()),
101        }
102    }
103
104    /// Get a service from the cache
105    pub fn get(&self, key: &Key) -> Option<AnyArc> {
106        let mut stats = self.stats.lock().unwrap();
107        
108        // Check if we have the entry and it's not expired
109        if let Ok(mut cache) = self.cache.write() {
110            if let Some(entry) = cache.get_mut(key) {
111                // Check TTL expiration
112                if let Some(ttl) = self.config.ttl {
113                    if entry.created_at.elapsed() > ttl {
114                        cache.remove(key);
115                        stats.ttl_evictions += 1;
116                        stats.misses += 1;
117                        return None;
118                    }
119                }
120
121                // Update access tracking
122                if self.config.track_access_count {
123                    entry.access_count += 1;
124                    entry.last_accessed = Instant::now();
125                }
126
127                stats.hits += 1;
128                return Some(entry.service.clone());
129            }
130        }
131
132        stats.misses += 1;
133        None
134    }
135
136    /// Put a service into the cache
137    pub fn put(&self, key: Key, service: AnyArc) -> DiResult<()> {
138        if let Ok(mut cache) = self.cache.write() {
139            // Check if we need to evict entries
140            if cache.len() >= self.config.max_entries {
141                if self.config.enable_lru {
142                    self.evict_lru(&mut cache)?;
143                } else {
144                    return Err(DiError::TypeMismatch("Cache capacity exceeded"));
145                }
146            }
147
148            let entry = CacheEntry {
149                service,
150                created_at: Instant::now(),
151                access_count: 0,
152                last_accessed: Instant::now(),
153            };
154
155            cache.insert(key, entry);
156        }
157
158        Ok(())
159    }
160
161    /// Evict the least recently used entry
162    fn evict_lru(&self, cache: &mut HashMap<Key, CacheEntry>) -> DiResult<()> {
163        if cache.is_empty() {
164            return Ok(());
165        }
166
167        // Find the LRU entry
168        let lru_key = cache
169            .iter()
170            .min_by_key(|(_, entry)| entry.last_accessed)
171            .map(|(key, _)| key.clone())
172            .ok_or(DiError::TypeMismatch("Failed to find LRU entry"))?;
173
174        cache.remove(&lru_key);
175        
176        // Update stats
177        if let Ok(mut stats) = self.stats.lock() {
178            stats.lru_evictions += 1;
179        }
180
181        Ok(())
182    }
183
184    /// Clear all cached entries
185    pub fn clear(&self) {
186        if let Ok(mut cache) = self.cache.write() {
187            cache.clear();
188        }
189    }
190
191    /// Get cache statistics
192    pub fn stats(&self) -> CacheStats {
193        let stats = self.stats.lock().unwrap();
194        CacheStats {
195            hits: stats.hits,
196            misses: stats.misses,
197            ttl_evictions: stats.ttl_evictions,
198            lru_evictions: stats.lru_evictions,
199        }
200    }
201
202    /// Get current cache size
203    pub fn size(&self) -> usize {
204        self.cache.read().unwrap().len()
205    }
206}
207
208impl Default for ResolutionCache {
209    fn default() -> Self {
210        Self::new()
211    }
212}
213
214/// Memory pool for reusing transient service allocations
215///
216/// Reduces allocation overhead for frequently created transient services
217/// by maintaining pools of pre-allocated instances.
218pub struct ServicePool<T> {
219    /// Pool of available service instances
220    pool: Mutex<Vec<T>>,
221    /// Factory function to create new instances
222    factory: Box<dyn Fn() -> T + Send + Sync>,
223    /// Maximum pool size
224    max_size: usize,
225}
226
227impl<T> std::fmt::Debug for ServicePool<T> 
228where 
229    T: Send + 'static,
230{
231    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232        f.debug_struct("ServicePool")
233            .field("pool_size", &self.size())
234            .field("max_size", &self.max_size)
235            .finish()
236    }
237}
238
239impl<T> ServicePool<T> 
240where 
241    T: Send + 'static,
242{
243    /// Create a new service pool with a factory function
244    pub fn new<F>(factory: F, max_size: usize) -> Self 
245    where 
246        F: Fn() -> T + Send + Sync + 'static,
247    {
248        Self {
249            pool: Mutex::new(Vec::new()),
250            factory: Box::new(factory),
251            max_size,
252        }
253    }
254
255    /// Get an instance from the pool or create a new one
256    pub fn get(&self) -> T {
257        if let Ok(mut pool) = self.pool.lock() {
258            if let Some(instance) = pool.pop() {
259                return instance;
260            }
261        }
262        
263        // Pool is empty, create new instance
264        (self.factory)()
265    }
266
267    /// Return an instance to the pool for reuse
268    pub fn put(&self, instance: T) {
269        if let Ok(mut pool) = self.pool.lock() {
270            if pool.len() < self.max_size {
271                pool.push(instance);
272            }
273            // If pool is full, just drop the instance
274        }
275    }
276
277    /// Get current pool size
278    pub fn size(&self) -> usize {
279        self.pool.lock().unwrap().len()
280    }
281
282    /// Clear the pool
283    pub fn clear(&self) {
284        if let Ok(mut pool) = self.pool.lock() {
285            pool.clear();
286        }
287    }
288}
289
290/// Lazy initialization wrapper for expensive singleton services
291///
292/// Defers service creation until first access, improving startup performance.
293pub struct LazyService<T> {
294    /// The lazily initialized value
295    value: RwLock<Option<Arc<T>>>,
296    /// Initialization function
297    init: Box<dyn Fn() -> DiResult<T> + Send + Sync>,
298}
299
300impl<T> std::fmt::Debug for LazyService<T> 
301where 
302    T: Send + Sync + 'static,
303{
304    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
305        f.debug_struct("LazyService")
306            .field("is_initialized", &self.is_initialized())
307            .finish()
308    }
309}
310
311impl<T> LazyService<T> 
312where 
313    T: Send + Sync + 'static,
314{
315    /// Create a new lazy service with an initialization function
316    pub fn new<F>(init: F) -> Self 
317    where 
318        F: Fn() -> DiResult<T> + Send + Sync + 'static,
319    {
320        Self {
321            value: RwLock::new(None),
322            init: Box::new(init),
323        }
324    }
325
326    /// Get the service instance, initializing if necessary
327    pub fn get(&self) -> DiResult<Arc<T>> {
328        // Fast path: check if already initialized
329        if let Ok(value) = self.value.read() {
330            if let Some(service) = value.as_ref() {
331                return Ok(service.clone());
332            }
333        }
334
335        // Slow path: need to initialize
336        if let Ok(mut value) = self.value.write() {
337            // Double-checked locking pattern
338            if let Some(service) = value.as_ref() {
339                return Ok(service.clone());
340            }
341
342            // Initialize the service
343            let service = (self.init)()?;
344            let service_arc = Arc::new(service);
345            *value = Some(service_arc.clone());
346            Ok(service_arc)
347        } else {
348            Err(DiError::TypeMismatch("Failed to acquire write lock for lazy initialization"))
349        }
350    }
351
352    /// Check if the service has been initialized
353    pub fn is_initialized(&self) -> bool {
354        if let Ok(value) = self.value.read() {
355            value.is_some()
356        } else {
357            false
358        }
359    }
360}
361
362/// Dependency resolution path optimization
363///
364/// Pre-computes and caches optimal resolution paths for complex dependency graphs.
365#[derive(Debug)]
366pub struct DependencyGraphOptimizer {
367    /// Cached resolution paths
368    paths: RwLock<HashMap<Key, ResolutionPath>>,
369    /// Graph analysis statistics
370    stats: Mutex<GraphStats>,
371}
372
373#[derive(Debug, Clone)]
374pub struct ResolutionPath {
375    /// Ordered list of services to resolve
376    pub steps: Vec<Key>,
377    /// Estimated resolution cost
378    pub cost: u32,
379    /// Path depth (number of dependencies)
380    pub depth: usize,
381}
382
383#[derive(Debug, Default)]
384pub struct GraphStats {
385    /// Total paths analyzed
386    pub paths_analyzed: u64,
387    /// Total paths optimized
388    pub paths_optimized: u64,
389    /// Average path depth
390    pub avg_depth: f64,
391    /// Maximum path depth found
392    pub max_depth: usize,
393}
394
395impl DependencyGraphOptimizer {
396    /// Create a new dependency graph optimizer
397    pub fn new() -> Self {
398        Self {
399            paths: RwLock::new(HashMap::new()),
400            stats: Mutex::new(GraphStats::default()),
401        }
402    }
403
404    /// Analyze and optimize a resolution path
405    pub fn optimize_path(&self, root_key: &Key, dependencies: &[Key]) -> DiResult<ResolutionPath> {
406        if let Ok(paths) = self.paths.read() {
407            if let Some(cached_path) = paths.get(root_key) {
408                return Ok(cached_path.clone());
409            }
410        }
411
412        // Compute optimal resolution order
413        let optimized_steps = self.compute_optimal_order(dependencies)?;
414        let path = ResolutionPath {
415            cost: optimized_steps.len() as u32 * 10, // Simple cost model
416            depth: optimized_steps.len(),
417            steps: optimized_steps,
418        };
419
420        // Cache the optimized path
421        if let Ok(mut paths) = self.paths.write() {
422            paths.insert(root_key.clone(), path.clone());
423        }
424
425        // Update statistics
426        if let Ok(mut stats) = self.stats.lock() {
427            stats.paths_analyzed += 1;
428            stats.paths_optimized += 1;
429            stats.max_depth = stats.max_depth.max(path.depth);
430            
431            // Update average depth
432            let total_depth = stats.avg_depth * (stats.paths_analyzed - 1) as f64 + path.depth as f64;
433            stats.avg_depth = total_depth / stats.paths_analyzed as f64;
434        }
435
436        Ok(path)
437    }
438
439    /// Compute optimal resolution order using topological sort
440    fn compute_optimal_order(&self, dependencies: &[Key]) -> DiResult<Vec<Key>> {
441        // For now, use simple ordering - in a full implementation,
442        // this would do topological sorting of the dependency graph
443        Ok(dependencies.to_vec())
444    }
445
446    /// Get optimization statistics
447    pub fn stats(&self) -> GraphStats {
448        let stats = self.stats.lock().unwrap();
449        GraphStats {
450            paths_analyzed: stats.paths_analyzed,
451            paths_optimized: stats.paths_optimized,
452            avg_depth: stats.avg_depth,
453            max_depth: stats.max_depth,
454        }
455    }
456
457    /// Clear cached paths
458    pub fn clear(&self) {
459        if let Ok(mut paths) = self.paths.write() {
460            paths.clear();
461        }
462    }
463}
464
465impl Default for DependencyGraphOptimizer {
466    fn default() -> Self {
467        Self::new()
468    }
469}
470
471#[cfg(test)]
472mod tests {
473    use super::*;
474    use std::any::TypeId;
475    use std::thread;
476    use std::time::Duration;
477
478    #[test]
479    fn test_resolution_cache_basic_operations() {
480        let cache = ResolutionCache::new();
481        let key = Key::Type(TypeId::of::<String>(), "String");
482        let service = Arc::new("test_service".to_string()) as AnyArc;
483
484        // Cache miss initially
485        assert!(cache.get(&key).is_none());
486
487        // Put and get
488        cache.put(key.clone(), service.clone()).unwrap();
489        let cached = cache.get(&key).unwrap();
490        
491        // Should be the same Arc
492        assert!(Arc::ptr_eq(&service, &cached));
493
494        // Check stats
495        let stats = cache.stats();
496        assert_eq!(stats.hits, 1);
497        assert_eq!(stats.misses, 1);
498        assert!(stats.hit_ratio() > 0.0);
499    }
500
501    #[test]
502    fn test_resolution_cache_ttl_expiration() {
503        let config = CacheConfig {
504            max_entries: 10,
505            ttl: Some(Duration::from_millis(50)),
506            enable_lru: true,
507            track_access_count: true,
508        };
509        
510        let cache = ResolutionCache::with_config(config);
511        let key = Key::Type(TypeId::of::<String>(), "String");
512        let service = Arc::new("test_service".to_string()) as AnyArc;
513
514        // Put service in cache
515        cache.put(key.clone(), service).unwrap();
516        
517        // Should be available immediately
518        assert!(cache.get(&key).is_some());
519
520        // Wait for TTL expiration
521        thread::sleep(Duration::from_millis(60));
522
523        // Should be expired now
524        assert!(cache.get(&key).is_none());
525
526        let stats = cache.stats();
527        assert_eq!(stats.ttl_evictions, 1);
528    }
529
530    #[test]
531    fn test_service_pool_reuse() {
532        let pool = ServicePool::new(|| "new_instance".to_string(), 5);
533
534        // Get instance from empty pool (creates new)
535        let instance1 = pool.get();
536        assert_eq!(instance1, "new_instance");
537
538        // Return to pool
539        pool.put(instance1);
540        assert_eq!(pool.size(), 1);
541
542        // Get from pool (should reuse)
543        let instance2 = pool.get();
544        assert_eq!(instance2, "new_instance");
545        assert_eq!(pool.size(), 0);
546    }
547
548    #[test]
549    fn test_lazy_service_initialization() {
550        let counter = Arc::new(Mutex::new(0));
551        let counter_clone = counter.clone();
552
553        let lazy = LazyService::new(move || {
554            let mut c = counter_clone.lock().unwrap();
555            *c += 1;
556            Ok(format!("initialized_{}", *c))
557        });
558
559        // Not initialized initially
560        assert!(!lazy.is_initialized());
561
562        // First access initializes
563        let service1 = lazy.get().unwrap();
564        assert!(lazy.is_initialized());
565        assert_eq!(*service1, "initialized_1");
566
567        // Second access returns same instance
568        let service2 = lazy.get().unwrap();
569        assert!(Arc::ptr_eq(&service1, &service2));
570
571        // Counter should only be incremented once
572        assert_eq!(*counter.lock().unwrap(), 1);
573    }
574
575    #[test]
576    fn test_dependency_graph_optimizer() {
577        let optimizer = DependencyGraphOptimizer::new();
578        let root_key = Key::Type(TypeId::of::<String>(), "Root");
579        let deps = vec![
580            Key::Type(TypeId::of::<i32>(), "Dep1"),
581            Key::Type(TypeId::of::<f64>(), "Dep2"),
582        ];
583
584        let path = optimizer.optimize_path(&root_key, &deps).unwrap();
585        
586        assert_eq!(path.steps.len(), 2);
587        assert_eq!(path.depth, 2);
588        assert!(path.cost > 0);
589
590        // Second call should use cached path
591        let path2 = optimizer.optimize_path(&root_key, &deps).unwrap();
592        assert_eq!(path.steps, path2.steps);
593
594        let stats = optimizer.stats();
595        assert_eq!(stats.paths_analyzed, 1);
596        assert_eq!(stats.paths_optimized, 1);
597    }
598}