1use std::collections::HashMap;
4use std::time::Instant;
5
6use crate::error::BenchError;
7use crate::receipt::BenchmarkResult;
8use crate::{BenchmarkReceipt, MachineFingerprint};
9
10pub struct BenchmarkSuite {
12 commit_hash: Option<String>,
13 machine_fingerprint: MachineFingerprint,
14 benchmarks: HashMap<String, Box<dyn Fn() -> Result<BenchmarkResult, String> + Send + Sync>>,
15}
16
17impl BenchmarkSuite {
18 pub fn new() -> Self {
20 let commit_hash = detect_git_commit_hash().ok();
21 Self {
22 commit_hash,
23 machine_fingerprint: MachineFingerprint::generate(),
24 benchmarks: HashMap::new(),
25 }
26 }
27
28 pub fn with_commit_hash(commit_hash: impl Into<String>) -> Self {
30 Self {
31 commit_hash: Some(commit_hash.into()),
32 machine_fingerprint: MachineFingerprint::generate(),
33 benchmarks: HashMap::new(),
34 }
35 }
36
37 pub fn register(
42 &mut self,
43 name: impl Into<String>,
44 bench: impl Fn() -> Result<BenchmarkResult, String> + Send + Sync + 'static,
45 ) {
46 self.benchmarks.insert(name.into(), Box::new(bench));
47 }
48
49 pub fn run(&self) -> Result<BenchmarkReceipt, BenchError> {
51 let commit_hash = self
52 .commit_hash
53 .clone()
54 .unwrap_or_else(|| "unknown".to_string());
55
56 let mut receipt = BenchmarkReceipt::new(commit_hash, self.machine_fingerprint.clone());
57
58 for (name, bench) in &self.benchmarks {
59 match bench() {
60 Ok(result) => {
61 let mut r = result;
62 r.name = name.clone();
63 receipt.add_result(r);
64 }
65 Err(e) => {
66 receipt.add_result(BenchmarkResult {
67 name: name.clone(),
68 iterations: 0,
69 elapsed_ns: 0,
70 ns_per_iter: 0,
71 throughput: None,
72 error: Some(e),
73 });
74 }
75 }
76 }
77
78 Ok(receipt)
79 }
80
81 pub fn run_one(&self, name: &str) -> Result<BenchmarkResult, BenchError> {
83 let bench = self
84 .benchmarks
85 .get(name)
86 .ok_or_else(|| BenchError::Execution(format!("No benchmark named '{}'", name)))?;
87
88 let result = bench()?;
89 Ok(result)
90 }
91
92 pub fn benchmark_names(&self) -> Vec<&String> {
94 self.benchmarks.keys().collect()
95 }
96}
97
98impl Default for BenchmarkSuite {
99 fn default() -> Self {
100 Self::new()
101 }
102}
103
104fn detect_git_commit_hash() -> Result<String, BenchError> {
106 use std::process::Command;
107
108 let output = Command::new("git")
109 .args(["rev-parse", "HEAD"])
110 .output()
111 .map_err(|e| BenchError::Git(e.to_string()))?;
112
113 if !output.status.success() {
114 return Err(BenchError::NoGitRepo);
115 }
116
117 let hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
118 if hash.is_empty() {
119 return Err(BenchError::NoGitRepo);
120 }
121
122 Ok(hash)
123}
124
125pub mod semantic_search {
133 use super::*;
134 use crate::receipt::BenchmarkResult;
135
136 #[allow(dead_code)]
141 pub fn run_benchmark(
142 dimensions: usize,
143 vector_size: usize,
144 iterations: u64,
145 ) -> Result<BenchmarkResult, String> {
146 let vectors: Vec<Vec<f32>> = (0..dimensions)
148 .map(|_| (0..vector_size).map(|_| rand_f32()).collect())
149 .collect();
150
151 let query = (0..vector_size).map(|_| rand_f32()).collect::<Vec<f32>>();
152
153 let start = Instant::now();
154 for _ in 0..iterations {
155 let _ = find_top_k(&vectors, &query, 10);
156 }
157 let elapsed = start.elapsed();
158
159 Ok(BenchmarkResult {
160 name: "semantic_search".to_string(),
161 iterations,
162 elapsed_ns: elapsed.as_nanos() as u64,
163 ns_per_iter: elapsed.as_nanos() as u64 / iterations.max(1),
164 throughput: Some(iterations as f64 / elapsed.as_secs_f64()),
165 error: None,
166 })
167 }
168
169 fn find_top_k(vectors: &[Vec<f32>], query: &[f32], k: usize) -> Vec<(usize, f32)> {
170 let mut scores: Vec<(usize, f32)> = vectors
171 .iter()
172 .enumerate()
173 .map(|(i, v)| (i, cosine_sim(query, v)))
174 .collect();
175 scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
176 scores.truncate(k);
177 scores
178 }
179
180 fn cosine_sim(a: &[f32], b: &[f32]) -> f32 {
181 let dot = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum::<f32>();
182 let mag_a = a.iter().map(|x| x * x).sum::<f32>().sqrt();
183 let mag_b = b.iter().map(|x| x * x).sum::<f32>().sqrt();
184 if mag_a == 0.0 || mag_b == 0.0 {
185 0.0
186 } else {
187 dot / (mag_a * mag_b)
188 }
189 }
190
191 fn rand_f32() -> f32 {
192 use std::time::{SystemTime, UNIX_EPOCH};
193 let seed = SystemTime::now()
194 .duration_since(UNIX_EPOCH)
195 .unwrap()
196 .subsec_nanos();
197 let x = seed.wrapping_mul(1103515245).wrapping_add(12345);
199 (x as f32 / u32::MAX as f32) * 2.0 - 1.0
200 }
201}
202
203pub mod compression {
205 use super::*;
206 use crate::receipt::BenchmarkResult;
207 use std::io::{Read, Write};
208
209 #[allow(dead_code)]
214 pub fn run_benchmark(data_size: usize, iterations: u64) -> Result<BenchmarkResult, String> {
215 let data = generate_data(data_size);
216
217 let mut total_encode_ns = 0u64;
218 let mut total_decode_ns = 0u64;
219
220 for _ in 0..iterations {
221 let start_encode = Instant::now();
223 let encoded = encode_zlib(&data).map_err(|e| e.to_string())?;
224 let encode_ns = start_encode.elapsed().as_nanos() as u64;
225 total_encode_ns += encode_ns;
226
227 let start_decode = Instant::now();
229 let decoded = decode_zlib(&encoded).map_err(|e| e.to_string())?;
230 let decode_ns = start_decode.elapsed().as_nanos() as u64;
231 total_decode_ns += decode_ns;
232
233 if decoded != data {
235 return Err("Decompressed data mismatch".to_string());
236 }
237 }
238
239 let elapsed_ns = total_encode_ns.saturating_add(total_decode_ns);
240 let ns_per_iter = elapsed_ns / iterations.max(1);
241
242 Ok(BenchmarkResult {
243 name: "compression_round_trip".to_string(),
244 iterations,
245 elapsed_ns,
246 ns_per_iter,
247 throughput: Some((iterations as f64 * data_size as f64) / (elapsed_ns as f64 / 1e9)),
248 error: None,
249 })
250 }
251
252 fn generate_data(size: usize) -> Vec<u8> {
253 use std::time::{SystemTime, UNIX_EPOCH};
254 let seed = SystemTime::now()
255 .duration_since(UNIX_EPOCH)
256 .unwrap()
257 .subsec_nanos();
258
259 (0..size)
260 .map(|i| {
261 ((seed
262 .wrapping_mul((i as u32).wrapping_add(1))
263 .wrapping_add(i as u32))
264 % 256) as u8
265 })
266 .collect()
267 }
268
269 fn encode_zlib(data: &[u8]) -> Result<Vec<u8>, std::io::Error> {
270 let mut encoder = flate2::write::ZlibEncoder::new(
271 Vec::with_capacity(data.len()),
272 flate2::Compression::default(),
273 );
274 encoder.write_all(data)?;
275 encoder.finish()
276 }
277
278 fn decode_zlib(data: &[u8]) -> Result<Vec<u8>, std::io::Error> {
279 use flate2::read::ZlibDecoder;
280 let mut decoder = ZlibDecoder::new(data);
281 let mut result = Vec::new();
282 decoder.read_to_end(&mut result)?;
283 Ok(result)
284 }
285}
286
287pub mod memory_lookup {
289 use super::*;
290 use crate::receipt::BenchmarkResult;
291
292 #[allow(dead_code)]
297 pub fn run_benchmark(num_entries: usize, iterations: u64) -> Result<BenchmarkResult, String> {
298 let entries: Vec<(String, u64)> = (0..num_entries)
299 .map(|i| (format!("key_{:08}", i), i as u64))
300 .collect();
301
302 let indices: Vec<usize> = {
304 use std::time::{SystemTime, UNIX_EPOCH};
305 let seed = SystemTime::now()
306 .duration_since(UNIX_EPOCH)
307 .unwrap()
308 .subsec_nanos() as u64;
309 (0..iterations)
310 .map(|i| seed.wrapping_mul(i.wrapping_add(1)) as usize % num_entries)
311 .collect()
312 };
313
314 let start = Instant::now();
315 for &idx in &indices {
316 let (ref key, _) = entries[idx];
317 let _ = entries.iter().find(|(k, _)| k == key);
319 }
320 let elapsed = start.elapsed();
321
322 Ok(BenchmarkResult {
323 name: "memory_lookups".to_string(),
324 iterations,
325 elapsed_ns: elapsed.as_nanos() as u64,
326 ns_per_iter: elapsed.as_nanos() as u64 / iterations.max(1),
327 throughput: Some(iterations as f64 / elapsed.as_secs_f64()),
328 error: None,
329 })
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336 use crate::receipt::BenchmarkResult;
337
338 #[test]
339 fn test_suite_creation() {
340 let suite = BenchmarkSuite::new();
341 assert!(suite.benchmark_names().is_empty());
342 }
343
344 #[test]
345 fn test_suite_register() {
346 let mut suite = BenchmarkSuite::new();
347 suite.register("dummy", || {
348 Ok(BenchmarkResult {
349 name: "dummy".to_string(),
350 iterations: 10,
351 elapsed_ns: 1000,
352 ns_per_iter: 100,
353 throughput: None,
354 error: None,
355 })
356 });
357 assert_eq!(suite.benchmark_names().len(), 1);
358 }
359
360 #[test]
361 fn test_suite_run() {
362 let mut suite = BenchmarkSuite::with_commit_hash("test123");
363 suite.register("dummy", || {
364 Ok(BenchmarkResult {
365 name: "dummy".to_string(),
366 iterations: 10,
367 elapsed_ns: 1000,
368 ns_per_iter: 100,
369 throughput: None,
370 error: None,
371 })
372 });
373 let receipt = suite.run().unwrap();
374 assert_eq!(receipt.commit_hash, "test123");
375 assert_eq!(receipt.results.len(), 1);
376 }
377
378 #[test]
379 fn test_semantic_search_benchmark() {
380 let result = semantic_search::run_benchmark(100, 64, 100).unwrap();
381 assert_eq!(result.name, "semantic_search");
382 assert_eq!(result.iterations, 100);
383 assert!(result.elapsed_ns > 0);
384 assert!(result.ns_per_iter > 0);
385 }
386
387 #[test]
388 fn test_memory_lookup_benchmark() {
389 let result = memory_lookup::run_benchmark(1000, 100).unwrap();
390 assert_eq!(result.name, "memory_lookups");
391 assert_eq!(result.iterations, 100);
392 assert!(result.elapsed_ns > 0);
393 }
394}