1use crate::benchmark_framework::{BenchmarkResult, BenchmarkSuite};
7
8#[cfg(not(feature = "no-std"))]
9use std::{
10 collections::{HashMap, HashSet},
11 fs::{File, OpenOptions},
12 io::{BufRead, BufReader, Write},
13 path::Path,
14 string::ToString,
15 time::{SystemTime, UNIX_EPOCH},
16};
17
18#[cfg(feature = "no-std")]
19use alloc::collections::{BTreeMap as HashMap, BTreeSet as HashSet};
20#[cfg(feature = "no-std")]
21use alloc::format;
22#[cfg(feature = "no-std")]
23use alloc::string::{String, ToString};
24#[cfg(feature = "no-std")]
25use alloc::vec::Vec;
26
27#[cfg(feature = "no-std")]
31fn current_timestamp() -> u64 {
32 0
34}
35
36#[derive(Debug)]
38pub enum PerformanceError {
39 #[cfg(not(feature = "no-std"))]
40 IoError(std::io::Error),
41 Message(String),
42}
43
44#[cfg(not(feature = "no-std"))]
45impl From<std::io::Error> for PerformanceError {
46 fn from(error: std::io::Error) -> Self {
47 PerformanceError::IoError(error)
48 }
49}
50
51pub struct PerformanceMonitor {
53 #[allow(dead_code)] results_file: String,
55 historical_data: Vec<PerformanceRecord>,
56 thresholds: PerformanceThresholds,
57}
58
59#[derive(Debug, Clone)]
61pub struct PerformanceRecord {
62 pub timestamp: u64,
63 pub git_commit: Option<String>,
64 pub operation: String,
65 pub duration_ns: u64,
66 pub throughput: Option<f64>,
67 pub architecture: String,
68 pub simd_width: usize,
69 pub optimization_level: String,
70}
71
72#[derive(Debug, Clone)]
74pub struct PerformanceThresholds {
75 pub regression_threshold: f64, pub improvement_threshold: f64, pub critical_slowdown: f64, }
79
80impl Default for PerformanceThresholds {
81 fn default() -> Self {
82 Self {
83 regression_threshold: 5.0, improvement_threshold: 10.0, critical_slowdown: 25.0, }
87 }
88}
89
90#[derive(Debug)]
92pub struct PerformanceAlert {
93 pub alert_type: AlertType,
94 pub operation: String,
95 pub current_performance: f64,
96 pub baseline_performance: f64,
97 pub change_percent: f64,
98 pub severity: AlertSeverity,
99 pub recommendation: String,
100}
101
102#[derive(Debug)]
103pub enum AlertType {
104 Regression,
105 Improvement,
106 CriticalSlowdown,
107 Anomaly,
108}
109
110#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
111pub enum AlertSeverity {
112 Info,
113 Warning,
114 Error,
115 Critical,
116}
117
118impl PerformanceMonitor {
119 #[cfg(not(feature = "no-std"))]
121 pub fn new(results_file: &str) -> std::io::Result<Self> {
122 let mut monitor = Self {
123 results_file: results_file.to_string(),
124 historical_data: Vec::new(),
125 thresholds: PerformanceThresholds::default(),
126 };
127
128 monitor.load_historical_data()?;
129 Ok(monitor)
130 }
131
132 #[cfg(feature = "no-std")]
134 pub fn new(results_file: &str) -> Result<Self, &'static str> {
135 let mut monitor = Self {
136 results_file: results_file.to_string(),
137 historical_data: Vec::new(),
138 thresholds: PerformanceThresholds::default(),
139 };
140
141 monitor.load_historical_data()?;
142 Ok(monitor)
143 }
144
145 pub fn set_thresholds(&mut self, thresholds: PerformanceThresholds) {
147 self.thresholds = thresholds;
148 }
149
150 #[cfg(not(feature = "no-std"))]
152 pub fn record_results(
153 &mut self,
154 results: &[BenchmarkResult],
155 git_commit: Option<String>,
156 ) -> Result<(), PerformanceError> {
157 #[cfg(not(feature = "no-std"))]
158 let timestamp = SystemTime::now()
159 .duration_since(UNIX_EPOCH)
160 .expect("operation should succeed")
161 .as_secs();
162 #[cfg(feature = "no-std")]
163 let timestamp = current_timestamp();
164
165 let mut file = OpenOptions::new()
166 .create(true)
167 .append(true)
168 .open(&self.results_file)?;
169
170 for result in results {
171 let record = PerformanceRecord {
172 timestamp,
173 git_commit: git_commit.clone(),
174 operation: result.name.clone(),
175 duration_ns: result.duration.as_nanos() as u64,
176 throughput: result.throughput,
177 architecture: result.architecture.clone(),
178 simd_width: result.simd_width,
179 optimization_level: "release".to_string(), };
181
182 writeln!(
184 file,
185 "{},{},{},{},{},{},{},{},",
186 record.timestamp,
187 record.git_commit.as_ref().unwrap_or(&"unknown".to_string()),
188 record.operation,
189 record.duration_ns,
190 record.throughput.map(|t| t.to_string()).unwrap_or_default(),
191 record.architecture,
192 record.simd_width,
193 record.optimization_level )?;
195
196 self.historical_data.push(record);
197 }
198
199 Ok(())
200 }
201
202 #[cfg(feature = "no-std")]
204 pub fn record_results(
205 &mut self,
206 results: &[BenchmarkResult],
207 git_commit: Option<String>,
208 ) -> Result<(), &'static str> {
209 let timestamp = 0; for result in results {
212 let record = PerformanceRecord {
213 timestamp,
214 git_commit: git_commit.clone(),
215 operation: result.name.clone(),
216 duration_ns: result.duration.as_nanos() as u64,
217 throughput: result.throughput,
218 architecture: result.architecture.clone(),
219 simd_width: result.simd_width,
220 optimization_level: "release".to_string(), };
222 self.historical_data.push(record);
223 }
224
225 Ok(())
226 }
227
228 #[cfg(not(feature = "no-std"))]
230 pub fn analyze_trends(&self, operation: &str, days_back: u64) -> PerformanceTrend {
231 #[cfg(not(feature = "no-std"))]
232 let cutoff_timestamp = {
233 let now = SystemTime::now()
234 .duration_since(UNIX_EPOCH)
235 .expect("operation should succeed")
236 .as_secs();
237 let window = days_back.saturating_mul(24 * 60 * 60);
238 now.saturating_sub(window)
239 };
240 #[cfg(feature = "no-std")]
241 let cutoff_timestamp = current_timestamp() - (days_back * 24 * 60 * 60);
242
243 let relevant_data: Vec<&PerformanceRecord> = self
244 .historical_data
245 .iter()
246 .filter(|record| record.operation == operation && record.timestamp >= cutoff_timestamp)
247 .collect();
248
249 if relevant_data.is_empty() {
250 return PerformanceTrend::NoData;
251 }
252
253 let durations: Vec<f64> = relevant_data.iter().map(|r| r.duration_ns as f64).collect();
255 let trend_slope = self.calculate_trend_slope(&durations);
256
257 let avg_duration = durations.iter().sum::<f64>() / durations.len() as f64;
258 let min_duration = durations.iter().fold(f64::INFINITY, |a, &b| a.min(b));
259 let max_duration = durations.iter().fold(0.0f64, |a, &b| a.max(b));
260
261 PerformanceTrend::Data {
262 operation: operation.to_string(),
263 data_points: relevant_data.len(),
264 trend_slope,
265 avg_duration_ns: avg_duration as u64,
266 min_duration_ns: min_duration as u64,
267 max_duration_ns: max_duration as u64,
268 variance: self.calculate_variance(&durations, avg_duration),
269 }
270 }
271
272 #[cfg(feature = "no-std")]
274 pub fn analyze_trends(&self, operation: &str, _days_back: u64) -> PerformanceTrend {
275 let relevant_data: Vec<&PerformanceRecord> = self
276 .historical_data
277 .iter()
278 .filter(|r| r.operation == operation)
279 .collect();
280
281 if relevant_data.is_empty() {
282 return PerformanceTrend::NoData;
283 }
284
285 let durations: Vec<f64> = relevant_data.iter().map(|r| r.duration_ns as f64).collect();
287 let trend_slope = self.calculate_trend_slope(&durations);
288
289 let avg_duration = durations.iter().sum::<f64>() / durations.len() as f64;
290 let min_duration = durations.iter().fold(f64::INFINITY, |a, &b| a.min(b));
291 let max_duration = durations.iter().fold(0.0f64, |a, &b| a.max(b));
292
293 PerformanceTrend::Data {
294 operation: operation.to_string(),
295 data_points: relevant_data.len(),
296 trend_slope,
297 avg_duration_ns: avg_duration as u64,
298 min_duration_ns: min_duration as u64,
299 max_duration_ns: max_duration as u64,
300 variance: self.calculate_variance(&durations, avg_duration),
301 }
302 }
303
304 pub fn check_alerts(&self, current_results: &[BenchmarkResult]) -> Vec<PerformanceAlert> {
306 let mut alerts = Vec::new();
307
308 for result in current_results {
309 if let Some(baseline) = self.get_baseline_performance(&result.name) {
310 let current_ns = result.duration.as_nanos() as f64;
311 let baseline_ns = baseline.duration_ns as f64;
312 let change_percent = ((current_ns - baseline_ns) / baseline_ns) * 100.0;
313
314 if change_percent > self.thresholds.critical_slowdown {
315 alerts.push(PerformanceAlert {
316 alert_type: AlertType::CriticalSlowdown,
317 operation: result.name.clone(),
318 current_performance: current_ns,
319 baseline_performance: baseline_ns,
320 change_percent,
321 severity: AlertSeverity::Critical,
322 recommendation:
323 "Critical performance regression detected. Investigate immediately."
324 .to_string(),
325 });
326 } else if change_percent > self.thresholds.regression_threshold {
327 alerts.push(PerformanceAlert {
328 alert_type: AlertType::Regression,
329 operation: result.name.clone(),
330 current_performance: current_ns,
331 baseline_performance: baseline_ns,
332 change_percent,
333 severity: if change_percent > 15.0 {
334 AlertSeverity::Error
335 } else {
336 AlertSeverity::Warning
337 },
338 recommendation: format!(
339 "Performance regression of {:.1}%. Review recent changes.",
340 change_percent
341 ),
342 });
343 } else if change_percent < -self.thresholds.improvement_threshold {
344 alerts.push(PerformanceAlert {
345 alert_type: AlertType::Improvement,
346 operation: result.name.clone(),
347 current_performance: current_ns,
348 baseline_performance: baseline_ns,
349 change_percent,
350 severity: AlertSeverity::Info,
351 recommendation: format!(
352 "Performance improvement of {:.1}%. Great work!",
353 -change_percent
354 ),
355 });
356 }
357 }
358 }
359
360 alerts
361 }
362
363 pub fn generate_performance_report(&self, days_back: u64) -> PerformanceReport {
365 let operations: HashSet<String> = self
366 .historical_data
367 .iter()
368 .map(|r| r.operation.clone())
369 .collect();
370
371 let mut trends = HashMap::new();
372 for operation in operations {
373 trends.insert(
374 operation.clone(),
375 self.analyze_trends(&operation, days_back),
376 );
377 }
378
379 #[cfg(not(feature = "no-std"))]
380 let cutoff_timestamp = SystemTime::now()
381 .duration_since(UNIX_EPOCH)
382 .expect("operation should succeed")
383 .as_secs();
384 #[cfg(feature = "no-std")]
385 let cutoff_timestamp = current_timestamp() - (days_back * 24 * 60 * 60);
386
387 let recent_records: Vec<&PerformanceRecord> = self
388 .historical_data
389 .iter()
390 .filter(|record| record.timestamp >= cutoff_timestamp)
391 .collect();
392
393 PerformanceReport {
394 period_days: days_back,
395 total_benchmarks: recent_records.len(),
396 unique_operations: trends.len(),
397 trends,
398 summary: self.generate_summary(&recent_records),
399 }
400 }
401
402 #[cfg(not(feature = "no-std"))]
404 fn load_historical_data(&mut self) -> std::io::Result<()> {
405 if !Path::new(&self.results_file).exists() {
406 return Ok(());
407 }
408
409 let file = File::open(&self.results_file)?;
410 let reader = BufReader::new(file);
411
412 for line in reader.lines() {
413 let line = line?;
414 if let Some(record) = self.parse_record_line(&line) {
415 self.historical_data.push(record);
416 }
417 }
418
419 self.historical_data.sort_by_key(|r| r.timestamp);
421 Ok(())
422 }
423
424 #[cfg(feature = "no-std")]
426 fn load_historical_data(&mut self) -> Result<(), &'static str> {
427 Ok(())
429 }
430
431 #[cfg(not(feature = "no-std"))]
432 fn parse_record_line(&self, line: &str) -> Option<PerformanceRecord> {
433 let parts: Vec<&str> = line.split(',').collect();
434 if parts.len() < 8 {
435 return None;
436 }
437
438 Some(PerformanceRecord {
439 timestamp: parts[0].parse().ok()?,
440 git_commit: if parts[1] == "unknown" {
441 None
442 } else {
443 Some(parts[1].to_string())
444 },
445 operation: parts[2].to_string(),
446 duration_ns: parts[3].parse().ok()?,
447 throughput: if parts[4].is_empty() {
448 None
449 } else {
450 parts[4].parse().ok()
451 },
452 architecture: parts[5].to_string(),
453 simd_width: parts[6].parse().ok()?,
454 optimization_level: parts[7].to_string(),
455 })
456 }
457
458 fn get_baseline_performance(&self, operation: &str) -> Option<&PerformanceRecord> {
459 let mut relevant: Vec<&PerformanceRecord> = self
461 .historical_data
462 .iter()
463 .filter(|r| r.operation == operation)
464 .collect();
465
466 if relevant.len() < 5 {
467 return None;
468 }
469
470 relevant.sort_by_key(|r| r.timestamp);
471 let recent = &relevant[relevant.len().saturating_sub(10)..];
472
473 if recent.is_empty() {
474 return None;
475 }
476
477 let mut durations: Vec<&PerformanceRecord> = recent.to_vec();
479 durations.sort_by_key(|r| r.duration_ns);
480 Some(durations[durations.len() / 2])
481 }
482
483 fn calculate_trend_slope(&self, values: &[f64]) -> f64 {
484 if values.len() < 2 {
485 return 0.0;
486 }
487
488 let n = values.len() as f64;
489 let x_mean = (n - 1.0) / 2.0;
490 let y_mean = values.iter().sum::<f64>() / n;
491
492 let numerator: f64 = values
493 .iter()
494 .enumerate()
495 .map(|(i, &y)| (i as f64 - x_mean) * (y - y_mean))
496 .sum();
497
498 let denominator: f64 = (0..values.len()).map(|i| (i as f64 - x_mean).powi(2)).sum();
499
500 if denominator == 0.0 {
501 0.0
502 } else {
503 numerator / denominator
504 }
505 }
506
507 fn calculate_variance(&self, values: &[f64], mean: f64) -> f64 {
508 if values.len() < 2 {
509 return 0.0;
510 }
511
512 let variance =
513 values.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / (values.len() - 1) as f64;
514
515 variance
516 }
517
518 fn generate_summary(&self, records: &[&PerformanceRecord]) -> String {
519 if records.is_empty() {
520 return "No data available for the specified period.".to_string();
521 }
522
523 let total_duration: u64 = records.iter().map(|r| r.duration_ns).sum();
524 let avg_duration = total_duration / records.len() as u64;
525
526 let architectures: HashSet<&String> = records.iter().map(|r| &r.architecture).collect();
527
528 format!(
529 "Period summary: {} benchmarks across {} architectures. Average duration: {}ns",
530 records.len(),
531 architectures.len(),
532 avg_duration
533 )
534 }
535}
536
537#[derive(Debug)]
539pub enum PerformanceTrend {
540 NoData,
541 Data {
542 operation: String,
543 data_points: usize,
544 trend_slope: f64,
545 avg_duration_ns: u64,
546 min_duration_ns: u64,
547 max_duration_ns: u64,
548 variance: f64,
549 },
550}
551
552#[derive(Debug)]
554pub struct PerformanceReport {
555 pub period_days: u64,
556 pub total_benchmarks: usize,
557 pub unique_operations: usize,
558 pub trends: HashMap<String, PerformanceTrend>,
559 pub summary: String,
560}
561
562impl PerformanceReport {
563 pub fn format_report(&self) -> String {
565 let mut report = String::new();
566
567 report.push_str("=== Performance Report ===\n");
568 report.push_str(&format!("Period: {} days\n", self.period_days));
569 report.push_str(&format!("Total benchmarks: {}\n", self.total_benchmarks));
570 report.push_str(&format!("Unique operations: {}\n", self.unique_operations));
571 report.push_str(&format!("Summary: {}\n\n", self.summary));
572
573 report.push_str("Trends by operation:\n");
574 for (operation, trend) in &self.trends {
575 match trend {
576 PerformanceTrend::NoData => {
577 report.push_str(&format!(" {}: No data\n", operation));
578 }
579 PerformanceTrend::Data {
580 data_points,
581 trend_slope,
582 avg_duration_ns,
583 ..
584 } => {
585 let trend_direction = if *trend_slope > 1000.0 {
586 "SLOWER"
587 } else if *trend_slope < -1000.0 {
588 "FASTER"
589 } else {
590 "STABLE"
591 };
592
593 report.push_str(&format!(
594 " {}: {} ({} data points, avg: {}ns, trend: {})\n",
595 operation, trend_direction, data_points, avg_duration_ns, trend_direction
596 ));
597 }
598 }
599 }
600
601 report.push_str("\n=== End Report ===\n");
602 report
603 }
604}
605
606pub struct CIIntegration;
608
609impl CIIntegration {
610 #[cfg(not(feature = "no-std"))]
612 pub fn run_ci_benchmarks() -> Result<Vec<BenchmarkResult>, Box<dyn std::error::Error>> {
613 let mut suite = BenchmarkSuite::new();
614 let mut results = Vec::new();
615
616 let data: Vec<f32> = (0..2048).map(|i| i as f32 + 1.0).collect();
617 let mut scratch = vec![0.0f32; data.len()];
618
619 results.push(suite.benchmark("ci_dot_product", 200, || {
620 let _ = crate::vector::basic_operations::dot_product(&data, &data);
621 }));
622
623 results.push(suite.benchmark("ci_norm_l2", 200, || {
624 let _ = crate::vector::statistics_ops::norm_l2(&data);
625 }));
626
627 results.push(suite.benchmark("ci_reciprocal", 100, || {
628 crate::vector::math_functions::reciprocal_vec(&data, &mut scratch);
629 }));
630
631 Ok(results)
632 }
633
634 #[cfg(feature = "no-std")]
635 pub fn run_ci_benchmarks() -> Result<Vec<BenchmarkResult>, crate::traits::SimdError> {
636 let mut suite = BenchmarkSuite::new();
637 let mut results = Vec::new();
638
639 let test_data = (0..1000).map(|i| i as f32).collect::<Vec<f32>>();
641
642 results.push(suite.benchmark("ci_dot_product", 100, || {
643 let _result = crate::vector::dot_product(&test_data, &test_data);
644 }));
645
646 results.push(suite.benchmark("ci_euclidean_distance", 100, || {
647 let _result = crate::distance::euclidean_distance(&test_data, &test_data);
648 }));
649
650 Ok(results)
651 }
652
653 pub fn check_ci_performance(monitor: &PerformanceMonitor, results: &[BenchmarkResult]) -> bool {
655 let alerts = monitor.check_alerts(results);
656
657 !alerts
659 .iter()
660 .any(|alert| alert.severity == AlertSeverity::Critical)
661 }
662
663 pub fn generate_ci_summary(alerts: &[PerformanceAlert]) -> String {
665 if alerts.is_empty() {
666 "✅ No performance regressions detected".to_string()
667 } else {
668 let critical_count = alerts
669 .iter()
670 .filter(|a| a.severity == AlertSeverity::Critical)
671 .count();
672 let error_count = alerts
673 .iter()
674 .filter(|a| a.severity == AlertSeverity::Error)
675 .count();
676 let warning_count = alerts
677 .iter()
678 .filter(|a| a.severity == AlertSeverity::Warning)
679 .count();
680
681 format!(
682 "⚠️ Performance alerts: {} critical, {} errors, {} warnings",
683 critical_count, error_count, warning_count
684 )
685 }
686 }
687}
688
689#[allow(non_snake_case)]
690#[cfg(all(test, not(feature = "no-std")))]
691mod tests {
692 use super::*;
693 #[cfg(not(feature = "no-std"))]
694 use std::fs;
695 #[cfg(not(feature = "no-std"))]
696 use std::time::Duration;
697
698 #[test]
699 #[cfg(not(feature = "no-std"))]
700 fn test_performance_monitor_creation() {
701 let temp_path = std::env::temp_dir().join("test_perf_monitor.csv");
702 let temp_file = temp_path.to_string_lossy().into_owned();
703 let _ = fs::remove_file(&temp_file); let monitor = PerformanceMonitor::new(&temp_file);
706 assert!(monitor.is_ok());
707
708 let _ = fs::remove_file(&temp_file); }
710
711 #[test]
712 #[cfg(not(feature = "no-std"))]
713 fn test_performance_record_parsing() {
714 let temp_path = std::env::temp_dir().join("test_perf_parsing.csv");
715 let temp_file = temp_path.to_string_lossy().into_owned();
716 let _ = fs::remove_file(&temp_file);
717
718 let mut monitor = PerformanceMonitor::new(&temp_file).expect("operation should succeed");
719
720 let test_results = vec![BenchmarkResult {
721 name: "test_op".to_string(),
722 duration: Duration::from_millis(10),
723 throughput: Some(1000.0),
724 simd_width: 8,
725 architecture: "AVX2".to_string(),
726 iterations: 1000,
727 }];
728
729 let result = monitor.record_results(&test_results, Some("abc123".to_string()));
730 assert!(result.is_ok());
731
732 let _ = fs::remove_file(&temp_file);
733 }
734
735 #[test]
736 #[cfg(not(feature = "no-std"))]
737 fn test_trend_analysis() {
738 let temp_path = std::env::temp_dir().join("test_trend_analysis.csv");
739 let temp_file = temp_path.to_string_lossy().into_owned();
740 let _ = fs::remove_file(&temp_file);
741
742 let monitor = PerformanceMonitor::new(&temp_file).expect("operation should succeed");
743 let trend = monitor.analyze_trends("nonexistent_op", 7);
744
745 match trend {
746 PerformanceTrend::NoData => {
747 }
749 _ => panic!("Expected NoData for empty dataset"),
750 }
751
752 let _ = fs::remove_file(&temp_file);
753 }
754
755 #[test]
756 #[cfg(not(feature = "no-std"))]
757 fn test_ci_integration() {
758 let results = CIIntegration::run_ci_benchmarks();
759 assert!(results.is_ok());
760
761 let results = results.expect("operation should succeed");
762 assert!(!results.is_empty());
763
764 for result in &results {
765 assert!(result.duration > Duration::from_nanos(0));
766 }
767 }
768
769 #[test]
770 fn test_performance_thresholds() {
771 let thresholds = PerformanceThresholds::default();
772 assert_eq!(thresholds.regression_threshold, 5.0);
773 assert_eq!(thresholds.improvement_threshold, 10.0);
774 assert_eq!(thresholds.critical_slowdown, 25.0);
775 }
776}