1use crate::algorithm_selector::{
26 AlgorithmRecommendation, AlgorithmSelector, FftAlgorithm, InputCharacteristics,
27 PerformanceEntry, SelectionConfig,
28};
29use crate::error::{FFTError, FFTResult};
30
31#[cfg(feature = "rustfft-backend")]
33use rustfft::FftPlanner;
34
35#[cfg(not(feature = "rustfft-backend"))]
36use oxifft::{Complex as OxiComplex, Direction};
37
38use scirs2_core::numeric::Complex64;
39use serde::{Deserialize, Serialize};
40use std::collections::HashMap;
41use std::time::{Duration, Instant};
42
43#[derive(Debug, Clone)]
45pub struct ProfileConfig {
46 pub warmup_iterations: usize,
48 pub benchmark_iterations: usize,
50 pub track_memory: bool,
52 pub sizes: Option<Vec<usize>>,
54 pub algorithms: Option<Vec<FftAlgorithm>>,
56 pub include_inverse: bool,
58 pub timeout_ms: u64,
60}
61
62impl Default for ProfileConfig {
63 fn default() -> Self {
64 Self {
65 warmup_iterations: 5,
66 benchmark_iterations: 20,
67 track_memory: true,
68 sizes: None,
69 algorithms: None,
70 include_inverse: true,
71 timeout_ms: 30000, }
73 }
74}
75
76const DEFAULT_SIZES: &[usize] = &[
78 64, 128, 256, 512, 1024, 2048, 4096, 8192, 100, 360, 480, 1000, 2000, 5000, 97, 101, 127, 251, 509, 1009, 2003, 1200, 3600, 7200, 65536, 131072, 262144, ];
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct Measurement {
88 pub time_ns: u64,
90 pub peak_memory_bytes: usize,
92 pub allocated_bytes: usize,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct ProfileResult {
99 pub size: usize,
101 pub algorithm: FftAlgorithm,
103 pub forward: bool,
105 pub measurements: Vec<Measurement>,
107 pub avg_time: Duration,
109 pub min_time: Duration,
111 pub max_time: Duration,
113 pub std_dev: Duration,
115 pub median_time: Duration,
117 pub p90_time: Duration,
119 pub p99_time: Duration,
121 pub ops_per_second: f64,
123 pub avg_peak_memory: usize,
125 pub elements_per_second: f64,
127 pub input_characteristics: Option<InputCharacteristics>,
129}
130
131impl ProfileResult {
132 pub fn from_measurements(
134 size: usize,
135 algorithm: FftAlgorithm,
136 forward: bool,
137 measurements: Vec<Measurement>,
138 input_characteristics: Option<InputCharacteristics>,
139 ) -> Self {
140 if measurements.is_empty() {
141 return Self {
142 size,
143 algorithm,
144 forward,
145 measurements: Vec::new(),
146 avg_time: Duration::ZERO,
147 min_time: Duration::ZERO,
148 max_time: Duration::ZERO,
149 std_dev: Duration::ZERO,
150 median_time: Duration::ZERO,
151 p90_time: Duration::ZERO,
152 p99_time: Duration::ZERO,
153 ops_per_second: 0.0,
154 avg_peak_memory: 0,
155 elements_per_second: 0.0,
156 input_characteristics,
157 };
158 }
159
160 let times_ns: Vec<u64> = measurements.iter().map(|m| m.time_ns).collect();
161 let n = times_ns.len();
162
163 let sum: u64 = times_ns.iter().sum();
165 let avg_ns = sum / n as u64;
166
167 let min_ns = times_ns.iter().min().copied().unwrap_or(0);
168 let max_ns = times_ns.iter().max().copied().unwrap_or(0);
169
170 let variance: f64 = times_ns
172 .iter()
173 .map(|&t| {
174 let diff = t as f64 - avg_ns as f64;
175 diff * diff
176 })
177 .sum::<f64>()
178 / n as f64;
179 let std_dev_ns = variance.sqrt() as u64;
180
181 let mut sorted_times = times_ns.clone();
183 sorted_times.sort_unstable();
184
185 let median_ns = if n % 2 == 0 {
186 (sorted_times[n / 2 - 1] + sorted_times[n / 2]) / 2
187 } else {
188 sorted_times[n / 2]
189 };
190
191 let p90_idx = (n as f64 * 0.90).ceil() as usize - 1;
192 let p99_idx = (n as f64 * 0.99).ceil() as usize - 1;
193 let p90_ns = sorted_times.get(p90_idx.min(n - 1)).copied().unwrap_or(0);
194 let p99_ns = sorted_times.get(p99_idx.min(n - 1)).copied().unwrap_or(0);
195
196 let ops_per_second = if avg_ns > 0 {
198 1_000_000_000.0 / avg_ns as f64
199 } else {
200 0.0
201 };
202
203 let elements_per_second = ops_per_second * size as f64;
204
205 let avg_peak_memory = if !measurements.is_empty() {
207 measurements
208 .iter()
209 .map(|m| m.peak_memory_bytes)
210 .sum::<usize>()
211 / measurements.len()
212 } else {
213 0
214 };
215
216 Self {
217 size,
218 algorithm,
219 forward,
220 measurements,
221 avg_time: Duration::from_nanos(avg_ns),
222 min_time: Duration::from_nanos(min_ns),
223 max_time: Duration::from_nanos(max_ns),
224 std_dev: Duration::from_nanos(std_dev_ns),
225 median_time: Duration::from_nanos(median_ns),
226 p90_time: Duration::from_nanos(p90_ns),
227 p99_time: Duration::from_nanos(p99_ns),
228 ops_per_second,
229 avg_peak_memory,
230 elements_per_second,
231 input_characteristics,
232 }
233 }
234
235 pub fn as_table_row(&self) -> String {
237 format!(
238 "{:>8} | {:>16} | {:>12?} | {:>12?} | {:>12?} | {:>12.2} | {:>10}",
239 self.size,
240 self.algorithm.to_string(),
241 self.avg_time,
242 self.min_time,
243 self.max_time,
244 self.ops_per_second,
245 format_bytes(self.avg_peak_memory)
246 )
247 }
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct AlgorithmComparison {
253 pub size: usize,
255 pub results: HashMap<FftAlgorithm, ProfileResult>,
257 pub best_algorithm: FftAlgorithm,
259 pub speedup: f64,
261 pub recommendation: String,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct PerformanceReport {
268 pub results: HashMap<(usize, FftAlgorithm), ProfileResult>,
270 pub best_by_size: HashMap<usize, FftAlgorithm>,
272 pub comparisons: Vec<AlgorithmComparison>,
274 pub total_time: Duration,
276 pub config_summary: String,
278}
279
280impl PerformanceReport {
281 pub fn to_text_report(&self) -> String {
283 let mut report = String::new();
284
285 report.push_str("=== FFT Performance Report ===\n\n");
286 report.push_str(&format!("Total profiling time: {:?}\n", self.total_time));
287 report.push_str(&format!("Configuration: {}\n\n", self.config_summary));
288
289 report.push_str("--- Results by Size ---\n\n");
290 report.push_str(&format!(
291 "{:>8} | {:>16} | {:>12} | {:>12} | {:>12} | {:>12} | {:>10}\n",
292 "Size", "Algorithm", "Avg Time", "Min Time", "Max Time", "Ops/sec", "Memory"
293 ));
294 report.push_str(&"-".repeat(100));
295 report.push('\n');
296
297 let mut sizes: Vec<usize> = self.best_by_size.keys().copied().collect();
298 sizes.sort_unstable();
299
300 for size in sizes {
301 if let Some(algo) = self.best_by_size.get(&size) {
302 if let Some(result) = self.results.get(&(size, *algo)) {
303 report.push_str(&result.as_table_row());
304 report.push('\n');
305 }
306 }
307 }
308
309 report.push_str("\n--- Recommendations ---\n\n");
310 for comparison in &self.comparisons {
311 report.push_str(&format!(
312 "Size {:>8}: {} (speedup: {:.2}x)\n",
313 comparison.size, comparison.recommendation, comparison.speedup
314 ));
315 }
316
317 report
318 }
319}
320
321pub struct PerformanceProfiler {
323 config: ProfileConfig,
325 selector: AlgorithmSelector,
327}
328
329impl Default for PerformanceProfiler {
330 fn default() -> Self {
331 Self::new()
332 }
333}
334
335impl PerformanceProfiler {
336 pub fn new() -> Self {
338 Self::with_config(ProfileConfig::default())
339 }
340
341 pub fn with_config(config: ProfileConfig) -> Self {
343 Self {
344 config,
345 selector: AlgorithmSelector::new(),
346 }
347 }
348
349 pub fn profile_size(&self, size: usize, forward: bool) -> FFTResult<ProfileResult> {
351 let recommendation = self.selector.select_algorithm(size, forward)?;
352 self.profile_size_with_algorithm(size, recommendation.algorithm, forward)
353 }
354
355 #[cfg(feature = "rustfft-backend")]
357 pub fn profile_size_with_algorithm(
358 &self,
359 size: usize,
360 algorithm: FftAlgorithm,
361 forward: bool,
362 ) -> FFTResult<ProfileResult> {
363 if size == 0 {
365 return Err(FFTError::ValueError("Size must be positive".to_string()));
366 }
367
368 let mut data: Vec<Complex64> = (0..size)
370 .map(|i| Complex64::new(i as f64, (i * 2) as f64))
371 .collect();
372
373 let mut planner = FftPlanner::new();
375 let fft = if forward {
376 planner.plan_fft_forward(size)
377 } else {
378 planner.plan_fft_inverse(size)
379 };
380
381 for _ in 0..self.config.warmup_iterations {
383 let mut work_data = data.clone();
384 fft.process(&mut work_data);
385 }
386
387 let mut measurements = Vec::with_capacity(self.config.benchmark_iterations);
389
390 for _ in 0..self.config.benchmark_iterations {
391 let mut work_data = data.clone();
392
393 let memory_before = if self.config.track_memory {
395 estimate_current_allocation()
396 } else {
397 0
398 };
399
400 let start = Instant::now();
402 fft.process(&mut work_data);
403 let elapsed = start.elapsed();
404
405 let memory_after = if self.config.track_memory {
407 estimate_current_allocation()
408 } else {
409 0
410 };
411
412 measurements.push(Measurement {
413 time_ns: elapsed.as_nanos() as u64,
414 peak_memory_bytes: size * 16, allocated_bytes: memory_after.saturating_sub(memory_before),
416 });
417 }
418
419 let chars = InputCharacteristics::analyze(size, &self.selector.hardware().cache_info);
421
422 Ok(ProfileResult::from_measurements(
423 size,
424 algorithm,
425 forward,
426 measurements,
427 Some(chars),
428 ))
429 }
430
431 #[cfg(not(feature = "rustfft-backend"))]
433 pub fn profile_size_with_algorithm(
434 &self,
435 size: usize,
436 algorithm: FftAlgorithm,
437 forward: bool,
438 ) -> FFTResult<ProfileResult> {
439 if size == 0 {
441 return Err(FFTError::ValueError("Size must be positive".to_string()));
442 }
443
444 let data: Vec<OxiComplex<f64>> = (0..size)
446 .map(|i| OxiComplex::new(i as f64, (i * 2) as f64))
447 .collect();
448
449 let mut plan = if forward {
451 oxifft::Plan::new(Direction::Forward, size, oxifft::Flags::Estimate).map_err(|e| {
452 FFTError::ComputationError(format!("Failed to create FFT plan: {:?}", e))
453 })?
454 } else {
455 oxifft::Plan::new(Direction::Backward, size, oxifft::Flags::Estimate).map_err(|e| {
456 FFTError::ComputationError(format!("Failed to create FFT plan: {:?}", e))
457 })?
458 };
459
460 for _ in 0..self.config.warmup_iterations {
462 let mut work_data = data.clone();
463 plan.execute_c2c(&mut work_data).map_err(|e| {
464 FFTError::ComputationError(format!("FFT execution failed: {:?}", e))
465 })?;
466 }
467
468 let mut measurements = Vec::with_capacity(self.config.benchmark_iterations);
470
471 for _ in 0..self.config.benchmark_iterations {
472 let mut work_data = data.clone();
473
474 let memory_before = if self.config.track_memory {
476 estimate_current_allocation()
477 } else {
478 0
479 };
480
481 let start = Instant::now();
483 plan.execute_c2c(&mut work_data).map_err(|e| {
484 FFTError::ComputationError(format!("FFT execution failed: {:?}", e))
485 })?;
486 let elapsed = start.elapsed();
487
488 let memory_after = if self.config.track_memory {
490 estimate_current_allocation()
491 } else {
492 0
493 };
494
495 measurements.push(Measurement {
496 time_ns: elapsed.as_nanos() as u64,
497 peak_memory_bytes: size * 16, allocated_bytes: memory_after.saturating_sub(memory_before),
499 });
500 }
501
502 let chars = InputCharacteristics::analyze(size, &self.selector.hardware().cache_info);
504
505 Ok(ProfileResult::from_measurements(
506 size,
507 algorithm,
508 forward,
509 measurements,
510 Some(chars),
511 ))
512 }
513
514 pub fn profile_sizes(&self, sizes: &[usize], forward: bool) -> FFTResult<Vec<ProfileResult>> {
516 let mut results = Vec::with_capacity(sizes.len());
517
518 for &size in sizes {
519 match self.profile_size(size, forward) {
520 Ok(result) => results.push(result),
521 Err(e) => {
522 eprintln!("Failed to profile size {}: {}", size, e);
524 }
525 }
526 }
527
528 Ok(results)
529 }
530
531 pub fn compare_algorithms(
533 &self,
534 size: usize,
535 algorithms: &[FftAlgorithm],
536 forward: bool,
537 ) -> FFTResult<AlgorithmComparison> {
538 let mut results = HashMap::new();
539
540 for &algorithm in algorithms {
541 match self.profile_size_with_algorithm(size, algorithm, forward) {
542 Ok(result) => {
543 results.insert(algorithm, result);
544 }
545 Err(e) => {
546 eprintln!("Failed to profile {} for size {}: {}", algorithm, size, e);
547 }
548 }
549 }
550
551 if results.is_empty() {
552 return Err(FFTError::ComputationError(
553 "No algorithms could be profiled".to_string(),
554 ));
555 }
556
557 let best_algorithm = results
559 .iter()
560 .min_by_key(|(_, r)| r.avg_time)
561 .map(|(&algo, _)| algo)
562 .unwrap_or(FftAlgorithm::MixedRadix);
563
564 let best_time = results
565 .get(&best_algorithm)
566 .map(|r| r.avg_time)
567 .unwrap_or(Duration::ZERO);
568
569 let worst_time = results
570 .values()
571 .map(|r| r.avg_time)
572 .max()
573 .unwrap_or(Duration::ZERO);
574
575 let speedup = if best_time.as_nanos() > 0 {
576 worst_time.as_nanos() as f64 / best_time.as_nanos() as f64
577 } else {
578 1.0
579 };
580
581 let recommendation = format!("{} is fastest ({:?})", best_algorithm, best_time);
582
583 Ok(AlgorithmComparison {
584 size,
585 results,
586 best_algorithm,
587 speedup,
588 recommendation,
589 })
590 }
591
592 pub fn run_comprehensive_profile(&self) -> FFTResult<PerformanceReport> {
594 let start = Instant::now();
595
596 let sizes = self.config.sizes.as_deref().unwrap_or(DEFAULT_SIZES);
597
598 let algorithms = self.config.algorithms.as_deref().unwrap_or(&[
599 FftAlgorithm::CooleyTukeyRadix2,
600 FftAlgorithm::SplitRadix,
601 FftAlgorithm::MixedRadix,
602 FftAlgorithm::Bluestein,
603 ]);
604
605 let mut results = HashMap::new();
606 let mut best_by_size = HashMap::new();
607 let mut comparisons = Vec::new();
608
609 for &size in sizes {
610 let comparison = self.compare_algorithms(size, algorithms, true)?;
612
613 for (algo, result) in &comparison.results {
614 results.insert((size, *algo), result.clone());
615 }
616
617 best_by_size.insert(size, comparison.best_algorithm);
618 comparisons.push(comparison);
619
620 if self.config.include_inverse {
622 if let Ok(inv_comparison) = self.compare_algorithms(size, algorithms, false) {
623 let _ = inv_comparison;
626 }
627 }
628 }
629
630 let total_time = start.elapsed();
631
632 let config_summary = format!(
633 "warmup={}, iterations={}, sizes={}, algorithms={}",
634 self.config.warmup_iterations,
635 self.config.benchmark_iterations,
636 sizes.len(),
637 algorithms.len()
638 );
639
640 Ok(PerformanceReport {
641 results,
642 best_by_size,
643 comparisons,
644 total_time,
645 config_summary,
646 })
647 }
648
649 pub fn auto_tune(&mut self, sizes: &[usize]) -> FFTResult<HashMap<usize, FftAlgorithm>> {
651 let algorithms = [
652 FftAlgorithm::CooleyTukeyRadix2,
653 FftAlgorithm::SplitRadix,
654 FftAlgorithm::MixedRadix,
655 FftAlgorithm::Bluestein,
656 ];
657
658 let mut best_algorithms = HashMap::new();
659
660 for &size in sizes {
661 let comparison = self.compare_algorithms(size, &algorithms, true)?;
662 best_algorithms.insert(size, comparison.best_algorithm);
663
664 if let Some(result) = comparison.results.get(&comparison.best_algorithm) {
666 let entry = PerformanceEntry {
667 size,
668 algorithm: comparison.best_algorithm,
669 forward: true,
670 execution_time_ns: result.avg_time.as_nanos() as u64,
671 peak_memory_bytes: result.avg_peak_memory,
672 timestamp: std::time::SystemTime::now()
673 .duration_since(std::time::UNIX_EPOCH)
674 .map(|d| d.as_secs())
675 .unwrap_or(0),
676 hardware_hash: 0,
677 };
678 let _ = self.selector.record_performance(entry);
679 }
680 }
681
682 Ok(best_algorithms)
683 }
684
685 pub fn selector(&self) -> &AlgorithmSelector {
687 &self.selector
688 }
689
690 pub fn selector_mut(&mut self) -> &mut AlgorithmSelector {
692 &mut self.selector
693 }
694}
695
696pub struct MemoryProfiler {
698 enabled: bool,
700 peak_memory: usize,
702 current_allocation: usize,
704}
705
706impl Default for MemoryProfiler {
707 fn default() -> Self {
708 Self::new()
709 }
710}
711
712impl MemoryProfiler {
713 pub fn new() -> Self {
715 Self {
716 enabled: true,
717 peak_memory: 0,
718 current_allocation: 0,
719 }
720 }
721
722 pub fn record_allocation(&mut self, bytes: usize) {
724 if self.enabled {
725 self.current_allocation += bytes;
726 if self.current_allocation > self.peak_memory {
727 self.peak_memory = self.current_allocation;
728 }
729 }
730 }
731
732 pub fn record_deallocation(&mut self, bytes: usize) {
734 if self.enabled {
735 self.current_allocation = self.current_allocation.saturating_sub(bytes);
736 }
737 }
738
739 pub fn reset(&mut self) {
741 self.peak_memory = 0;
742 self.current_allocation = 0;
743 }
744
745 pub fn peak_memory(&self) -> usize {
747 self.peak_memory
748 }
749
750 pub fn current_allocation(&self) -> usize {
752 self.current_allocation
753 }
754
755 pub fn set_enabled(&mut self, enabled: bool) {
757 self.enabled = enabled;
758 }
759}
760
761pub fn estimate_fft_memory(size: usize, algorithm: FftAlgorithm) -> usize {
763 let base_memory = size * 16; let overhead = match algorithm {
767 FftAlgorithm::CooleyTukeyRadix2 => base_memory / 4, FftAlgorithm::Radix4 => base_memory / 4,
769 FftAlgorithm::SplitRadix => base_memory / 3,
770 FftAlgorithm::MixedRadix => base_memory / 2,
771 FftAlgorithm::Bluestein => base_memory * 2, FftAlgorithm::Rader => base_memory,
773 FftAlgorithm::Winograd => base_memory / 2,
774 FftAlgorithm::GoodThomas => base_memory / 3,
775 FftAlgorithm::Streaming => base_memory / 8, FftAlgorithm::CacheOblivious => base_memory / 4,
777 FftAlgorithm::InPlace => 0, FftAlgorithm::SimdOptimized => base_memory / 4,
779 FftAlgorithm::Parallel => base_memory, FftAlgorithm::Hybrid => base_memory / 2,
781 };
782
783 base_memory + overhead
784}
785
786fn estimate_current_allocation() -> usize {
788 0
791}
792
793fn format_bytes(bytes: usize) -> String {
795 if bytes >= 1024 * 1024 * 1024 {
796 format!("{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
797 } else if bytes >= 1024 * 1024 {
798 format!("{:.2} MB", bytes as f64 / (1024.0 * 1024.0))
799 } else if bytes >= 1024 {
800 format!("{:.2} KB", bytes as f64 / 1024.0)
801 } else {
802 format!("{} B", bytes)
803 }
804}
805
806#[cfg(test)]
807mod tests {
808 use super::*;
809
810 #[test]
811 fn test_profile_result_statistics() {
812 let measurements = vec![
813 Measurement {
814 time_ns: 1000,
815 peak_memory_bytes: 1000,
816 allocated_bytes: 100,
817 },
818 Measurement {
819 time_ns: 1200,
820 peak_memory_bytes: 1000,
821 allocated_bytes: 100,
822 },
823 Measurement {
824 time_ns: 1100,
825 peak_memory_bytes: 1000,
826 allocated_bytes: 100,
827 },
828 Measurement {
829 time_ns: 1050,
830 peak_memory_bytes: 1000,
831 allocated_bytes: 100,
832 },
833 ];
834
835 let result = ProfileResult::from_measurements(
836 256,
837 FftAlgorithm::MixedRadix,
838 true,
839 measurements,
840 None,
841 );
842
843 assert_eq!(result.size, 256);
844 assert!(result.avg_time.as_nanos() > 0);
845 assert!(result.min_time <= result.avg_time);
846 assert!(result.max_time >= result.avg_time);
847 assert!(result.ops_per_second > 0.0);
848 }
849
850 #[test]
851 fn test_profile_single_size() {
852 let profiler = PerformanceProfiler::new();
853 let result = profiler.profile_size(256, true);
854
855 assert!(result.is_ok());
856 let profile = result.expect("Profiling failed");
857 assert_eq!(profile.size, 256);
858 assert!(profile.avg_time.as_nanos() > 0);
859 }
860
861 #[test]
862 fn test_compare_algorithms() {
863 let profiler = PerformanceProfiler::new();
864 let algorithms = [FftAlgorithm::MixedRadix, FftAlgorithm::Bluestein];
865
866 let result = profiler.compare_algorithms(256, &algorithms, true);
867
868 assert!(result.is_ok());
869 let comparison = result.expect("Comparison failed");
870 assert_eq!(comparison.size, 256);
871 assert!(!comparison.results.is_empty());
872 assert!(comparison.speedup >= 1.0);
873 }
874
875 #[test]
876 fn test_memory_profiler() {
877 let mut mp = MemoryProfiler::new();
878
879 mp.record_allocation(1000);
880 assert_eq!(mp.current_allocation(), 1000);
881 assert_eq!(mp.peak_memory(), 1000);
882
883 mp.record_allocation(500);
884 assert_eq!(mp.current_allocation(), 1500);
885 assert_eq!(mp.peak_memory(), 1500);
886
887 mp.record_deallocation(1000);
888 assert_eq!(mp.current_allocation(), 500);
889 assert_eq!(mp.peak_memory(), 1500);
890
891 mp.reset();
892 assert_eq!(mp.current_allocation(), 0);
893 assert_eq!(mp.peak_memory(), 0);
894 }
895
896 #[test]
897 fn test_estimate_fft_memory() {
898 let size = 1024;
899
900 let inplace_mem = estimate_fft_memory(size, FftAlgorithm::InPlace);
901 let bluestein_mem = estimate_fft_memory(size, FftAlgorithm::Bluestein);
902
903 assert!(bluestein_mem > inplace_mem);
905 }
906
907 #[test]
908 fn test_format_bytes() {
909 assert_eq!(format_bytes(512), "512 B");
910 assert_eq!(format_bytes(1024), "1.00 KB");
911 assert_eq!(format_bytes(1024 * 1024), "1.00 MB");
912 assert_eq!(format_bytes(1024 * 1024 * 1024), "1.00 GB");
913 }
914
915 #[test]
916 fn test_profile_config_default() {
917 let config = ProfileConfig::default();
918 assert!(config.warmup_iterations > 0);
919 assert!(config.benchmark_iterations > 0);
920 assert!(config.track_memory);
921 }
922}