//! Benchmark report generation for RuVector Cloud Run GPU use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs::{self, File}; use std::io::{BufReader, BufWriter, Write}; use std::path::Path; use crate::benchmark::BenchmarkResult; /// Generate report from benchmark results pub fn generate_report(input_dir: &Path, output: &Path, format: &str) -> Result<()> { println!( "📊 Generating {} report from: {}", format, input_dir.display() ); // Load all benchmark results let results = load_results(input_dir)?; if results.is_empty() { anyhow::bail!("No benchmark results found in {}", input_dir.display()); } println!(" Found {} benchmark results", results.len()); // Create output directory if needed if let Some(parent) = output.parent() { fs::create_dir_all(parent)?; } match format.to_lowercase().as_str() { "json" => generate_json_report(&results, output)?, "csv" => generate_csv_report(&results, output)?, "html" => generate_html_report(&results, output)?, "markdown" | "md" => generate_markdown_report(&results, output)?, _ => anyhow::bail!( "Unknown format: {}. Use json, csv, html, or markdown", format ), } println!("✓ Report saved to: {}", output.display()); Ok(()) } /// Load all benchmark results from a directory fn load_results(dir: &Path) -> Result> { let mut all_results = Vec::new(); for entry in fs::read_dir(dir)? { let entry = entry?; let path = entry.path(); if path.extension().map_or(false, |ext| ext == "json") { let file = File::open(&path)?; let reader = BufReader::new(file); // Try to parse as either a single result or wrapped results if let Ok(data) = serde_json::from_reader::<_, serde_json::Value>(reader) { if let Some(results) = data.get("results").and_then(|r| r.as_array()) { for result in results { if let Ok(r) = serde_json::from_value::(result.clone()) { all_results.push(r); } } } else if let Ok(r) = serde_json::from_value::(data) { all_results.push(r); } } } } Ok(all_results) } /// Generate JSON report fn generate_json_report(results: &[BenchmarkResult], output: &Path) -> Result<()> { let report = generate_report_data(results); let file = File::create(output)?; let writer = BufWriter::new(file); serde_json::to_writer_pretty(writer, &report)?; Ok(()) } /// Generate CSV report fn generate_csv_report(results: &[BenchmarkResult], output: &Path) -> Result<()> { let mut file = File::create(output)?; // Write header writeln!( file, "name,operation,dimensions,num_vectors,batch_size,mean_ms,p50_ms,p95_ms,p99_ms,qps,memory_mb,gpu_enabled" )?; // Write data rows for r in results { writeln!( file, "{},{},{},{},{},{:.3},{:.3},{:.3},{:.3},{:.1},{:.1},{}", r.name, r.operation, r.dimensions, r.num_vectors, r.batch_size, r.mean_time_ms, r.p50_ms, r.p95_ms, r.p99_ms, r.qps, r.memory_mb, r.gpu_enabled )?; } Ok(()) } /// Generate HTML report fn generate_html_report(results: &[BenchmarkResult], output: &Path) -> Result<()> { let report = generate_report_data(results); let html = format!( r#" RuVector Cloud Run GPU Benchmark Report

🚀 RuVector GPU Benchmark Report

Cloud Run GPU Performance Analysis | Generated: {timestamp}

Total Benchmarks

{total_benchmarks}

Peak QPS

{peak_qps:.0}q/s

Best P99 Latency

{best_p99:.2}ms

GPU Enabled

{gpu_status}

📈 Latency Distribution

âš¡ Throughput Comparison

📊 Detailed Results

{table_rows}
Operation Dimensions Vectors Mean (ms) P50 (ms) P95 (ms) P99 (ms) QPS Memory

Generated by RuVector Cloud Run GPU Benchmark Suite

© 2024 RuVector Team | MIT License

