use crate::storage::{AccessContext, AccessLevel, Storage};
use crate::{Document, DocumentType, Result, Source, SourceType};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use std::time::{Duration, Instant};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageBenchmarkResults {
pub document_storage: OperationBenchmark,
pub document_retrieval: OperationBenchmark,
pub embedding_storage: OperationBenchmark,
pub embedding_retrieval: OperationBenchmark,
pub vector_search: OperationBenchmark,
pub memory_stats: MemoryStats,
pub metadata: BenchmarkMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OperationBenchmark {
pub operations_count: usize,
pub total_duration: Duration,
pub avg_latency: Duration,
pub min_latency: Duration,
pub max_latency: Duration,
pub p95_latency: Duration,
pub ops_per_second: f64,
pub error_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryStats {
pub peak_memory_bytes: u64,
pub avg_memory_bytes: u64,
pub memory_efficiency: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchmarkMetadata {
pub start_time: chrono::DateTime<chrono::Utc>,
pub duration: Duration,
pub backend_type: String,
pub config: serde_json::Value,
pub dataset_size: usize,
}
pub struct StorageBenchmarker {
storage: Storage,
context: AccessContext,
}
impl StorageBenchmarker {
pub fn new(storage: Storage, user_id: String, access_level: AccessLevel) -> Self {
let context = AccessContext::new(user_id, access_level, "benchmark".to_string());
Self { storage, context }
}
pub async fn run_comprehensive_benchmark(
&self,
document_count: usize,
embedding_count: usize,
vector_size: usize,
) -> Result<StorageBenchmarkResults> {
let start_time = Utc::now();
let benchmark_start = Instant::now();
let documents = self.generate_test_documents(document_count)?;
let embeddings = self.generate_test_embeddings(embedding_count, vector_size)?;
let document_storage = self.benchmark_document_storage(&documents).await?;
let document_retrieval = self.benchmark_document_retrieval(&documents).await?;
let embedding_storage = self.benchmark_embedding_storage(&embeddings).await?;
let embedding_retrieval = self.benchmark_embedding_retrieval(&embeddings).await?;
let vector_search = self.benchmark_vector_search(&embeddings, 100).await?;
let memory_stats = self.collect_memory_stats().await?;
let metadata = BenchmarkMetadata {
start_time,
duration: benchmark_start.elapsed(),
backend_type: "qdrant".to_string(), config: serde_json::json!({}),
dataset_size: document_count + embedding_count,
};
Ok(StorageBenchmarkResults {
document_storage,
document_retrieval,
embedding_storage,
embedding_retrieval,
vector_search,
memory_stats,
metadata,
})
}
async fn benchmark_document_storage(
&self,
documents: &[Document],
) -> Result<OperationBenchmark> {
let mut latencies = Vec::new();
let mut error_count = 0;
for doc in documents {
let start = Instant::now();
match self.storage.store_document(doc, &self.context).await {
Ok(_) => {
latencies.push(start.elapsed());
}
Err(_) => {
error_count += 1;
}
}
}
Ok(self.calculate_operation_stats(latencies, error_count))
}
async fn benchmark_document_retrieval(
&self,
documents: &[Document],
) -> Result<OperationBenchmark> {
let mut latencies = Vec::new();
let mut error_count = 0;
for doc in documents {
let start = Instant::now();
match self.storage.get_document(&doc.id, &self.context).await {
Ok(Some(_)) => {
latencies.push(start.elapsed());
}
Ok(None) => {
error_count += 1; }
Err(_) => {
error_count += 1;
}
}
}
Ok(self.calculate_operation_stats(latencies, error_count))
}
async fn benchmark_embedding_storage(
&self,
embeddings: &[(Uuid, Vec<f32>)],
) -> Result<OperationBenchmark> {
let mut latencies = Vec::new();
let mut error_count = 0;
for (chunk_id, embedding) in embeddings {
let start = Instant::now();
match self
.storage
.store_embeddings(chunk_id, embedding, &self.context)
.await
{
Ok(_) => {
latencies.push(start.elapsed());
}
Err(_) => {
error_count += 1;
}
}
}
Ok(self.calculate_operation_stats(latencies, error_count))
}
async fn benchmark_embedding_retrieval(
&self,
embeddings: &[(Uuid, Vec<f32>)],
) -> Result<OperationBenchmark> {
let mut latencies = Vec::new();
let mut error_count = 0;
for (chunk_id, _) in embeddings {
let start = Instant::now();
match self.storage.get_embeddings(chunk_id, &self.context).await {
Ok(Some(_)) => {
latencies.push(start.elapsed());
}
Ok(None) => {
error_count += 1; }
Err(_) => {
error_count += 1;
}
}
}
Ok(self.calculate_operation_stats(latencies, error_count))
}
async fn benchmark_vector_search(
&self,
embeddings: &[(Uuid, Vec<f32>)],
searches_per_embedding: usize,
) -> Result<OperationBenchmark> {
let mut latencies = Vec::new();
let mut error_count = 0;
if let Some((_, query_vector)) = embeddings.first() {
for _ in 0..(embeddings.len() * searches_per_embedding) {
let start = Instant::now();
match self
.storage
.search_by_vector(query_vector, 10, &self.context)
.await
{
Ok(_) => {
latencies.push(start.elapsed());
}
Err(_) => {
error_count += 1;
}
}
}
}
Ok(self.calculate_operation_stats(latencies, error_count))
}
fn calculate_operation_stats(
&self,
mut latencies: Vec<Duration>,
error_count: usize,
) -> OperationBenchmark {
if latencies.is_empty() {
return OperationBenchmark {
operations_count: 0,
total_duration: Duration::ZERO,
avg_latency: Duration::ZERO,
min_latency: Duration::ZERO,
max_latency: Duration::ZERO,
p95_latency: Duration::ZERO,
ops_per_second: 0.0,
error_count,
};
}
latencies.sort();
let operations_count = latencies.len();
let total_duration: Duration = latencies.iter().sum();
let avg_latency = total_duration / operations_count as u32;
let min_latency = latencies[0];
let max_latency = latencies[latencies.len() - 1];
let p95_index = (operations_count as f64 * 0.95) as usize;
let p95_index = p95_index.min(latencies.len() - 1);
let p95_latency = latencies[p95_index];
let ops_per_second = operations_count as f64 / total_duration.as_secs_f64();
OperationBenchmark {
operations_count,
total_duration,
avg_latency,
min_latency,
max_latency,
p95_latency,
ops_per_second,
error_count,
}
}
async fn collect_memory_stats(&self) -> Result<MemoryStats> {
let stats = self.storage.stats(&self.context).await?;
let estimated_memory_bytes = (stats.document_count + stats.chunk_count) * 1024;
Ok(MemoryStats {
peak_memory_bytes: estimated_memory_bytes as u64,
avg_memory_bytes: estimated_memory_bytes as u64,
memory_efficiency: stats.document_count as f64 / estimated_memory_bytes as f64,
})
}
fn generate_test_documents(&self, count: usize) -> Result<Vec<Document>> {
let mut documents = Vec::new();
for i in 0..count {
let source = Source {
source_type: SourceType::Local,
url: None,
path: Some(format!("/test/doc_{}.md", i)),
arxiv_id: None,
github_repo: None,
retrieved_at: Utc::now(),
version: None,
};
let doc = Document::new(DocumentType::Note, source)
.with_content(format!("Test document content {}", i));
documents.push(doc);
}
Ok(documents)
}
fn generate_test_embeddings(
&self,
count: usize,
vector_size: usize,
) -> Result<Vec<(Uuid, Vec<f32>)>> {
let mut embeddings = Vec::new();
for _ in 0..count {
let chunk_id = Uuid::new_v4();
let embedding = (0..vector_size).map(|i| (i as f32 * 0.1).sin()).collect();
embeddings.push((chunk_id, embedding));
}
Ok(embeddings)
}
}
pub async fn run_storage_benchmarks(
storage: Storage,
document_count: usize,
embedding_count: usize,
vector_size: usize,
) -> Result<StorageBenchmarkResults> {
let benchmarker =
StorageBenchmarker::new(storage, "benchmark_user".to_string(), AccessLevel::Admin);
benchmarker
.run_comprehensive_benchmark(document_count, embedding_count, vector_size)
.await
}