use crate::analyzer::{StorageAnalysis, WorkloadType};
use crate::traits::BlockStore;
use ipfrs_core::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TuningRecommendation {
pub parameter: String,
pub current_value: String,
pub recommended_value: String,
pub rationale: String,
pub expected_impact: f64,
pub confidence: f64,
}
#[derive(Debug, Clone)]
pub struct AutoTunerConfig {
pub observation_period: Duration,
pub confidence_threshold: f64,
pub target_cache_hit_rate: f64,
pub target_bloom_fp_rate: f64,
pub aggressive: bool,
}
impl Default for AutoTunerConfig {
fn default() -> Self {
Self {
observation_period: Duration::from_secs(300), confidence_threshold: 0.7,
target_cache_hit_rate: 0.85,
target_bloom_fp_rate: 0.01,
aggressive: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TuningReport {
pub analysis: Option<String>,
pub recommendations: Vec<TuningRecommendation>,
pub score: u8,
pub summary: String,
}
pub struct AutoTuner {
config: AutoTunerConfig,
}
impl AutoTuner {
pub fn new(config: AutoTunerConfig) -> Self {
Self { config }
}
pub fn default_config() -> Self {
Self::new(AutoTunerConfig::default())
}
#[allow(clippy::unused_async)]
pub async fn analyze_and_tune<S: BlockStore>(
&self,
_store: &S,
analysis: &StorageAnalysis,
) -> Result<TuningReport> {
let mut recommendations = Vec::new();
let mut score = 100u8;
if let Some(cache_rec) = self.tune_cache(analysis) {
if cache_rec.confidence >= self.config.confidence_threshold {
score = score.saturating_sub(5);
recommendations.push(cache_rec);
}
}
if let Some(bloom_rec) = self.tune_bloom_filter(analysis) {
if bloom_rec.confidence >= self.config.confidence_threshold {
score = score.saturating_sub(5);
recommendations.push(bloom_rec);
}
}
if let Some(concurrency_rec) = self.tune_concurrency(analysis) {
if concurrency_rec.confidence >= self.config.confidence_threshold {
score = score.saturating_sub(5);
recommendations.push(concurrency_rec);
}
}
if let Some(compression_rec) = self.tune_compression(analysis) {
if compression_rec.confidence >= self.config.confidence_threshold {
score = score.saturating_sub(5);
recommendations.push(compression_rec);
}
}
if let Some(dedup_rec) = self.tune_deduplication(analysis) {
if dedup_rec.confidence >= self.config.confidence_threshold {
score = score.saturating_sub(5);
recommendations.push(dedup_rec);
}
}
if let Some(backend_rec) = self.tune_backend_selection(analysis) {
if backend_rec.confidence >= self.config.confidence_threshold {
score = score.saturating_sub(10);
recommendations.push(backend_rec);
}
}
let summary = self.generate_summary(&recommendations, &analysis.workload.workload_type);
Ok(TuningReport {
analysis: Some(format!("Workload: {:?}", analysis.workload.workload_type)),
recommendations,
score,
summary,
})
}
fn tune_cache(&self, analysis: &StorageAnalysis) -> Option<TuningRecommendation> {
let cache_hit_rate = analysis.diagnostics.health.success_rate * 0.7;
if cache_hit_rate < self.config.target_cache_hit_rate {
let current_size = "current"; let increase_factor = if self.config.aggressive { 2.0 } else { 1.5 };
let recommended_size = format!("{increase_factor}x current");
let confidence = if analysis.workload.read_write_ratio > 0.7 {
0.9 } else {
0.7
};
Some(TuningRecommendation {
parameter: "cache_size".to_string(),
current_value: current_size.to_string(),
recommended_value: recommended_size,
rationale: format!(
"Cache hit rate ({:.1}%) is below target ({:.1}%). \
Increasing cache size will improve read performance.",
cache_hit_rate * 100.0,
self.config.target_cache_hit_rate * 100.0
),
expected_impact: (self.config.target_cache_hit_rate - cache_hit_rate) * 50.0,
confidence,
})
} else {
None
}
}
fn tune_bloom_filter(&self, analysis: &StorageAnalysis) -> Option<TuningRecommendation> {
if analysis.workload.read_write_ratio > 0.7 {
Some(TuningRecommendation {
parameter: "bloom_filter_size".to_string(),
current_value: "current".to_string(),
recommended_value: "2x expected items".to_string(),
rationale: "Read-heavy workload benefits from larger bloom filter \
to reduce false positives and unnecessary disk lookups."
.to_string(),
expected_impact: 5.0,
confidence: 0.8,
})
} else {
None
}
}
fn tune_concurrency(&self, analysis: &StorageAnalysis) -> Option<TuningRecommendation> {
let avg_latency = analysis
.performance_breakdown
.values()
.map(|stats| stats.avg_latency_us)
.max()
.unwrap_or(0);
if avg_latency > 10_000 {
Some(TuningRecommendation {
parameter: "concurrency_limit".to_string(),
current_value: "unlimited".to_string(),
recommended_value: "8-16 concurrent operations".to_string(),
rationale: "High latency detected. Limiting concurrency \
can reduce contention and improve throughput."
.to_string(),
expected_impact: 15.0,
confidence: 0.75,
})
} else {
None
}
}
fn tune_compression(&self, analysis: &StorageAnalysis) -> Option<TuningRecommendation> {
let avg_block_size = analysis.workload.avg_block_size;
if avg_block_size > 16384 {
Some(TuningRecommendation {
parameter: "compression".to_string(),
current_value: "disabled".to_string(),
recommended_value: "Zstd level 3".to_string(),
rationale: format!(
"Average block size ({avg_block_size} bytes) is large enough to benefit \
from compression. Zstd level 3 provides good balance."
),
expected_impact: 30.0, confidence: 0.85,
})
} else {
None
}
}
fn tune_deduplication(&self, analysis: &StorageAnalysis) -> Option<TuningRecommendation> {
if matches!(analysis.workload.workload_type, WorkloadType::WriteHeavy) {
Some(TuningRecommendation {
parameter: "deduplication".to_string(),
current_value: "disabled".to_string(),
recommended_value: "enabled with 16KB chunks".to_string(),
rationale: "Write-heavy workload likely has redundant data. \
Deduplication can significantly reduce storage."
.to_string(),
expected_impact: 25.0, confidence: 0.7,
})
} else {
None
}
}
fn tune_backend_selection(&self, analysis: &StorageAnalysis) -> Option<TuningRecommendation> {
match analysis.workload.workload_type {
WorkloadType::WriteHeavy if analysis.backend == "Sled" => {
Some(TuningRecommendation {
parameter: "backend".to_string(),
current_value: "Sled".to_string(),
recommended_value: "ParityDB".to_string(),
rationale: "Write-heavy workload. ParityDB offers 2-5x better \
write performance with lower write amplification."
.to_string(),
expected_impact: 100.0, confidence: 0.9,
})
}
WorkloadType::ReadHeavy if analysis.backend == "ParityDB" => {
Some(TuningRecommendation {
parameter: "backend".to_string(),
current_value: "ParityDB".to_string(),
recommended_value: "Sled".to_string(),
rationale: "Read-heavy workload. Sled offers better read \
performance with B-tree indexing."
.to_string(),
expected_impact: 50.0, confidence: 0.85,
})
}
_ => None,
}
}
fn generate_summary(
&self,
recommendations: &[TuningRecommendation],
workload: &WorkloadType,
) -> String {
if recommendations.is_empty() {
return "Configuration is well-tuned for current workload. No changes recommended."
.to_string();
}
let high_impact: Vec<_> = recommendations
.iter()
.filter(|r| r.expected_impact > 20.0)
.collect();
if high_impact.is_empty() {
format!(
"Found {} minor optimization opportunities for {:?} workload.",
recommendations.len(),
workload
)
} else {
format!(
"Found {} optimization opportunities for {:?} workload, \
including {} high-impact changes. Implementing all recommendations \
could improve performance by up to {:.0}%.",
recommendations.len(),
workload,
high_impact.len(),
recommendations
.iter()
.map(|r| r.expected_impact)
.sum::<f64>()
)
}
}
pub fn apply_recommendations(&self, report: &TuningReport) -> HashMap<String, String> {
let mut config = HashMap::new();
for rec in &report.recommendations {
if rec.confidence >= self.config.confidence_threshold {
config.insert(rec.parameter.clone(), rec.recommended_value.clone());
}
}
config
}
pub fn quick_tune(&self, workload_type: WorkloadType) -> HashMap<String, String> {
let mut config = HashMap::new();
match workload_type {
WorkloadType::ReadHeavy => {
config.insert("cache_size".to_string(), "1GB".to_string());
config.insert("bloom_filter".to_string(), "large".to_string());
config.insert("backend".to_string(), "Sled".to_string());
config.insert("compression".to_string(), "disabled".to_string());
}
WorkloadType::WriteHeavy => {
config.insert("cache_size".to_string(), "256MB".to_string());
config.insert("backend".to_string(), "ParityDB".to_string());
config.insert("deduplication".to_string(), "enabled".to_string());
config.insert("batch_size".to_string(), "100".to_string());
}
WorkloadType::Balanced => {
config.insert("cache_size".to_string(), "512MB".to_string());
config.insert("bloom_filter".to_string(), "medium".to_string());
config.insert("backend".to_string(), "ParityDB".to_string());
}
WorkloadType::BatchOriented => {
config.insert("batch_size".to_string(), "1000".to_string());
config.insert("concurrency".to_string(), "16".to_string());
config.insert("backend".to_string(), "ParityDB".to_string());
}
WorkloadType::Mixed => {
config.insert("cache_size".to_string(), "512MB".to_string());
config.insert("backend".to_string(), "ParityDB".to_string());
}
}
config
}
}
pub struct TuningPresets;
impl TuningPresets {
pub fn conservative() -> AutoTunerConfig {
AutoTunerConfig {
observation_period: Duration::from_secs(600), confidence_threshold: 0.85,
target_cache_hit_rate: 0.80,
target_bloom_fp_rate: 0.01,
aggressive: false,
}
}
pub fn balanced() -> AutoTunerConfig {
AutoTunerConfig::default()
}
pub fn aggressive() -> AutoTunerConfig {
AutoTunerConfig {
observation_period: Duration::from_secs(300), confidence_threshold: 0.6,
target_cache_hit_rate: 0.90,
target_bloom_fp_rate: 0.005,
aggressive: true,
}
}
pub fn performance() -> AutoTunerConfig {
AutoTunerConfig {
observation_period: Duration::from_secs(300),
confidence_threshold: 0.7,
target_cache_hit_rate: 0.95,
target_bloom_fp_rate: 0.001,
aggressive: true,
}
}
pub fn cost_optimized() -> AutoTunerConfig {
AutoTunerConfig {
observation_period: Duration::from_secs(900), confidence_threshold: 0.9,
target_cache_hit_rate: 0.75,
target_bloom_fp_rate: 0.02,
aggressive: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analyzer::{SizeDistribution, StorageAnalysis, WorkloadCharacterization};
use crate::diagnostics::{DiagnosticsReport, HealthMetrics, PerformanceMetrics};
fn create_test_analysis(workload_type: WorkloadType) -> StorageAnalysis {
let diagnostics = DiagnosticsReport {
backend: "Sled".to_string(),
total_blocks: 10_000,
performance: PerformanceMetrics {
avg_write_latency: Duration::from_micros(1000),
avg_read_latency: Duration::from_micros(500),
avg_batch_write_latency: Duration::from_millis(50),
avg_batch_read_latency: Duration::from_millis(20),
write_throughput: 500.0,
read_throughput: 1000.0,
peak_memory_usage: 100_000_000,
},
health: HealthMetrics {
successful_ops: 9000,
failed_ops: 0,
success_rate: 1.0,
integrity_ok: true,
responsive: true,
},
recommendations: Vec::new(),
health_score: 85,
};
StorageAnalysis {
backend: "Sled".to_string(),
diagnostics,
performance_breakdown: HashMap::new(),
workload: WorkloadCharacterization {
read_write_ratio: 0.7,
avg_block_size: 32768,
size_distribution: SizeDistribution {
small_pct: 0.3,
medium_pct: 0.5,
large_pct: 0.2,
},
workload_type,
},
recommendations: Vec::new(),
grade: "B".to_string(),
}
}
#[tokio::test]
async fn test_auto_tuner_cache_recommendation() {
let tuner = AutoTuner::default_config();
let analysis = create_test_analysis(WorkloadType::ReadHeavy);
let cache_rec = tuner.tune_cache(&analysis);
assert!(cache_rec.is_some());
let rec = cache_rec.unwrap();
assert_eq!(rec.parameter, "cache_size");
assert!(rec.confidence > 0.0);
}
#[tokio::test]
async fn test_auto_tuner_backend_recommendation() {
let tuner = AutoTuner::default_config();
let mut analysis = create_test_analysis(WorkloadType::WriteHeavy);
analysis.backend = "Sled".to_string();
let backend_rec = tuner.tune_backend_selection(&analysis);
assert!(backend_rec.is_some());
let rec = backend_rec.unwrap();
assert_eq!(rec.parameter, "backend");
assert_eq!(rec.recommended_value, "ParityDB");
}
#[tokio::test]
async fn test_quick_tune() {
let tuner = AutoTuner::default_config();
let read_config = tuner.quick_tune(WorkloadType::ReadHeavy);
assert_eq!(read_config.get("backend"), Some(&"Sled".to_string()));
let write_config = tuner.quick_tune(WorkloadType::WriteHeavy);
assert_eq!(write_config.get("backend"), Some(&"ParityDB".to_string()));
}
#[test]
fn test_tuning_presets() {
let _conservative = TuningPresets::conservative();
let _balanced = TuningPresets::balanced();
let _aggressive = TuningPresets::aggressive();
let _performance = TuningPresets::performance();
let _cost = TuningPresets::cost_optimized();
}
}