"#, timestamp = report.timestamp, total_benchmarks = report.total_benchmarks, peak_qps = report.peak_qps, best_p99 = report.best_p99_ms, gpu_status = if report.gpu_enabled { "Yes ✓" } else { "No" }, table_rows = generate_table_rows(results), latency_labels = serde_json::to_string(&report.chart_labels).unwrap(), latency_p50 = serde_json::to_string(&report.latency_p50).unwrap(), latency_p95 = serde_json::to_string(&report.latency_p95).unwrap(), latency_p99 = serde_json::to_string(&report.latency_p99).unwrap(), throughput_labels = serde_json::to_string(&report.chart_labels).unwrap(), throughput_values = serde_json::to_string(&report.throughput_qps).unwrap(), ); let mut file = File::create(output)?; file.write_all(html.as_bytes())?; Ok(()) } /// Generate Markdown report fn generate_markdown_report(results: &[BenchmarkResult], output: &Path) -> Result<()> { let report = generate_report_data(results); let mut md = String::new(); md.push_str("# RuVector Cloud Run GPU Benchmark Report\n\n"); md.push_str(&format!("**Generated:** {}\n\n", report.timestamp)); md.push_str("## Summary\n\n"); md.push_str(&format!( "- **Total Benchmarks:** {}\n", report.total_benchmarks )); md.push_str(&format!("- **Peak QPS:** {:.0}\n", report.peak_qps)); md.push_str(&format!( "- **Best P99 Latency:** {:.2} ms\n", report.best_p99_ms )); md.push_str(&format!( "- **GPU Enabled:** {}\n\n", if report.gpu_enabled { "Yes" } else { "No" } )); md.push_str("## Detailed Results\n\n"); md.push_str("| Operation | Dims | Vectors | Mean (ms) | P50 (ms) | P95 (ms) | P99 (ms) | QPS | Memory (MB) |\n"); md.push_str("|-----------|------|---------|-----------|----------|----------|----------|-----|-------------|\n"); for r in results { md.push_str(&format!( "| {} | {} | {} | {:.3} | {:.3} | {:.3} | {:.3} | {:.0} | {:.1} |\n", r.operation, r.dimensions, r.num_vectors, r.mean_time_ms, r.p50_ms, r.p95_ms, r.p99_ms, r.qps, r.memory_mb )); } md.push_str("\n---\n"); md.push_str("*Generated by RuVector Cloud Run GPU Benchmark Suite*\n"); let mut file = File::create(output)?; file.write_all(md.as_bytes())?; Ok(()) } /// Report data structure #[derive(Debug, Serialize)] struct ReportData { timestamp: String, total_benchmarks: usize, peak_qps: f64, best_p99_ms: f64, gpu_enabled: bool, chart_labels: Vec, latency_p50: Vec, latency_p95: Vec, latency_p99: Vec, throughput_qps: Vec, results: Vec, } fn generate_report_data(results: &[BenchmarkResult]) -> ReportData { let peak_qps = results.iter().map(|r| r.qps).fold(0.0f64, f64::max); let best_p99 = results .iter() .map(|r| r.p99_ms) .filter(|&p| p > 0.0) .fold(f64::INFINITY, f64::min); let gpu_enabled = results.iter().any(|r| r.gpu_enabled); let chart_labels: Vec = results .iter() .take(10) .map(|r| format!("{}d", r.dimensions)) .collect(); let latency_p50: Vec = results.iter().take(10).map(|r| r.p50_ms).collect(); let latency_p95: Vec = results.iter().take(10).map(|r| r.p95_ms).collect(); let latency_p99: Vec = results.iter().take(10).map(|r| r.p99_ms).collect(); let throughput_qps: Vec = results.iter().take(10).map(|r| r.qps).collect(); ReportData { timestamp: chrono::Utc::now() .format("%Y-%m-%d %H:%M:%S UTC") .to_string(), total_benchmarks: results.len(), peak_qps, best_p99_ms: if best_p99.is_infinite() { 0.0 } else { best_p99 }, gpu_enabled, chart_labels, latency_p50, latency_p95, latency_p99, throughput_qps, results: results.to_vec(), } } fn generate_table_rows(results: &[BenchmarkResult]) -> String { results .iter() .map(|r| { format!( r#" {} {} {} {:.3} {:.3} {:.3} {:.3} {:.0} {:.1} MB "#, r.operation, r.dimensions, r.num_vectors, r.mean_time_ms, r.p50_ms, r.p95_ms, r.p99_ms, r.qps, r.memory_mb ) }) .collect::>() .join("\n") }