scirs2_stats/
memory_profiler_v3.rs

1//! Advanced memory profiling and optimization for statistical operations
2//!
3//! This module provides comprehensive memory profiling tools and adaptive
4//! memory management strategies for optimal performance.
5
6use crate::error::{StatsError, StatsResult};
7use scirs2_core::ndarray::{ArrayBase, Data, Ix1};
8use scirs2_core::numeric::{Float, NumCast};
9use std::collections::HashMap;
10use std::sync::{Arc, Mutex};
11use std::time::{Duration, Instant};
12
13/// Comprehensive memory profiler for statistical operations
14pub struct MemoryProfiler {
15    allocations: Arc<Mutex<HashMap<String, AllocationStats>>>,
16    peak_memory: Arc<Mutex<usize>>,
17    current_memory: Arc<Mutex<usize>>,
18    enabled: bool,
19}
20
21/// Statistics for memory allocations
22#[derive(Debug, Clone, Default)]
23pub struct AllocationStats {
24    pub total_allocations: usize,
25    pub total_bytes: usize,
26    pub peak_bytes: usize,
27    pub averagesize: f64,
28    pub allocation_times: Vec<Duration>,
29}
30
31impl Default for MemoryProfiler {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37impl MemoryProfiler {
38    pub fn new() -> Self {
39        Self {
40            allocations: Arc::new(Mutex::new(HashMap::new())),
41            peak_memory: Arc::new(Mutex::new(0)),
42            current_memory: Arc::new(Mutex::new(0)),
43            enabled: true,
44        }
45    }
46
47    /// Record an allocation
48    pub fn record_allocation(&self, category: &str, size: usize, duration: Duration) {
49        if !self.enabled {
50            return;
51        }
52
53        let mut allocations = self.allocations.lock().expect("Operation failed");
54        let stats = allocations.entry(category.to_string()).or_default();
55
56        stats.total_allocations += 1;
57        stats.total_bytes += size;
58        stats.peak_bytes = stats.peak_bytes.max(size);
59        stats.averagesize = stats.total_bytes as f64 / stats.total_allocations as f64;
60        stats.allocation_times.push(duration);
61
62        // Update global memory tracking
63        let mut current = self.current_memory.lock().expect("Operation failed");
64        *current += size;
65
66        let mut peak = self.peak_memory.lock().expect("Operation failed");
67        *peak = (*peak).max(*current);
68    }
69
70    /// Record a deallocation
71    pub fn record_deallocation(&self, size: usize) {
72        if !self.enabled {
73            return;
74        }
75
76        let mut current = self.current_memory.lock().expect("Operation failed");
77        *current = current.saturating_sub(size);
78    }
79
80    /// Get memory usage report
81    pub fn get_report(&self) -> MemoryReport {
82        let allocations = self.allocations.lock().expect("Operation failed").clone();
83        let peak_memory = *self.peak_memory.lock().expect("Operation failed");
84        let current_memory = *self.current_memory.lock().expect("Operation failed");
85
86        let recommendations = self.generate_recommendations(&allocations);
87        MemoryReport {
88            allocations,
89            peak_memory,
90            current_memory,
91            recommendations,
92        }
93    }
94
95    /// Generate memory optimization recommendations
96    fn generate_recommendations(
97        &self,
98        allocations: &HashMap<String, AllocationStats>,
99    ) -> Vec<String> {
100        let mut recommendations = Vec::new();
101
102        for (category, stats) in allocations {
103            // Check for frequent small allocations
104            if stats.total_allocations > 1000 && stats.averagesize < 1024.0 {
105                recommendations.push(format!(
106                    "Consider memory pooling for '{}' category (many small allocations: {} allocations, avg size: {:.1} bytes)",
107                    category, stats.total_allocations, stats.averagesize
108                ));
109            }
110
111            // Check for large allocations
112            if stats.peak_bytes > 10 * 1024 * 1024 {
113                // 10MB
114                recommendations.push(format!(
115                    "Consider streaming processing for '{}' category (large allocation: {:.1} MB)",
116                    category,
117                    stats.peak_bytes as f64 / 1024.0 / 1024.0
118                ));
119            }
120
121            // Check for slow allocations
122            if let Some(&max_time) = stats.allocation_times.iter().max() {
123                if max_time > Duration::from_millis(10) {
124                    recommendations.push(format!(
125                        "Consider pre-allocation for '{}' category (slow allocation: {:?})",
126                        category, max_time
127                    ));
128                }
129            }
130        }
131
132        recommendations
133    }
134
135    /// Enable or disable profiling
136    pub fn set_enabled(&mut self, enabled: bool) {
137        self.enabled = enabled;
138    }
139
140    /// Reset all profiling data
141    pub fn reset(&self) {
142        self.allocations.lock().expect("Operation failed").clear();
143        *self.peak_memory.lock().expect("Operation failed") = 0;
144        *self.current_memory.lock().expect("Operation failed") = 0;
145    }
146}
147
148/// Memory usage report
149#[derive(Debug)]
150pub struct MemoryReport {
151    pub allocations: HashMap<String, AllocationStats>,
152    pub peak_memory: usize,
153    pub current_memory: usize,
154    pub recommendations: Vec<String>,
155}
156
157impl MemoryReport {
158    /// Print formatted report
159    pub fn print_report(&self) {
160        println!("=== Memory Usage Report ===");
161        println!(
162            "Peak Memory Usage: {:.2} MB",
163            self.peak_memory as f64 / 1024.0 / 1024.0
164        );
165        println!(
166            "Current Memory Usage: {:.2} MB",
167            self.current_memory as f64 / 1024.0 / 1024.0
168        );
169        println!();
170
171        println!("Allocation Statistics by Category:");
172        for (category, stats) in &self.allocations {
173            println!("  {}:", category);
174            println!("    Total Allocations: {}", stats.total_allocations);
175            println!(
176                "    Total Bytes: {:.2} MB",
177                stats.total_bytes as f64 / 1024.0 / 1024.0
178            );
179            println!(
180                "    Peak Allocation: {:.2} KB",
181                stats.peak_bytes as f64 / 1024.0
182            );
183            println!("    Average Size: {:.1} bytes", stats.averagesize);
184
185            if !stats.allocation_times.is_empty() {
186                let avg_time = stats.allocation_times.iter().sum::<Duration>().as_micros() as f64
187                    / stats.allocation_times.len() as f64;
188                println!("    Average Allocation Time: {:.1} µs", avg_time);
189            }
190            println!();
191        }
192
193        if !self.recommendations.is_empty() {
194            println!("Optimization Recommendations:");
195            for (i, rec) in self.recommendations.iter().enumerate() {
196                println!("  {}. {}", i + 1, rec);
197            }
198        }
199    }
200}
201
202/// Memory-efficient cache for statistical computations
203pub struct StatisticsCache<F> {
204    cache: HashMap<String, CachedResult<F>>,
205    max_entries: usize,
206    max_memory: usize,
207    current_memory: usize,
208    profiler: Option<Arc<MemoryProfiler>>,
209}
210
211#[derive(Clone)]
212struct CachedResult<F> {
213    value: F,
214    timestamp: Instant,
215    memorysize: usize,
216    access_count: usize,
217}
218
219impl<F: Float + Clone + std::fmt::Display> StatisticsCache<F> {
220    pub fn new(_max_entries: usize, maxmemory: usize) -> Self {
221        Self {
222            cache: HashMap::new(),
223            max_entries: _max_entries,
224            max_memory: maxmemory,
225            current_memory: 0,
226            profiler: None,
227        }
228    }
229
230    pub fn with_profiler(mut self, profiler: Arc<MemoryProfiler>) -> Self {
231        self.profiler = Some(profiler);
232        self
233    }
234
235    /// Cache a computed result
236    pub fn put(&mut self, key: String, value: F) {
237        let memorysize = std::mem::size_of::<F>() + key.len();
238
239        // Check if we need to evict entries
240        self.maybe_evict(memorysize);
241
242        let cached_result = CachedResult {
243            value,
244            timestamp: Instant::now(),
245            memorysize,
246            access_count: 0,
247        };
248
249        if let Some(old_result) = self.cache.insert(key.clone(), cached_result) {
250            self.current_memory -= old_result.memorysize;
251        }
252
253        self.current_memory += memorysize;
254
255        if let Some(profiler) = &self.profiler {
256            profiler.record_allocation("statistics_cache", memorysize, Duration::from_nanos(0));
257        }
258    }
259
260    /// Retrieve a cached result
261    pub fn get(&mut self, key: &str) -> Option<F> {
262        if let Some(entry) = self.cache.get_mut(key) {
263            entry.access_count += 1;
264            Some(entry.value)
265        } else {
266            None
267        }
268    }
269
270    /// Evict entries to make room for new ones
271    fn maybe_evict(&mut self, neededsize: usize) {
272        // Check memory limit
273        while self.current_memory + neededsize > self.max_memory && !self.cache.is_empty() {
274            self.evict_lru();
275        }
276
277        // Check entry count limit
278        while self.cache.len() >= self.max_entries && !self.cache.is_empty() {
279            self.evict_lru();
280        }
281    }
282
283    /// Evict least recently used entry
284    fn evict_lru(&mut self) {
285        if let Some((key_to_remove, entry_to_remove)) = self
286            .cache
287            .iter()
288            .min_by_key(|(_, entry)| (entry.access_count, entry.timestamp))
289            .map(|(k, v)| (k.clone(), v.clone()))
290        {
291            self.cache.remove(&key_to_remove);
292            self.current_memory -= entry_to_remove.memorysize;
293
294            if let Some(profiler) = &self.profiler {
295                profiler.record_deallocation(entry_to_remove.memorysize);
296            }
297        }
298    }
299
300    /// Get cache statistics
301    pub fn get_stats(&self) -> CacheStats {
302        CacheStats {
303            entries: self.cache.len(),
304            memory_usage: self.current_memory,
305            hit_rate: self.calculate_hit_rate(),
306        }
307    }
308
309    fn calculate_hit_rate(&self) -> f64 {
310        let total_accesses: usize = self.cache.values().map(|entry| entry.access_count).sum();
311        if total_accesses == 0 {
312            0.0
313        } else {
314            total_accesses as f64 / (total_accesses + self.cache.len()) as f64
315        }
316    }
317
318    /// Clear the cache
319    pub fn clear(&mut self) {
320        if let Some(profiler) = &self.profiler {
321            for entry in self.cache.values() {
322                profiler.record_deallocation(entry.memorysize);
323            }
324        }
325
326        self.cache.clear();
327        self.current_memory = 0;
328    }
329}
330
331#[derive(Debug)]
332pub struct CacheStats {
333    pub entries: usize,
334    pub memory_usage: usize,
335    pub hit_rate: f64,
336}
337
338/// Adaptive memory manager that adjusts algorithms based on available memory
339pub struct AdaptiveMemoryManager {
340    memory_threshold_low: usize,
341    memory_threshold_high: usize,
342    profiler: Arc<MemoryProfiler>,
343}
344
345impl AdaptiveMemoryManager {
346    pub fn new(profiler: Arc<MemoryProfiler>) -> Self {
347        Self {
348            memory_threshold_low: 100 * 1024 * 1024,   // 100MB
349            memory_threshold_high: 1024 * 1024 * 1024, // 1GB
350            profiler,
351        }
352    }
353
354    /// Choose optimal algorithm based on current memory usage
355    pub fn choose_algorithm(&self, datasize: usize) -> AlgorithmChoice {
356        let current_memory = *self
357            .profiler
358            .current_memory
359            .lock()
360            .expect("Operation failed");
361
362        if current_memory > self.memory_threshold_high {
363            // High memory usage - use most memory-efficient algorithms
364            if datasize > 1_000_000 {
365                AlgorithmChoice::Streaming
366            } else {
367                AlgorithmChoice::InPlace
368            }
369        } else if current_memory > self.memory_threshold_low {
370            // Medium memory usage - balance speed and memory
371            if datasize > 100_000 {
372                AlgorithmChoice::Chunked
373            } else {
374                AlgorithmChoice::Standard
375            }
376        } else {
377            // Low memory usage - prioritize speed
378            if datasize > 10_000 {
379                AlgorithmChoice::Parallel
380            } else {
381                AlgorithmChoice::Standard
382            }
383        }
384    }
385
386    /// Suggest chunk size based on available memory
387    pub fn suggest_chunksize(&self, datasize: usize, elementsize: usize) -> usize {
388        let current_memory = *self
389            .profiler
390            .current_memory
391            .lock()
392            .expect("Operation failed");
393        let available_memory = self.memory_threshold_high.saturating_sub(current_memory);
394
395        // Use at most 10% of available memory for chunking
396        let max_chunk_memory = available_memory / 10;
397        let max_chunk_elements = max_chunk_memory / elementsize;
398
399        // Clamp to reasonable bounds
400        max_chunk_elements.clamp(1000, datasize / 4)
401    }
402}
403
404#[derive(Debug, Clone, Copy, PartialEq)]
405pub enum AlgorithmChoice {
406    Standard,  // Normal algorithms
407    InPlace,   // In-place algorithms to save memory
408    Chunked,   // Process data in chunks
409    Streaming, // Stream processing for minimal memory
410    Parallel,  // Parallel algorithms for speed
411}
412
413/// Memory-efficient statistical operations with profiling
414pub struct ProfiledStatistics<F> {
415    profiler: Arc<MemoryProfiler>,
416    cache: StatisticsCache<F>,
417    adaptive_manager: AdaptiveMemoryManager,
418}
419
420impl<F> ProfiledStatistics<F>
421where
422    F: Float + NumCast + Clone + Send + Sync + std::fmt::Display,
423{
424    pub fn new(profiler: Arc<MemoryProfiler>) -> Self {
425        let cache = StatisticsCache::new(1000, 50 * 1024 * 1024) // 50MB cache
426            .with_profiler(profiler.clone());
427        let adaptive_manager = AdaptiveMemoryManager::new(profiler.clone());
428
429        Self {
430            profiler: profiler.clone(),
431            cache,
432            adaptive_manager,
433        }
434    }
435
436    /// Compute mean with memory profiling and caching
437    pub fn mean_profiled<D>(&mut self, data: &ArrayBase<D, Ix1>) -> StatsResult<F>
438    where
439        D: Data<Elem = F>,
440    {
441        let start_time = Instant::now();
442
443        // Generate cache key
444        let cache_key = format!("mean_{}", data.len());
445
446        // Check cache first
447        if let Some(cached_result) = self.cache.get(&cache_key) {
448            return Ok(cached_result);
449        }
450
451        // Choose algorithm based on memory situation
452        let algorithm = self.adaptive_manager.choose_algorithm(data.len());
453
454        let result = match algorithm {
455            AlgorithmChoice::Streaming => self.compute_mean_streaming(data),
456            AlgorithmChoice::Chunked => self.compute_mean_chunked(data),
457            _ => self.compute_mean_standard(data),
458        }?;
459
460        // Record allocation timing
461        let duration = start_time.elapsed();
462        self.profiler.record_allocation(
463            "mean_computation",
464            data.len() * std::mem::size_of::<F>(),
465            duration,
466        );
467
468        // Cache result
469        self.cache.put(cache_key, result);
470
471        Ok(result)
472    }
473
474    fn compute_mean_streaming<D>(&self, data: &ArrayBase<D, Ix1>) -> StatsResult<F>
475    where
476        D: Data<Elem = F>,
477    {
478        // Streaming mean computation (minimal memory)
479        let mut sum = F::zero();
480        let mut count = 0;
481
482        for &value in data.iter() {
483            sum = sum + value;
484            count += 1;
485        }
486
487        if count == 0 {
488            return Err(StatsError::invalid_argument(
489                "Cannot compute mean of empty array",
490            ));
491        }
492
493        Ok(sum / F::from(count).expect("Failed to convert to float"))
494    }
495
496    fn compute_mean_chunked<D>(&self, data: &ArrayBase<D, Ix1>) -> StatsResult<F>
497    where
498        D: Data<Elem = F>,
499    {
500        // Chunked processing
501        let chunksize = self
502            .adaptive_manager
503            .suggest_chunksize(data.len(), std::mem::size_of::<F>());
504        let mut total_sum = F::zero();
505        let mut total_count = 0;
506
507        for chunk_start in (0..data.len()).step_by(chunksize) {
508            let chunk_end = (chunk_start + chunksize).min(data.len());
509            let chunk = data.slice(scirs2_core::ndarray::s![chunk_start..chunk_end]);
510
511            let chunk_sum = chunk.iter().fold(F::zero(), |acc, &x| acc + x);
512            total_sum = total_sum + chunk_sum;
513            total_count += chunk.len();
514        }
515
516        if total_count == 0 {
517            return Err(StatsError::invalid_argument(
518                "Cannot compute mean of empty array",
519            ));
520        }
521
522        Ok(total_sum / F::from(total_count).expect("Failed to convert to float"))
523    }
524
525    fn compute_mean_standard<D>(&self, data: &ArrayBase<D, Ix1>) -> StatsResult<F>
526    where
527        D: Data<Elem = F>,
528    {
529        // Standard computation
530        let sum = data.iter().fold(F::zero(), |acc, &x| acc + x);
531        let count = data.len();
532
533        if count == 0 {
534            return Err(StatsError::invalid_argument(
535                "Cannot compute mean of empty array",
536            ));
537        }
538
539        Ok(sum / F::from(count).expect("Failed to convert to float"))
540    }
541
542    /// Get memory report
543    pub fn get_memory_report(&self) -> MemoryReport {
544        self.profiler.get_report()
545    }
546
547    /// Get cache statistics
548    pub fn get_cache_stats(&self) -> CacheStats {
549        self.cache.get_stats()
550    }
551}
552
553#[cfg(test)]
554mod tests {
555    use super::*;
556    use approx::assert_relative_eq;
557    use scirs2_core::ndarray::array;
558
559    #[test]
560    fn test_memory_profiler() {
561        let profiler = MemoryProfiler::new();
562
563        profiler.record_allocation("test", 1024, Duration::from_millis(5));
564        profiler.record_allocation("test", 2048, Duration::from_millis(10));
565
566        let report = profiler.get_report();
567
568        assert_eq!(report.allocations["test"].total_allocations, 2);
569        assert_eq!(report.allocations["test"].total_bytes, 3072);
570        assert_eq!(report.allocations["test"].peak_bytes, 2048);
571    }
572
573    #[test]
574    fn test_statistics_cache() {
575        let mut cache = StatisticsCache::new(2, 1024);
576
577        cache.put("key1".to_string(), 42.0);
578        cache.put("key2".to_string(), 24.0);
579
580        assert_eq!(cache.get("key1"), Some(42.0));
581        assert_eq!(cache.get("key2"), Some(24.0));
582        assert_eq!(cache.get("key3"), None);
583
584        // Test eviction
585        cache.put("key3".to_string(), 12.0);
586        assert_eq!(cache.cache.len(), 2); // Should evict least recently used
587    }
588
589    #[test]
590    fn test_profiled_statistics() {
591        let profiler = Arc::new(MemoryProfiler::new());
592        let mut stats = ProfiledStatistics::new(profiler.clone());
593
594        let data = array![1.0, 2.0, 3.0, 4.0, 5.0];
595        let mean = stats.mean_profiled(&data.view()).expect("Operation failed");
596
597        assert_relative_eq!(mean, 3.0, epsilon = 1e-10);
598
599        // Test caching
600        let mean2 = stats.mean_profiled(&data.view()).expect("Operation failed");
601        assert_relative_eq!(mean2, 3.0, epsilon = 1e-10);
602
603        let report = stats.get_memory_report();
604        assert!(!report.allocations.is_empty());
605    }
606
607    #[test]
608    fn test_adaptive_memory_manager() {
609        let profiler = Arc::new(MemoryProfiler::new());
610        let manager = AdaptiveMemoryManager::new(profiler);
611
612        // Test algorithm choice for different data sizes
613        let choice_small = manager.choose_algorithm(1000);
614        let choice_large = manager.choose_algorithm(1_000_000);
615
616        // Should choose different algorithms based on size
617        assert_ne!(choice_small, choice_large);
618
619        // Test chunk size suggestion
620        let chunksize = manager.suggest_chunksize(100_000, 8);
621        assert!(chunksize > 0);
622        assert!(chunksize <= 25_000); // Should be reasonable fraction of data size
623    }
624}