1use 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#[derive(Debug, Clone)]
18pub struct MemorySnapshot {
19 pub virtual_memory: u64,
21 pub resident_memory: u64,
23 pub timestamp: Instant,
25 pub label: Option<String>,
27}
28
29impl MemorySnapshot {
30 pub fn take() -> Self {
32 Self::take_with_label(None)
33 }
34
35 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 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#[derive(Debug, Clone)]
58pub struct MemoryDiff {
59 pub virtual_memory_delta: i64,
61 pub resident_memory_delta: i64,
63 pub duration: Duration,
65}
66
67impl MemoryDiff {
68 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 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
85pub struct MemoryLeakDetector {
87 snapshots: Vec<MemorySnapshot>,
89 leak_threshold: u64,
91 max_snapshots: usize,
93}
94
95impl MemoryLeakDetector {
96 pub fn new() -> Self {
98 Self {
99 snapshots: Vec::new(),
100 leak_threshold: 10_000_000, max_snapshots: 1000,
102 }
103 }
104
105 pub fn with_threshold(mut self, threshold: u64) -> Self {
107 self.leak_threshold = threshold;
108 self
109 }
110
111 pub fn with_max_snapshots(mut self, max: usize) -> Self {
113 self.max_snapshots = max;
114 self
115 }
116
117 pub fn snapshot(&mut self) -> &MemorySnapshot {
119 self.snapshot_with_label(None)
120 }
121
122 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 if self.snapshots.len() > self.max_snapshots {
129 self.snapshots.remove(0);
130 }
131
132 self.snapshots.last().expect("empty collection")
133 }
134
135 pub fn get_snapshots(&self) -> &[MemorySnapshot] {
137 &self.snapshots
138 }
139
140 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 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 pub fn clear(&mut self) {
200 self.snapshots.clear();
201 }
202
203 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#[derive(Debug, Clone)]
217pub struct MemoryStats {
218 pub virtual_memory_min: u64,
220 pub virtual_memory_max: u64,
222 pub virtual_memory_avg: f64,
224 pub resident_memory_min: u64,
226 pub resident_memory_max: u64,
228 pub resident_memory_avg: f64,
230 pub total_snapshots: usize,
232 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#[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; }
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; }
269 }
270 }
271
272 (vm_size, vm_rss)
273 } else {
274 (0, 0)
275 }
276}
277
278#[cfg(target_os = "macos")]
280fn get_memory_usage() -> (u64, u64) {
281 use std::process::Command;
282
283 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; let rss = values[1].parse::<u64>().unwrap_or(0) * 1024; return (vsz, rss);
297 }
298 }
299 }
300 }
301
302 (0, 0)
303}
304
305#[cfg(not(any(target_os = "linux", target_os = "macos")))]
307fn get_memory_usage() -> (u64, u64) {
308 (0, 0)
310}
311
312pub struct MemoryLeakTestSuite;
314
315impl MemoryLeakTestSuite {
316 pub fn test_mlp_classifier_training() -> Result<MemoryStats, Box<dyn std::error::Error>> {
318 let mut detector = MemoryLeakDetector::new().with_threshold(5_000_000); 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 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 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 #[cfg(feature = "force_gc")]
352 {
353 std::gc::collect();
354 }
355
356 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 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 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 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 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 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 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 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 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, resident_memory_delta: 5_000_000, duration: Duration::from_secs(1),
582 };
583
584 assert!(diff.is_potential_leak(10_000_000)); assert!(!diff.is_potential_leak(20_000_000)); let growth_rate = diff.memory_growth_rate();
588 assert!((growth_rate - 5_000_000.0).abs() < 1.0); }
590
591 #[test]
592 fn test_memory_usage_function() {
593 let (virtual_mem, resident_mem) = get_memory_usage();
594 #[cfg(any(target_os = "linux", target_os = "macos"))]
597 {
598 assert!(virtual_mem > 0 || resident_mem > 0); }
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}