1use crate::error::{StatsError, StatsResult};
16use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19use std::time::{Duration, Instant};
20
21#[derive(Debug)]
23pub struct ScipyBenchmarkFramework {
24 config: BenchmarkConfig,
25 results_cache: HashMap<String, BenchmarkResult>,
26 testdata_generator: TestDataGenerator,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct BenchmarkConfig {
32 pub absolute_tolerance: f64,
34 pub relative_tolerance: f64,
36 pub performance_iterations: usize,
38 pub warmup_iterations: usize,
40 pub max_performance_regression: f64,
42 pub testsizes: Vec<usize>,
44 pub enable_statistical_tests: bool,
46 pub scipy_reference_path: Option<String>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct BenchmarkResult {
53 pub function_name: String,
55 pub datasize: usize,
57 pub accuracy: AccuracyComparison,
59 pub performance: PerformanceComparison,
61 pub status: BenchmarkStatus,
63 pub timestamp: chrono::DateTime<chrono::Utc>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct AccuracyComparison {
70 pub max_abs_difference: f64,
72 pub mean_abs_difference: f64,
74 pub relativeerror: f64,
76 pub outlier_count: usize,
78 pub accuracy_grade: AccuracyGrade,
80 pub passes_tolerance: bool,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct PerformanceComparison {
87 pub scirs2_timing: TimingStatistics,
89 pub scipy_timing: Option<TimingStatistics>,
91 pub performance_ratio: Option<f64>,
93 pub performance_grade: PerformanceGrade,
95 pub memory_usage: MemoryComparison,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct TimingStatistics {
102 pub mean: Duration,
104 pub std_dev: Duration,
106 pub min: Duration,
108 pub max: Duration,
110 pub p50: Duration,
112 pub p95: Duration,
114 pub p99: Duration,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct MemoryComparison {
131 pub peak_memory: usize,
133 pub average_memory: usize,
135 pub efficiency_ratio: Option<f64>,
137}
138
139#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
141pub enum AccuracyGrade {
142 A,
144 B,
146 C,
148 D,
150 F,
152}
153
154#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
156pub enum PerformanceGrade {
157 A,
159 B,
161 C,
163 D,
165 F,
167}
168
169#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
171pub enum BenchmarkStatus {
172 Pass,
174 AccuracyPass,
176 PerformancePass,
178 Fail,
180 Error,
182}
183
184#[derive(Debug)]
186pub struct TestDataGenerator {
187 config: TestDataConfig,
188}
189
190#[derive(Debug, Clone)]
192pub struct TestDataConfig {
193 pub seed: u64,
195 pub include_edge_cases: bool,
197 pub data_distribution: DataDistribution,
199}
200
201#[derive(Debug, Clone)]
203pub enum DataDistribution {
204 Normal,
206 Uniform { min: f64, max: f64 },
208 Exponential { lambda: f64 },
210 Mixed(Vec<DataDistribution>),
212}
213
214impl Default for BenchmarkConfig {
215 fn default() -> Self {
216 Self {
217 absolute_tolerance: 1e-12,
218 relative_tolerance: 1e-9,
219 performance_iterations: 100,
220 warmup_iterations: 10,
221 max_performance_regression: 2.0, testsizes: vec![100, 1000, 10000, 100000],
223 enable_statistical_tests: true,
224 scipy_reference_path: None,
225 }
226 }
227}
228
229impl Default for TestDataConfig {
230 fn default() -> Self {
231 Self {
232 seed: 42,
233 include_edge_cases: true,
234 data_distribution: DataDistribution::Normal,
235 }
236 }
237}
238
239impl ScipyBenchmarkFramework {
240 pub fn new(config: BenchmarkConfig) -> Self {
242 Self {
243 config,
244 results_cache: HashMap::new(),
245 testdata_generator: TestDataGenerator::new(TestDataConfig::default()),
246 }
247 }
248
249 pub fn default() -> Self {
251 Self::new(BenchmarkConfig::default())
252 }
253
254 pub fn benchmark_function<F, G>(
256 &mut self,
257 function_name: &str,
258 scirs2_impl: F,
259 scipy_reference: G,
260 ) -> StatsResult<Vec<BenchmarkResult>>
261 where
262 F: Fn(&ArrayView1<f64>) -> StatsResult<f64>,
263 G: Fn(&ArrayView1<f64>) -> f64,
264 {
265 let mut results = Vec::new();
266
267 for &size in &self.config.testsizes {
268 let testdata = self.testdata_generator.generate_1ddata(size)?;
269
270 let accuracy =
272 self.compare_accuracy(&scirs2_impl, &scipy_reference, &testdata.view())?;
273
274 let performance =
276 self.compare_performance(&scirs2_impl, Some(&scipy_reference), &testdata.view())?;
277
278 let status = self.determine_status(&accuracy, &performance);
280
281 let result = BenchmarkResult {
282 function_name: function_name.to_string(),
283 datasize: size,
284 accuracy,
285 performance,
286 status,
287 timestamp: chrono::Utc::now(),
288 };
289
290 results.push(result.clone());
291 self.results_cache
292 .insert(format!("{}_{}", function_name, size), result);
293 }
294
295 Ok(results)
296 }
297
298 fn compare_accuracy<F, G>(
300 &self,
301 scirs2_impl: &F,
302 scipy_reference: &G,
303 testdata: &ArrayView1<f64>,
304 ) -> StatsResult<AccuracyComparison>
305 where
306 F: Fn(&ArrayView1<f64>) -> StatsResult<f64>,
307 G: Fn(&ArrayView1<f64>) -> f64,
308 {
309 let scirs2_result = scirs2_impl(testdata)?;
310 let scipy_result = scipy_reference(testdata);
311
312 if !scirs2_result.is_finite() || !scipy_result.is_finite() {
319 let agree =
320 (scirs2_result.is_nan() && scipy_result.is_nan()) || scirs2_result == scipy_result; return Ok(AccuracyComparison {
322 max_abs_difference: if agree { 0.0 } else { f64::INFINITY },
323 mean_abs_difference: if agree { 0.0 } else { f64::INFINITY },
324 relativeerror: if agree { 0.0 } else { f64::INFINITY },
325 outlier_count: if agree { 0 } else { 1 },
326 accuracy_grade: if agree {
327 AccuracyGrade::A
328 } else {
329 AccuracyGrade::F
330 },
331 passes_tolerance: agree,
332 });
333 }
334
335 let abs_difference = (scirs2_result - scipy_result).abs();
336 let relativeerror = if scipy_result.abs() > 1e-15 {
337 abs_difference / scipy_result.abs()
338 } else {
339 abs_difference
340 };
341
342 let passes_tolerance = abs_difference <= self.config.absolute_tolerance
343 || relativeerror <= self.config.relative_tolerance;
344
345 let accuracy_grade = self.grade_accuracy(relativeerror);
346
347 Ok(AccuracyComparison {
348 max_abs_difference: abs_difference,
349 mean_abs_difference: abs_difference,
350 relativeerror,
351 outlier_count: if passes_tolerance { 0 } else { 1 },
352 accuracy_grade,
353 passes_tolerance,
354 })
355 }
356
357 fn compare_performance<F, G>(
359 &self,
360 scirs2_impl: &F,
361 scipy_reference: Option<&G>,
362 testdata: &ArrayView1<f64>,
363 ) -> StatsResult<PerformanceComparison>
364 where
365 F: Fn(&ArrayView1<f64>) -> StatsResult<f64>,
366 G: Fn(&ArrayView1<f64>) -> f64,
367 {
368 let (scirs2_timing, scirs2_memory) =
370 self.measure_timing(|| scirs2_impl(testdata).map(|_| ()))?;
371
372 let (scipy_timing, scipy_memory) = if let Some(scipy_func) = scipy_reference {
374 let (timing, memory) = self.measure_timing_scipy(|| {
375 scipy_func(testdata);
376 })?;
377 (Some(timing), Some(memory))
378 } else {
379 (None, None)
380 };
381
382 let performance_ratio = scipy_timing
384 .as_ref()
385 .map(|scipy_stats| scirs2_timing.mean.as_secs_f64() / scipy_stats.mean.as_secs_f64());
386
387 let performance_grade = self.grade_performance(performance_ratio);
388
389 let efficiency_ratio = scipy_memory.as_ref().and_then(|scipy_mem| {
393 if scipy_mem.average_memory > 0 {
394 Some(scirs2_memory.average_memory as f64 / scipy_mem.average_memory as f64)
395 } else {
396 None
397 }
398 });
399
400 Ok(PerformanceComparison {
401 scirs2_timing,
402 scipy_timing,
403 performance_ratio,
404 performance_grade,
405 memory_usage: MemoryComparison {
406 peak_memory: scirs2_memory.peak_memory,
407 average_memory: scirs2_memory.average_memory,
408 efficiency_ratio,
409 },
410 })
411 }
412
413 #[cfg(feature = "memory_tracking")]
427 fn measure_timing<F, R>(&self, mut func: F) -> StatsResult<(TimingStatistics, MemoryComparison)>
428 where
429 F: FnMut() -> StatsResult<R>,
430 {
431 use scirs2_core::profiling::MemoryStats;
432
433 let mut times = Vec::with_capacity(self.config.performance_iterations);
434 let mut memory_deltas = Vec::with_capacity(self.config.performance_iterations);
435
436 for _ in 0..self.config.warmup_iterations {
438 func()?;
439 }
440
441 for _ in 0..self.config.performance_iterations {
447 let before_resident = MemoryStats::current()?.resident;
448 let start = Instant::now();
449 let result = func()?;
450 let elapsed = start.elapsed();
451 let after_resident = MemoryStats::current()?.resident;
452 drop(result);
453
454 times.push(elapsed);
455 memory_deltas.push(after_resident.saturating_sub(before_resident));
459 }
460
461 let timing_stats = self.calculate_timing_statistics(×)?;
462 let memory_stats = Self::summarize_memory_deltas(&memory_deltas);
463
464 Ok((timing_stats, memory_stats))
465 }
466
467 #[cfg(not(feature = "memory_tracking"))]
473 fn measure_timing<F, R>(&self, mut func: F) -> StatsResult<(TimingStatistics, MemoryComparison)>
474 where
475 F: FnMut() -> StatsResult<R>,
476 {
477 let mut times = Vec::with_capacity(self.config.performance_iterations);
478
479 for _ in 0..self.config.warmup_iterations {
481 func()?;
482 }
483
484 for _ in 0..self.config.performance_iterations {
486 let start = Instant::now();
487 func()?;
488 let elapsed = start.elapsed();
489 times.push(elapsed);
490 }
491
492 let timing_stats = self.calculate_timing_statistics(×)?;
493 let memory_stats = MemoryComparison {
496 peak_memory: 0,
497 average_memory: 0,
498 efficiency_ratio: None,
499 };
500
501 Ok((timing_stats, memory_stats))
502 }
503
504 #[cfg(feature = "memory_tracking")]
508 fn measure_timing_scipy<F>(
509 &self,
510 mut func: F,
511 ) -> StatsResult<(TimingStatistics, MemoryComparison)>
512 where
513 F: FnMut(),
514 {
515 use scirs2_core::profiling::MemoryStats;
516
517 let mut times = Vec::with_capacity(self.config.performance_iterations);
518 let mut memory_deltas = Vec::with_capacity(self.config.performance_iterations);
519
520 for _ in 0..self.config.warmup_iterations {
522 func();
523 }
524
525 for _ in 0..self.config.performance_iterations {
527 let before_resident = MemoryStats::current()?.resident;
528 let start = Instant::now();
529 func();
530 let elapsed = start.elapsed();
531 let after_resident = MemoryStats::current()?.resident;
532
533 times.push(elapsed);
534 memory_deltas.push(after_resident.saturating_sub(before_resident));
535 }
536
537 let timing_stats = self.calculate_timing_statistics(×)?;
538 let memory_stats = Self::summarize_memory_deltas(&memory_deltas);
539
540 Ok((timing_stats, memory_stats))
541 }
542
543 #[cfg(not(feature = "memory_tracking"))]
546 fn measure_timing_scipy<F>(
547 &self,
548 mut func: F,
549 ) -> StatsResult<(TimingStatistics, MemoryComparison)>
550 where
551 F: FnMut(),
552 {
553 let mut times = Vec::with_capacity(self.config.performance_iterations);
554
555 for _ in 0..self.config.warmup_iterations {
557 func();
558 }
559
560 for _ in 0..self.config.performance_iterations {
562 let start = Instant::now();
563 func();
564 let elapsed = start.elapsed();
565 times.push(elapsed);
566 }
567
568 let timing_stats = self.calculate_timing_statistics(×)?;
569 let memory_stats = MemoryComparison {
570 peak_memory: 0,
571 average_memory: 0,
572 efficiency_ratio: None,
573 };
574
575 Ok((timing_stats, memory_stats))
576 }
577
578 #[cfg(feature = "memory_tracking")]
585 fn summarize_memory_deltas(deltas: &[usize]) -> MemoryComparison {
586 let peak_memory = deltas.iter().copied().max().unwrap_or(0);
587 let average_memory = if deltas.is_empty() {
588 0
589 } else {
590 (deltas.iter().sum::<usize>() as f64 / deltas.len() as f64).round() as usize
591 };
592
593 MemoryComparison {
594 peak_memory,
595 average_memory,
596 efficiency_ratio: None,
597 }
598 }
599
600 fn calculate_timing_statistics(&self, times: &[Duration]) -> StatsResult<TimingStatistics> {
602 if times.is_empty() {
603 return Err(StatsError::InvalidInput(
604 "No timing measurements".to_string(),
605 ));
606 }
607
608 let mut sorted_times = times.to_vec();
609 sorted_times.sort();
610
611 let mean_nanos: f64 =
612 times.iter().map(|d| d.as_nanos() as f64).sum::<f64>() / times.len() as f64;
613 let mean = Duration::from_nanos(mean_nanos as u64);
614
615 let variance: f64 = times
616 .iter()
617 .map(|d| {
618 let diff = d.as_nanos() as f64 - mean_nanos;
619 diff * diff
620 })
621 .sum::<f64>()
622 / times.len() as f64;
623 let std_dev = Duration::from_nanos(variance.sqrt() as u64);
624
625 let p50_idx = times.len() / 2;
626 let p95_idx = (times.len() as f64 * 0.95) as usize;
627 let p99_idx = (times.len() as f64 * 0.99) as usize;
628
629 Ok(TimingStatistics {
630 mean,
631 std_dev,
632 min: sorted_times[0],
633 max: sorted_times[times.len() - 1],
634 p50: sorted_times[p50_idx],
635 p95: sorted_times[p95_idx.min(times.len() - 1)],
636 p99: sorted_times[p99_idx.min(times.len() - 1)],
637 })
638 }
639
640 fn grade_accuracy(&self, relativeerror: f64) -> AccuracyGrade {
642 if relativeerror < 1e-12 {
643 AccuracyGrade::A
644 } else if relativeerror < 1e-9 {
645 AccuracyGrade::B
646 } else if relativeerror < 1e-6 {
647 AccuracyGrade::C
648 } else if relativeerror < 1e-3 {
649 AccuracyGrade::D
650 } else {
651 AccuracyGrade::F
652 }
653 }
654
655 fn grade_performance(&self, ratio: Option<f64>) -> PerformanceGrade {
657 match ratio {
658 Some(r) if r < 0.5 => PerformanceGrade::A,
659 Some(r) if r < 0.67 => PerformanceGrade::B,
660 Some(r) if r < 1.25 => PerformanceGrade::C,
661 Some(r) if r < 2.0 => PerformanceGrade::D,
662 Some(_) => PerformanceGrade::F,
663 None => PerformanceGrade::C, }
665 }
666
667 fn determine_status(
669 &self,
670 accuracy: &AccuracyComparison,
671 performance: &PerformanceComparison,
672 ) -> BenchmarkStatus {
673 let accuracy_pass = accuracy.passes_tolerance;
674 let performance_pass = matches!(
675 performance.performance_grade,
676 PerformanceGrade::A | PerformanceGrade::B | PerformanceGrade::C | PerformanceGrade::D
677 );
678
679 match (accuracy_pass, performance_pass) {
680 (true, true) => BenchmarkStatus::Pass,
681 (true, false) => BenchmarkStatus::AccuracyPass,
682 (false, true) => BenchmarkStatus::PerformancePass,
683 (false, false) => BenchmarkStatus::Fail,
684 }
685 }
686
687 pub fn generate_report(&self) -> BenchmarkReport {
689 let results: Vec<_> = self.results_cache.values().cloned().collect();
690
691 BenchmarkReport {
692 total_tests: results.len(),
693 passed_tests: results
694 .iter()
695 .filter(|r| r.status == BenchmarkStatus::Pass)
696 .count(),
697 failed_tests: results
698 .iter()
699 .filter(|r| r.status == BenchmarkStatus::Fail)
700 .count(),
701 results,
702 generated_at: chrono::Utc::now(),
703 }
704 }
705}
706
707impl TestDataGenerator {
708 pub fn new(config: TestDataConfig) -> Self {
710 Self { config }
711 }
712
713 pub fn generate_1ddata(&self, size: usize) -> StatsResult<Array1<f64>> {
715 use scirs2_core::random::prelude::*;
716 use scirs2_core::random::{Distribution, Normal, Uniform as UniformDist};
717
718 let mut rng = StdRng::seed_from_u64(self.config.seed);
719 let mut data = Array1::zeros(size);
720
721 match &self.config.data_distribution {
722 DataDistribution::Normal => {
723 let normal = Normal::new(0.0, 1.0).map_err(|e| {
724 StatsError::InvalidInput(format!("Normal distribution error: {}", e))
725 })?;
726 for val in data.iter_mut() {
727 *val = normal.sample(&mut rng);
728 }
729 }
730 DataDistribution::Uniform { min, max } => {
731 let uniform = UniformDist::new(*min, *max).expect("Operation failed");
732 for val in data.iter_mut() {
733 *val = uniform.sample(&mut rng);
734 }
735 }
736 DataDistribution::Exponential { lambda } => {
737 for val in data.iter_mut() {
738 *val = -lambda.ln() / rng.random::<f64>().ln();
739 }
740 }
741 DataDistribution::Mixed(_) => {
742 let normal = Normal::new(0.0, 1.0).map_err(|e| {
744 StatsError::InvalidInput(format!("Normal distribution error: {}", e))
745 })?;
746 for val in data.iter_mut() {
747 *val = normal.sample(&mut rng);
748 }
749 }
750 }
751
752 if self.config.include_edge_cases && size > 10 {
754 data[0] = f64::INFINITY;
755 data[1] = f64::NEG_INFINITY;
756 data[2] = f64::NAN;
757 data[3] = f64::MAX;
758 data[4] = f64::MIN;
759 }
760
761 Ok(data)
762 }
763
764 pub fn generate_2ddata(&self, rows: usize, cols: usize) -> StatsResult<Array2<f64>> {
766 use scirs2_core::random::prelude::*;
767 use scirs2_core::random::{Distribution, Normal};
768
769 let mut rng = StdRng::seed_from_u64(self.config.seed);
770 let mut data = Array2::zeros((rows, cols));
771
772 let normal = Normal::new(0.0, 1.0)
773 .map_err(|e| StatsError::InvalidInput(format!("Normal distribution error: {}", e)))?;
774
775 for val in data.iter_mut() {
776 *val = normal.sample(&mut rng);
777 }
778
779 Ok(data)
780 }
781}
782
783#[derive(Debug, Clone, Serialize, Deserialize)]
785pub struct BenchmarkReport {
786 pub total_tests: usize,
788 pub passed_tests: usize,
790 pub failed_tests: usize,
792 pub results: Vec<BenchmarkResult>,
794 pub generated_at: chrono::DateTime<chrono::Utc>,
796}
797
798impl BenchmarkReport {
799 pub fn pass_rate(&self) -> f64 {
801 if self.total_tests == 0 {
802 0.0
803 } else {
804 self.passed_tests as f64 / self.total_tests as f64
805 }
806 }
807
808 pub fn summary(&self) -> BenchmarkSummary {
810 let accuracy_grades: Vec<_> = self
811 .results
812 .iter()
813 .map(|r| r.accuracy.accuracy_grade)
814 .collect();
815 let performance_grades: Vec<_> = self
816 .results
817 .iter()
818 .map(|r| r.performance.performance_grade)
819 .collect();
820
821 BenchmarkSummary {
822 pass_rate: self.pass_rate(),
823 average_accuracy_grade: self.average_accuracy_grade(&accuracy_grades),
824 average_performance_grade: self.average_performance_grade(&performance_grades),
825 total_runtime: self.total_runtime(),
826 }
827 }
828
829 fn average_accuracy_grade(&self, grades: &[AccuracyGrade]) -> AccuracyGrade {
830 AccuracyGrade::C }
833
834 fn average_performance_grade(&self, grades: &[PerformanceGrade]) -> PerformanceGrade {
835 PerformanceGrade::C }
838
839 fn total_runtime(&self) -> Duration {
840 self.results
842 .iter()
843 .map(|r| r.performance.scirs2_timing.mean)
844 .sum()
845 }
846}
847
848#[derive(Debug, Clone)]
850pub struct BenchmarkSummary {
851 pub pass_rate: f64,
852 pub average_accuracy_grade: AccuracyGrade,
853 pub average_performance_grade: PerformanceGrade,
854 pub total_runtime: Duration,
855}
856
857#[cfg(test)]
858mod tests {
859 use super::*;
860 use crate::descriptive::mean;
861
862 #[test]
863 fn test_benchmark_framework_creation() {
864 let framework = ScipyBenchmarkFramework::default();
865 assert_eq!(framework.config.absolute_tolerance, 1e-12);
866 assert_eq!(framework.config.relative_tolerance, 1e-9);
867 }
868
869 #[test]
870 fn test_testdata_generation() {
871 let generator = TestDataGenerator::new(TestDataConfig::default());
872 let data = generator.generate_1ddata(100).expect("Operation failed");
873 assert_eq!(data.len(), 100);
874 }
875
876 #[test]
877 fn test_accuracy_grading() {
878 let framework = ScipyBenchmarkFramework::default();
879
880 assert_eq!(framework.grade_accuracy(1e-15), AccuracyGrade::A);
881 assert_eq!(framework.grade_accuracy(1e-10), AccuracyGrade::B);
882 assert_eq!(framework.grade_accuracy(1e-7), AccuracyGrade::C);
883 assert_eq!(framework.grade_accuracy(1e-4), AccuracyGrade::D);
884 assert_eq!(framework.grade_accuracy(1e-1), AccuracyGrade::F);
885 }
886
887 #[test]
888 fn test_performance_grading() {
889 let framework = ScipyBenchmarkFramework::default();
890
891 assert_eq!(framework.grade_performance(Some(0.3)), PerformanceGrade::A);
892 assert_eq!(framework.grade_performance(Some(0.6)), PerformanceGrade::B);
893 assert_eq!(framework.grade_performance(Some(1.0)), PerformanceGrade::C);
894 assert_eq!(framework.grade_performance(Some(1.5)), PerformanceGrade::D);
895 assert_eq!(framework.grade_performance(Some(3.0)), PerformanceGrade::F);
896 assert_eq!(framework.grade_performance(None), PerformanceGrade::C);
897 }
898
899 #[test]
900 fn test_benchmark_integration() {
901 let mut framework = ScipyBenchmarkFramework::new(BenchmarkConfig {
902 testsizes: vec![100],
903 performance_iterations: 5,
904 warmup_iterations: 1,
905 ..Default::default()
906 });
907
908 let scipy_mean = |data: &ArrayView1<f64>| -> f64 { data.sum() / data.len() as f64 };
910
911 let results = framework
912 .benchmark_function("mean", |data| mean(data), scipy_mean)
913 .expect("Operation failed");
914
915 assert_eq!(results.len(), 1);
916 assert_eq!(results[0].function_name, "mean");
917 assert!(results[0].accuracy.passes_tolerance);
918 }
919
920 #[cfg(feature = "memory_tracking")]
935 const MEMORY_TEST_GROWTH_LEN: usize = 200_000;
936
937 #[cfg(feature = "memory_tracking")]
938 #[test]
939 fn test_memory_tracking_allocating_closure_reports_nonzero_memory() {
940 let framework = ScipyBenchmarkFramework::new(BenchmarkConfig {
941 performance_iterations: 20,
942 warmup_iterations: 2,
943 ..Default::default()
944 });
945
946 let mut buffer: Vec<f64> = Vec::new();
957 let (_, memory) = framework
958 .measure_timing(move || -> StatsResult<()> {
959 buffer.extend(std::iter::repeat_n(1.0_f64, MEMORY_TEST_GROWTH_LEN));
960 Ok(())
961 })
962 .expect("Operation failed");
963
964 assert!(
965 memory.peak_memory > 0,
966 "expected nonzero peak resident-memory delta for an allocating closure, got {}",
967 memory.peak_memory
968 );
969 assert!(
970 memory.average_memory > 0,
971 "expected nonzero average resident-memory delta for an allocating closure, got {}",
972 memory.average_memory
973 );
974 }
975
976 #[cfg(feature = "memory_tracking")]
977 #[test]
978 fn test_memory_tracking_trivial_closure_much_smaller_than_allocating() {
979 let framework = ScipyBenchmarkFramework::new(BenchmarkConfig {
980 performance_iterations: 20,
981 warmup_iterations: 2,
982 ..Default::default()
983 });
984
985 let mut buffer: Vec<f64> = Vec::new();
989 let (_, allocating_memory) = framework
990 .measure_timing(move || -> StatsResult<()> {
991 buffer.extend(std::iter::repeat_n(1.0_f64, MEMORY_TEST_GROWTH_LEN));
992 Ok(())
993 })
994 .expect("Operation failed");
995
996 let (_, trivial_memory) = framework
998 .measure_timing(|| -> StatsResult<i32> { Ok(1 + 1) })
999 .expect("Operation failed");
1000
1001 assert!(
1002 allocating_memory.peak_memory > 0,
1003 "sanity check: allocating closure should itself report nonzero peak memory, got {}",
1004 allocating_memory.peak_memory
1005 );
1006 assert!(
1010 trivial_memory.peak_memory < allocating_memory.peak_memory,
1011 "expected trivial closure's peak memory ({}) to be much smaller than the \
1012 allocating closure's ({})",
1013 trivial_memory.peak_memory,
1014 allocating_memory.peak_memory
1015 );
1016 assert!(
1017 trivial_memory.average_memory < allocating_memory.average_memory,
1018 "expected trivial closure's average memory ({}) to be much smaller than the \
1019 allocating closure's ({})",
1020 trivial_memory.average_memory,
1021 allocating_memory.average_memory
1022 );
1023 }
1024
1025 #[cfg(feature = "memory_tracking")]
1026 #[test]
1027 fn test_memory_tracking_wired_into_compare_performance() {
1028 use std::cell::RefCell;
1029
1030 let scirs2_growing: RefCell<Vec<f64>> = RefCell::new(Vec::new());
1044 let scipy_growing: RefCell<Vec<f64>> = RefCell::new(Vec::new());
1045 const GROWTH_PER_CALL: usize = 200_000; let framework = ScipyBenchmarkFramework::new(BenchmarkConfig {
1048 performance_iterations: 10,
1049 warmup_iterations: 1,
1050 ..Default::default()
1051 });
1052
1053 let testdata = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
1054
1055 let scirs2_impl = |data: &ArrayView1<f64>| -> StatsResult<f64> {
1061 scirs2_growing
1062 .borrow_mut()
1063 .extend(std::iter::repeat_n(1.0_f64, GROWTH_PER_CALL));
1064 Ok(data.sum())
1065 };
1066 let scipy_reference = |data: &ArrayView1<f64>| -> f64 {
1067 scipy_growing
1068 .borrow_mut()
1069 .extend(std::iter::repeat_n(1.0_f64, GROWTH_PER_CALL));
1070 data.sum()
1071 };
1072
1073 let performance = framework
1074 .compare_performance(&scirs2_impl, Some(&scipy_reference), &testdata.view())
1075 .expect("Operation failed");
1076
1077 assert!(
1078 performance.memory_usage.peak_memory > 0,
1079 "expected nonzero peak memory from an allocating benchmarked closure, got {}",
1080 performance.memory_usage.peak_memory
1081 );
1082 assert!(
1083 performance.memory_usage.average_memory > 0,
1084 "expected nonzero average memory from an allocating benchmarked closure, got {}",
1085 performance.memory_usage.average_memory
1086 );
1087 assert!(
1088 performance.memory_usage.efficiency_ratio.is_some(),
1089 "expected an efficiency_ratio once both SciRS2 and SciPy sides allocate"
1090 );
1091
1092 assert!(scirs2_growing.borrow().len() >= GROWTH_PER_CALL);
1096 assert!(scipy_growing.borrow().len() >= GROWTH_PER_CALL);
1097 }
1098}