use crate::utils::file_utils::write_file_with_context;
use anyhow::Result;
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchmarkResults {
pub test_name: String,
pub iterations: u64,
pub total_duration: Duration,
pub avg_duration_ns: u64,
pub min_duration_ns: u64,
pub max_duration_ns: u64,
pub percentile_95_ns: u64,
pub percentile_99_ns: u64,
pub throughput_ops_per_sec: f64,
pub memory_usage_mb: Option<f64>,
pub cpu_usage_percent: Option<f64>,
}
#[derive(Debug, Clone, Default)]
pub struct ResourceMetrics {
pub memory_used_mb: f64,
pub cpu_percent: f64,
pub network_bytes_sent: u64,
pub network_bytes_received: u64,
pub disk_reads: u64,
pub disk_writes: u64,
}
pub struct PerformanceProfiler {
sessions: Arc<RwLock<HashMap<String, BenchmarkSession>>>,
resource_monitor: Arc<ResourceMonitor>,
history: Arc<RwLock<Vec<BenchmarkResults>>>,
}
#[derive(Debug)]
pub struct BenchmarkSession {
pub name: String,
pub start_time: Instant,
pub iterations: u64,
pub durations: Vec<Duration>,
pub resource_snapshots: Vec<ResourceMetrics>,
}
pub struct ResourceMonitor {
current_metrics: Arc<RwLock<ResourceMetrics>>,
monitor_interval: Duration,
is_monitoring: Arc<RwLock<bool>>,
}
impl PerformanceProfiler {
pub fn new() -> Self {
Self {
sessions: Arc::new(RwLock::new(HashMap::new())),
resource_monitor: Arc::new(ResourceMonitor::new(Duration::from_millis(100))),
history: Arc::new(RwLock::new(Vec::new())),
}
}
pub async fn start_benchmark(&self, name: &str) -> Result<()> {
let session = BenchmarkSession {
name: name.to_string(),
start_time: Instant::now(),
iterations: 0,
durations: Vec::new(),
resource_snapshots: Vec::new(),
};
self.sessions
.write()
.await
.insert(name.to_string(), session);
self.resource_monitor.start_monitoring().await?;
Ok(())
}
pub async fn record_operation(&self, session_name: &str, duration: Duration) -> Result<()> {
let mut sessions = self.sessions.write().await;
if let Some(session) = sessions.get_mut(session_name) {
session.iterations += 1;
session.durations.push(duration);
if session.iterations % 100 == 0 {
let metrics = self.resource_monitor.get_current_metrics().await;
session.resource_snapshots.push(metrics);
}
}
Ok(())
}
pub async fn end_benchmark(&self, session_name: &str) -> Result<BenchmarkResults> {
let session = {
let mut sessions = self.sessions.write().await;
sessions
.remove(session_name)
.ok_or_else(|| anyhow::anyhow!("Benchmark session '{}' not found", session_name))?
};
self.resource_monitor.stop_monitoring().await?;
let results = self.calculate_results(session).await;
self.history.write().await.push(results.clone());
Ok(results)
}
async fn calculate_results(&self, session: BenchmarkSession) -> BenchmarkResults {
let total_duration = session.start_time.elapsed();
let mut durations_ns: Vec<u64> = session
.durations
.iter()
.map(|d| d.as_nanos() as u64)
.collect();
durations_ns.sort_unstable();
let avg_duration_ns = if !durations_ns.is_empty() {
durations_ns.iter().sum::<u64>() / durations_ns.len() as u64
} else {
0
};
let min_duration_ns = durations_ns.first().copied().unwrap_or(0);
let max_duration_ns = durations_ns.last().copied().unwrap_or(0);
let percentile_95_ns = if !durations_ns.is_empty() {
let index = (durations_ns.len() as f64 * 0.95) as usize;
durations_ns
.get(index.min(durations_ns.len() - 1))
.copied()
.unwrap_or(0)
} else {
0
};
let percentile_99_ns = if !durations_ns.is_empty() {
let index = (durations_ns.len() as f64 * 0.99) as usize;
durations_ns
.get(index.min(durations_ns.len() - 1))
.copied()
.unwrap_or(0)
} else {
0
};
let throughput_ops_per_sec = if total_duration.as_secs_f64() > 0.0 {
session.iterations as f64 / total_duration.as_secs_f64()
} else {
0.0
};
let avg_memory_mb = if !session.resource_snapshots.is_empty() {
Some(
session
.resource_snapshots
.iter()
.map(|m| m.memory_used_mb)
.sum::<f64>()
/ session.resource_snapshots.len() as f64,
)
} else {
None
};
let avg_cpu_percent = if !session.resource_snapshots.is_empty() {
Some(
session
.resource_snapshots
.iter()
.map(|m| m.cpu_percent)
.sum::<f64>()
/ session.resource_snapshots.len() as f64,
)
} else {
None
};
BenchmarkResults {
test_name: session.name,
iterations: session.iterations,
total_duration,
avg_duration_ns,
min_duration_ns,
max_duration_ns,
percentile_95_ns,
percentile_99_ns,
throughput_ops_per_sec,
memory_usage_mb: avg_memory_mb,
cpu_usage_percent: avg_cpu_percent,
}
}
pub async fn get_history(&self) -> Vec<BenchmarkResults> {
self.history.read().await.clone()
}
pub fn compare_results(
&self,
baseline: &BenchmarkResults,
current: &BenchmarkResults,
) -> ComparisonReport {
let throughput_change = if baseline.throughput_ops_per_sec > 0.0 {
((current.throughput_ops_per_sec - baseline.throughput_ops_per_sec)
/ baseline.throughput_ops_per_sec)
* 100.0
} else {
0.0
};
let avg_latency_change = if baseline.avg_duration_ns > 0 {
((current.avg_duration_ns as f64 - baseline.avg_duration_ns as f64)
/ baseline.avg_duration_ns as f64)
* 100.0
} else {
0.0
};
let memory_change = match (baseline.memory_usage_mb, current.memory_usage_mb) {
(Some(baseline_mem), Some(current_mem)) => {
Some(((current_mem - baseline_mem) / baseline_mem) * 100.0)
}
_ => None,
};
ComparisonReport {
baseline_name: baseline.test_name.clone(),
current_name: current.test_name.clone(),
throughput_change_percent: throughput_change,
avg_latency_change_percent: avg_latency_change,
memory_change_percent: memory_change,
is_improvement: throughput_change > 0.0 && avg_latency_change < 0.0,
}
}
pub async fn export_results(&self, file_path: &str) -> Result<()> {
let history = self.get_history().await;
let json = serde_json::to_string_pretty(&history)?;
write_file_with_context(Path::new(file_path), &json, "benchmark results").await?;
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComparisonReport {
pub baseline_name: String,
pub current_name: String,
pub throughput_change_percent: f64,
pub avg_latency_change_percent: f64,
pub memory_change_percent: Option<f64>,
pub is_improvement: bool,
}
impl ResourceMonitor {
pub fn new(monitor_interval: Duration) -> Self {
Self {
current_metrics: Arc::new(RwLock::new(ResourceMetrics::default())),
monitor_interval,
is_monitoring: Arc::new(RwLock::new(false)),
}
}
pub async fn start_monitoring(&self) -> Result<()> {
let mut is_monitoring = self.is_monitoring.write().await;
if *is_monitoring {
return Ok(()); }
*is_monitoring = true;
drop(is_monitoring);
let current_metrics = Arc::clone(&self.current_metrics);
let is_monitoring_flag = Arc::clone(&self.is_monitoring);
let interval = self.monitor_interval;
tokio::spawn(async move {
let mut interval_timer = tokio::time::interval(interval);
while *is_monitoring_flag.read().await {
interval_timer.tick().await;
let metrics = Self::collect_system_metrics().await;
*current_metrics.write().await = metrics;
}
});
Ok(())
}
pub async fn stop_monitoring(&self) -> Result<()> {
*self.is_monitoring.write().await = false;
Ok(())
}
pub async fn get_current_metrics(&self) -> ResourceMetrics {
self.current_metrics.read().await.clone()
}
async fn collect_system_metrics() -> ResourceMetrics {
ResourceMetrics {
memory_used_mb: Self::get_memory_usage_mb(),
cpu_percent: Self::get_cpu_usage_percent(),
network_bytes_sent: 0,
network_bytes_received: 0,
disk_reads: 0,
disk_writes: 0,
}
}
fn get_memory_usage_mb() -> f64 {
#[cfg(target_os = "linux")]
{
if let Ok(contents) = std::fs::read_to_string("/proc/self/status") {
for line in contents.lines() {
if line.starts_with("VmRSS:")
&& let Some(kb_str) = line.split_whitespace().nth(1)
&& let Ok(kb) = kb_str.parse::<f64>()
{
return kb / 1024.0; }
}
}
}
100.0
}
fn get_cpu_usage_percent() -> f64 {
0.0
}
}
#[macro_export]
macro_rules! benchmark {
($profiler:expr, $name:expr, $code:block) => {{
let start = std::time::Instant::now();
let result = $code;
let duration = start.elapsed();
$profiler.record_operation($name, duration).await?;
result
}};
}
pub struct BenchmarkUtils;
impl BenchmarkUtils {
pub async fn benchmark_function<F, R>(
profiler: &PerformanceProfiler,
name: &str,
iterations: u64,
mut func: F,
) -> Result<BenchmarkResults>
where
F: FnMut() -> R,
{
profiler.start_benchmark(name).await?;
for _ in 0..iterations {
let start = Instant::now();
let _ = func();
let duration = start.elapsed();
profiler.record_operation(name, duration).await?;
}
profiler.end_benchmark(name).await
}
pub async fn benchmark_async_function<F, Fut, R>(
profiler: &PerformanceProfiler,
name: &str,
iterations: u64,
mut func: F,
) -> Result<BenchmarkResults>
where
F: FnMut() -> Fut,
Fut: Future<Output = R>,
{
profiler.start_benchmark(name).await?;
for _ in 0..iterations {
let start = Instant::now();
let _ = func().await;
let duration = start.elapsed();
profiler.record_operation(name, duration).await?;
}
profiler.end_benchmark(name).await
}
pub async fn regression_test(
profiler: &PerformanceProfiler,
baseline_name: &str,
current_name: &str,
max_regression_percent: f64,
) -> Result<bool> {
let history = profiler.get_history().await;
let baseline = history
.iter()
.find(|r| r.test_name == baseline_name)
.ok_or_else(|| anyhow::anyhow!("Baseline '{}' not found", baseline_name))?;
let current = history
.iter()
.find(|r| r.test_name == current_name)
.ok_or_else(|| anyhow::anyhow!("Current '{}' not found", current_name))?;
let comparison = profiler.compare_results(baseline, current);
let regression = comparison.avg_latency_change_percent > max_regression_percent
|| comparison.throughput_change_percent < -max_regression_percent;
if regression {
tracing::warn!(
latency_change_percent = comparison.avg_latency_change_percent,
throughput_change_percent = comparison.throughput_change_percent,
"Performance regression detected"
);
}
Ok(!regression)
}
}
impl Default for PerformanceProfiler {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::time::sleep;
#[tokio::test]
async fn test_benchmark_session() -> Result<()> {
let profiler = PerformanceProfiler::new();
profiler.start_benchmark("test_session").await?;
for i in 0..10 {
let duration = Duration::from_millis(10 + i);
profiler.record_operation("test_session", duration).await?;
}
let results = profiler.end_benchmark("test_session").await?;
assert_eq!(results.test_name, "test_session");
assert_eq!(results.iterations, 10);
assert!(results.avg_duration_ns > 0);
Ok(())
}
#[tokio::test]
async fn test_benchmark_utils() -> Result<()> {
let profiler = PerformanceProfiler::new();
let results = BenchmarkUtils::benchmark_function(&profiler, "test_function", 100, || {
std::thread::sleep(Duration::from_micros(100));
42
})
.await?;
assert_eq!(results.iterations, 100);
assert!(results.throughput_ops_per_sec > 0.0);
Ok(())
}
#[tokio::test]
async fn test_async_benchmark() -> Result<()> {
let profiler = PerformanceProfiler::new();
let results = BenchmarkUtils::benchmark_async_function(
&profiler,
"test_async_function",
50,
|| async {
sleep(Duration::from_micros(200)).await;
"result"
},
)
.await?;
assert_eq!(results.iterations, 50);
assert!(results.avg_duration_ns > 0);
Ok(())
}
}