1use crate::{Vector, VectorIndex};
11use anyhow::{anyhow, Result};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::time::{Duration, Instant};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct BenchmarkConfig {
19 pub warmup_runs: usize,
21 pub benchmark_runs: usize,
23 pub max_duration: Duration,
25 pub profile_memory: bool,
27 pub detailed_timing: bool,
29 pub quality_metrics: bool,
31 pub random_seed: Option<u64>,
33 pub output_format: BenchmarkOutputFormat,
35}
36
37impl Default for BenchmarkConfig {
38 fn default() -> Self {
39 Self {
40 warmup_runs: 3,
41 benchmark_runs: 10,
42 max_duration: Duration::from_secs(300), profile_memory: true,
44 detailed_timing: true,
45 quality_metrics: true,
46 random_seed: Some(42),
47 output_format: BenchmarkOutputFormat::Json,
48 }
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub enum BenchmarkOutputFormat {
55 Json,
56 Csv,
57 Table,
58 AnnBenchmarks,
59}
60
61pub struct BenchmarkSuite {
63 config: BenchmarkConfig,
64 datasets: Vec<BenchmarkDataset>,
65 algorithms: Vec<Box<dyn VectorIndex>>,
66 results: Vec<BenchmarkResult>,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct BenchmarkDataset {
72 pub name: String,
74 pub description: String,
76 pub train_vectors: Vec<Vector>,
78 pub query_vectors: Vec<Vector>,
80 pub ground_truth: Option<Vec<Vec<usize>>>, pub metadata: HashMap<String, String>,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct BenchmarkTestCase {
89 pub name: String,
91 pub description: String,
93 pub dataset: String,
95 pub algorithm: String,
97 pub parameters: HashMap<String, serde_json::Value>,
99 pub query_count: usize,
101 pub k: usize,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct BenchmarkResult {
108 pub test_case: BenchmarkTestCase,
110 pub performance: PerformanceMetrics,
112 pub quality: Option<QualityMetrics>,
114 pub memory: Option<MemoryMetrics>,
116 pub scalability: Option<ScalabilityMetrics>,
118 pub system_info: SystemInfo,
120 pub timestamp: std::time::SystemTime,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct PerformanceMetrics {
127 pub avg_query_time: Duration,
129 pub median_query_time: Duration,
131 pub p95_query_time: Duration,
133 pub p99_query_time: Duration,
135 pub min_query_time: Duration,
137 pub max_query_time: Duration,
139 pub std_dev_query_time: Duration,
141 pub queries_per_second: f64,
143 pub index_build_time: Option<Duration>,
145 pub index_update_time: Option<Duration>,
147 pub throughput: f64,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct QualityMetrics {
154 pub recall_at_k: f64,
156 pub precision_at_k: f64,
158 pub mean_average_precision: f64,
160 pub ndcg_at_k: f64,
162 pub distance_ratio: Option<f64>,
164 pub relative_error: Option<f64>,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct MemoryMetrics {
171 pub peak_memory_bytes: usize,
173 pub avg_memory_bytes: usize,
175 pub memory_per_vector: f64,
177 pub index_overhead_bytes: usize,
179 pub memory_efficiency: f64,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct ScalabilityMetrics {
186 pub performance_scaling: Vec<(usize, Duration)>, pub memory_scaling: Vec<(usize, usize)>, pub build_time_scaling: Vec<(usize, Duration)>, pub concurrency_scaling: Vec<(usize, f64)>, }
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct SystemInfo {
199 pub cpu_info: String,
201 pub total_ram_gb: f64,
203 pub available_ram_gb: f64,
205 pub os: String,
207 pub rust_version: String,
209 pub simd_features: Vec<String>,
211 pub gpu_info: Option<String>,
213}
214
215pub struct PerformanceProfiler {
217 start_time: Instant,
218 checkpoints: Vec<(String, Instant)>,
219 memory_samples: Vec<(Instant, usize)>,
220}
221
222impl PerformanceProfiler {
223 pub fn new() -> Self {
224 Self {
225 start_time: Instant::now(),
226 checkpoints: Vec::new(),
227 memory_samples: Vec::new(),
228 }
229 }
230
231 pub fn checkpoint(&mut self, name: &str) {
233 self.checkpoints.push((name.to_string(), Instant::now()));
234 }
235
236 pub fn sample_memory(&mut self) {
238 let memory_usage = self.get_current_memory_usage();
239 self.memory_samples.push((Instant::now(), memory_usage));
240 }
241
242 pub fn get_timing_breakdown(&self) -> Vec<(String, Duration)> {
244 let mut breakdown = Vec::new();
245 let mut last_time = self.start_time;
246
247 for (name, time) in &self.checkpoints {
248 breakdown.push((name.clone(), time.duration_since(last_time)));
249 last_time = *time;
250 }
251
252 breakdown
253 }
254
255 pub fn get_memory_profile(&self) -> Vec<(Duration, usize)> {
257 self.memory_samples
258 .iter()
259 .map(|(time, memory)| (time.duration_since(self.start_time), *memory))
260 .collect()
261 }
262
263 fn get_current_memory_usage(&self) -> usize {
264 #[cfg(target_os = "linux")]
266 {
267 if let Ok(content) = std::fs::read_to_string("/proc/self/status") {
268 for line in content.lines() {
269 if line.starts_with("VmRSS:") {
270 if let Some(kb_str) = line.split_whitespace().nth(1) {
271 if let Ok(kb) = kb_str.parse::<usize>() {
272 return kb * 1024; }
274 }
275 }
276 }
277 }
278 }
279
280 #[cfg(target_os = "macos")]
281 {
282 use std::process::Command;
283 if let Ok(output) = Command::new("ps")
284 .args(["-o", "rss=", "-p"])
285 .arg(std::process::id().to_string())
286 .output()
287 {
288 if let Ok(rss_str) = String::from_utf8(output.stdout) {
289 if let Ok(rss_kb) = rss_str.trim().parse::<usize>() {
290 return rss_kb * 1024; }
292 }
293 }
294 }
295
296 0
298 }
299}
300
301impl Default for PerformanceProfiler {
302 fn default() -> Self {
303 Self::new()
304 }
305}
306
307impl BenchmarkSuite {
308 pub fn new(config: BenchmarkConfig) -> Self {
310 Self {
311 config,
312 datasets: Vec::new(),
313 algorithms: Vec::new(),
314 results: Vec::new(),
315 }
316 }
317
318 pub fn add_dataset(&mut self, dataset: BenchmarkDataset) {
320 self.datasets.push(dataset);
321 }
322
323 pub fn add_algorithm(&mut self, algorithm: Box<dyn VectorIndex>) {
325 self.algorithms.push(algorithm);
326 }
327
328 pub fn generate_synthetic_datasets(&mut self) -> Result<()> {
330 self.generate_random_dataset("random_1000", 1000, 128, 100)?;
332 self.generate_random_dataset("random_10000", 10000, 256, 1000)?;
333 self.generate_clustered_dataset("clustered_5000", 5000, 384, 500, 10)?;
334 self.generate_uniform_dataset("uniform_2000", 2000, 512, 200)?;
335
336 Ok(())
337 }
338
339 fn generate_random_dataset(
341 &mut self,
342 name: &str,
343 size: usize,
344 dimensions: usize,
345 query_count: usize,
346 ) -> Result<()> {
347 let mut train_vectors = Vec::new();
348 let mut query_vectors = Vec::new();
349
350 for i in 0..size {
352 let vector = crate::utils::random_vector(dimensions, Some(i as u64));
353 train_vectors.push(vector);
354 }
355
356 for i in 0..query_count {
358 let vector = crate::utils::random_vector(dimensions, Some((size + i) as u64));
359 query_vectors.push(vector);
360 }
361
362 let dataset = BenchmarkDataset {
363 name: name.to_string(),
364 description: format!("Random dataset with {size} vectors of {dimensions} dimensions"),
365 train_vectors,
366 query_vectors,
367 ground_truth: None,
368 metadata: {
369 let mut meta = HashMap::new();
370 meta.insert("type".to_string(), "random".to_string());
371 meta.insert("size".to_string(), size.to_string());
372 meta.insert("dimensions".to_string(), dimensions.to_string());
373 meta
374 },
375 };
376
377 self.add_dataset(dataset);
378 Ok(())
379 }
380
381 fn generate_clustered_dataset(
383 &mut self,
384 name: &str,
385 size: usize,
386 dimensions: usize,
387 query_count: usize,
388 num_clusters: usize,
389 ) -> Result<()> {
390 let mut train_vectors = Vec::new();
391 let mut query_vectors = Vec::new();
392
393 let mut cluster_centers = Vec::new();
395 for i in 0..num_clusters {
396 let center = crate::utils::random_vector(dimensions, Some(i as u64));
397 cluster_centers.push(center);
398 }
399
400 for i in 0..size {
402 let cluster_idx = i % num_clusters;
403 let center = &cluster_centers[cluster_idx];
404 let center_f32 = center.as_f32();
405
406 let mut noisy_vector = center_f32.clone();
408 let noise_scale = 0.1;
409
410 for val in &mut noisy_vector {
411 let noise = self.gaussian_random(0.0, noise_scale, i as u64);
412 *val += noise;
413 }
414
415 train_vectors.push(Vector::new(noisy_vector));
416 }
417
418 for i in 0..query_count {
420 let cluster_idx = i % num_clusters;
421 let center = &cluster_centers[cluster_idx];
422 let center_f32 = center.as_f32();
423
424 let mut noisy_vector = center_f32.clone();
425 let noise_scale = 0.05; for val in &mut noisy_vector {
428 let noise = self.gaussian_random(0.0, noise_scale, (size + i) as u64);
429 *val += noise;
430 }
431
432 query_vectors.push(Vector::new(noisy_vector));
433 }
434
435 let dataset = BenchmarkDataset {
436 name: name.to_string(),
437 description: format!(
438 "Clustered dataset with {size} vectors in {num_clusters} clusters of {dimensions} dimensions"
439 ),
440 train_vectors,
441 query_vectors,
442 ground_truth: None,
443 metadata: {
444 let mut meta = HashMap::new();
445 meta.insert("type".to_string(), "clustered".to_string());
446 meta.insert("size".to_string(), size.to_string());
447 meta.insert("dimensions".to_string(), dimensions.to_string());
448 meta.insert("clusters".to_string(), num_clusters.to_string());
449 meta
450 },
451 };
452
453 self.add_dataset(dataset);
454 Ok(())
455 }
456
457 fn generate_uniform_dataset(
459 &mut self,
460 name: &str,
461 size: usize,
462 dimensions: usize,
463 query_count: usize,
464 ) -> Result<()> {
465 let mut train_vectors = Vec::new();
466 let mut query_vectors = Vec::new();
467
468 for i in 0..size {
470 let mut values = Vec::with_capacity(dimensions);
471 let mut state = i as u64;
472
473 for _ in 0..dimensions {
474 state = state.wrapping_mul(1103515245).wrapping_add(12345);
475 let normalized = (state as f32) / (u64::MAX as f32);
476 values.push(normalized); }
478
479 train_vectors.push(Vector::new(values));
480 }
481
482 for i in 0..query_count {
484 let mut values = Vec::with_capacity(dimensions);
485 let mut state = (size + i) as u64;
486
487 for _ in 0..dimensions {
488 state = state.wrapping_mul(1103515245).wrapping_add(12345);
489 let normalized = (state as f32) / (u64::MAX as f32);
490 values.push(normalized);
491 }
492
493 query_vectors.push(Vector::new(values));
494 }
495
496 let dataset = BenchmarkDataset {
497 name: name.to_string(),
498 description: format!("Uniform dataset with {size} vectors of {dimensions} dimensions"),
499 train_vectors,
500 query_vectors,
501 ground_truth: None,
502 metadata: {
503 let mut meta = HashMap::new();
504 meta.insert("type".to_string(), "uniform".to_string());
505 meta.insert("size".to_string(), size.to_string());
506 meta.insert("dimensions".to_string(), dimensions.to_string());
507 meta
508 },
509 };
510
511 self.add_dataset(dataset);
512 Ok(())
513 }
514
515 fn gaussian_random(&self, mean: f32, std_dev: f32, seed: u64) -> f32 {
517 let mut state = seed;
519 state = state.wrapping_mul(1103515245).wrapping_add(12345);
520 let u1 = (state as f32) / (u64::MAX as f32);
521 state = state.wrapping_mul(1103515245).wrapping_add(12345);
522 let u2 = (state as f32) / (u64::MAX as f32);
523
524 let z0 = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f32::consts::PI * u2).cos();
525 mean + std_dev * z0
526 }
527
528 pub fn run_all_benchmarks(&mut self) -> Result<Vec<BenchmarkResult>> {
530 let mut all_results = Vec::new();
531
532 for dataset in &self.datasets {
534 for (alg_idx, algorithm) in self.algorithms.iter().enumerate() {
535 let test_case = BenchmarkTestCase {
536 name: format!("{}_{alg_idx}", dataset.name),
537 description: format!("Benchmark {alg_idx} on {}", dataset.name),
538 dataset: dataset.name.clone(),
539 algorithm: format!("algorithm_{alg_idx}"),
540 parameters: HashMap::new(),
541 query_count: dataset.query_vectors.len(),
542 k: 10,
543 };
544
545 let result = self.run_single_benchmark(&test_case, dataset, algorithm.as_ref())?;
546 all_results.push(result);
547 }
548 }
549
550 self.results.extend(all_results.clone());
551 Ok(all_results)
552 }
553
554 fn run_single_benchmark(
556 &self,
557 test_case: &BenchmarkTestCase,
558 dataset: &BenchmarkDataset,
559 algorithm: &dyn VectorIndex,
560 ) -> Result<BenchmarkResult> {
561 let mut profiler = PerformanceProfiler::new();
562
563 tracing::info!("Running benchmark: {}", test_case.name);
564 profiler.checkpoint("benchmark_start");
565
566 profiler.checkpoint("index_build_start");
568 let mut index = self.create_index_copy(algorithm)?;
569
570 for (i, vector) in dataset.train_vectors.iter().enumerate() {
571 index.insert(format!("vec_{i}"), vector.clone())?;
572 }
573 profiler.checkpoint("index_build_end");
574
575 profiler.checkpoint("warmup_start");
577 for _ in 0..self.config.warmup_runs {
578 for query in dataset.query_vectors.iter().take(10) {
579 let _ = index.search_knn(query, test_case.k)?;
580 }
581 }
582 profiler.checkpoint("warmup_end");
583
584 profiler.checkpoint("benchmark_queries_start");
586 let mut query_times = Vec::new();
587
588 for query in &dataset.query_vectors {
589 profiler.sample_memory();
590
591 let start = Instant::now();
592 let _results = index.search_knn(query, test_case.k)?;
593 let query_time = start.elapsed();
594
595 query_times.push(query_time);
596 }
597 profiler.checkpoint("benchmark_queries_end");
598
599 let performance = self.calculate_performance_metrics(&query_times)?;
601
602 let quality = if let Some(ground_truth) = &dataset.ground_truth {
604 Some(self.calculate_quality_metrics(
605 index.as_ref(),
606 &dataset.query_vectors,
607 ground_truth,
608 test_case.k,
609 )?)
610 } else {
611 None
612 };
613
614 let memory = if self.config.profile_memory {
616 Some(self.calculate_memory_metrics(&profiler, dataset.train_vectors.len())?)
617 } else {
618 None
619 };
620
621 let system_info = self.get_system_info();
623
624 Ok(BenchmarkResult {
625 test_case: test_case.clone(),
626 performance,
627 quality,
628 memory,
629 scalability: None, system_info,
631 timestamp: std::time::SystemTime::now(),
632 })
633 }
634
635 fn create_index_copy(&self, _algorithm: &dyn VectorIndex) -> Result<Box<dyn VectorIndex>> {
637 Ok(Box::new(crate::MemoryVectorIndex::new()))
640 }
641
642 fn calculate_performance_metrics(
644 &self,
645 query_times: &[Duration],
646 ) -> Result<PerformanceMetrics> {
647 if query_times.is_empty() {
648 return Err(anyhow!("No query times to analyze"));
649 }
650
651 let mut sorted_times = query_times.to_vec();
652 sorted_times.sort();
653
654 let avg_query_time = Duration::from_nanos(
655 (query_times.iter().map(|d| d.as_nanos()).sum::<u128>() / query_times.len() as u128)
656 .try_into()
657 .unwrap(),
658 );
659
660 let median_query_time = sorted_times[sorted_times.len() / 2];
661 let p95_idx = (sorted_times.len() as f64 * 0.95) as usize;
662 let p99_idx = (sorted_times.len() as f64 * 0.99) as usize;
663 let p95_query_time = sorted_times[p95_idx.min(sorted_times.len() - 1)];
664 let p99_query_time = sorted_times[p99_idx.min(sorted_times.len() - 1)];
665
666 let min_query_time = sorted_times[0];
667 let max_query_time = sorted_times[sorted_times.len() - 1];
668
669 let mean_nanos = avg_query_time.as_nanos() as f64;
671 let variance = query_times
672 .iter()
673 .map(|d| {
674 let diff = d.as_nanos() as f64 - mean_nanos;
675 diff * diff
676 })
677 .sum::<f64>()
678 / query_times.len() as f64;
679 let std_dev_query_time = Duration::from_nanos(variance.sqrt() as u64);
680
681 let queries_per_second = 1.0 / avg_query_time.as_secs_f64();
682 let throughput =
683 query_times.len() as f64 / query_times.iter().map(|d| d.as_secs_f64()).sum::<f64>();
684
685 Ok(PerformanceMetrics {
686 avg_query_time,
687 median_query_time,
688 p95_query_time,
689 p99_query_time,
690 min_query_time,
691 max_query_time,
692 std_dev_query_time,
693 queries_per_second,
694 index_build_time: None, index_update_time: None,
696 throughput,
697 })
698 }
699
700 fn calculate_quality_metrics(
702 &self,
703 index: &dyn VectorIndex,
704 queries: &[Vector],
705 ground_truth: &[Vec<usize>],
706 k: usize,
707 ) -> Result<QualityMetrics> {
708 let mut total_recall = 0.0;
709 let mut total_precision = 0.0;
710 let mut total_queries = 0;
711
712 for (query_idx, query) in queries.iter().enumerate() {
713 if query_idx >= ground_truth.len() {
714 break;
715 }
716
717 let results = index.search_knn(query, k)?;
718 let returned_indices: Vec<usize> = results
719 .iter()
720 .filter_map(|(uri, _)| {
721 uri.strip_prefix("vec_")
723 .and_then(|s| s.parse::<usize>().ok())
724 })
725 .collect();
726
727 let true_neighbors = &ground_truth[query_idx];
728 let true_neighbors_k: std::collections::HashSet<usize> =
729 true_neighbors.iter().take(k).copied().collect();
730
731 let found_true = returned_indices
733 .iter()
734 .filter(|&idx| true_neighbors_k.contains(idx))
735 .count();
736
737 let recall = found_true as f64 / k.min(true_neighbors.len()) as f64;
738 let precision = found_true as f64 / returned_indices.len() as f64;
739
740 total_recall += recall;
741 total_precision += precision;
742 total_queries += 1;
743 }
744
745 let avg_recall = total_recall / total_queries as f64;
746 let avg_precision = total_precision / total_queries as f64;
747
748 Ok(QualityMetrics {
749 recall_at_k: avg_recall,
750 precision_at_k: avg_precision,
751 mean_average_precision: avg_precision, ndcg_at_k: avg_recall, distance_ratio: None,
754 relative_error: None,
755 })
756 }
757
758 fn calculate_memory_metrics(
760 &self,
761 profiler: &PerformanceProfiler,
762 vector_count: usize,
763 ) -> Result<MemoryMetrics> {
764 let memory_profile = profiler.get_memory_profile();
765
766 if memory_profile.is_empty() {
767 return Ok(MemoryMetrics {
768 peak_memory_bytes: 0,
769 avg_memory_bytes: 0,
770 memory_per_vector: 0.0,
771 index_overhead_bytes: 0,
772 memory_efficiency: 0.0,
773 });
774 }
775
776 let peak_memory_bytes = memory_profile
777 .iter()
778 .map(|(_, mem)| *mem)
779 .max()
780 .unwrap_or(0);
781 let avg_memory_bytes =
782 memory_profile.iter().map(|(_, mem)| *mem).sum::<usize>() / memory_profile.len();
783 let memory_per_vector = avg_memory_bytes as f64 / vector_count as f64;
784
785 Ok(MemoryMetrics {
786 peak_memory_bytes,
787 avg_memory_bytes,
788 memory_per_vector,
789 index_overhead_bytes: 0, memory_efficiency: 1.0, })
792 }
793
794 fn get_system_info(&self) -> SystemInfo {
796 SystemInfo {
797 cpu_info: self.get_cpu_info(),
798 total_ram_gb: self.get_total_ram_gb(),
799 available_ram_gb: self.get_available_ram_gb(),
800 os: std::env::consts::OS.to_string(),
801 rust_version: self.get_rust_version(),
802 simd_features: self.get_simd_features(),
803 gpu_info: self.get_gpu_info(),
804 }
805 }
806
807 fn get_cpu_info(&self) -> String {
808 #[cfg(target_os = "linux")]
809 {
810 if let Ok(content) = std::fs::read_to_string("/proc/cpuinfo") {
811 for line in content.lines() {
812 if line.starts_with("model name") {
813 if let Some(name) = line.split(':').nth(1) {
814 return name.trim().to_string();
815 }
816 }
817 }
818 }
819 }
820
821 #[cfg(target_os = "macos")]
822 {
823 use std::process::Command;
824 if let Ok(output) = Command::new("sysctl")
825 .args(["-n", "machdep.cpu.brand_string"])
826 .output()
827 {
828 if let Ok(cpu_name) = String::from_utf8(output.stdout) {
829 return cpu_name.trim().to_string();
830 }
831 }
832 }
833
834 "Unknown CPU".to_string()
835 }
836
837 fn get_total_ram_gb(&self) -> f64 {
838 #[cfg(target_os = "linux")]
839 {
840 if let Ok(content) = std::fs::read_to_string("/proc/meminfo") {
841 for line in content.lines() {
842 if line.starts_with("MemTotal:") {
843 if let Some(kb_str) = line.split_whitespace().nth(1) {
844 if let Ok(kb) = kb_str.parse::<u64>() {
845 return kb as f64 / 1024.0 / 1024.0; }
847 }
848 }
849 }
850 }
851 }
852
853 #[cfg(target_os = "macos")]
854 {
855 use std::process::Command;
856 if let Ok(output) = Command::new("sysctl").args(["-n", "hw.memsize"]).output() {
857 if let Ok(mem_str) = String::from_utf8(output.stdout) {
858 if let Ok(bytes) = mem_str.trim().parse::<u64>() {
859 return bytes as f64 / 1024.0 / 1024.0 / 1024.0; }
861 }
862 }
863 }
864
865 8.0 }
867
868 fn get_available_ram_gb(&self) -> f64 {
869 self.get_total_ram_gb() * 0.8
871 }
872
873 fn get_rust_version(&self) -> String {
874 std::env::var("RUSTC_VERSION").unwrap_or_else(|_| "unknown".to_string())
875 }
876
877 fn get_simd_features(&self) -> Vec<String> {
878 let mut features = Vec::new();
879
880 #[cfg(target_arch = "x86_64")]
881 {
882 if is_x86_feature_detected!("sse") {
883 features.push("SSE".to_string());
884 }
885 if is_x86_feature_detected!("sse2") {
886 features.push("SSE2".to_string());
887 }
888 if is_x86_feature_detected!("sse3") {
889 features.push("SSE3".to_string());
890 }
891 if is_x86_feature_detected!("sse4.1") {
892 features.push("SSE4.1".to_string());
893 }
894 if is_x86_feature_detected!("sse4.2") {
895 features.push("SSE4.2".to_string());
896 }
897 if is_x86_feature_detected!("avx") {
898 features.push("AVX".to_string());
899 }
900 if is_x86_feature_detected!("avx2") {
901 features.push("AVX2".to_string());
902 }
903 }
904
905 #[cfg(target_arch = "aarch64")]
906 {
907 features.push("NEON".to_string());
908 }
909
910 features
911 }
912
913 fn get_gpu_info(&self) -> Option<String> {
914 None
916 }
917
918 pub fn export_results(&self, format: BenchmarkOutputFormat) -> Result<String> {
920 match format {
921 BenchmarkOutputFormat::Json => serde_json::to_string_pretty(&self.results)
922 .map_err(|e| anyhow!("Failed to serialize to JSON: {}", e)),
923 BenchmarkOutputFormat::Csv => self.export_csv(),
924 BenchmarkOutputFormat::Table => self.export_table(),
925 BenchmarkOutputFormat::AnnBenchmarks => self.export_ann_benchmarks(),
926 }
927 }
928
929 fn export_csv(&self) -> Result<String> {
930 let mut csv = String::new();
931 csv.push_str(
932 "dataset,algorithm,avg_query_time_ms,queries_per_second,recall_at_k,memory_mb\n",
933 );
934
935 for result in &self.results {
936 csv.push_str(&format!(
937 "{},{},{:.3},{:.2},{:.3},{:.2}\n",
938 result.test_case.dataset,
939 result.test_case.algorithm,
940 result.performance.avg_query_time.as_millis(),
941 result.performance.queries_per_second,
942 result
943 .quality
944 .as_ref()
945 .map(|q| q.recall_at_k)
946 .unwrap_or(0.0),
947 result
948 .memory
949 .as_ref()
950 .map(|m| m.avg_memory_bytes as f64 / 1024.0 / 1024.0)
951 .unwrap_or(0.0),
952 ));
953 }
954
955 Ok(csv)
956 }
957
958 fn export_table(&self) -> Result<String> {
959 let mut table = String::new();
960 table.push_str(&format!(
961 "{:<20} {:<15} {:<15} {:<15} {:<10}\n",
962 "Dataset", "Algorithm", "Avg Time (ms)", "QPS", "Recall@K"
963 ));
964 table.push_str(&"-".repeat(80));
965 table.push('\n');
966
967 for result in &self.results {
968 table.push_str(&format!(
969 "{:<20} {:<15} {:<15.3} {:<15.2} {:<10.3}\n",
970 result.test_case.dataset,
971 result.test_case.algorithm,
972 result.performance.avg_query_time.as_millis(),
973 result.performance.queries_per_second,
974 result
975 .quality
976 .as_ref()
977 .map(|q| q.recall_at_k)
978 .unwrap_or(0.0),
979 ));
980 }
981
982 Ok(table)
983 }
984
985 fn export_ann_benchmarks(&self) -> Result<String> {
986 let mut results = serde_json::Map::new();
988
989 for result in &self.results {
990 let mut entry = serde_json::Map::new();
991 entry.insert(
992 "k".to_string(),
993 serde_json::Value::Number(result.test_case.k.into()),
994 );
995 entry.insert(
996 "recall".to_string(),
997 serde_json::Value::Number(
998 serde_json::Number::from_f64(
999 result
1000 .quality
1001 .as_ref()
1002 .map(|q| q.recall_at_k)
1003 .unwrap_or(0.0),
1004 )
1005 .unwrap_or(serde_json::Number::from(0)),
1006 ),
1007 );
1008 entry.insert(
1009 "qps".to_string(),
1010 serde_json::Value::Number(
1011 serde_json::Number::from_f64(result.performance.queries_per_second)
1012 .unwrap_or(serde_json::Number::from(0)),
1013 ),
1014 );
1015
1016 let key = format!(
1017 "{}_{}",
1018 result.test_case.dataset, result.test_case.algorithm
1019 );
1020 results.insert(key, serde_json::Value::Object(entry));
1021 }
1022
1023 serde_json::to_string_pretty(&results)
1024 .map_err(|e| anyhow!("Failed to serialize ANN-Benchmarks format: {}", e))
1025 }
1026
1027 pub fn run_scalability_benchmark(&mut self, dataset_name: &str) -> Result<ScalabilityMetrics> {
1029 let dataset = self
1030 .datasets
1031 .iter()
1032 .find(|d| d.name == dataset_name)
1033 .ok_or_else(|| anyhow!("Dataset not found: {}", dataset_name))?;
1034
1035 let mut performance_scaling = Vec::new();
1036 let mut memory_scaling = Vec::new();
1037 let mut build_time_scaling = Vec::new();
1038
1039 let test_sizes = [100, 500, 1000, 2000, 5000];
1041
1042 for &size in &test_sizes {
1043 if size > dataset.train_vectors.len() {
1044 continue;
1045 }
1046
1047 let subset_vectors = &dataset.train_vectors[..size];
1048 let test_queries = &dataset.query_vectors[..10.min(dataset.query_vectors.len())];
1049
1050 let build_start = Instant::now();
1052 let mut index = Box::new(crate::MemoryVectorIndex::new());
1053
1054 for (i, vector) in subset_vectors.iter().enumerate() {
1055 index.insert(format!("vec_{i}"), vector.clone())?;
1056 }
1057 let build_time = build_start.elapsed();
1058
1059 let mut query_times = Vec::new();
1061 let memory_start = self.get_current_memory_usage();
1062
1063 for query in test_queries {
1064 let start = Instant::now();
1065 let _ = index.search_knn(query, 10)?;
1066 query_times.push(start.elapsed());
1067 }
1068
1069 let memory_end = self.get_current_memory_usage();
1070 let avg_query_time = Duration::from_nanos(
1071 (query_times.iter().map(|d| d.as_nanos()).sum::<u128>()
1072 / query_times.len() as u128)
1073 .try_into()
1074 .unwrap(),
1075 );
1076
1077 performance_scaling.push((size, avg_query_time));
1078 memory_scaling.push((size, memory_end.saturating_sub(memory_start)));
1079 build_time_scaling.push((size, build_time));
1080 }
1081
1082 let mut concurrency_scaling = Vec::new();
1084 let thread_counts = [1, 2, 4, 8];
1085
1086 for &thread_count in &thread_counts {
1087 let qps = self.measure_concurrent_performance(
1088 &dataset.query_vectors[..100.min(dataset.query_vectors.len())],
1089 thread_count,
1090 )?;
1091 concurrency_scaling.push((thread_count, qps));
1092 }
1093
1094 Ok(ScalabilityMetrics {
1095 performance_scaling,
1096 memory_scaling,
1097 build_time_scaling,
1098 concurrency_scaling,
1099 })
1100 }
1101
1102 fn get_current_memory_usage(&self) -> usize {
1103 0 }
1106
1107 fn measure_concurrent_performance(
1108 &self,
1109 queries: &[Vector],
1110 thread_count: usize,
1111 ) -> Result<f64> {
1112 use std::sync::Arc;
1113 use std::thread;
1114
1115 let index = Arc::new(crate::MemoryVectorIndex::new());
1116 let queries = Arc::new(queries.to_vec());
1117
1118 let start_time = Instant::now();
1119 let mut handles = Vec::new();
1120
1121 for thread_id in 0..thread_count {
1122 let index = Arc::clone(&index);
1123 let queries = Arc::clone(&queries);
1124
1125 let handle = thread::spawn(move || {
1126 let queries_per_thread = queries.len() / thread_count;
1127 let start_idx = thread_id * queries_per_thread;
1128 let end_idx = if thread_id == thread_count - 1 {
1129 queries.len()
1130 } else {
1131 start_idx + queries_per_thread
1132 };
1133
1134 let mut query_count = 0;
1135 for query in &queries[start_idx..end_idx] {
1136 if index.search_knn(query, 10).is_ok() {
1137 query_count += 1;
1138 }
1139 }
1140 query_count
1141 });
1142
1143 handles.push(handle);
1144 }
1145
1146 let total_queries: usize = handles.into_iter().map(|h| h.join().unwrap_or(0)).sum();
1147 let elapsed = start_time.elapsed();
1148
1149 Ok(total_queries as f64 / elapsed.as_secs_f64())
1150 }
1151}
1152
1153pub struct BenchmarkRunner;
1155
1156impl BenchmarkRunner {
1157 pub fn run_standard_benchmarks() -> Result<Vec<BenchmarkResult>> {
1159 let config = BenchmarkConfig::default();
1160 let mut suite = BenchmarkSuite::new(config);
1161
1162 suite.generate_synthetic_datasets()?;
1164
1165 suite.add_algorithm(Box::new(crate::MemoryVectorIndex::new()));
1167
1168 suite.run_all_benchmarks()
1170 }
1171
1172 pub fn run_quick_benchmarks() -> Result<Vec<BenchmarkResult>> {
1174 let config = BenchmarkConfig {
1175 warmup_runs: 1,
1176 benchmark_runs: 3,
1177 max_duration: Duration::from_secs(30),
1178 ..BenchmarkConfig::default()
1179 };
1180
1181 let mut suite = BenchmarkSuite::new(config);
1182
1183 suite.generate_random_dataset("quick_test", 100, 64, 10)?;
1185 suite.add_algorithm(Box::new(crate::MemoryVectorIndex::new()));
1186
1187 suite.run_all_benchmarks()
1188 }
1189
1190 pub fn run_comprehensive_benchmarks() -> Result<String> {
1192 let results = Self::run_standard_benchmarks()?;
1193
1194 let config = BenchmarkConfig::default();
1196 let suite = BenchmarkSuite {
1197 config,
1198 datasets: Vec::new(),
1199 algorithms: Vec::new(),
1200 results,
1201 };
1202
1203 suite.export_results(BenchmarkOutputFormat::Table)
1204 }
1205}
1206
1207#[cfg(test)]
1208mod tests {
1209 use super::*;
1210
1211 #[test]
1212 fn test_benchmark_suite_creation() {
1213 let config = BenchmarkConfig::default();
1214 let suite = BenchmarkSuite::new(config);
1215 assert_eq!(suite.datasets.len(), 0);
1216 assert_eq!(suite.algorithms.len(), 0);
1217 }
1218
1219 #[test]
1220 fn test_synthetic_dataset_generation() {
1221 let config = BenchmarkConfig::default();
1222 let mut suite = BenchmarkSuite::new(config);
1223
1224 suite.generate_synthetic_datasets().unwrap();
1225 assert!(!suite.datasets.is_empty());
1226
1227 for dataset in &suite.datasets {
1228 assert!(!dataset.train_vectors.is_empty());
1229 assert!(!dataset.query_vectors.is_empty());
1230 }
1231 }
1232
1233 #[test]
1234 fn test_performance_metrics_calculation() {
1235 let config = BenchmarkConfig::default();
1236 let suite = BenchmarkSuite::new(config);
1237
1238 let query_times = vec![
1239 Duration::from_millis(10),
1240 Duration::from_millis(15),
1241 Duration::from_millis(12),
1242 Duration::from_millis(20),
1243 Duration::from_millis(8),
1244 ];
1245
1246 let metrics = suite.calculate_performance_metrics(&query_times).unwrap();
1247
1248 assert!(metrics.avg_query_time.as_millis() > 0);
1249 assert!(metrics.queries_per_second > 0.0);
1250 assert!(metrics.min_query_time <= metrics.median_query_time);
1251 assert!(metrics.median_query_time <= metrics.max_query_time);
1252 }
1253
1254 #[test]
1255 fn test_quick_benchmarks() {
1256 let results = BenchmarkRunner::run_quick_benchmarks();
1257 assert!(results.is_ok());
1258 let results = results.unwrap();
1259 assert!(!results.is_empty());
1260
1261 for result in results {
1262 assert!(result.performance.avg_query_time.as_nanos() > 0);
1263 assert!(result.performance.queries_per_second > 0.0);
1264 }
1265 }
1266
1267 #[test]
1268 fn test_export_formats() {
1269 let config = BenchmarkConfig::default();
1270 let mut suite = BenchmarkSuite::new(config);
1271
1272 let test_case = BenchmarkTestCase {
1274 name: "test".to_string(),
1275 description: "test case".to_string(),
1276 dataset: "test_data".to_string(),
1277 algorithm: "test_alg".to_string(),
1278 parameters: HashMap::new(),
1279 query_count: 10,
1280 k: 5,
1281 };
1282
1283 let result = BenchmarkResult {
1284 test_case,
1285 performance: PerformanceMetrics {
1286 avg_query_time: Duration::from_millis(10),
1287 median_query_time: Duration::from_millis(10),
1288 p95_query_time: Duration::from_millis(15),
1289 p99_query_time: Duration::from_millis(20),
1290 min_query_time: Duration::from_millis(5),
1291 max_query_time: Duration::from_millis(25),
1292 std_dev_query_time: Duration::from_millis(3),
1293 queries_per_second: 100.0,
1294 index_build_time: None,
1295 index_update_time: None,
1296 throughput: 100.0,
1297 },
1298 quality: None,
1299 memory: None,
1300 scalability: None,
1301 system_info: SystemInfo {
1302 cpu_info: "Test CPU".to_string(),
1303 total_ram_gb: 16.0,
1304 available_ram_gb: 12.0,
1305 os: "test".to_string(),
1306 rust_version: "1.70.0".to_string(),
1307 simd_features: vec!["AVX2".to_string()],
1308 gpu_info: None,
1309 },
1310 timestamp: std::time::SystemTime::now(),
1311 };
1312
1313 suite.results.push(result);
1314
1315 let json_output = suite.export_results(BenchmarkOutputFormat::Json);
1317 assert!(json_output.is_ok());
1318
1319 let csv_output = suite.export_results(BenchmarkOutputFormat::Csv);
1320 assert!(csv_output.is_ok());
1321
1322 let table_output = suite.export_results(BenchmarkOutputFormat::Table);
1323 assert!(table_output.is_ok());
1324 }
1325
1326 #[test]
1327 fn test_profiler() {
1328 let mut profiler = PerformanceProfiler::new();
1329
1330 profiler.checkpoint("start");
1331 std::thread::sleep(Duration::from_millis(50));
1332 profiler.checkpoint("middle");
1333 std::thread::sleep(Duration::from_millis(100));
1334 profiler.checkpoint("end");
1335
1336 let breakdown = profiler.get_timing_breakdown();
1337 assert_eq!(breakdown.len(), 3);
1338
1339 for (name, duration) in breakdown {
1341 assert!(!name.is_empty());
1342 assert!(duration.as_nanos() > 0);
1344 }
1345 }
1346}