1use std::collections::HashMap;
120use std::time::{Duration, Instant};
121use torsh_core::error::Result;
122use torsh_nn::Module;
123
124use crate::mobile_optimizer::{
125 MobileBenchmarkResults, MobilePlatform, OptimizedModel, PlatformBenchmarkInfo, ThermalState,
126};
127
128#[derive(Debug, Clone)]
130pub struct BenchmarkConfig {
131 pub warmup_iterations: usize,
132 pub benchmark_iterations: usize,
133 pub batch_sizes: Vec<usize>,
134 pub input_shapes: Vec<Vec<usize>>,
135 pub profile_memory: bool,
136 pub profile_backward: bool,
137 pub device: torsh_core::DeviceType,
138 pub mobile_config: Option<MobileBenchmarkConfig>,
139}
140
141#[derive(Debug, Clone)]
143pub struct MobileBenchmarkConfig {
144 pub platform_info: PlatformBenchmarkInfo,
146 pub monitor_thermal: bool,
148 pub measure_power: bool,
150 pub test_frequency_scaling: bool,
152 pub test_memory_pressure: bool,
154 pub stress_test_duration_minutes: Option<u32>,
156 pub latency_thresholds: LatencyThresholds,
158 pub energy_targets: Option<EnergyTargets>,
160}
161
162#[derive(Debug, Clone)]
164pub struct LatencyThresholds {
165 pub realtime_ms: f32,
167 pub interactive_ms: f32,
169 pub batch_ms: f32,
171}
172
173impl Default for LatencyThresholds {
174 fn default() -> Self {
175 Self {
176 realtime_ms: 16.67, interactive_ms: 100.0, batch_ms: 1000.0, }
180 }
181}
182
183#[derive(Debug, Clone)]
185pub struct EnergyTargets {
186 pub inferences_per_joule: f32,
188 pub max_power_watts: f32,
190 pub target_battery_hours: f32,
192}
193
194impl Default for EnergyTargets {
195 fn default() -> Self {
196 Self {
197 inferences_per_joule: 1000.0,
198 max_power_watts: 5.0,
199 target_battery_hours: 8.0,
200 }
201 }
202}
203
204impl Default for BenchmarkConfig {
205 fn default() -> Self {
206 Self {
207 warmup_iterations: 10,
208 benchmark_iterations: 100,
209 batch_sizes: vec![1, 8, 16, 32],
210 input_shapes: vec![vec![3, 224, 224]],
211 profile_memory: true,
212 profile_backward: true,
213 device: torsh_core::DeviceType::Cpu,
214 mobile_config: None,
215 }
216 }
217}
218
219#[derive(Debug, Clone)]
221pub struct BenchmarkResult {
222 pub model_name: String,
223 pub total_params: usize,
224 pub results_by_batch: HashMap<usize, BatchResult>,
225 pub summary: BenchmarkSummary,
226 pub mobile_results: Option<MobileBenchmarkResults>,
227 pub validation_results: Option<ValidationResults>,
228}
229
230#[derive(Debug, Clone)]
232pub struct ValidationResults {
233 pub meets_realtime_latency: bool,
235 pub meets_interactive_latency: bool,
237 pub meets_energy_targets: bool,
239 pub thermal_throttling_detected: bool,
241 pub memory_pressure_impact: Option<f32>,
243 pub sustained_performance_degradation: Option<f32>,
245 pub platform_validation: PlatformValidationResults,
247 pub recommendations: Vec<String>,
249}
250
251#[derive(Debug, Clone)]
253pub struct PlatformValidationResults {
254 pub ios_app_store_compliant: Option<bool>,
256 pub android_performance_class: Option<String>,
258 pub device_compatibility_score: f32,
260 pub device_support_percentage: f32,
262}
263
264#[derive(Debug, Clone)]
266pub struct BatchResult {
267 pub batch_size: usize,
268 pub forward_time: TimingStats,
269 pub backward_time: Option<TimingStats>,
270 pub total_time: TimingStats,
271 pub throughput: f32,
272 pub memory_stats: Option<MemoryStats>,
273}
274
275#[derive(Debug, Clone)]
277pub struct TimingStats {
278 pub mean: Duration,
279 pub std: Duration,
280 pub min: Duration,
281 pub max: Duration,
282 pub median: Duration,
283 pub p95: Duration,
284 pub p99: Duration,
285}
286
287#[derive(Debug, Clone)]
289pub struct MemoryStats {
290 pub peak_allocated_mb: f32,
291 pub peak_reserved_mb: f32,
292 pub avg_allocated_mb: f32,
293}
294
295#[derive(Debug, Clone)]
297pub struct BenchmarkSummary {
298 pub best_batch_size: usize,
299 pub best_throughput: f32,
300 pub optimal_memory_batch: usize,
301 pub recommendations: Vec<String>,
302}
303
304pub fn benchmark_model<M: Module>(model: &M, config: BenchmarkConfig) -> Result<BenchmarkResult> {
306 let model_name = std::any::type_name::<M>()
307 .split("::")
308 .last()
309 .unwrap_or("UnknownModel")
310 .to_string();
311
312 let total_params = count_parameters(model);
313 let mut results_by_batch = HashMap::new();
314
315 for batch_size in &config.batch_sizes {
316 println!("Benchmarking batch size: {}", batch_size);
317
318 let result = benchmark_batch_size(model, *batch_size, &config.input_shapes[0], &config)?;
319
320 results_by_batch.insert(*batch_size, result);
321 }
322
323 let summary = generate_summary(&results_by_batch);
324
325 let mobile_results = if let Some(mobile_config) = &config.mobile_config {
327 Some(benchmark_mobile_model(model, mobile_config)?)
328 } else {
329 None
330 };
331
332 let validation_results = if let Some(mobile_config) = &config.mobile_config {
338 let mut validation =
339 validate_mobile_performance(&results_by_batch, mobile_config, mobile_results.as_ref())?;
340
341 let mut mobile_input_shape = vec![1usize];
344 mobile_input_shape.extend_from_slice(&config.input_shapes[0]);
345
346 if mobile_config.test_memory_pressure {
347 validation.memory_pressure_impact =
348 run_memory_pressure_test(model, &mobile_input_shape, mobile_config)?;
349 }
350
351 if mobile_config.test_frequency_scaling {
352 if let Err(e) = run_frequency_scaling_test(model, mobile_config) {
357 validation
358 .recommendations
359 .push(format!("Frequency scaling test skipped: {e}"));
360 }
361 }
362
363 if let Some(duration) = mobile_config.stress_test_duration_minutes {
364 validation.sustained_performance_degradation = run_sustained_performance_test(
365 model,
366 &mobile_input_shape,
367 mobile_config,
368 duration,
369 )?;
370 }
371
372 Some(validation)
373 } else {
374 None
375 };
376
377 Ok(BenchmarkResult {
378 model_name,
379 total_params,
380 results_by_batch,
381 summary,
382 mobile_results,
383 validation_results,
384 })
385}
386
387pub fn benchmark_mobile_model<M: Module>(
389 model: &M,
390 mobile_config: &MobileBenchmarkConfig,
391) -> Result<MobileBenchmarkResults> {
392 use crate::mobile_optimizer::benchmark_mobile_model_advanced;
393
394 println!("Running mobile-specific benchmark...");
395
396 let optimized_model = convert_to_optimized_model(model)?;
399
400 let input_shapes = vec![vec![1, 3, 224, 224]]; let mobile_results = benchmark_mobile_model_advanced(
405 &optimized_model,
406 input_shapes,
407 mobile_config.stress_test_duration_minutes.unwrap_or(5) as usize * 60, &mobile_config.platform_info,
409 );
410
411 Ok(mobile_results)
418}
419
420pub fn validate_mobile_performance(
422 batch_results: &HashMap<usize, BatchResult>,
423 mobile_config: &MobileBenchmarkConfig,
424 mobile_results: Option<&MobileBenchmarkResults>,
425) -> Result<ValidationResults> {
426 let thresholds = &mobile_config.latency_thresholds;
427
428 let batch_1_result = batch_results.get(&1);
430 let meets_realtime = batch_1_result
431 .map(|r| r.total_time.mean.as_millis() as f32 <= thresholds.realtime_ms)
432 .unwrap_or(false);
433
434 let meets_interactive = batch_1_result
435 .map(|r| r.total_time.mean.as_millis() as f32 <= thresholds.interactive_ms)
436 .unwrap_or(false);
437
438 let meets_energy = if let (Some(targets), Some(mobile_res)) =
440 (&mobile_config.energy_targets, mobile_results)
441 {
442 mobile_res
443 .detailed_metrics
444 .energy_efficiency
445 .map(|eff| eff >= targets.inferences_per_joule / 1000.0) .unwrap_or(false)
447 } else {
448 true };
450
451 let thermal_throttling = mobile_results
453 .map(|r| {
454 matches!(
455 r.detailed_metrics.thermal_state,
456 ThermalState::Hot | ThermalState::Critical
457 )
458 })
459 .unwrap_or(false);
460
461 let platform_validation = validate_platform_requirements(&mobile_config.platform_info);
463
464 let mut recommendations = Vec::new();
466
467 if !meets_realtime {
468 recommendations.push(
469 "Model latency exceeds real-time requirements. Consider quantization or pruning."
470 .to_string(),
471 );
472 }
473
474 if !meets_interactive {
475 recommendations.push(
476 "Model latency exceeds interactive requirements. Optimize critical path operations."
477 .to_string(),
478 );
479 }
480
481 if !meets_energy {
482 recommendations.push("Model energy efficiency below target. Consider lower precision or architectural changes.".to_string());
483 }
484
485 if thermal_throttling {
486 recommendations.push(
487 "Thermal throttling detected. Reduce computational intensity or add thermal breaks."
488 .to_string(),
489 );
490 }
491
492 Ok(ValidationResults {
493 meets_realtime_latency: meets_realtime,
494 meets_interactive_latency: meets_interactive,
495 meets_energy_targets: meets_energy,
496 thermal_throttling_detected: thermal_throttling,
497 memory_pressure_impact: None, sustained_performance_degradation: None, platform_validation,
500 recommendations,
501 })
502}
503
504fn convert_to_optimized_model<M: Module>(model: &M) -> Result<OptimizedModel> {
520 use crate::mobile_optimizer::{ModelGraph, OptimizationMetadata};
521
522 let total_bytes: usize = model
523 .all_parameters()
524 .values()
525 .map(|param| {
526 let tensor = param.tensor();
527 let tensor = tensor.read();
528 tensor.numel() * tensor.dtype().size_bytes()
529 })
530 .sum();
531
532 Ok(OptimizedModel {
533 graph: ModelGraph {
534 nodes: vec![],
535 edges: vec![],
536 inputs: vec![],
537 outputs: vec![],
538 },
539 weights: HashMap::new(),
540 metadata: OptimizationMetadata {
541 original_size: total_bytes,
542 optimized_size: total_bytes,
543 compression_ratio: 1.0,
548 applied_passes: vec!["benchmark_conversion".to_string()],
549 estimated_speedup: 1.0,
550 backend_metadata: HashMap::new(),
551 },
552 backend_data: None,
553 })
554}
555
556fn time_forward_passes<M: Module>(
559 model: &M,
560 input_shape: &[usize],
561 samples: usize,
562) -> Result<Vec<f32>> {
563 let mut latencies_ms = Vec::with_capacity(samples);
564 for _ in 0..samples {
565 let input = torsh_tensor::creation::randn(input_shape)?;
566 let start = Instant::now();
567 let _ = model.forward(&input)?;
568 latencies_ms.push(start.elapsed().as_secs_f32() * 1000.0);
569 }
570 Ok(latencies_ms)
571}
572
573fn mean(values: &[f32]) -> f32 {
575 if values.is_empty() {
576 0.0
577 } else {
578 values.iter().sum::<f32>() / values.len() as f32
579 }
580}
581
582fn run_memory_pressure_test<M: Module>(
592 model: &M,
593 input_shape: &[usize],
594 _config: &MobileBenchmarkConfig,
595) -> Result<Option<f32>> {
596 const SAMPLES: usize = 8;
597 const PRESSURE_BYTES: usize = 64 * 1024 * 1024;
601
602 let baseline = time_forward_passes(model, input_shape, SAMPLES)?;
603
604 let mut pressure = vec![0u8; PRESSURE_BYTES];
610 for page in pressure.chunks_mut(4096) {
611 page[0] = 1;
612 }
613
614 let pressured = time_forward_passes(model, input_shape, SAMPLES)?;
615 drop(pressure);
616
617 let baseline_mean_ms = mean(&baseline);
618 let pressured_mean_ms = mean(&pressured);
619 if baseline_mean_ms <= 0.0 {
620 return Ok(None);
621 }
622
623 Ok(Some(
624 (pressured_mean_ms - baseline_mean_ms) / baseline_mean_ms * 100.0,
625 ))
626}
627
628fn run_frequency_scaling_test<M: Module>(
636 _model: &M,
637 _config: &MobileBenchmarkConfig,
638) -> Result<()> {
639 Err(torsh_core::TorshError::NotImplemented(
640 "CPU/GPU frequency-scaling control requires privileged, platform-specific system APIs \
641 not available in pure Rust"
642 .to_string(),
643 ))
644}
645
646fn run_sustained_performance_test<M: Module>(
661 model: &M,
662 input_shape: &[usize],
663 _config: &MobileBenchmarkConfig,
664 duration_minutes: u32,
665) -> Result<Option<f32>> {
666 const MIN_SAMPLES: usize = 8;
667 const MAX_SAMPLES: usize = 100_000;
668
669 let budget = Duration::from_secs(u64::from(duration_minutes) * 60);
670 let start = Instant::now();
671 let mut latencies_ms = Vec::new();
672
673 loop {
674 let input = torsh_tensor::creation::randn(input_shape)?;
675 let sample_start = Instant::now();
676 let _ = model.forward(&input)?;
677 latencies_ms.push(sample_start.elapsed().as_secs_f32() * 1000.0);
678
679 let min_met = latencies_ms.len() >= MIN_SAMPLES;
680 let time_up = start.elapsed() >= budget;
681 let hit_cap = latencies_ms.len() >= MAX_SAMPLES;
682 if (min_met && time_up) || hit_cap {
683 break;
684 }
685 }
686
687 if latencies_ms.len() < 2 {
688 return Ok(None);
689 }
690
691 let half = latencies_ms.len() / 2;
692 let first_half_mean = mean(&latencies_ms[..half]);
693 let second_half_mean = mean(&latencies_ms[half..]);
694 if first_half_mean <= 0.0 {
695 return Ok(None);
696 }
697
698 Ok(Some(
699 (second_half_mean - first_half_mean) / first_half_mean * 100.0,
700 ))
701}
702
703fn validate_platform_requirements(
705 platform_info: &PlatformBenchmarkInfo,
706) -> PlatformValidationResults {
707 match &platform_info.platform {
708 MobilePlatform::iOS { .. } => PlatformValidationResults {
709 ios_app_store_compliant: Some(true), android_performance_class: None,
711 device_compatibility_score: 85.0,
712 device_support_percentage: 95.0,
713 },
714 MobilePlatform::Android { .. } => PlatformValidationResults {
715 ios_app_store_compliant: None,
716 android_performance_class: Some("T".to_string()), device_compatibility_score: 80.0,
718 device_support_percentage: 90.0,
719 },
720 MobilePlatform::Other(_) => PlatformValidationResults {
721 ios_app_store_compliant: None,
722 android_performance_class: None,
723 device_compatibility_score: 70.0,
724 device_support_percentage: 75.0,
725 },
726 }
727}
728
729fn benchmark_batch_size<M: Module>(
731 model: &M,
732 batch_size: usize,
733 base_shape: &[usize],
734 config: &BenchmarkConfig,
735) -> Result<BatchResult> {
736 let mut input_shape = vec![batch_size];
737 input_shape.extend_from_slice(base_shape);
738
739 let mut forward_times = Vec::new();
740 let mut backward_times = Vec::new();
741 let mut memory_samples = Vec::new();
742
743 for _ in 0..config.warmup_iterations {
745 let input = torsh_tensor::creation::randn(&input_shape)?;
746 let _ = model.forward(&input)?;
747 }
748
749 let benchmark_start = Instant::now();
751
752 for _ in 0..config.benchmark_iterations {
753 let input = torsh_tensor::creation::randn(&input_shape)?;
754
755 let forward_start = Instant::now();
757 let output = model.forward(&input)?;
758 let forward_time = forward_start.elapsed();
759 forward_times.push(forward_time);
760
761 if config.profile_backward && output.requires_grad() {
763 let backward_start = Instant::now();
764 output.sum()?.backward()?;
765 let backward_time = backward_start.elapsed();
766 backward_times.push(backward_time);
767 }
768
769 if config.profile_memory {
771 if let Ok((allocated, reserved)) = get_current_memory() {
772 memory_samples.push((allocated, reserved));
773 }
774 }
775 }
776
777 let _total_time = benchmark_start.elapsed();
778
779 let forward_stats = calculate_timing_stats(&forward_times);
781 let backward_stats = if !backward_times.is_empty() {
782 Some(calculate_timing_stats(&backward_times))
783 } else {
784 None
785 };
786
787 let total_times: Vec<Duration> = forward_times
788 .iter()
789 .zip(
790 backward_times
791 .iter()
792 .chain(std::iter::repeat(&Duration::ZERO)),
793 )
794 .map(|(f, b)| *f + *b)
795 .collect();
796
797 let total_stats = calculate_timing_stats(&total_times);
798
799 let avg_time_per_sample = total_stats.mean.as_secs_f32() / batch_size as f32;
801 let throughput = 1.0 / avg_time_per_sample;
802
803 let memory_stats = if !memory_samples.is_empty() {
805 Some(calculate_memory_stats(&memory_samples))
806 } else {
807 None
808 };
809
810 Ok(BatchResult {
811 batch_size,
812 forward_time: forward_stats,
813 backward_time: backward_stats,
814 total_time: total_stats,
815 throughput,
816 memory_stats,
817 })
818}
819
820fn count_parameters<M: Module>(model: &M) -> usize {
825 model
826 .all_parameters()
827 .values()
828 .map(|p| p.tensor().read().numel())
829 .sum()
830}
831
832fn calculate_timing_stats(times: &[Duration]) -> TimingStats {
834 let mut sorted_times = times.to_vec();
835 sorted_times.sort();
836
837 let n = sorted_times.len() as f32;
838 let mean = sorted_times.iter().sum::<Duration>() / sorted_times.len() as u32;
839
840 let variance = sorted_times
841 .iter()
842 .map(|t| {
843 let diff = t.as_secs_f32() - mean.as_secs_f32();
844 diff * diff
845 })
846 .sum::<f32>()
847 / n;
848
849 let std = Duration::from_secs_f32(variance.sqrt());
850
851 TimingStats {
852 mean,
853 std,
854 min: sorted_times[0],
855 max: sorted_times[sorted_times.len() - 1],
856 median: sorted_times[sorted_times.len() / 2],
857 p95: sorted_times[(0.95 * n) as usize],
858 p99: sorted_times[(0.99 * n) as usize],
859 }
860}
861
862fn calculate_memory_stats(samples: &[(f32, f32)]) -> MemoryStats {
864 let peak_allocated = samples.iter().map(|(a, _)| *a).fold(0.0f32, f32::max);
865 let peak_reserved = samples.iter().map(|(_, r)| *r).fold(0.0f32, f32::max);
866 let avg_allocated = samples.iter().map(|(a, _)| *a).sum::<f32>() / samples.len() as f32;
867
868 MemoryStats {
869 peak_allocated_mb: peak_allocated,
870 peak_reserved_mb: peak_reserved,
871 avg_allocated_mb: avg_allocated,
872 }
873}
874
875fn get_current_memory() -> Result<(f32, f32)> {
878 #[cfg(target_os = "linux")]
879 {
880 let status = std::fs::read_to_string("/proc/self/status").map_err(|e| {
881 torsh_core::TorshError::IoError(format!("Failed to read /proc/self/status: {}", e))
882 })?;
883 let mut rss_kb: Option<u64> = None;
884 let mut peak_kb: Option<u64> = None;
885 for line in status.lines() {
886 if let Some(rest) = line.strip_prefix("VmRSS:") {
887 rss_kb = rest.split_whitespace().next().and_then(|s| s.parse().ok());
888 } else if let Some(rest) = line.strip_prefix("VmPeak:") {
889 peak_kb = rest.split_whitespace().next().and_then(|s| s.parse().ok());
890 }
891 if rss_kb.is_some() && peak_kb.is_some() {
892 break;
893 }
894 }
895 let rss_mb = rss_kb.unwrap_or(0) as f32 / 1024.0;
896 let peak_mb = peak_kb.unwrap_or(0) as f32 / 1024.0;
897 return Ok((rss_mb, peak_mb));
898 }
899 #[cfg(not(target_os = "linux"))]
900 {
901 Ok((0.0, 0.0))
903 }
904}
905
906fn generate_summary(results: &HashMap<usize, BatchResult>) -> BenchmarkSummary {
908 let mut best_throughput = 0.0;
909 let mut best_batch_size = 0;
910 let mut optimal_memory_batch = 0;
911 let mut min_memory_per_sample = f32::INFINITY;
912
913 for (batch_size, result) in results {
914 if result.throughput > best_throughput {
915 best_throughput = result.throughput;
916 best_batch_size = *batch_size;
917 }
918
919 if let Some(mem) = &result.memory_stats {
920 let memory_per_sample = mem.peak_allocated_mb / *batch_size as f32;
921 if memory_per_sample < min_memory_per_sample {
922 min_memory_per_sample = memory_per_sample;
923 optimal_memory_batch = *batch_size;
924 }
925 }
926 }
927
928 let mut recommendations = Vec::new();
929
930 recommendations.push(format!(
932 "Best throughput: {:.1} samples/sec at batch size {}",
933 best_throughput, best_batch_size
934 ));
935
936 if optimal_memory_batch > 0 {
938 recommendations.push(format!(
939 "Most memory efficient: batch size {} ({:.1} MB/sample)",
940 optimal_memory_batch, min_memory_per_sample
941 ));
942 }
943
944 let batch_sizes: Vec<usize> = results.keys().copied().collect();
946 if batch_sizes.len() >= 2 {
947 let min_batch = *batch_sizes.iter().min().expect("reduction should succeed");
948 let max_batch = *batch_sizes.iter().max().expect("reduction should succeed");
949
950 let min_result = &results[&min_batch];
951 let max_result = &results[&max_batch];
952
953 let scaling_efficiency =
954 (max_result.throughput * max_batch as f32) / (min_result.throughput * min_batch as f32);
955
956 if scaling_efficiency < 0.8 {
957 recommendations.push(format!(
958 "Poor scaling efficiency ({:.1}%). Consider optimizing data loading.",
959 scaling_efficiency * 100.0
960 ));
961 }
962 }
963
964 BenchmarkSummary {
965 best_batch_size,
966 best_throughput,
967 optimal_memory_batch,
968 recommendations,
969 }
970}
971
972pub fn print_benchmark_results(results: &BenchmarkResult) {
974 println!("=== Benchmark Results for {} ===", results.model_name);
975 println!("Total parameters: {}", results.total_params);
976 println!();
977
978 println!(
979 "{:<10} {:<15} {:<15} {:<15} {:<15}",
980 "Batch", "Forward (ms)", "Backward (ms)", "Total (ms)", "Throughput"
981 );
982 println!("{}", "-".repeat(75));
983
984 for batch_size in results.results_by_batch.keys() {
985 let result = &results.results_by_batch[batch_size];
986 let backward_str = result
987 .backward_time
988 .as_ref()
989 .map(|t| format!("{:.2}", t.mean.as_secs_f32() * 1000.0))
990 .unwrap_or_else(|| "N/A".to_string());
991
992 println!(
993 "{:<10} {:<15.2} {:<15} {:<15.2} {:<15.1}",
994 batch_size,
995 result.forward_time.mean.as_secs_f32() * 1000.0,
996 backward_str,
997 result.total_time.mean.as_secs_f32() * 1000.0,
998 result.throughput
999 );
1000 }
1001 println!();
1002
1003 println!("Summary:");
1004 for rec in &results.summary.recommendations {
1005 println!(" - {}", rec);
1006 }
1007}
1008
1009#[cfg(test)]
1010mod tests {
1011 use super::*;
1012
1013 #[test]
1014 fn test_get_current_memory_nonnegative() {
1015 let (rss, peak) = get_current_memory().unwrap_or((0.0, 0.0));
1016 assert!(rss >= 0.0, "RSS should be non-negative, got {}", rss);
1017 assert!(peak >= 0.0, "peak should be non-negative, got {}", peak);
1018 #[cfg(target_os = "linux")]
1019 {
1020 assert!(rss > 0.0, "RSS should be positive on Linux, got {}", rss);
1021 }
1022 }
1023
1024 #[test]
1025 fn test_timing_stats() {
1026 let times = vec![
1027 Duration::from_millis(10),
1028 Duration::from_millis(12),
1029 Duration::from_millis(11),
1030 Duration::from_millis(13),
1031 Duration::from_millis(14),
1032 ];
1033
1034 let stats = calculate_timing_stats(×);
1035 assert_eq!(stats.min, Duration::from_millis(10));
1036 assert_eq!(stats.max, Duration::from_millis(14));
1037 assert_eq!(stats.median, Duration::from_millis(12));
1038 }
1039
1040 fn f173_test_platform_info() -> PlatformBenchmarkInfo {
1050 use crate::mobile_optimizer::{CpuInfo, MemoryInfo};
1051
1052 PlatformBenchmarkInfo {
1053 platform: MobilePlatform::iOS {
1054 chip: "A15".to_string(),
1055 neural_engine: true,
1056 },
1057 device_model: "test-device".to_string(),
1058 os_version: "1.0".to_string(),
1059 cpu_info: CpuInfo {
1060 cores_performance: 2,
1061 cores_efficiency: 4,
1062 max_frequency_ghz: 3.0,
1063 cache_l1_kb: 128,
1064 cache_l2_kb: 4096,
1065 cache_l3_kb: None,
1066 },
1067 memory_info: MemoryInfo {
1068 total_mb: 4096,
1069 bandwidth_gb_s: 30.0,
1070 memory_type: "LPDDR5".to_string(),
1071 },
1072 thermal_design_power: None,
1073 }
1074 }
1075
1076 fn f173_test_mobile_config(
1077 test_memory_pressure: bool,
1078 test_frequency_scaling: bool,
1079 ) -> MobileBenchmarkConfig {
1080 MobileBenchmarkConfig {
1081 platform_info: f173_test_platform_info(),
1082 monitor_thermal: false,
1083 measure_power: false,
1084 test_frequency_scaling,
1085 test_memory_pressure,
1086 stress_test_duration_minutes: None,
1087 latency_thresholds: LatencyThresholds::default(),
1088 energy_targets: None,
1089 }
1090 }
1091
1092 #[test]
1096 fn test_convert_to_optimized_model_uses_real_model_size() {
1097 use torsh_nn::layers::Linear;
1098
1099 let small = Linear::new(4, 4, true);
1100 let large = Linear::new(256, 256, true);
1101
1102 let small_result = convert_to_optimized_model(&small).expect("conversion should succeed");
1103 let large_result = convert_to_optimized_model(&large).expect("conversion should succeed");
1104
1105 assert_ne!(
1106 small_result.metadata.original_size, 10_000_000,
1107 "original_size must not be the historical fixed placeholder"
1108 );
1109 assert_ne!(
1110 small_result.metadata.original_size, large_result.metadata.original_size,
1111 "differently-sized real models must report different real sizes"
1112 );
1113
1114 assert_eq!(small_result.metadata.original_size, 20 * 4);
1116 assert_eq!(large_result.metadata.original_size, 65_792 * 4);
1118
1119 for result in [&small_result, &large_result] {
1123 assert_eq!(
1124 result.metadata.optimized_size,
1125 result.metadata.original_size
1126 );
1127 assert_eq!(result.metadata.compression_ratio, 1.0);
1128 assert_eq!(result.metadata.estimated_speedup, 1.0);
1129 }
1130 }
1131
1132 #[test]
1136 fn test_run_frequency_scaling_test_is_honest_about_being_unimplemented() {
1137 use torsh_nn::layers::Linear;
1138
1139 let model = Linear::new(4, 4, true);
1140 let config = f173_test_mobile_config(false, true);
1141
1142 let result = run_frequency_scaling_test(&model, &config);
1143 assert!(
1144 result.is_err(),
1145 "frequency scaling control must return an honest error, not a fabricated Ok(())"
1146 );
1147 }
1148
1149 #[test]
1153 fn test_run_memory_pressure_test_returns_finite_measured_value() {
1154 use torsh_nn::layers::Linear;
1155
1156 let model = Linear::new(8, 8, true);
1157 let config = f173_test_mobile_config(true, false);
1158
1159 let result = run_memory_pressure_test(&model, &[1, 8], &config)
1160 .expect("memory pressure test should succeed on a real, working model");
1161 let value = result
1162 .expect("enough real timing samples were collected, so a comparison must be Some(_)");
1163 assert!(
1164 value.is_finite(),
1165 "measured degradation must be a real finite number, got {value}"
1166 );
1167 }
1168
1169 #[test]
1175 fn test_run_sustained_performance_test_returns_finite_measured_value() {
1176 use torsh_nn::layers::Linear;
1177
1178 let model = Linear::new(8, 8, true);
1179 let config = f173_test_mobile_config(false, false);
1180
1181 let result = run_sustained_performance_test(&model, &[1, 8], &config, 0)
1182 .expect("sustained performance test should succeed on a real, working model");
1183 let value = result.expect("enough real timing samples were collected for a comparison");
1184 assert!(
1185 value.is_finite(),
1186 "measured degradation must be a real finite number, got {value}"
1187 );
1188 }
1189}