use crate::error::QuantEvalError;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchmarkReceipt {
pub timestamp: DateTime<Utc>,
pub commit_hash: String,
pub machine_fingerprint: String,
pub results: Vec<BenchmarkResult>,
pub note: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchmarkResult {
pub name: String,
pub iterations: u64,
pub elapsed_ns: u64,
pub ns_per_iter: u64,
pub throughput: Option<f64>,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReceiptDiff {
pub baseline_hash: String,
pub target_hash: String,
pub computed_at: DateTime<Utc>,
pub benchmark_diffs: Vec<BenchmarkDiff>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchmarkDiff {
pub name: String,
pub baseline_ns_per_iter: u64,
pub target_ns_per_iter: u64,
pub diff_ns: i64,
pub percent_change: f64,
}
impl BenchmarkReceipt {
pub fn new(commit_hash: String, machine_fingerprint: String) -> Self {
Self {
timestamp: Utc::now(),
commit_hash,
machine_fingerprint,
results: Vec::new(),
note: None,
}
}
pub fn with_fingerprint(commit_hash: String, fingerprint: &super::MachineFingerprint) -> Self {
Self {
timestamp: Utc::now(),
commit_hash,
machine_fingerprint: fingerprint.as_str().to_string(),
results: Vec::new(),
note: None,
}
}
pub fn add_result(&mut self, result: BenchmarkResult) {
self.results.push(result);
}
pub fn receipt_hash(&self) -> String {
let mut hasher = Sha256::new();
hasher.update(self.commit_hash.as_bytes());
hasher.update(self.machine_fingerprint.as_bytes());
for result in &self.results {
hasher.update(result.name.as_bytes());
hasher.update(result.ns_per_iter.to_le_bytes());
}
hex::encode(&hasher.finalize()[..16])
}
pub fn set_note(&mut self, note: impl Into<String>) {
self.note = Some(note.into());
}
pub fn to_json(&self) -> Result<Vec<u8>, QuantEvalError> {
serde_json::to_vec(self).map_err(|e| QuantEvalError::Serialization(e.to_string()))
}
pub fn from_json(bytes: &[u8]) -> Result<Self, QuantEvalError> {
serde_json::from_slice(bytes).map_err(|e| QuantEvalError::Serialization(e.to_string()))
}
}
impl ReceiptDiff {
pub fn compare(baseline: &BenchmarkReceipt, target: &BenchmarkReceipt) -> Self {
let mut benchmark_diffs = Vec::new();
let baseline_map: std::collections::HashMap<_, _> = baseline
.results
.iter()
.map(|r| (r.name.as_str(), r))
.collect();
for target_result in &target.results {
if let Some(baseline_result) = baseline_map.get(target_result.name.as_str()) {
let diff_ns = target_result.ns_per_iter as i64 - baseline_result.ns_per_iter as i64;
let percent_change = if baseline_result.ns_per_iter > 0 {
(diff_ns as f64 / baseline_result.ns_per_iter as f64) * 100.0
} else {
0.0
};
benchmark_diffs.push(BenchmarkDiff {
name: target_result.name.clone(),
baseline_ns_per_iter: baseline_result.ns_per_iter,
target_ns_per_iter: target_result.ns_per_iter,
diff_ns,
percent_change,
});
}
}
Self {
baseline_hash: baseline.receipt_hash(),
target_hash: target.receipt_hash(),
computed_at: Utc::now(),
benchmark_diffs,
}
}
pub fn to_json(&self) -> Result<Vec<u8>, QuantEvalError> {
serde_json::to_vec(self).map_err(|e| QuantEvalError::Serialization(e.to_string()))
}
pub fn from_json(bytes: &[u8]) -> Result<Self, QuantEvalError> {
serde_json::from_slice(bytes).map_err(|e| QuantEvalError::Serialization(e.to_string()))
}
}
mod hex {
const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
pub fn encode(data: impl AsRef<[u8]>) -> String {
let bytes = data.as_ref();
let mut s = String::with_capacity(bytes.len() * 2);
for &b in bytes {
s.push(HEX_CHARS[(b >> 4) as usize] as char);
s.push(HEX_CHARS[(b & 0xf) as usize] as char);
}
s
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dummy_fingerprint() -> String {
"0".repeat(64)
}
#[test]
fn test_receipt_creation() {
let receipt = BenchmarkReceipt::new("abc123".to_string(), dummy_fingerprint());
assert_eq!(receipt.commit_hash, "abc123");
assert!(receipt.results.is_empty());
}
#[test]
fn test_receipt_add_result() {
let mut receipt = BenchmarkReceipt::new("abc123".to_string(), dummy_fingerprint());
receipt.add_result(BenchmarkResult {
name: "test_bench".to_string(),
iterations: 1000,
elapsed_ns: 1_000_000,
ns_per_iter: 1000,
throughput: Some(1_000_000.0),
error: None,
});
assert_eq!(receipt.results.len(), 1);
assert_eq!(receipt.results[0].name, "test_bench");
}
#[test]
fn test_receipt_serialization() {
let mut receipt = BenchmarkReceipt::new("abc123".to_string(), dummy_fingerprint());
receipt.add_result(BenchmarkResult {
name: "test_bench".to_string(),
iterations: 1000,
elapsed_ns: 1_000_000,
ns_per_iter: 1000,
throughput: Some(1_000_000.0),
error: None,
});
let json = receipt.to_json().expect("serialization should work");
let deserialized = BenchmarkReceipt::from_json(&json).expect("deserialization should work");
assert_eq!(deserialized.commit_hash, receipt.commit_hash);
assert_eq!(deserialized.results.len(), 1);
}
#[test]
fn test_diff_comparison() {
let mut baseline = BenchmarkReceipt::new("abc123".to_string(), dummy_fingerprint());
baseline.add_result(BenchmarkResult {
name: "test_bench".to_string(),
iterations: 1000,
elapsed_ns: 1_000_000,
ns_per_iter: 1000,
throughput: Some(1_000_000.0),
error: None,
});
let mut target = BenchmarkReceipt::new("def456".to_string(), dummy_fingerprint());
target.add_result(BenchmarkResult {
name: "test_bench".to_string(),
iterations: 1000,
elapsed_ns: 1_200_000,
ns_per_iter: 1200,
throughput: Some(833_333.0),
error: None,
});
let diff = ReceiptDiff::compare(&baseline, &target);
assert_eq!(diff.benchmark_diffs.len(), 1);
assert_eq!(diff.benchmark_diffs[0].name, "test_bench");
assert_eq!(diff.benchmark_diffs[0].diff_ns, 200);
assert!((diff.benchmark_diffs[0].percent_change - 20.0).abs() < 0.01);
}
}