1use crate::error::QuantEvalError;
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct BenchmarkReceipt {
20 pub timestamp: DateTime<Utc>,
22
23 pub commit_hash: String,
25
26 pub machine_fingerprint: String,
28
29 pub results: Vec<BenchmarkResult>,
31
32 pub note: Option<String>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct BenchmarkResult {
39 pub name: String,
41
42 pub iterations: u64,
44
45 pub elapsed_ns: u64,
47
48 pub ns_per_iter: u64,
50
51 pub throughput: Option<f64>,
53
54 pub error: Option<String>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ReceiptDiff {
61 pub baseline_hash: String,
63
64 pub target_hash: String,
66
67 pub computed_at: DateTime<Utc>,
69
70 pub benchmark_diffs: Vec<BenchmarkDiff>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct BenchmarkDiff {
77 pub name: String,
78 pub baseline_ns_per_iter: u64,
79 pub target_ns_per_iter: u64,
80 pub diff_ns: i64,
82 pub percent_change: f64,
84}
85
86impl BenchmarkReceipt {
87 pub fn new(commit_hash: String, machine_fingerprint: String) -> Self {
89 Self {
90 timestamp: Utc::now(),
91 commit_hash,
92 machine_fingerprint,
93 results: Vec::new(),
94 note: None,
95 }
96 }
97
98 pub fn with_fingerprint(commit_hash: String, fingerprint: &super::MachineFingerprint) -> Self {
100 Self {
101 timestamp: Utc::now(),
102 commit_hash,
103 machine_fingerprint: fingerprint.as_str().to_string(),
104 results: Vec::new(),
105 note: None,
106 }
107 }
108
109 pub fn add_result(&mut self, result: BenchmarkResult) {
111 self.results.push(result);
112 }
113
114 pub fn receipt_hash(&self) -> String {
119 let mut hasher = Sha256::new();
120 hasher.update(self.commit_hash.as_bytes());
121 hasher.update(self.machine_fingerprint.as_bytes());
122 for result in &self.results {
123 hasher.update(result.name.as_bytes());
124 hasher.update(result.ns_per_iter.to_le_bytes());
125 }
126 hex::encode(&hasher.finalize()[..16])
127 }
128
129 pub fn set_note(&mut self, note: impl Into<String>) {
131 self.note = Some(note.into());
132 }
133
134 pub fn to_json(&self) -> Result<Vec<u8>, QuantEvalError> {
136 serde_json::to_vec(self).map_err(|e| QuantEvalError::Serialization(e.to_string()))
137 }
138
139 pub fn from_json(bytes: &[u8]) -> Result<Self, QuantEvalError> {
141 serde_json::from_slice(bytes).map_err(|e| QuantEvalError::Serialization(e.to_string()))
142 }
143}
144
145impl ReceiptDiff {
146 pub fn compare(baseline: &BenchmarkReceipt, target: &BenchmarkReceipt) -> Self {
148 let mut benchmark_diffs = Vec::new();
149
150 let baseline_map: std::collections::HashMap<_, _> = baseline
152 .results
153 .iter()
154 .map(|r| (r.name.as_str(), r))
155 .collect();
156
157 for target_result in &target.results {
158 if let Some(baseline_result) = baseline_map.get(target_result.name.as_str()) {
159 let diff_ns = target_result.ns_per_iter as i64 - baseline_result.ns_per_iter as i64;
160 let percent_change = if baseline_result.ns_per_iter > 0 {
161 (diff_ns as f64 / baseline_result.ns_per_iter as f64) * 100.0
162 } else {
163 0.0
164 };
165
166 benchmark_diffs.push(BenchmarkDiff {
167 name: target_result.name.clone(),
168 baseline_ns_per_iter: baseline_result.ns_per_iter,
169 target_ns_per_iter: target_result.ns_per_iter,
170 diff_ns,
171 percent_change,
172 });
173 }
174 }
175
176 Self {
177 baseline_hash: baseline.receipt_hash(),
178 target_hash: target.receipt_hash(),
179 computed_at: Utc::now(),
180 benchmark_diffs,
181 }
182 }
183
184 pub fn to_json(&self) -> Result<Vec<u8>, QuantEvalError> {
186 serde_json::to_vec(self).map_err(|e| QuantEvalError::Serialization(e.to_string()))
187 }
188
189 pub fn from_json(bytes: &[u8]) -> Result<Self, QuantEvalError> {
191 serde_json::from_slice(bytes).map_err(|e| QuantEvalError::Serialization(e.to_string()))
192 }
193}
194
195mod hex {
197 const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
198
199 pub fn encode(data: impl AsRef<[u8]>) -> String {
200 let bytes = data.as_ref();
201 let mut s = String::with_capacity(bytes.len() * 2);
202 for &b in bytes {
203 s.push(HEX_CHARS[(b >> 4) as usize] as char);
204 s.push(HEX_CHARS[(b & 0xf) as usize] as char);
205 }
206 s
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 fn dummy_fingerprint() -> String {
215 "0".repeat(64)
216 }
217
218 #[test]
219 fn test_receipt_creation() {
220 let receipt = BenchmarkReceipt::new("abc123".to_string(), dummy_fingerprint());
221 assert_eq!(receipt.commit_hash, "abc123");
222 assert!(receipt.results.is_empty());
223 }
224
225 #[test]
226 fn test_receipt_add_result() {
227 let mut receipt = BenchmarkReceipt::new("abc123".to_string(), dummy_fingerprint());
228 receipt.add_result(BenchmarkResult {
229 name: "test_bench".to_string(),
230 iterations: 1000,
231 elapsed_ns: 1_000_000,
232 ns_per_iter: 1000,
233 throughput: Some(1_000_000.0),
234 error: None,
235 });
236 assert_eq!(receipt.results.len(), 1);
237 assert_eq!(receipt.results[0].name, "test_bench");
238 }
239
240 #[test]
241 fn test_receipt_serialization() {
242 let mut receipt = BenchmarkReceipt::new("abc123".to_string(), dummy_fingerprint());
243 receipt.add_result(BenchmarkResult {
244 name: "test_bench".to_string(),
245 iterations: 1000,
246 elapsed_ns: 1_000_000,
247 ns_per_iter: 1000,
248 throughput: Some(1_000_000.0),
249 error: None,
250 });
251
252 let json = receipt.to_json().expect("serialization should work");
253 let deserialized = BenchmarkReceipt::from_json(&json).expect("deserialization should work");
254 assert_eq!(deserialized.commit_hash, receipt.commit_hash);
255 assert_eq!(deserialized.results.len(), 1);
256 }
257
258 #[test]
259 fn test_diff_comparison() {
260 let mut baseline = BenchmarkReceipt::new("abc123".to_string(), dummy_fingerprint());
261 baseline.add_result(BenchmarkResult {
262 name: "test_bench".to_string(),
263 iterations: 1000,
264 elapsed_ns: 1_000_000,
265 ns_per_iter: 1000,
266 throughput: Some(1_000_000.0),
267 error: None,
268 });
269
270 let mut target = BenchmarkReceipt::new("def456".to_string(), dummy_fingerprint());
271 target.add_result(BenchmarkResult {
272 name: "test_bench".to_string(),
273 iterations: 1000,
274 elapsed_ns: 1_200_000,
275 ns_per_iter: 1200,
276 throughput: Some(833_333.0),
277 error: None,
278 });
279
280 let diff = ReceiptDiff::compare(&baseline, &target);
281 assert_eq!(diff.benchmark_diffs.len(), 1);
282 assert_eq!(diff.benchmark_diffs[0].name, "test_bench");
283 assert_eq!(diff.benchmark_diffs[0].diff_ns, 200);
284 assert!((diff.benchmark_diffs[0].percent_change - 20.0).abs() < 0.01);
285 }
286}