use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use super::types::{GoldenTraceError, GoldenTraceResult, SyscallBreakdownDelta, TraceMetrics};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoldenTrace {
pub version: String,
pub name: String,
pub timestamp: u64,
#[serde(default)]
pub git_commit: Option<String>,
pub metrics: TraceMetrics,
#[serde(default)]
pub hash: String,
#[serde(default)]
pub environment: HashMap<String, String>,
}
impl GoldenTrace {
pub fn new(name: &str, metrics: TraceMetrics) -> Self {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_secs();
let mut trace = Self {
version: "1.0".to_string(),
name: name.to_string(),
timestamp,
git_commit: None,
metrics,
hash: String::new(),
environment: HashMap::new(),
};
trace.hash = trace.compute_hash();
trace
}
pub fn with_version(name: &str, version: &str, metrics: TraceMetrics) -> Self {
let mut trace = Self::new(name, metrics);
trace.version = version.to_string();
trace.hash = trace.compute_hash();
trace
}
pub fn git_commit(mut self, commit: &str) -> Self {
self.git_commit = Some(commit.to_string());
self.hash = self.compute_hash();
self
}
pub fn with_env(mut self, key: &str, value: &str) -> Self {
self.environment.insert(key.to_string(), value.to_string());
self
}
pub fn compute_hash(&self) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
self.name.hash(&mut hasher);
self.version.hash(&mut hasher);
self.timestamp.hash(&mut hasher);
(self.metrics.total_time_us as u64).hash(&mut hasher);
(self.metrics.p50_latency_us as u64).hash(&mut hasher);
(self.metrics.p99_latency_us as u64).hash(&mut hasher);
(self.metrics.throughput as u64).hash(&mut hasher);
self.metrics.peak_memory_bytes.hash(&mut hasher);
format!("{:016x}", hasher.finish())
}
pub fn verify_hash(&self) -> bool {
self.hash == self.compute_hash()
}
pub fn to_json(&self) -> GoldenTraceResult<String> {
serde_json::to_string_pretty(self).map_err(|e| GoldenTraceError::ParseError(e.to_string()))
}
pub fn from_json(json: &str) -> GoldenTraceResult<Self> {
serde_json::from_str(json).map_err(|e| GoldenTraceError::ParseError(e.to_string()))
}
pub fn to_toml(&self) -> GoldenTraceResult<String> {
toml::to_string_pretty(self).map_err(|e| GoldenTraceError::ParseError(e.to_string()))
}
pub fn from_toml(toml_str: &str) -> GoldenTraceResult<Self> {
toml::from_str(toml_str).map_err(|e| GoldenTraceError::ParseError(e.to_string()))
}
pub fn save(&self, path: &Path) -> GoldenTraceResult<()> {
let content = if path.extension().map_or(false, |ext| ext == "json") {
self.to_json()?
} else {
self.to_toml()?
};
std::fs::write(path, content).map_err(|e| GoldenTraceError::IoError(e.to_string()))
}
pub fn load(path: &Path) -> GoldenTraceResult<Self> {
if !path.exists() {
return Err(GoldenTraceError::NoBaseline);
}
let content =
std::fs::read_to_string(path).map_err(|e| GoldenTraceError::IoError(e.to_string()))?;
if path.extension().map_or(false, |ext| ext == "json") {
Self::from_json(&content)
} else {
Self::from_toml(&content)
}
}
}
#[derive(Debug, Clone)]
pub struct TraceComparison {
pub golden_name: String,
pub golden_version: String,
pub time_delta_percent: f64,
pub p50_delta_percent: f64,
pub p99_delta_percent: f64,
pub throughput_delta_percent: f64,
pub memory_delta_percent: f64,
pub syscall_delta: SyscallBreakdownDelta,
pub is_regression: bool,
pub threshold_percent: f64,
pub summary: String,
}
impl TraceComparison {
pub fn max_regression(&self) -> f64 {
self.time_delta_percent
.max(self.p50_delta_percent)
.max(self.p99_delta_percent)
.max(-self.throughput_delta_percent) .max(self.memory_delta_percent)
}
pub fn regressions(&self) -> Vec<(String, f64)> {
let mut regressions = Vec::new();
let threshold = self.threshold_percent;
if self.time_delta_percent > threshold {
regressions.push(("total_time".to_string(), self.time_delta_percent));
}
if self.p50_delta_percent > threshold {
regressions.push(("p50_latency".to_string(), self.p50_delta_percent));
}
if self.p99_delta_percent > threshold {
regressions.push(("p99_latency".to_string(), self.p99_delta_percent));
}
if -self.throughput_delta_percent > threshold {
regressions.push(("throughput".to_string(), self.throughput_delta_percent));
}
if self.memory_delta_percent > threshold {
regressions.push(("memory".to_string(), self.memory_delta_percent));
}
regressions
}
}
#[derive(Debug, Clone)]
pub struct GoldenComparator {
pub threshold_percent: f64,
pub allow_version_mismatch: bool,
}
impl Default for GoldenComparator {
fn default() -> Self {
Self {
threshold_percent: 10.0,
allow_version_mismatch: false,
}
}
}
impl GoldenComparator {
pub fn new() -> Self {
Self::default()
}
pub fn with_threshold(mut self, percent: f64) -> Self {
self.threshold_percent = percent;
self
}
pub fn allow_version_mismatch(mut self) -> Self {
self.allow_version_mismatch = true;
self
}
pub fn compare(
&self,
current: &TraceMetrics,
golden: &GoldenTrace,
) -> GoldenTraceResult<TraceComparison> {
if !current.is_valid() {
return Err(GoldenTraceError::InvalidTrace(
"Current metrics are invalid".to_string(),
));
}
if !golden.metrics.is_valid() {
return Err(GoldenTraceError::InvalidTrace(
"Golden metrics are invalid".to_string(),
));
}
let time_delta = Self::calc_delta(current.total_time_us, golden.metrics.total_time_us);
let p50_delta = Self::calc_delta(current.p50_latency_us, golden.metrics.p50_latency_us);
let p99_delta = Self::calc_delta(current.p99_latency_us, golden.metrics.p99_latency_us);
let throughput_delta = Self::calc_delta(current.throughput, golden.metrics.throughput);
let memory_delta = Self::calc_delta(
current.peak_memory_bytes as f64,
golden.metrics.peak_memory_bytes as f64,
);
let syscall_delta = current.syscalls.percentage_diff(&golden.metrics.syscalls);
let is_regression = time_delta > self.threshold_percent
|| p50_delta > self.threshold_percent
|| p99_delta > self.threshold_percent
|| -throughput_delta > self.threshold_percent
|| memory_delta > self.threshold_percent;
let summary = if is_regression {
format!(
"REGRESSION detected vs {}: max delta {:.1}%",
golden.name,
time_delta
.max(p50_delta)
.max(p99_delta)
.max(-throughput_delta)
.max(memory_delta)
)
} else {
format!(
"No regression vs {}: within {:.1}% threshold",
golden.name, self.threshold_percent
)
};
Ok(TraceComparison {
golden_name: golden.name.clone(),
golden_version: golden.version.clone(),
time_delta_percent: time_delta,
p50_delta_percent: p50_delta,
p99_delta_percent: p99_delta,
throughput_delta_percent: throughput_delta,
memory_delta_percent: memory_delta,
syscall_delta,
is_regression,
threshold_percent: self.threshold_percent,
summary,
})
}
fn calc_delta(current: f64, baseline: f64) -> f64 {
if baseline == 0.0 {
if current == 0.0 {
0.0
} else {
100.0
}
} else {
((current - baseline) / baseline) * 100.0
}
}
}