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