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().unwrap();
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().unwrap();
64        *current += size;
65
66        let mut peak = self.peak_memory.lock().unwrap();
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().unwrap();
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().unwrap().clone();
83        let peak_memory = *self.peak_memory.lock().unwrap();
84        let current_memory = *self.current_memory.lock().unwrap();
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().unwrap().clear();
143        *self.peak_memory.lock().unwrap() = 0;
144        *self.current_memory.lock().unwrap() = 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.profiler.current_memory.lock().unwrap();
357
358        if current_memory > self.memory_threshold_high {
359            // High memory usage - use most memory-efficient algorithms
360            if datasize > 1_000_000 {
361                AlgorithmChoice::Streaming
362            } else {
363                AlgorithmChoice::InPlace
364            }
365        } else if current_memory > self.memory_threshold_low {
366            // Medium memory usage - balance speed and memory
367            if datasize > 100_000 {
368                AlgorithmChoice::Chunked
369            } else {
370                AlgorithmChoice::Standard
371            }
372        } else {
373            // Low memory usage - prioritize speed
374            if datasize > 10_000 {
375                AlgorithmChoice::Parallel
376            } else {
377                AlgorithmChoice::Standard
378            }
379        }
380    }
381
382    /// Suggest chunk size based on available memory
383    pub fn suggest_chunksize(&self, datasize: usize, elementsize: usize) -> usize {
384        let current_memory = *self.profiler.current_memory.lock().unwrap();
385        let available_memory = self.memory_threshold_high.saturating_sub(current_memory);
386
387        // Use at most 10% of available memory for chunking
388        let max_chunk_memory = available_memory / 10;
389        let max_chunk_elements = max_chunk_memory / elementsize;
390
391        // Clamp to reasonable bounds
392        max_chunk_elements.clamp(1000, datasize / 4)
393    }
394}
395
396#[derive(Debug, Clone, Copy, PartialEq)]
397pub enum AlgorithmChoice {
398    Standard,  // Normal algorithms
399    InPlace,   // In-place algorithms to save memory
400    Chunked,   // Process data in chunks
401    Streaming, // Stream processing for minimal memory
402    Parallel,  // Parallel algorithms for speed
403}
404
405/// Memory-efficient statistical operations with profiling
406pub struct ProfiledStatistics<F> {
407    profiler: Arc<MemoryProfiler>,
408    cache: StatisticsCache<F>,
409    adaptive_manager: AdaptiveMemoryManager,
410}
411
412impl<F> ProfiledStatistics<F>
413where
414    F: Float + NumCast + Clone + Send + Sync + std::fmt::Display,
415{
416    pub fn new(profiler: Arc<MemoryProfiler>) -> Self {
417        let cache = StatisticsCache::new(1000, 50 * 1024 * 1024) // 50MB cache
418            .with_profiler(profiler.clone());
419        let adaptive_manager = AdaptiveMemoryManager::new(profiler.clone());
420
421        Self {
422            profiler: profiler.clone(),
423            cache,
424            adaptive_manager,
425        }
426    }
427
428    /// Compute mean with memory profiling and caching
429    pub fn mean_profiled<D>(&mut self, data: &ArrayBase<D, Ix1>) -> StatsResult<F>
430    where
431        D: Data<Elem = F>,
432    {
433        let start_time = Instant::now();
434
435        // Generate cache key
436        let cache_key = format!("mean_{}", data.len());
437
438        // Check cache first
439        if let Some(cached_result) = self.cache.get(&cache_key) {
440            return Ok(cached_result);
441        }
442
443        // Choose algorithm based on memory situation
444        let algorithm = self.adaptive_manager.choose_algorithm(data.len());
445
446        let result = match algorithm {
447            AlgorithmChoice::Streaming => self.compute_mean_streaming(data),
448            AlgorithmChoice::Chunked => self.compute_mean_chunked(data),
449            _ => self.compute_mean_standard(data),
450        }?;
451
452        // Record allocation timing
453        let duration = start_time.elapsed();
454        self.profiler.record_allocation(
455            "mean_computation",
456            data.len() * std::mem::size_of::<F>(),
457            duration,
458        );
459
460        // Cache result
461        self.cache.put(cache_key, result);
462
463        Ok(result)
464    }
465
466    fn compute_mean_streaming<D>(&self, data: &ArrayBase<D, Ix1>) -> StatsResult<F>
467    where
468        D: Data<Elem = F>,
469    {
470        // Streaming mean computation (minimal memory)
471        let mut sum = F::zero();
472        let mut count = 0;
473
474        for &value in data.iter() {
475            sum = sum + value;
476            count += 1;
477        }
478
479        if count == 0 {
480            return Err(StatsError::invalid_argument(
481                "Cannot compute mean of empty array",
482            ));
483        }
484
485        Ok(sum / F::from(count).unwrap())
486    }
487
488    fn compute_mean_chunked<D>(&self, data: &ArrayBase<D, Ix1>) -> StatsResult<F>
489    where
490        D: Data<Elem = F>,
491    {
492        // Chunked processing
493        let chunksize = self
494            .adaptive_manager
495            .suggest_chunksize(data.len(), std::mem::size_of::<F>());
496        let mut total_sum = F::zero();
497        let mut total_count = 0;
498
499        for chunk_start in (0..data.len()).step_by(chunksize) {
500            let chunk_end = (chunk_start + chunksize).min(data.len());
501            let chunk = data.slice(scirs2_core::ndarray::s![chunk_start..chunk_end]);
502
503            let chunk_sum = chunk.iter().fold(F::zero(), |acc, &x| acc + x);
504            total_sum = total_sum + chunk_sum;
505            total_count += chunk.len();
506        }
507
508        if total_count == 0 {
509            return Err(StatsError::invalid_argument(
510                "Cannot compute mean of empty array",
511            ));
512        }
513
514        Ok(total_sum / F::from(total_count).unwrap())
515    }
516
517    fn compute_mean_standard<D>(&self, data: &ArrayBase<D, Ix1>) -> StatsResult<F>
518    where
519        D: Data<Elem = F>,
520    {
521        // Standard computation
522        let sum = data.iter().fold(F::zero(), |acc, &x| acc + x);
523        let count = data.len();
524
525        if count == 0 {
526            return Err(StatsError::invalid_argument(
527                "Cannot compute mean of empty array",
528            ));
529        }
530
531        Ok(sum / F::from(count).unwrap())
532    }
533
534    /// Get memory report
535    pub fn get_memory_report(&self) -> MemoryReport {
536        self.profiler.get_report()
537    }
538
539    /// Get cache statistics
540    pub fn get_cache_stats(&self) -> CacheStats {
541        self.cache.get_stats()
542    }
543}
544
545#[cfg(test)]
546mod tests {
547    use super::*;
548    use approx::assert_relative_eq;
549    use scirs2_core::ndarray::array;
550
551    #[test]
552    fn test_memory_profiler() {
553        let profiler = MemoryProfiler::new();
554
555        profiler.record_allocation("test", 1024, Duration::from_millis(5));
556        profiler.record_allocation("test", 2048, Duration::from_millis(10));
557
558        let report = profiler.get_report();
559
560        assert_eq!(report.allocations["test"].total_allocations, 2);
561        assert_eq!(report.allocations["test"].total_bytes, 3072);
562        assert_eq!(report.allocations["test"].peak_bytes, 2048);
563    }
564
565    #[test]
566    fn test_statistics_cache() {
567        let mut cache = StatisticsCache::new(2, 1024);
568
569        cache.put("key1".to_string(), 42.0);
570        cache.put("key2".to_string(), 24.0);
571
572        assert_eq!(cache.get("key1"), Some(42.0));
573        assert_eq!(cache.get("key2"), Some(24.0));
574        assert_eq!(cache.get("key3"), None);
575
576        // Test eviction
577        cache.put("key3".to_string(), 12.0);
578        assert_eq!(cache.cache.len(), 2); // Should evict least recently used
579    }
580
581    #[test]
582    fn test_profiled_statistics() {
583        let profiler = Arc::new(MemoryProfiler::new());
584        let mut stats = ProfiledStatistics::new(profiler.clone());
585
586        let data = array![1.0, 2.0, 3.0, 4.0, 5.0];
587        let mean = stats.mean_profiled(&data.view()).unwrap();
588
589        assert_relative_eq!(mean, 3.0, epsilon = 1e-10);
590
591        // Test caching
592        let mean2 = stats.mean_profiled(&data.view()).unwrap();
593        assert_relative_eq!(mean2, 3.0, epsilon = 1e-10);
594
595        let report = stats.get_memory_report();
596        assert!(!report.allocations.is_empty());
597    }
598
599    #[test]
600    fn test_adaptive_memory_manager() {
601        let profiler = Arc::new(MemoryProfiler::new());
602        let manager = AdaptiveMemoryManager::new(profiler);
603
604        // Test algorithm choice for different data sizes
605        let choice_small = manager.choose_algorithm(1000);
606        let choice_large = manager.choose_algorithm(1_000_000);
607
608        // Should choose different algorithms based on size
609        assert_ne!(choice_small, choice_large);
610
611        // Test chunk size suggestion
612        let chunksize = manager.suggest_chunksize(100_000, 8);
613        assert!(chunksize > 0);
614        assert!(chunksize <= 25_000); // Should be reasonable fraction of data size
615    }
616}