1use crate::utils::file_utils::write_file_with_context;
4use anyhow::Result;
5use hashbrown::HashMap;
6use serde::{Deserialize, Serialize};
7use std::path::Path;
8use std::sync::Arc;
9use std::time::{Duration, Instant};
10use tokio::sync::RwLock;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct BenchmarkResults {
15 pub test_name: String,
16 pub iterations: u64,
17 pub total_duration: Duration,
18 pub avg_duration_ns: u64,
19 pub min_duration_ns: u64,
20 pub max_duration_ns: u64,
21 pub percentile_95_ns: u64,
22 pub percentile_99_ns: u64,
23 pub throughput_ops_per_sec: f64,
24 pub memory_usage_mb: Option<f64>,
25 pub cpu_usage_percent: Option<f64>,
26}
27
28#[derive(Debug, Clone, Default)]
30pub struct ResourceMetrics {
31 pub memory_used_mb: f64,
32 pub cpu_percent: f64,
33 pub network_bytes_sent: u64,
34 pub network_bytes_received: u64,
35 pub disk_reads: u64,
36 pub disk_writes: u64,
37}
38
39pub struct PerformanceProfiler {
41 sessions: Arc<RwLock<HashMap<String, BenchmarkSession>>>,
43
44 resource_monitor: Arc<ResourceMonitor>,
46
47 history: Arc<RwLock<Vec<BenchmarkResults>>>,
49}
50
51#[derive(Debug)]
53pub struct BenchmarkSession {
54 pub name: String,
55 pub start_time: Instant,
56 pub iterations: u64,
57 pub durations: Vec<Duration>,
58 pub resource_snapshots: Vec<ResourceMetrics>,
59}
60
61pub struct ResourceMonitor {
63 current_metrics: Arc<RwLock<ResourceMetrics>>,
65
66 monitor_interval: Duration,
68
69 is_monitoring: Arc<RwLock<bool>>,
71}
72
73impl PerformanceProfiler {
74 pub fn new() -> Self {
75 Self {
76 sessions: Arc::new(RwLock::new(HashMap::new())),
77 resource_monitor: Arc::new(ResourceMonitor::new(Duration::from_millis(100))),
78 history: Arc::new(RwLock::new(Vec::new())),
79 }
80 }
81
82 pub async fn start_benchmark(&self, name: &str) -> Result<()> {
84 let session = BenchmarkSession {
85 name: name.to_string(),
86 start_time: Instant::now(),
87 iterations: 0,
88 durations: Vec::new(),
89 resource_snapshots: Vec::new(),
90 };
91
92 self.sessions
93 .write()
94 .await
95 .insert(name.to_string(), session);
96 self.resource_monitor.start_monitoring().await?;
97
98 Ok(())
99 }
100
101 pub async fn record_operation(&self, session_name: &str, duration: Duration) -> Result<()> {
103 let mut sessions = self.sessions.write().await;
104 if let Some(session) = sessions.get_mut(session_name) {
105 session.iterations += 1;
106 session.durations.push(duration);
107
108 if session.iterations % 100 == 0 {
110 let metrics = self.resource_monitor.get_current_metrics().await;
111 session.resource_snapshots.push(metrics);
112 }
113 }
114
115 Ok(())
116 }
117
118 pub async fn end_benchmark(&self, session_name: &str) -> Result<BenchmarkResults> {
120 let session = {
121 let mut sessions = self.sessions.write().await;
122 sessions
123 .remove(session_name)
124 .ok_or_else(|| anyhow::anyhow!("Benchmark session '{}' not found", session_name))?
125 };
126
127 self.resource_monitor.stop_monitoring().await?;
128
129 let results = self.calculate_results(session).await;
130
131 self.history.write().await.push(results.clone());
133
134 Ok(results)
135 }
136
137 async fn calculate_results(&self, session: BenchmarkSession) -> BenchmarkResults {
139 let total_duration = session.start_time.elapsed();
140 let mut durations_ns: Vec<u64> = session
141 .durations
142 .iter()
143 .map(|d| d.as_nanos() as u64)
144 .collect();
145
146 durations_ns.sort_unstable();
147
148 let avg_duration_ns = if !durations_ns.is_empty() {
149 durations_ns.iter().sum::<u64>() / durations_ns.len() as u64
150 } else {
151 0
152 };
153
154 let min_duration_ns = durations_ns.first().copied().unwrap_or(0);
155 let max_duration_ns = durations_ns.last().copied().unwrap_or(0);
156
157 let percentile_95_ns = if !durations_ns.is_empty() {
158 let index = (durations_ns.len() as f64 * 0.95) as usize;
159 durations_ns
160 .get(index.min(durations_ns.len() - 1))
161 .copied()
162 .unwrap_or(0)
163 } else {
164 0
165 };
166
167 let percentile_99_ns = if !durations_ns.is_empty() {
168 let index = (durations_ns.len() as f64 * 0.99) as usize;
169 durations_ns
170 .get(index.min(durations_ns.len() - 1))
171 .copied()
172 .unwrap_or(0)
173 } else {
174 0
175 };
176
177 let throughput_ops_per_sec = if total_duration.as_secs_f64() > 0.0 {
178 session.iterations as f64 / total_duration.as_secs_f64()
179 } else {
180 0.0
181 };
182
183 let avg_memory_mb = if !session.resource_snapshots.is_empty() {
185 Some(
186 session
187 .resource_snapshots
188 .iter()
189 .map(|m| m.memory_used_mb)
190 .sum::<f64>()
191 / session.resource_snapshots.len() as f64,
192 )
193 } else {
194 None
195 };
196
197 let avg_cpu_percent = if !session.resource_snapshots.is_empty() {
198 Some(
199 session
200 .resource_snapshots
201 .iter()
202 .map(|m| m.cpu_percent)
203 .sum::<f64>()
204 / session.resource_snapshots.len() as f64,
205 )
206 } else {
207 None
208 };
209
210 BenchmarkResults {
211 test_name: session.name,
212 iterations: session.iterations,
213 total_duration,
214 avg_duration_ns,
215 min_duration_ns,
216 max_duration_ns,
217 percentile_95_ns,
218 percentile_99_ns,
219 throughput_ops_per_sec,
220 memory_usage_mb: avg_memory_mb,
221 cpu_usage_percent: avg_cpu_percent,
222 }
223 }
224
225 pub async fn get_history(&self) -> Vec<BenchmarkResults> {
227 self.history.read().await.clone()
228 }
229
230 pub fn compare_results(
232 &self,
233 baseline: &BenchmarkResults,
234 current: &BenchmarkResults,
235 ) -> ComparisonReport {
236 let throughput_change = if baseline.throughput_ops_per_sec > 0.0 {
237 ((current.throughput_ops_per_sec - baseline.throughput_ops_per_sec)
238 / baseline.throughput_ops_per_sec)
239 * 100.0
240 } else {
241 0.0
242 };
243
244 let avg_latency_change = if baseline.avg_duration_ns > 0 {
245 ((current.avg_duration_ns as f64 - baseline.avg_duration_ns as f64)
246 / baseline.avg_duration_ns as f64)
247 * 100.0
248 } else {
249 0.0
250 };
251
252 let memory_change = match (baseline.memory_usage_mb, current.memory_usage_mb) {
253 (Some(baseline_mem), Some(current_mem)) => {
254 Some(((current_mem - baseline_mem) / baseline_mem) * 100.0)
255 }
256 _ => None,
257 };
258
259 ComparisonReport {
260 baseline_name: baseline.test_name.clone(),
261 current_name: current.test_name.clone(),
262 throughput_change_percent: throughput_change,
263 avg_latency_change_percent: avg_latency_change,
264 memory_change_percent: memory_change,
265 is_improvement: throughput_change > 0.0 && avg_latency_change < 0.0,
266 }
267 }
268
269 pub async fn export_results(&self, file_path: &str) -> Result<()> {
271 let history = self.get_history().await;
272 let json = serde_json::to_string_pretty(&history)?;
273 write_file_with_context(Path::new(file_path), &json, "benchmark results").await?;
274 Ok(())
275 }
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct ComparisonReport {
281 pub baseline_name: String,
282 pub current_name: String,
283 pub throughput_change_percent: f64,
284 pub avg_latency_change_percent: f64,
285 pub memory_change_percent: Option<f64>,
286 pub is_improvement: bool,
287}
288
289impl ResourceMonitor {
290 pub fn new(monitor_interval: Duration) -> Self {
291 Self {
292 current_metrics: Arc::new(RwLock::new(ResourceMetrics::default())),
293 monitor_interval,
294 is_monitoring: Arc::new(RwLock::new(false)),
295 }
296 }
297
298 pub async fn start_monitoring(&self) -> Result<()> {
300 let mut is_monitoring = self.is_monitoring.write().await;
301 if *is_monitoring {
302 return Ok(()); }
304 *is_monitoring = true;
305 drop(is_monitoring);
306
307 let current_metrics = Arc::clone(&self.current_metrics);
308 let is_monitoring_flag = Arc::clone(&self.is_monitoring);
309 let interval = self.monitor_interval;
310
311 tokio::spawn(async move {
312 let mut interval_timer = tokio::time::interval(interval);
313
314 while *is_monitoring_flag.read().await {
315 interval_timer.tick().await;
316
317 let metrics = Self::collect_system_metrics().await;
318 *current_metrics.write().await = metrics;
319 }
320 });
321
322 Ok(())
323 }
324
325 pub async fn stop_monitoring(&self) -> Result<()> {
327 *self.is_monitoring.write().await = false;
328 Ok(())
329 }
330
331 pub async fn get_current_metrics(&self) -> ResourceMetrics {
333 self.current_metrics.read().await.clone()
334 }
335
336 async fn collect_system_metrics() -> ResourceMetrics {
338 ResourceMetrics {
342 memory_used_mb: Self::get_memory_usage_mb(),
343 cpu_percent: Self::get_cpu_usage_percent(),
344 network_bytes_sent: 0,
345 network_bytes_received: 0,
346 disk_reads: 0,
347 disk_writes: 0,
348 }
349 }
350
351 fn get_memory_usage_mb() -> f64 {
353 #[cfg(target_os = "linux")]
355 {
356 if let Ok(contents) = std::fs::read_to_string("/proc/self/status") {
357 for line in contents.lines() {
358 if line.starts_with("VmRSS:")
359 && let Some(kb_str) = line.split_whitespace().nth(1)
360 && let Ok(kb) = kb_str.parse::<f64>()
361 {
362 return kb / 1024.0; }
364 }
365 }
366 }
367
368 100.0
370 }
371
372 fn get_cpu_usage_percent() -> f64 {
374 0.0
377 }
378}
379
380#[macro_export]
382macro_rules! benchmark {
383 ($profiler:expr, $name:expr, $code:block) => {{
384 let start = std::time::Instant::now();
385 let result = $code;
386 let duration = start.elapsed();
387 $profiler.record_operation($name, duration).await?;
388 result
389 }};
390}
391
392pub struct BenchmarkUtils;
394
395impl BenchmarkUtils {
396 pub async fn benchmark_function<F, R>(
398 profiler: &PerformanceProfiler,
399 name: &str,
400 iterations: u64,
401 mut func: F,
402 ) -> Result<BenchmarkResults>
403 where
404 F: FnMut() -> R,
405 {
406 profiler.start_benchmark(name).await?;
407
408 for _ in 0..iterations {
409 let start = Instant::now();
410 let _ = func();
411 let duration = start.elapsed();
412 profiler.record_operation(name, duration).await?;
413 }
414
415 profiler.end_benchmark(name).await
416 }
417
418 pub async fn benchmark_async_function<F, Fut, R>(
420 profiler: &PerformanceProfiler,
421 name: &str,
422 iterations: u64,
423 mut func: F,
424 ) -> Result<BenchmarkResults>
425 where
426 F: FnMut() -> Fut,
427 Fut: Future<Output = R>,
428 {
429 profiler.start_benchmark(name).await?;
430
431 for _ in 0..iterations {
432 let start = Instant::now();
433 let _ = func().await;
434 let duration = start.elapsed();
435 profiler.record_operation(name, duration).await?;
436 }
437
438 profiler.end_benchmark(name).await
439 }
440
441 pub async fn regression_test(
443 profiler: &PerformanceProfiler,
444 baseline_name: &str,
445 current_name: &str,
446 max_regression_percent: f64,
447 ) -> Result<bool> {
448 let history = profiler.get_history().await;
449
450 let baseline = history
451 .iter()
452 .find(|r| r.test_name == baseline_name)
453 .ok_or_else(|| anyhow::anyhow!("Baseline '{}' not found", baseline_name))?;
454
455 let current = history
456 .iter()
457 .find(|r| r.test_name == current_name)
458 .ok_or_else(|| anyhow::anyhow!("Current '{}' not found", current_name))?;
459
460 let comparison = profiler.compare_results(baseline, current);
461
462 let regression = comparison.avg_latency_change_percent > max_regression_percent
464 || comparison.throughput_change_percent < -max_regression_percent;
465
466 if regression {
467 tracing::warn!(
468 latency_change_percent = comparison.avg_latency_change_percent,
469 throughput_change_percent = comparison.throughput_change_percent,
470 "Performance regression detected"
471 );
472 }
473
474 Ok(!regression)
475 }
476}
477
478impl Default for PerformanceProfiler {
479 fn default() -> Self {
480 Self::new()
481 }
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487 use tokio::time::sleep;
488
489 #[tokio::test]
490 async fn test_benchmark_session() -> Result<()> {
491 let profiler = PerformanceProfiler::new();
492
493 profiler.start_benchmark("test_session").await?;
494
495 for i in 0..10 {
497 let duration = Duration::from_millis(10 + i);
498 profiler.record_operation("test_session", duration).await?;
499 }
500
501 let results = profiler.end_benchmark("test_session").await?;
502
503 assert_eq!(results.test_name, "test_session");
504 assert_eq!(results.iterations, 10);
505 assert!(results.avg_duration_ns > 0);
506
507 Ok(())
508 }
509
510 #[tokio::test]
511 async fn test_benchmark_utils() -> Result<()> {
512 let profiler = PerformanceProfiler::new();
513
514 let results = BenchmarkUtils::benchmark_function(&profiler, "test_function", 100, || {
515 std::thread::sleep(Duration::from_micros(100));
517 42
518 })
519 .await?;
520
521 assert_eq!(results.iterations, 100);
522 assert!(results.throughput_ops_per_sec > 0.0);
523
524 Ok(())
525 }
526
527 #[tokio::test]
528 async fn test_async_benchmark() -> Result<()> {
529 let profiler = PerformanceProfiler::new();
530
531 let results = BenchmarkUtils::benchmark_async_function(
532 &profiler,
533 "test_async_function",
534 50,
535 || async {
536 sleep(Duration::from_micros(200)).await;
537 "result"
538 },
539 )
540 .await?;
541
542 assert_eq!(results.iterations, 50);
543 assert!(results.avg_duration_ns > 0);
544
545 Ok(())
546 }
547}