Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
[package]
name = "performance-report"
version = "0.1.0"
edition = "2021"
description = "Performance report generator for 7sense benchmarks"
publish = false
[[bin]]
name = "performance-report"
path = "performance_report.rs"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = "0.4"

View File

@@ -0,0 +1,603 @@
//! Performance Report Generator for 7sense
//!
//! This utility parses Criterion benchmark output and generates
//! a comprehensive performance report with pass/fail status against targets.
//!
//! Performance Targets (from ADR-004):
//! - HNSW Search: 150x speedup vs brute force
//! - Query Latency p50: < 10ms
//! - Query Latency p99: < 50ms
//! - Recall@10: >= 0.95
//! - Recall@100: >= 0.98
//! - Embedding inference: >100 segments/second
//! - Batch ingestion: 1M vectors/minute (16,667 vectors/second)
//! - Total query latency: <100ms
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::Duration;
// ============================================================================
// Performance Targets
// ============================================================================
mod targets {
use std::time::Duration;
// HNSW Performance
pub const HNSW_SPEEDUP_VS_BRUTE_FORCE: f64 = 150.0;
pub const QUERY_LATENCY_P50: Duration = Duration::from_millis(10);
pub const QUERY_LATENCY_P99: Duration = Duration::from_millis(50);
pub const RECALL_AT_10: f64 = 0.95;
pub const RECALL_AT_100: f64 = 0.98;
pub const INSERT_THROUGHPUT_PER_SEC: f64 = 10_000.0;
pub const BUILD_TIME_1M_VECTORS: Duration = Duration::from_secs(30 * 60);
// Embedding Performance
pub const EMBEDDING_SEGMENTS_PER_SECOND: f64 = 100.0;
pub const SPECTROGRAM_LATENCY: Duration = Duration::from_millis(20);
pub const NORMALIZATION_LATENCY: Duration = Duration::from_millis(5);
// Batch Ingestion
pub const BATCH_VECTORS_PER_MINUTE: f64 = 1_000_000.0;
pub const BATCH_VECTORS_PER_SECOND: f64 = BATCH_VECTORS_PER_MINUTE / 60.0;
// API Performance
pub const TOTAL_QUERY_LATENCY: Duration = Duration::from_millis(100);
pub const EVIDENCE_PACK_LATENCY: Duration = Duration::from_millis(200);
// Quantization
pub const MAX_RECALL_LOSS_INT8: f64 = 0.03;
}
// ============================================================================
// Criterion Output Parsing
// ============================================================================
#[derive(Debug, Clone)]
struct BenchmarkEstimates {
mean: Duration,
median: Duration,
std_dev: Duration,
confidence_interval: (Duration, Duration),
}
#[derive(Debug, Clone)]
struct BenchmarkResult {
name: String,
group: String,
estimates: BenchmarkEstimates,
throughput: Option<f64>,
iterations: u64,
}
/// Parse a Criterion estimates.json file
fn parse_estimates_json(path: &Path) -> Option<BenchmarkEstimates> {
let content = fs::read_to_string(path).ok()?;
let json: serde_json::Value = serde_json::from_str(&content).ok()?;
let mean = json.get("mean")?.get("point_estimate")?.as_f64()?;
let median = json.get("median")?.get("point_estimate")?.as_f64()?;
let std_dev = json.get("std_dev")?.get("point_estimate")?.as_f64()?;
let ci_lower = json.get("mean")?.get("confidence_interval")?.get("lower_bound")?.as_f64()?;
let ci_upper = json.get("mean")?.get("confidence_interval")?.get("upper_bound")?.as_f64()?;
Some(BenchmarkEstimates {
mean: Duration::from_nanos(mean as u64),
median: Duration::from_nanos(median as u64),
std_dev: Duration::from_nanos(std_dev as u64),
confidence_interval: (
Duration::from_nanos(ci_lower as u64),
Duration::from_nanos(ci_upper as u64),
),
})
}
/// Scan criterion output directory and collect all benchmark results
fn collect_benchmark_results(criterion_dir: &Path) -> Vec<BenchmarkResult> {
let mut results = Vec::new();
if !criterion_dir.exists() {
return results;
}
// Walk through criterion output directories
for bench_entry in fs::read_dir(criterion_dir).into_iter().flatten() {
let bench_path = match bench_entry {
Ok(e) => e.path(),
Err(_) => continue,
};
if !bench_path.is_dir() {
continue;
}
let bench_name = bench_path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();
// Look for group directories
for group_entry in fs::read_dir(&bench_path).into_iter().flatten() {
let group_path = match group_entry {
Ok(e) => e.path(),
Err(_) => continue,
};
if !group_path.is_dir() {
continue;
}
let group_name = group_path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();
// Look for estimates.json
let estimates_path = group_path.join("new").join("estimates.json");
if let Some(estimates) = parse_estimates_json(&estimates_path) {
results.push(BenchmarkResult {
name: format!("{}/{}", bench_name, group_name),
group: bench_name.clone(),
estimates,
throughput: None, // Would need to parse from benchmark.json
iterations: 0,
});
}
// Also check for sub-benchmarks
for sub_entry in fs::read_dir(&group_path).into_iter().flatten() {
let sub_path = match sub_entry {
Ok(e) => e.path(),
Err(_) => continue,
};
if !sub_path.is_dir() {
continue;
}
let sub_name = sub_path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();
let estimates_path = sub_path.join("new").join("estimates.json");
if let Some(estimates) = parse_estimates_json(&estimates_path) {
results.push(BenchmarkResult {
name: format!("{}/{}/{}", bench_name, group_name, sub_name),
group: bench_name.clone(),
estimates,
throughput: None,
iterations: 0,
});
}
}
}
}
results
}
// ============================================================================
// Performance Checks
// ============================================================================
#[derive(Debug, Clone)]
enum CheckResult {
Pass,
Fail(String),
Warning(String),
Unknown,
}
impl CheckResult {
fn symbol(&self) -> &str {
match self {
CheckResult::Pass => "[PASS]",
CheckResult::Fail(_) => "[FAIL]",
CheckResult::Warning(_) => "[WARN]",
CheckResult::Unknown => "[????]",
}
}
fn is_pass(&self) -> bool {
matches!(self, CheckResult::Pass)
}
}
#[derive(Debug)]
struct PerformanceCheck {
name: String,
target: String,
actual: String,
result: CheckResult,
}
/// Check HNSW query latency
fn check_hnsw_latency(results: &[BenchmarkResult]) -> Vec<PerformanceCheck> {
let mut checks = Vec::new();
// Find HNSW search benchmarks
for result in results {
if result.name.contains("hnsw_search") && result.name.contains("k_10") {
let actual_ms = result.estimates.mean.as_secs_f64() * 1000.0;
// p50 check (using mean as approximation)
checks.push(PerformanceCheck {
name: format!("{} p50", result.name),
target: format!("< {}ms", targets::QUERY_LATENCY_P50.as_millis()),
actual: format!("{:.2}ms", actual_ms),
result: if result.estimates.mean <= targets::QUERY_LATENCY_P50 {
CheckResult::Pass
} else {
CheckResult::Fail(format!(
"Exceeds target by {:.2}ms",
actual_ms - targets::QUERY_LATENCY_P50.as_secs_f64() * 1000.0
))
},
});
// p99 check (using mean + 2*stddev as approximation)
let p99_estimate = result.estimates.mean + result.estimates.std_dev * 2;
let p99_ms = p99_estimate.as_secs_f64() * 1000.0;
checks.push(PerformanceCheck {
name: format!("{} p99 (estimated)", result.name),
target: format!("< {}ms", targets::QUERY_LATENCY_P99.as_millis()),
actual: format!("{:.2}ms", p99_ms),
result: if p99_estimate <= targets::QUERY_LATENCY_P99 {
CheckResult::Pass
} else {
CheckResult::Fail(format!(
"Exceeds target by {:.2}ms",
p99_ms - targets::QUERY_LATENCY_P99.as_secs_f64() * 1000.0
))
},
});
}
}
checks
}
/// Check embedding throughput
fn check_embedding_throughput(results: &[BenchmarkResult]) -> Vec<PerformanceCheck> {
let mut checks = Vec::new();
for result in results {
if result.name.contains("full_embedding_pipeline") && result.name.contains("single") {
let latency_ms = result.estimates.mean.as_secs_f64() * 1000.0;
let throughput = 1000.0 / latency_ms;
checks.push(PerformanceCheck {
name: "Embedding throughput".to_string(),
target: format!(">= {} segments/sec", targets::EMBEDDING_SEGMENTS_PER_SECOND),
actual: format!("{:.1} segments/sec ({:.2}ms/segment)", throughput, latency_ms),
result: if throughput >= targets::EMBEDDING_SEGMENTS_PER_SECOND {
CheckResult::Pass
} else {
CheckResult::Fail(format!(
"Below target by {:.1} segments/sec",
targets::EMBEDDING_SEGMENTS_PER_SECOND - throughput
))
},
});
}
}
checks
}
/// Check API latency
fn check_api_latency(results: &[BenchmarkResult]) -> Vec<PerformanceCheck> {
let mut checks = Vec::new();
for result in results {
if result.name.contains("neighbor_search_endpoint") {
let latency_ms = result.estimates.mean.as_secs_f64() * 1000.0;
checks.push(PerformanceCheck {
name: result.name.clone(),
target: format!("< {}ms", targets::TOTAL_QUERY_LATENCY.as_millis()),
actual: format!("{:.2}ms", latency_ms),
result: if result.estimates.mean <= targets::TOTAL_QUERY_LATENCY {
CheckResult::Pass
} else {
CheckResult::Fail(format!(
"Exceeds target by {:.2}ms",
latency_ms - targets::TOTAL_QUERY_LATENCY.as_secs_f64() * 1000.0
))
},
});
}
if result.name.contains("evidence_pack") {
let latency_ms = result.estimates.mean.as_secs_f64() * 1000.0;
checks.push(PerformanceCheck {
name: result.name.clone(),
target: format!("< {}ms", targets::EVIDENCE_PACK_LATENCY.as_millis()),
actual: format!("{:.2}ms", latency_ms),
result: if result.estimates.mean <= targets::EVIDENCE_PACK_LATENCY {
CheckResult::Pass
} else {
CheckResult::Fail(format!(
"Exceeds target by {:.2}ms",
latency_ms - targets::EVIDENCE_PACK_LATENCY.as_secs_f64() * 1000.0
))
},
});
}
}
checks
}
/// Check insert throughput
fn check_insert_throughput(results: &[BenchmarkResult]) -> Vec<PerformanceCheck> {
let mut checks = Vec::new();
for result in results {
if result.name.contains("hnsw_insert_batch") {
// Try to extract batch size from name
let batch_size = if result.name.contains("1000") {
1000.0
} else if result.name.contains("5000") {
5000.0
} else {
continue;
};
let batch_time_secs = result.estimates.mean.as_secs_f64();
let throughput = batch_size / batch_time_secs;
checks.push(PerformanceCheck {
name: format!("{} throughput", result.name),
target: format!(">= {:.0} vectors/sec", targets::INSERT_THROUGHPUT_PER_SEC),
actual: format!("{:.0} vectors/sec", throughput),
result: if throughput >= targets::INSERT_THROUGHPUT_PER_SEC {
CheckResult::Pass
} else if throughput >= targets::INSERT_THROUGHPUT_PER_SEC * 0.5 {
CheckResult::Warning(format!(
"Below target but acceptable ({:.1}% of target)",
throughput / targets::INSERT_THROUGHPUT_PER_SEC * 100.0
))
} else {
CheckResult::Fail(format!(
"Significantly below target ({:.1}% of target)",
throughput / targets::INSERT_THROUGHPUT_PER_SEC * 100.0
))
},
});
}
}
checks
}
// ============================================================================
// Report Generation
// ============================================================================
#[derive(Debug)]
struct PerformanceReport {
timestamp: String,
total_benchmarks: usize,
checks: Vec<PerformanceCheck>,
pass_count: usize,
fail_count: usize,
warning_count: usize,
}
impl PerformanceReport {
fn generate(criterion_dir: &Path) -> Self {
let results = collect_benchmark_results(criterion_dir);
let mut checks = Vec::new();
// Run all checks
checks.extend(check_hnsw_latency(&results));
checks.extend(check_embedding_throughput(&results));
checks.extend(check_api_latency(&results));
checks.extend(check_insert_throughput(&results));
let pass_count = checks.iter().filter(|c| c.result.is_pass()).count();
let fail_count = checks.iter().filter(|c| matches!(c.result, CheckResult::Fail(_))).count();
let warning_count = checks.iter().filter(|c| matches!(c.result, CheckResult::Warning(_))).count();
Self {
timestamp: chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
total_benchmarks: results.len(),
checks,
pass_count,
fail_count,
warning_count,
}
}
fn print_text(&self) {
println!();
println!("================================================================");
println!(" 7sense Performance Report");
println!("================================================================");
println!();
println!("Generated: {}", self.timestamp);
println!("Total Benchmarks Analyzed: {}", self.total_benchmarks);
println!();
println!("Performance Targets (ADR-004):");
println!(" - HNSW Search: 150x speedup vs brute force");
println!(" - Query Latency p50: < 10ms");
println!(" - Query Latency p99: < 50ms");
println!(" - Recall@10: >= 0.95");
println!(" - Recall@100: >= 0.98");
println!(" - Embedding: >100 segments/second");
println!(" - Batch: 1M vectors/minute");
println!(" - Total query: <100ms");
println!();
println!("================================================================");
println!(" Check Results");
println!("================================================================");
println!();
for check in &self.checks {
let symbol = check.result.symbol();
println!("{} {}", symbol, check.name);
println!(" Target: {}", check.target);
println!(" Actual: {}", check.actual);
if let CheckResult::Fail(msg) | CheckResult::Warning(msg) = &check.result {
println!(" Note: {}", msg);
}
println!();
}
println!("================================================================");
println!(" Summary");
println!("================================================================");
println!();
println!(" Total Checks: {}", self.checks.len());
println!(" Passed: {} ({}%)",
self.pass_count,
if self.checks.is_empty() { 0 } else { self.pass_count * 100 / self.checks.len() }
);
println!(" Failed: {}", self.fail_count);
println!(" Warnings: {}", self.warning_count);
println!();
if self.fail_count == 0 {
println!(" Status: ALL TARGETS MET");
} else {
println!(" Status: {} TARGET(S) NOT MET", self.fail_count);
}
println!();
println!("================================================================");
}
fn to_json(&self) -> String {
let checks_json: Vec<serde_json::Value> = self.checks.iter().map(|c| {
serde_json::json!({
"name": c.name,
"target": c.target,
"actual": c.actual,
"status": match &c.result {
CheckResult::Pass => "pass",
CheckResult::Fail(_) => "fail",
CheckResult::Warning(_) => "warning",
CheckResult::Unknown => "unknown",
},
"message": match &c.result {
CheckResult::Fail(msg) | CheckResult::Warning(msg) => msg.clone(),
_ => String::new(),
}
})
}).collect();
serde_json::to_string_pretty(&serde_json::json!({
"timestamp": self.timestamp,
"total_benchmarks": self.total_benchmarks,
"summary": {
"total_checks": self.checks.len(),
"pass_count": self.pass_count,
"fail_count": self.fail_count,
"warning_count": self.warning_count,
"all_targets_met": self.fail_count == 0
},
"checks": checks_json
})).unwrap_or_else(|_| "{}".to_string())
}
fn exit_code(&self) -> i32 {
if self.fail_count > 0 {
1
} else {
0
}
}
}
// ============================================================================
// Main Entry Point
// ============================================================================
fn main() {
let args: Vec<String> = std::env::args().collect();
let criterion_dir = if args.len() > 1 {
PathBuf::from(&args[1])
} else {
PathBuf::from("target/criterion")
};
let output_format = if args.len() > 2 {
args[2].as_str()
} else {
"text"
};
let report = PerformanceReport::generate(&criterion_dir);
match output_format {
"json" => println!("{}", report.to_json()),
"text" | _ => report.print_text(),
}
std::process::exit(report.exit_code());
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_check_result_symbols() {
assert_eq!(CheckResult::Pass.symbol(), "[PASS]");
assert_eq!(CheckResult::Fail("test".to_string()).symbol(), "[FAIL]");
assert_eq!(CheckResult::Warning("test".to_string()).symbol(), "[WARN]");
}
#[test]
fn test_performance_check_latency() {
let result = BenchmarkResult {
name: "hnsw_search/k_10".to_string(),
group: "hnsw_benchmark".to_string(),
estimates: BenchmarkEstimates {
mean: Duration::from_millis(5),
median: Duration::from_millis(5),
std_dev: Duration::from_millis(1),
confidence_interval: (Duration::from_millis(4), Duration::from_millis(6)),
},
throughput: None,
iterations: 100,
};
let checks = check_hnsw_latency(&[result]);
assert!(!checks.is_empty());
assert!(checks[0].result.is_pass());
}
#[test]
fn test_performance_check_latency_fail() {
let result = BenchmarkResult {
name: "hnsw_search/k_10".to_string(),
group: "hnsw_benchmark".to_string(),
estimates: BenchmarkEstimates {
mean: Duration::from_millis(100), // Exceeds target
median: Duration::from_millis(100),
std_dev: Duration::from_millis(10),
confidence_interval: (Duration::from_millis(90), Duration::from_millis(110)),
},
throughput: None,
iterations: 100,
};
let checks = check_hnsw_latency(&[result]);
assert!(!checks.is_empty());
assert!(!checks[0].result.is_pass());
}
}

