Skip to main content

sklears_neural/
memory_leak_tests.rs

1//! Memory leak detection tests for neural network training and inference.
2//!
3//! This module provides utilities to detect and test for memory leaks in
4//! neural network operations, including training loops, batch processing,
5//! and model inference. It monitors memory usage patterns and identifies
6//! potential memory leaks.
7
8use crate::activation::Activation;
9use crate::mlp_classifier::MLPClassifier;
10use crate::mlp_regressor::MLPRegressor;
11use crate::solvers::Solver;
12use scirs2_core::ndarray::Array2;
13use sklears_core::traits::{Fit, Predict};
14use std::time::{Duration, Instant};
15
16/// Memory usage snapshot
17#[derive(Debug, Clone)]
18pub struct MemorySnapshot {
19    /// Virtual memory size in bytes
20    pub virtual_memory: u64,
21    /// Resident set size in bytes
22    pub resident_memory: u64,
23    /// Timestamp when snapshot was taken
24    pub timestamp: Instant,
25    /// Optional label for the snapshot
26    pub label: Option<String>,
27}
28
29impl MemorySnapshot {
30    /// Take a new memory snapshot
31    pub fn take() -> Self {
32        Self::take_with_label(None)
33    }
34
35    /// Take a memory snapshot with a label
36    pub fn take_with_label(label: Option<String>) -> Self {
37        let (virtual_memory, resident_memory) = get_memory_usage();
38        Self {
39            virtual_memory,
40            resident_memory,
41            timestamp: Instant::now(),
42            label,
43        }
44    }
45
46    /// Calculate memory difference from another snapshot
47    pub fn diff_from(&self, other: &MemorySnapshot) -> MemoryDiff {
48        MemoryDiff {
49            virtual_memory_delta: self.virtual_memory as i64 - other.virtual_memory as i64,
50            resident_memory_delta: self.resident_memory as i64 - other.resident_memory as i64,
51            duration: self.timestamp.duration_since(other.timestamp),
52        }
53    }
54}
55
56/// Memory usage difference between snapshots
57#[derive(Debug, Clone)]
58pub struct MemoryDiff {
59    /// Change in virtual memory (bytes)
60    pub virtual_memory_delta: i64,
61    /// Change in resident memory (bytes)
62    pub resident_memory_delta: i64,
63    /// Duration between snapshots
64    pub duration: Duration,
65}
66
67impl MemoryDiff {
68    /// Check if this represents a potential memory leak
69    pub fn is_potential_leak(&self, threshold_bytes: u64) -> bool {
70        self.virtual_memory_delta > threshold_bytes as i64
71            || self.resident_memory_delta > threshold_bytes as i64
72    }
73
74    /// Get memory growth rate in bytes per second
75    pub fn memory_growth_rate(&self) -> f64 {
76        let duration_secs = self.duration.as_secs_f64();
77        if duration_secs > 0.0 {
78            self.resident_memory_delta as f64 / duration_secs
79        } else {
80            0.0
81        }
82    }
83}
84
85/// Memory leak detector for tracking memory usage over time
86pub struct MemoryLeakDetector {
87    /// History of memory snapshots
88    snapshots: Vec<MemorySnapshot>,
89    /// Threshold for leak detection (bytes)
90    leak_threshold: u64,
91    /// Maximum number of snapshots to keep
92    max_snapshots: usize,
93}
94
95impl MemoryLeakDetector {
96    /// Create a new memory leak detector
97    pub fn new() -> Self {
98        Self {
99            snapshots: Vec::new(),
100            leak_threshold: 10_000_000, // 10MB default threshold
101            max_snapshots: 1000,
102        }
103    }
104
105    /// Set the leak detection threshold in bytes
106    pub fn with_threshold(mut self, threshold: u64) -> Self {
107        self.leak_threshold = threshold;
108        self
109    }
110
111    /// Set maximum number of snapshots to keep
112    pub fn with_max_snapshots(mut self, max: usize) -> Self {
113        self.max_snapshots = max;
114        self
115    }
116
117    /// Take a new memory snapshot and add to history
118    pub fn snapshot(&mut self) -> &MemorySnapshot {
119        self.snapshot_with_label(None)
120    }
121
122    /// Take a labeled memory snapshot
123    pub fn snapshot_with_label(&mut self, label: Option<String>) -> &MemorySnapshot {
124        let snapshot = MemorySnapshot::take_with_label(label);
125        self.snapshots.push(snapshot);
126
127        // Keep only the most recent snapshots
128        if self.snapshots.len() > self.max_snapshots {
129            self.snapshots.remove(0);
130        }
131
132        self.snapshots.last().expect("empty collection")
133    }
134
135    /// Get all memory snapshots
136    pub fn get_snapshots(&self) -> &[MemorySnapshot] {
137        &self.snapshots
138    }
139
140    /// Check for potential memory leaks between consecutive snapshots
141    pub fn detect_leaks(&self) -> Vec<(usize, MemoryDiff)> {
142        let mut leaks = Vec::new();
143
144        for i in 1..self.snapshots.len() {
145            let diff = self.snapshots[i].diff_from(&self.snapshots[i - 1]);
146            if diff.is_potential_leak(self.leak_threshold) {
147                leaks.push((i, diff));
148            }
149        }
150
151        leaks
152    }
153
154    /// Get memory usage statistics
155    pub fn get_statistics(&self) -> MemoryStats {
156        if self.snapshots.is_empty() {
157            return MemoryStats::default();
158        }
159
160        let virtual_memories: Vec<u64> = self.snapshots.iter().map(|s| s.virtual_memory).collect();
161        let resident_memories: Vec<u64> =
162            self.snapshots.iter().map(|s| s.resident_memory).collect();
163
164        let min_virtual = *virtual_memories
165            .iter()
166            .min()
167            .expect("collection should not be empty");
168        let max_virtual = *virtual_memories
169            .iter()
170            .max()
171            .expect("collection should not be empty");
172        let avg_virtual =
173            virtual_memories.iter().sum::<u64>() as f64 / virtual_memories.len() as f64;
174
175        let min_resident = *resident_memories
176            .iter()
177            .min()
178            .expect("collection should not be empty");
179        let max_resident = *resident_memories
180            .iter()
181            .max()
182            .expect("collection should not be empty");
183        let avg_resident =
184            resident_memories.iter().sum::<u64>() as f64 / resident_memories.len() as f64;
185
186        MemoryStats {
187            virtual_memory_min: min_virtual,
188            virtual_memory_max: max_virtual,
189            virtual_memory_avg: avg_virtual,
190            resident_memory_min: min_resident,
191            resident_memory_max: max_resident,
192            resident_memory_avg: avg_resident,
193            total_snapshots: self.snapshots.len(),
194            potential_leaks: self.detect_leaks().len(),
195        }
196    }
197
198    /// Clear all snapshots
199    pub fn clear(&mut self) {
200        self.snapshots.clear();
201    }
202
203    /// Get the current leak threshold
204    pub fn get_leak_threshold(&self) -> u64 {
205        self.leak_threshold
206    }
207}
208
209impl Default for MemoryLeakDetector {
210    fn default() -> Self {
211        Self::new()
212    }
213}
214
215/// Memory usage statistics
216#[derive(Debug, Clone)]
217pub struct MemoryStats {
218    /// Minimum virtual memory observed across all snapshots (bytes)
219    pub virtual_memory_min: u64,
220    /// Maximum virtual memory observed across all snapshots (bytes)
221    pub virtual_memory_max: u64,
222    /// Mean virtual memory across all snapshots (bytes)
223    pub virtual_memory_avg: f64,
224    /// Minimum resident (physical) memory across all snapshots (bytes)
225    pub resident_memory_min: u64,
226    /// Maximum resident (physical) memory across all snapshots (bytes)
227    pub resident_memory_max: u64,
228    /// Mean resident (physical) memory across all snapshots (bytes)
229    pub resident_memory_avg: f64,
230    /// Number of memory snapshots taken during the monitoring period
231    pub total_snapshots: usize,
232    /// Number of allocation sites that appear to be leaking memory
233    pub potential_leaks: usize,
234}
235
236impl Default for MemoryStats {
237    fn default() -> Self {
238        Self {
239            virtual_memory_min: 0,
240            virtual_memory_max: 0,
241            virtual_memory_avg: 0.0,
242            resident_memory_min: 0,
243            resident_memory_max: 0,
244            resident_memory_avg: 0.0,
245            total_snapshots: 0,
246            potential_leaks: 0,
247        }
248    }
249}
250
251/// Get current memory usage (platform-specific implementation)
252#[cfg(target_os = "linux")]
253fn get_memory_usage() -> (u64, u64) {
254    use std::fs;
255
256    if let Ok(contents) = fs::read_to_string("/proc/self/status") {
257        let mut vm_size = 0u64;
258        let mut vm_rss = 0u64;
259
260        for line in contents.lines() {
261            if line.starts_with("VmSize:") {
262                if let Some(size_str) = line.split_whitespace().nth(1) {
263                    vm_size = size_str.parse::<u64>().unwrap_or(0) * 1024; // Convert kB to bytes
264                }
265            } else if line.starts_with("VmRSS:") {
266                if let Some(rss_str) = line.split_whitespace().nth(1) {
267                    vm_rss = rss_str.parse::<u64>().unwrap_or(0) * 1024; // Convert kB to bytes
268                }
269            }
270        }
271
272        (vm_size, vm_rss)
273    } else {
274        (0, 0)
275    }
276}
277
278/// Get current memory usage (macOS implementation)
279#[cfg(target_os = "macos")]
280fn get_memory_usage() -> (u64, u64) {
281    use std::process::Command;
282
283    // Use ps command to get memory info
284    if let Ok(output) = Command::new("ps")
285        .args(["-o", "vsz,rss", "-p"])
286        .arg(std::process::id().to_string())
287        .output()
288    {
289        if let Ok(output_str) = String::from_utf8(output.stdout) {
290            let lines: Vec<&str> = output_str.trim().lines().collect();
291            if lines.len() >= 2 {
292                let values: Vec<&str> = lines[1].split_whitespace().collect();
293                if values.len() >= 2 {
294                    let vsz = values[0].parse::<u64>().unwrap_or(0) * 1024; // Convert kB to bytes
295                    let rss = values[1].parse::<u64>().unwrap_or(0) * 1024; // Convert kB to bytes
296                    return (vsz, rss);
297                }
298            }
299        }
300    }
301
302    (0, 0)
303}
304
305/// Fallback implementation for other platforms
306#[cfg(not(any(target_os = "linux", target_os = "macos")))]
307fn get_memory_usage() -> (u64, u64) {
308    // Return zeros for unsupported platforms
309    (0, 0)
310}
311
312/// Memory leak test suite for neural networks
313pub struct MemoryLeakTestSuite;
314
315impl MemoryLeakTestSuite {
316    /// Test for memory leaks during MLP classifier training
317    pub fn test_mlp_classifier_training() -> Result<MemoryStats, Box<dyn std::error::Error>> {
318        let mut detector = MemoryLeakDetector::new().with_threshold(5_000_000); // 5MB threshold
319
320        // Generate test data
321        let n_samples = 1000;
322        let n_features = 20;
323        let mut x = Array2::zeros((n_samples, n_features));
324        let mut y = vec![0; n_samples];
325
326        // Simple binary classification data
327        for i in 0..n_samples {
328            for j in 0..n_features {
329                x[[i, j]] = if j % 2 == 0 { 1.0 } else { -1.0 };
330            }
331            y[i] = if i % 2 == 0 { 0 } else { 1 };
332        }
333
334        detector.snapshot_with_label(Some("Initial".to_string()));
335
336        // Test multiple training runs to detect leaks
337        for iteration in 0..10 {
338            let classifier = MLPClassifier::new()
339                .hidden_layer_sizes(&[50, 30])
340                .activation(Activation::Relu)
341                .solver(Solver::Adam)
342                .learning_rate_init(0.001)
343                .max_iter(50)
344                .random_state(42);
345
346            let _trained = classifier.fit(&x, &y)?;
347
348            detector.snapshot_with_label(Some(format!("Training iteration {}", iteration + 1)));
349
350            // Force garbage collection if available
351            #[cfg(feature = "force_gc")]
352            {
353                std::gc::collect();
354            }
355
356            // Small delay to allow cleanup
357            std::thread::sleep(Duration::from_millis(100));
358        }
359
360        detector.snapshot_with_label(Some("Final".to_string()));
361
362        let stats = detector.get_statistics();
363        let leaks = detector.detect_leaks();
364
365        if !leaks.is_empty() {
366            println!("Potential memory leaks detected in MLP classifier training:");
367            for (idx, diff) in &leaks {
368                println!(
369                    "  Snapshot {}: +{} bytes virtual, +{} bytes resident ({:.2} bytes/sec)",
370                    idx,
371                    diff.virtual_memory_delta,
372                    diff.resident_memory_delta,
373                    diff.memory_growth_rate()
374                );
375            }
376        }
377
378        Ok(stats)
379    }
380
381    /// Test for memory leaks during MLP regressor training
382    pub fn test_mlp_regressor_training() -> Result<MemoryStats, Box<dyn std::error::Error>> {
383        let mut detector = MemoryLeakDetector::new().with_threshold(5_000_000);
384
385        // Generate test data
386        let n_samples = 1000;
387        let n_features = 20;
388        let mut x = Array2::zeros((n_samples, n_features));
389        let mut y = Array2::zeros((n_samples, 1));
390
391        // Simple regression data
392        for i in 0..n_samples {
393            for j in 0..n_features {
394                x[[i, j]] = (i as f64) / 100.0 + (j as f64) * 0.1;
395            }
396            y[[i, 0]] = x.row(i).sum();
397        }
398
399        detector.snapshot_with_label(Some("Initial".to_string()));
400
401        // Test multiple training runs
402        for iteration in 0..10 {
403            let regressor = MLPRegressor::new()
404                .hidden_layer_sizes(&[50, 30])
405                .activation(Activation::Relu)
406                .solver(Solver::Adam)
407                .learning_rate_init(0.001)
408                .max_iter(50)
409                .random_state(42);
410
411            let _trained = regressor.fit(&x, &y)?;
412
413            detector.snapshot_with_label(Some(format!("Training iteration {}", iteration + 1)));
414
415            std::thread::sleep(Duration::from_millis(100));
416        }
417
418        detector.snapshot_with_label(Some("Final".to_string()));
419        Ok(detector.get_statistics())
420    }
421
422    /// Test for memory leaks during batch prediction
423    pub fn test_batch_prediction() -> Result<MemoryStats, Box<dyn std::error::Error>> {
424        let mut detector = MemoryLeakDetector::new().with_threshold(2_000_000);
425
426        // Create a trained classifier
427        let n_samples = 500;
428        let n_features = 10;
429        let mut x_train = Array2::zeros((n_samples, n_features));
430        let mut y_train = vec![0; n_samples];
431
432        for i in 0..n_samples {
433            for j in 0..n_features {
434                x_train[[i, j]] = if (i + j) % 2 == 0 { 1.0 } else { -1.0 };
435            }
436            y_train[i] = if i % 2 == 0 { 0 } else { 1 };
437        }
438
439        let classifier = MLPClassifier::new()
440            .hidden_layer_sizes(&[20, 10])
441            .max_iter(10)
442            .random_state(42);
443
444        let trained = classifier.fit(&x_train, &y_train)?;
445
446        detector.snapshot_with_label(Some("After training".to_string()));
447
448        // Test multiple batch predictions
449        let batch_size = 1000;
450        for batch in 0..20 {
451            let mut x_batch = Array2::zeros((batch_size, n_features));
452            for i in 0..batch_size {
453                for j in 0..n_features {
454                    x_batch[[i, j]] = ((batch * batch_size + i) as f64) * 0.01;
455                }
456            }
457
458            let _predictions = trained.predict(&x_batch)?;
459
460            detector.snapshot_with_label(Some(format!("Batch prediction {}", batch + 1)));
461
462            std::thread::sleep(Duration::from_millis(50));
463        }
464
465        detector.snapshot_with_label(Some("Final".to_string()));
466        Ok(detector.get_statistics())
467    }
468
469    /// Run all memory leak tests
470    pub fn run_all_tests() -> Result<(), Box<dyn std::error::Error>> {
471        println!("Running memory leak detection tests...\n");
472
473        println!("1. Testing MLP classifier training for memory leaks:");
474        let classifier_stats = Self::test_mlp_classifier_training()?;
475        println!(
476            "   Memory usage: {:.2} MB avg virtual, {:.2} MB avg resident",
477            classifier_stats.virtual_memory_avg / 1_000_000.0,
478            classifier_stats.resident_memory_avg / 1_000_000.0
479        );
480        if classifier_stats.potential_leaks > 0 {
481            println!(
482                "   ⚠️  {} potential leaks detected!",
483                classifier_stats.potential_leaks
484            );
485        } else {
486            println!("   ✅ No memory leaks detected");
487        }
488
489        println!("\n2. Testing MLP regressor training for memory leaks:");
490        let regressor_stats = Self::test_mlp_regressor_training()?;
491        println!(
492            "   Memory usage: {:.2} MB avg virtual, {:.2} MB avg resident",
493            regressor_stats.virtual_memory_avg / 1_000_000.0,
494            regressor_stats.resident_memory_avg / 1_000_000.0
495        );
496        if regressor_stats.potential_leaks > 0 {
497            println!(
498                "   ⚠️  {} potential leaks detected!",
499                regressor_stats.potential_leaks
500            );
501        } else {
502            println!("   ✅ No memory leaks detected");
503        }
504
505        println!("\n3. Testing batch prediction for memory leaks:");
506        let prediction_stats = Self::test_batch_prediction()?;
507        println!(
508            "   Memory usage: {:.2} MB avg virtual, {:.2} MB avg resident",
509            prediction_stats.virtual_memory_avg / 1_000_000.0,
510            prediction_stats.resident_memory_avg / 1_000_000.0
511        );
512        if prediction_stats.potential_leaks > 0 {
513            println!(
514                "   ⚠️  {} potential leaks detected!",
515                prediction_stats.potential_leaks
516            );
517        } else {
518            println!("   ✅ No memory leaks detected");
519        }
520
521        println!("\nMemory leak detection tests completed.");
522        Ok(())
523    }
524}
525
526#[allow(non_snake_case)]
527#[cfg(test)]
528mod tests {
529    use super::*;
530
531    #[test]
532    fn test_memory_snapshot() {
533        let snapshot1 = MemorySnapshot::take();
534        std::thread::sleep(Duration::from_millis(10));
535        let snapshot2 = MemorySnapshot::take();
536
537        let diff = snapshot2.diff_from(&snapshot1);
538        assert!(diff.duration > Duration::from_millis(5));
539    }
540
541    #[test]
542    fn test_memory_leak_detector() {
543        let mut detector = MemoryLeakDetector::new().with_threshold(1_000_000);
544
545        detector.snapshot_with_label(Some("Start".to_string()));
546        detector.snapshot_with_label(Some("End".to_string()));
547
548        let stats = detector.get_statistics();
549        assert_eq!(stats.total_snapshots, 2);
550    }
551
552    #[test]
553    fn test_memory_diff() {
554        let snapshot1 = MemorySnapshot {
555            virtual_memory: 1_000_000,
556            resident_memory: 500_000,
557            timestamp: Instant::now(),
558            label: None,
559        };
560
561        std::thread::sleep(Duration::from_millis(10));
562
563        let snapshot2 = MemorySnapshot {
564            virtual_memory: 1_100_000,
565            resident_memory: 550_000,
566            timestamp: Instant::now(),
567            label: None,
568        };
569
570        let diff = snapshot2.diff_from(&snapshot1);
571        assert_eq!(diff.virtual_memory_delta, 100_000);
572        assert_eq!(diff.resident_memory_delta, 50_000);
573        assert!(diff.duration > Duration::from_millis(5));
574    }
575
576    #[test]
577    fn test_leak_detection() {
578        let diff = MemoryDiff {
579            virtual_memory_delta: 15_000_000, // 15MB
580            resident_memory_delta: 5_000_000, // 5MB
581            duration: Duration::from_secs(1),
582        };
583
584        assert!(diff.is_potential_leak(10_000_000)); // 10MB threshold
585        assert!(!diff.is_potential_leak(20_000_000)); // 20MB threshold
586
587        let growth_rate = diff.memory_growth_rate();
588        assert!((growth_rate - 5_000_000.0).abs() < 1.0); // ~5MB/sec
589    }
590
591    #[test]
592    fn test_memory_usage_function() {
593        let (virtual_mem, resident_mem) = get_memory_usage();
594        // On supported platforms, we should get some memory usage
595        // On unsupported platforms, both will be 0
596        #[cfg(any(target_os = "linux", target_os = "macos"))]
597        {
598            assert!(virtual_mem > 0 || resident_mem > 0); // At least one should be non-zero
599        }
600        #[cfg(not(any(target_os = "linux", target_os = "macos")))]
601        {
602            assert_eq!(virtual_mem, 0);
603            assert_eq!(resident_mem, 0);
604        }
605    }
606}