use crate::hnsw::VectorIndex;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HealthStatus {
Healthy,
Warning,
Degraded,
Critical,
}
#[derive(Debug, Clone)]
pub struct DiagnosticReport {
pub status: HealthStatus,
pub size: usize,
pub memory_usage: usize,
pub issues: Vec<DiagnosticIssue>,
pub recommendations: Vec<String>,
pub performance: PerformanceMetrics,
}
#[derive(Debug, Clone)]
pub struct DiagnosticIssue {
pub severity: IssueSeverity,
pub category: IssueCategory,
pub description: String,
pub suggested_fix: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum IssueSeverity {
Info,
Warning,
Error,
Critical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IssueCategory {
Memory,
Performance,
Configuration,
DataQuality,
IndexStructure,
}
#[derive(Debug, Clone)]
pub struct PerformanceMetrics {
pub avg_query_latency: Option<Duration>,
pub cache_hit_rate: Option<f32>,
pub estimated_qps: Option<f32>,
}
pub fn diagnose_index(index: &VectorIndex) -> DiagnosticReport {
let mut issues = Vec::new();
let mut recommendations = Vec::new();
let size = index.len();
let dimension = index.dimension();
let vector_memory = size * dimension * 4;
let graph_memory = size * 16 * 8; let overhead = size * 100; let memory_usage = vector_memory + graph_memory + overhead;
if size == 0 {
issues.push(DiagnosticIssue {
severity: IssueSeverity::Warning,
category: IssueCategory::IndexStructure,
description: "Index is empty".to_string(),
suggested_fix: Some("Add vectors to the index before querying".to_string()),
});
} else if size > 10_000_000 {
issues.push(DiagnosticIssue {
severity: IssueSeverity::Warning,
category: IssueCategory::Performance,
description: format!("Very large index ({} vectors)", size),
suggested_fix: Some("Consider using DiskANN for datasets > 10M vectors".to_string()),
});
recommendations
.push("Consider partitioning the index or using distributed search".to_string());
}
if memory_usage > 10 * 1024 * 1024 * 1024 {
issues.push(DiagnosticIssue {
severity: IssueSeverity::Warning,
category: IssueCategory::Memory,
description: format!("High memory usage: ~{:.2} GB", memory_usage as f64 / 1e9),
suggested_fix: Some("Consider using quantization or DiskANN".to_string()),
});
}
if dimension > 2048 {
issues.push(DiagnosticIssue {
severity: IssueSeverity::Info,
category: IssueCategory::Performance,
description: format!("High dimensionality: {}", dimension),
suggested_fix: Some("Consider dimensionality reduction or PCA".to_string()),
});
recommendations
.push("High-dimensional vectors may benefit from dimensionality reduction".to_string());
}
let status = if issues.iter().any(|i| i.severity == IssueSeverity::Critical) {
HealthStatus::Critical
} else if issues.iter().any(|i| i.severity == IssueSeverity::Error) {
HealthStatus::Degraded
} else if issues.iter().any(|i| i.severity == IssueSeverity::Warning) {
HealthStatus::Warning
} else {
HealthStatus::Healthy
};
DiagnosticReport {
status,
size,
memory_usage,
issues,
recommendations,
performance: PerformanceMetrics {
avg_query_latency: None,
cache_hit_rate: None,
estimated_qps: None,
},
}
}
pub struct SearchProfiler {
start_time: Instant,
query_count: usize,
total_duration: Duration,
min_latency: Option<Duration>,
max_latency: Option<Duration>,
}
impl SearchProfiler {
pub fn new() -> Self {
Self {
start_time: Instant::now(),
query_count: 0,
total_duration: Duration::from_secs(0),
min_latency: None,
max_latency: None,
}
}
pub fn record_query(&mut self, duration: Duration) {
self.query_count += 1;
self.total_duration += duration;
self.min_latency = Some(match self.min_latency {
Some(min) => min.min(duration),
None => duration,
});
self.max_latency = Some(match self.max_latency {
Some(max) => max.max(duration),
None => duration,
});
}
pub fn stats(&self) -> ProfilerStats {
let avg_latency = if self.query_count > 0 {
self.total_duration / self.query_count as u32
} else {
Duration::from_secs(0)
};
let elapsed = self.start_time.elapsed();
let qps = if elapsed.as_secs() > 0 {
self.query_count as f64 / elapsed.as_secs_f64()
} else {
0.0
};
ProfilerStats {
total_queries: self.query_count,
avg_latency,
min_latency: self.min_latency,
max_latency: self.max_latency,
qps,
elapsed,
}
}
pub fn reset(&mut self) {
self.start_time = Instant::now();
self.query_count = 0;
self.total_duration = Duration::from_secs(0);
self.min_latency = None;
self.max_latency = None;
}
}
impl Default for SearchProfiler {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct ProfilerStats {
pub total_queries: usize,
pub avg_latency: Duration,
pub min_latency: Option<Duration>,
pub max_latency: Option<Duration>,
pub qps: f64,
pub elapsed: Duration,
}
pub struct HealthMonitor {
last_report: Option<DiagnosticReport>,
last_check: Option<Instant>,
check_interval: Duration,
}
impl HealthMonitor {
pub fn new(check_interval: Duration) -> Self {
Self {
last_report: None,
last_check: None,
check_interval,
}
}
pub fn should_check(&self) -> bool {
match self.last_check {
Some(last) => last.elapsed() >= self.check_interval,
None => true,
}
}
pub fn check(&mut self, index: &VectorIndex) -> &DiagnosticReport {
self.last_report = Some(diagnose_index(index));
self.last_check = Some(Instant::now());
self.last_report.as_ref().expect("just assigned above")
}
pub fn last_report(&self) -> Option<&DiagnosticReport> {
self.last_report.as_ref()
}
pub fn time_since_last_check(&self) -> Option<Duration> {
self.last_check.map(|t| t.elapsed())
}
}
impl Default for HealthMonitor {
fn default() -> Self {
Self::new(Duration::from_secs(300)) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_diagnose_empty_index() {
let index =
VectorIndex::with_defaults(128).expect("test: VectorIndex::with_defaults failed");
let report = diagnose_index(&index);
assert_eq!(report.size, 0);
assert!(!report.issues.is_empty());
assert!(report
.issues
.iter()
.any(|i| i.category == IssueCategory::IndexStructure));
}
#[test]
fn test_diagnose_normal_index() {
let mut index =
VectorIndex::with_defaults(128).expect("test: VectorIndex::with_defaults failed");
let cid: ipfrs_core::Cid = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
.parse()
.expect("test: CID parse failed");
index
.insert(&cid, &vec![0.1; 128])
.expect("test: index insert failed");
let report = diagnose_index(&index);
assert_eq!(report.size, 1);
assert!(report.status == HealthStatus::Healthy || report.status == HealthStatus::Warning);
}
#[test]
fn test_search_profiler() {
let mut profiler = SearchProfiler::new();
profiler.record_query(Duration::from_millis(10));
profiler.record_query(Duration::from_millis(20));
profiler.record_query(Duration::from_millis(15));
let stats = profiler.stats();
assert_eq!(stats.total_queries, 3);
assert!(stats.avg_latency.as_millis() >= 10);
assert!(stats.avg_latency.as_millis() <= 20);
assert_eq!(stats.min_latency, Some(Duration::from_millis(10)));
assert_eq!(stats.max_latency, Some(Duration::from_millis(20)));
}
#[test]
fn test_health_monitor() {
let mut monitor = HealthMonitor::new(Duration::from_millis(100));
let index = VectorIndex::with_defaults(128)
.expect("test: VectorIndex creation with dim 128 should succeed");
assert!(monitor.should_check());
monitor.check(&index);
assert!(!monitor.should_check());
assert!(monitor.last_report().is_some());
std::thread::sleep(Duration::from_millis(150));
assert!(monitor.should_check());
}
#[test]
fn test_profiler_reset() {
let mut profiler = SearchProfiler::new();
profiler.record_query(Duration::from_millis(10));
profiler.record_query(Duration::from_millis(20));
assert_eq!(profiler.stats().total_queries, 2);
profiler.reset();
assert_eq!(profiler.stats().total_queries, 0);
assert_eq!(profiler.stats().min_latency, None);
assert_eq!(profiler.stats().max_latency, None);
}
}