View File

@@ -0,0 +1,320 @@
#!/bin/bash
# 7sense Benchmark Runner
#
# This script runs all benchmarks and generates reports.
# Performance targets from ADR-004:
# - HNSW Search: 150x speedup vs brute force, p99 < 50ms
# - Embedding inference: >100 segments/second
# - Batch ingestion: 1M vectors/minute
# - Query latency: <100ms total
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
BASELINE_NAME="${BASELINE_NAME:-main}"
OUTPUT_DIR="${OUTPUT_DIR:-target/criterion}"
REPORT_DIR="${REPORT_DIR:-target/benchmark-reports}"
COMPARE_BASELINE="${COMPARE_BASELINE:-false}"
# Benchmark suites
ALL_BENCHMARKS=(
"hnsw_benchmark"
"embedding_benchmark"
"clustering_benchmark"
"api_benchmark"
)
print_header() {
echo ""
echo "============================================================"
echo -e "${BLUE}$1${NC}"
echo "============================================================"
echo ""
}
print_status() {
echo -e "${GREEN}[INFO]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Create output directories
setup_directories() {
print_status "Setting up output directories..."
mkdir -p "$OUTPUT_DIR"
mkdir -p "$REPORT_DIR"
mkdir -p "$REPORT_DIR/html"
mkdir -p "$REPORT_DIR/json"
}
# Run a single benchmark suite
run_benchmark() {
local bench_name="$1"
local extra_args="${2:-}"
print_header "Running $bench_name"
if [[ "$COMPARE_BASELINE" == "true" ]]; then
print_status "Comparing against baseline: $BASELINE_NAME"
cargo bench --bench "$bench_name" -- --baseline "$BASELINE_NAME" $extra_args
else
cargo bench --bench "$bench_name" -- $extra_args
fi
# Check if benchmark completed successfully
if [[ $? -eq 0 ]]; then
print_status "$bench_name completed successfully"
else
print_error "$bench_name failed"
return 1
fi
}
# Run all benchmarks
run_all_benchmarks() {
print_header "Running All Benchmarks"
local failed=()
for bench in "${ALL_BENCHMARKS[@]}"; do
if ! run_benchmark "$bench"; then
failed+=("$bench")
fi
done
if [[ ${#failed[@]} -gt 0 ]]; then
print_error "Failed benchmarks: ${failed[*]}"
return 1
fi
print_status "All benchmarks completed successfully"
}
# Save baseline for future comparisons
save_baseline() {
local baseline_name="${1:-$BASELINE_NAME}"
print_header "Saving Baseline: $baseline_name"
for bench in "${ALL_BENCHMARKS[@]}"; do
print_status "Saving baseline for $bench..."
cargo bench --bench "$bench" -- --save-baseline "$baseline_name"
done
print_status "Baseline '$baseline_name' saved"
}
# Run quick benchmarks (reduced sample size)
run_quick() {
print_header "Running Quick Benchmarks"
# Set sample size to minimum for quick testing
export CRITERION_DEBUG=1
for bench in "${ALL_BENCHMARKS[@]}"; do
print_status "Quick run: $bench"
cargo bench --bench "$bench" -- --quick
done
}
# Run specific benchmark groups
run_hnsw() {
run_benchmark "hnsw_benchmark" "$@"
}
run_embedding() {
run_benchmark "embedding_benchmark" "$@"
}
run_clustering() {
run_benchmark "clustering_benchmark" "$@"
}
run_api() {
run_benchmark "api_benchmark" "$@"
}
# Generate summary report
generate_summary() {
print_header "Generating Summary Report"
local report_file="$REPORT_DIR/summary_$(date +%Y%m%d_%H%M%S).txt"
{
echo "7sense Benchmark Summary Report"
echo "Generated: $(date)"
echo "============================================================"
echo ""
echo "Performance Targets (from ADR-004):"
echo " - HNSW Search: 150x speedup vs brute force"
echo " - Query Latency p50: < 10ms"
echo " - Query Latency p99: < 50ms"
echo " - Recall@10: >= 0.95"
echo " - Recall@100: >= 0.98"
echo " - Embedding inference: >100 segments/second"
echo " - Batch ingestion: 1M vectors/minute"
echo " - Total query latency: <100ms"
echo ""
echo "============================================================"
echo ""
# Parse criterion output if available
if [[ -d "$OUTPUT_DIR" ]]; then
echo "Benchmark Results:"
echo ""
for bench in "${ALL_BENCHMARKS[@]}"; do
if [[ -d "$OUTPUT_DIR/$bench" ]]; then
echo "--- $bench ---"
# List all benchmark groups
find "$OUTPUT_DIR/$bench" -name "estimates.json" -exec dirname {} \; 2>/dev/null | \
while read -r dir; do
local name=$(basename "$dir")
echo " $name"
done
echo ""
fi
done
else
echo "No benchmark results found. Run benchmarks first."
fi
} > "$report_file"
print_status "Summary saved to: $report_file"
cat "$report_file"
}
# Run performance analysis tests
run_analysis() {
print_header "Running Performance Analysis"
print_status "Running speedup analysis..."
cargo test --release -- --ignored run_speedup_analysis --nocapture 2>/dev/null || true
print_status "Running latency analysis..."
cargo test --release -- --ignored run_latency_analysis --nocapture 2>/dev/null || true
print_status "Running throughput analysis..."
cargo test --release -- --ignored run_throughput_analysis --nocapture 2>/dev/null || true
print_status "Running API latency analysis..."
cargo test --release -- --ignored run_api_latency_analysis --nocapture 2>/dev/null || true
}
# Clean benchmark artifacts
clean() {
print_header "Cleaning Benchmark Artifacts"
rm -rf "$OUTPUT_DIR"
rm -rf "$REPORT_DIR"
print_status "Cleaned benchmark artifacts"
}
# Show help
show_help() {
echo "7sense Benchmark Runner"
echo ""
echo "Usage: $0 [command] [options]"
echo ""
echo "Commands:"
echo " all Run all benchmarks (default)"
echo " quick Run quick benchmarks with reduced samples"
echo " hnsw Run HNSW benchmarks only"
echo " embedding Run embedding benchmarks only"
echo " clustering Run clustering benchmarks only"
echo " api Run API benchmarks only"
echo " save-baseline Save current results as baseline"
echo " compare Run benchmarks and compare against baseline"
echo " analysis Run detailed performance analysis"
echo " summary Generate summary report"
echo " clean Clean benchmark artifacts"
echo " help Show this help message"
echo ""
echo "Environment Variables:"
echo " BASELINE_NAME Name for baseline comparison (default: main)"
echo " OUTPUT_DIR Criterion output directory (default: target/criterion)"
echo " REPORT_DIR Report output directory (default: target/benchmark-reports)"
echo " COMPARE_BASELINE Set to 'true' to compare against baseline"
echo ""
echo "Examples:"
echo " $0 # Run all benchmarks"
echo " $0 hnsw # Run HNSW benchmarks only"
echo " $0 save-baseline v1.0.0 # Save baseline named 'v1.0.0'"
echo " COMPARE_BASELINE=true $0 # Compare against baseline"
echo ""
echo "Performance Targets (ADR-004):"
echo " - HNSW Search: 150x speedup vs brute force"
echo " - Query Latency p99: < 50ms"
echo " - Recall@10: >= 0.95"
echo " - Embedding: >100 segments/second"
echo " - Batch: 1M vectors/minute"
}
# Main entry point
main() {
local command="${1:-all}"
shift || true
setup_directories
case "$command" in
all)
run_all_benchmarks
generate_summary
;;
quick)
run_quick
;;
hnsw)
run_hnsw "$@"
;;
embedding)
run_embedding "$@"
;;
clustering)
run_clustering "$@"
;;
api)
run_api "$@"
;;
save-baseline)
save_baseline "${1:-$BASELINE_NAME}"
;;
compare)
COMPARE_BASELINE=true run_all_benchmarks
;;
analysis)
run_analysis
;;
summary)
generate_summary
;;
clean)
clean
;;
help|--help|-h)
show_help
;;
*)
print_error "Unknown command: $command"
show_help
exit 1
;;
esac
}
main "$@"