ipfrs_storage/
auto_tuner.rs

1//! Automatic configuration tuning based on workload patterns
2//!
3//! This module provides automatic tuning of storage configuration parameters
4//! based on observed workload characteristics and performance metrics.
5
6use crate::analyzer::{StorageAnalysis, WorkloadType};
7use crate::traits::BlockStore;
8use ipfrs_core::Result;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::time::Duration;
12
13/// Tuning recommendation
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct TuningRecommendation {
16    /// Parameter name
17    pub parameter: String,
18    /// Current value
19    pub current_value: String,
20    /// Recommended value
21    pub recommended_value: String,
22    /// Rationale for the recommendation
23    pub rationale: String,
24    /// Expected impact (percentage improvement)
25    pub expected_impact: f64,
26    /// Confidence level (0.0 - 1.0)
27    pub confidence: f64,
28}
29
30/// Auto-tuning configuration
31#[derive(Debug, Clone)]
32pub struct AutoTunerConfig {
33    /// Minimum observation period before making recommendations
34    pub observation_period: Duration,
35    /// Minimum confidence threshold for recommendations (0.0 - 1.0)
36    pub confidence_threshold: f64,
37    /// Target cache hit rate (0.0 - 1.0)
38    pub target_cache_hit_rate: f64,
39    /// Target bloom filter false positive rate (0.0 - 1.0)
40    pub target_bloom_fp_rate: f64,
41    /// Enable aggressive tuning (may suggest larger changes)
42    pub aggressive: bool,
43}
44
45impl Default for AutoTunerConfig {
46    fn default() -> Self {
47        Self {
48            observation_period: Duration::from_secs(300), // 5 minutes
49            confidence_threshold: 0.7,
50            target_cache_hit_rate: 0.85,
51            target_bloom_fp_rate: 0.01,
52            aggressive: false,
53        }
54    }
55}
56
57/// Tuning report with recommendations
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct TuningReport {
60    /// Workload analysis
61    pub analysis: Option<String>,
62    /// List of recommendations
63    pub recommendations: Vec<TuningRecommendation>,
64    /// Overall tuning score (0-100)
65    pub score: u8,
66    /// Summary of findings
67    pub summary: String,
68}
69
70/// Automatic configuration tuner
71pub struct AutoTuner {
72    config: AutoTunerConfig,
73}
74
75impl AutoTuner {
76    /// Create a new auto-tuner with the given configuration
77    pub fn new(config: AutoTunerConfig) -> Self {
78        Self { config }
79    }
80
81    /// Create an auto-tuner with default configuration
82    pub fn default_config() -> Self {
83        Self::new(AutoTunerConfig::default())
84    }
85
86    /// Analyze storage and generate tuning recommendations
87    #[allow(clippy::unused_async)]
88    pub async fn analyze_and_tune<S: BlockStore>(
89        &self,
90        _store: &S,
91        analysis: &StorageAnalysis,
92    ) -> Result<TuningReport> {
93        let mut recommendations = Vec::new();
94        let mut score = 100u8;
95
96        // Analyze cache performance
97        if let Some(cache_rec) = self.tune_cache(analysis) {
98            if cache_rec.confidence >= self.config.confidence_threshold {
99                score = score.saturating_sub(5);
100                recommendations.push(cache_rec);
101            }
102        }
103
104        // Analyze bloom filter
105        if let Some(bloom_rec) = self.tune_bloom_filter(analysis) {
106            if bloom_rec.confidence >= self.config.confidence_threshold {
107                score = score.saturating_sub(5);
108                recommendations.push(bloom_rec);
109            }
110        }
111
112        // Analyze concurrency settings
113        if let Some(concurrency_rec) = self.tune_concurrency(analysis) {
114            if concurrency_rec.confidence >= self.config.confidence_threshold {
115                score = score.saturating_sub(5);
116                recommendations.push(concurrency_rec);
117            }
118        }
119
120        // Analyze compression settings
121        if let Some(compression_rec) = self.tune_compression(analysis) {
122            if compression_rec.confidence >= self.config.confidence_threshold {
123                score = score.saturating_sub(5);
124                recommendations.push(compression_rec);
125            }
126        }
127
128        // Analyze deduplication settings
129        if let Some(dedup_rec) = self.tune_deduplication(analysis) {
130            if dedup_rec.confidence >= self.config.confidence_threshold {
131                score = score.saturating_sub(5);
132                recommendations.push(dedup_rec);
133            }
134        }
135
136        // Analyze backend selection
137        if let Some(backend_rec) = self.tune_backend_selection(analysis) {
138            if backend_rec.confidence >= self.config.confidence_threshold {
139                score = score.saturating_sub(10);
140                recommendations.push(backend_rec);
141            }
142        }
143
144        let summary = self.generate_summary(&recommendations, &analysis.workload.workload_type);
145
146        Ok(TuningReport {
147            analysis: Some(format!("Workload: {:?}", analysis.workload.workload_type)),
148            recommendations,
149            score,
150            summary,
151        })
152    }
153
154    /// Tune cache size based on workload
155    fn tune_cache(&self, analysis: &StorageAnalysis) -> Option<TuningRecommendation> {
156        // Use success rate as a proxy for cache effectiveness
157        // In a real implementation, this would come from cache statistics
158        let cache_hit_rate = analysis.diagnostics.health.success_rate * 0.7; // Approximation
159
160        if cache_hit_rate < self.config.target_cache_hit_rate {
161            let current_size = "current"; // Would be extracted from actual config
162            let increase_factor = if self.config.aggressive { 2.0 } else { 1.5 };
163            let recommended_size = format!("{increase_factor}x current");
164
165            let confidence = if analysis.workload.read_write_ratio > 0.7 {
166                0.9 // High confidence for read-heavy workloads
167            } else {
168                0.7
169            };
170
171            Some(TuningRecommendation {
172                parameter: "cache_size".to_string(),
173                current_value: current_size.to_string(),
174                recommended_value: recommended_size,
175                rationale: format!(
176                    "Cache hit rate ({:.1}%) is below target ({:.1}%). \
177                     Increasing cache size will improve read performance.",
178                    cache_hit_rate * 100.0,
179                    self.config.target_cache_hit_rate * 100.0
180                ),
181                expected_impact: (self.config.target_cache_hit_rate - cache_hit_rate) * 50.0,
182                confidence,
183            })
184        } else {
185            None
186        }
187    }
188
189    /// Tune bloom filter parameters
190    fn tune_bloom_filter(&self, analysis: &StorageAnalysis) -> Option<TuningRecommendation> {
191        // For read-heavy workloads, a larger bloom filter helps
192        if analysis.workload.read_write_ratio > 0.7 {
193            Some(TuningRecommendation {
194                parameter: "bloom_filter_size".to_string(),
195                current_value: "current".to_string(),
196                recommended_value: "2x expected items".to_string(),
197                rationale: "Read-heavy workload benefits from larger bloom filter \
198                           to reduce false positives and unnecessary disk lookups."
199                    .to_string(),
200                expected_impact: 5.0,
201                confidence: 0.8,
202            })
203        } else {
204            None
205        }
206    }
207
208    /// Tune concurrency settings
209    fn tune_concurrency(&self, analysis: &StorageAnalysis) -> Option<TuningRecommendation> {
210        // Check if there are latency issues
211        let avg_latency = analysis
212            .performance_breakdown
213            .values()
214            .map(|stats| stats.avg_latency_us)
215            .max()
216            .unwrap_or(0);
217
218        if avg_latency > 10_000 {
219            // > 10ms
220            Some(TuningRecommendation {
221                parameter: "concurrency_limit".to_string(),
222                current_value: "unlimited".to_string(),
223                recommended_value: "8-16 concurrent operations".to_string(),
224                rationale: "High latency detected. Limiting concurrency \
225                           can reduce contention and improve throughput."
226                    .to_string(),
227                expected_impact: 15.0,
228                confidence: 0.75,
229            })
230        } else {
231            None
232        }
233    }
234
235    /// Tune compression settings
236    fn tune_compression(&self, analysis: &StorageAnalysis) -> Option<TuningRecommendation> {
237        let avg_block_size = analysis.workload.avg_block_size;
238
239        // Recommend compression for larger blocks
240        if avg_block_size > 16384 {
241            // > 16KB
242            Some(TuningRecommendation {
243                parameter: "compression".to_string(),
244                current_value: "disabled".to_string(),
245                recommended_value: "Zstd level 3".to_string(),
246                rationale: format!(
247                    "Average block size ({avg_block_size} bytes) is large enough to benefit \
248                     from compression. Zstd level 3 provides good balance."
249                ),
250                expected_impact: 30.0, // 30% storage reduction
251                confidence: 0.85,
252            })
253        } else {
254            None
255        }
256    }
257
258    /// Tune deduplication settings
259    fn tune_deduplication(&self, analysis: &StorageAnalysis) -> Option<TuningRecommendation> {
260        // For write-heavy workloads with redundancy, recommend dedup
261        if matches!(analysis.workload.workload_type, WorkloadType::WriteHeavy) {
262            Some(TuningRecommendation {
263                parameter: "deduplication".to_string(),
264                current_value: "disabled".to_string(),
265                recommended_value: "enabled with 16KB chunks".to_string(),
266                rationale: "Write-heavy workload likely has redundant data. \
267                           Deduplication can significantly reduce storage."
268                    .to_string(),
269                expected_impact: 25.0, // 25% storage reduction
270                confidence: 0.7,
271            })
272        } else {
273            None
274        }
275    }
276
277    /// Tune backend selection
278    fn tune_backend_selection(&self, analysis: &StorageAnalysis) -> Option<TuningRecommendation> {
279        match analysis.workload.workload_type {
280            WorkloadType::WriteHeavy if analysis.backend == "Sled" => {
281                Some(TuningRecommendation {
282                    parameter: "backend".to_string(),
283                    current_value: "Sled".to_string(),
284                    recommended_value: "ParityDB".to_string(),
285                    rationale: "Write-heavy workload. ParityDB offers 2-5x better \
286                               write performance with lower write amplification."
287                        .to_string(),
288                    expected_impact: 100.0, // 2x throughput
289                    confidence: 0.9,
290                })
291            }
292            WorkloadType::ReadHeavy if analysis.backend == "ParityDB" => {
293                Some(TuningRecommendation {
294                    parameter: "backend".to_string(),
295                    current_value: "ParityDB".to_string(),
296                    recommended_value: "Sled".to_string(),
297                    rationale: "Read-heavy workload. Sled offers better read \
298                               performance with B-tree indexing."
299                        .to_string(),
300                    expected_impact: 50.0, // 1.5x read throughput
301                    confidence: 0.85,
302                })
303            }
304            _ => None,
305        }
306    }
307
308    /// Generate summary text
309    fn generate_summary(
310        &self,
311        recommendations: &[TuningRecommendation],
312        workload: &WorkloadType,
313    ) -> String {
314        if recommendations.is_empty() {
315            return "Configuration is well-tuned for current workload. No changes recommended."
316                .to_string();
317        }
318
319        let high_impact: Vec<_> = recommendations
320            .iter()
321            .filter(|r| r.expected_impact > 20.0)
322            .collect();
323
324        if high_impact.is_empty() {
325            format!(
326                "Found {} minor optimization opportunities for {:?} workload.",
327                recommendations.len(),
328                workload
329            )
330        } else {
331            format!(
332                "Found {} optimization opportunities for {:?} workload, \
333                 including {} high-impact changes. Implementing all recommendations \
334                 could improve performance by up to {:.0}%.",
335                recommendations.len(),
336                workload,
337                high_impact.len(),
338                recommendations
339                    .iter()
340                    .map(|r| r.expected_impact)
341                    .sum::<f64>()
342            )
343        }
344    }
345
346    /// Apply automatic tuning (returns recommended configuration as key-value pairs)
347    pub fn apply_recommendations(&self, report: &TuningReport) -> HashMap<String, String> {
348        let mut config = HashMap::new();
349
350        for rec in &report.recommendations {
351            if rec.confidence >= self.config.confidence_threshold {
352                config.insert(rec.parameter.clone(), rec.recommended_value.clone());
353            }
354        }
355
356        config
357    }
358
359    /// Quick tune based on workload type (doesn't require analysis)
360    pub fn quick_tune(&self, workload_type: WorkloadType) -> HashMap<String, String> {
361        let mut config = HashMap::new();
362
363        match workload_type {
364            WorkloadType::ReadHeavy => {
365                config.insert("cache_size".to_string(), "1GB".to_string());
366                config.insert("bloom_filter".to_string(), "large".to_string());
367                config.insert("backend".to_string(), "Sled".to_string());
368                config.insert("compression".to_string(), "disabled".to_string());
369            }
370            WorkloadType::WriteHeavy => {
371                config.insert("cache_size".to_string(), "256MB".to_string());
372                config.insert("backend".to_string(), "ParityDB".to_string());
373                config.insert("deduplication".to_string(), "enabled".to_string());
374                config.insert("batch_size".to_string(), "100".to_string());
375            }
376            WorkloadType::Balanced => {
377                config.insert("cache_size".to_string(), "512MB".to_string());
378                config.insert("bloom_filter".to_string(), "medium".to_string());
379                config.insert("backend".to_string(), "ParityDB".to_string());
380            }
381            WorkloadType::BatchOriented => {
382                config.insert("batch_size".to_string(), "1000".to_string());
383                config.insert("concurrency".to_string(), "16".to_string());
384                config.insert("backend".to_string(), "ParityDB".to_string());
385            }
386            WorkloadType::Mixed => {
387                config.insert("cache_size".to_string(), "512MB".to_string());
388                config.insert("backend".to_string(), "ParityDB".to_string());
389            }
390        }
391
392        config
393    }
394}
395
396/// Tuning presets for common scenarios
397pub struct TuningPresets;
398
399impl TuningPresets {
400    /// Conservative tuning (minimal changes, high confidence)
401    pub fn conservative() -> AutoTunerConfig {
402        AutoTunerConfig {
403            observation_period: Duration::from_secs(600), // 10 minutes
404            confidence_threshold: 0.85,
405            target_cache_hit_rate: 0.80,
406            target_bloom_fp_rate: 0.01,
407            aggressive: false,
408        }
409    }
410
411    /// Balanced tuning (moderate changes, good confidence)
412    pub fn balanced() -> AutoTunerConfig {
413        AutoTunerConfig::default()
414    }
415
416    /// Aggressive tuning (larger changes, lower confidence threshold)
417    pub fn aggressive() -> AutoTunerConfig {
418        AutoTunerConfig {
419            observation_period: Duration::from_secs(300), // 5 minutes
420            confidence_threshold: 0.6,
421            target_cache_hit_rate: 0.90,
422            target_bloom_fp_rate: 0.005,
423            aggressive: true,
424        }
425    }
426
427    /// Performance-focused tuning (maximize throughput)
428    pub fn performance() -> AutoTunerConfig {
429        AutoTunerConfig {
430            observation_period: Duration::from_secs(300),
431            confidence_threshold: 0.7,
432            target_cache_hit_rate: 0.95,
433            target_bloom_fp_rate: 0.001,
434            aggressive: true,
435        }
436    }
437
438    /// Cost-focused tuning (minimize resource usage)
439    pub fn cost_optimized() -> AutoTunerConfig {
440        AutoTunerConfig {
441            observation_period: Duration::from_secs(900), // 15 minutes
442            confidence_threshold: 0.9,
443            target_cache_hit_rate: 0.75,
444            target_bloom_fp_rate: 0.02,
445            aggressive: false,
446        }
447    }
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453    use crate::analyzer::{SizeDistribution, StorageAnalysis, WorkloadCharacterization};
454    use crate::diagnostics::{DiagnosticsReport, HealthMetrics, PerformanceMetrics};
455
456    fn create_test_analysis(workload_type: WorkloadType) -> StorageAnalysis {
457        let diagnostics = DiagnosticsReport {
458            backend: "Sled".to_string(),
459            total_blocks: 10_000,
460            performance: PerformanceMetrics {
461                avg_write_latency: Duration::from_micros(1000),
462                avg_read_latency: Duration::from_micros(500),
463                avg_batch_write_latency: Duration::from_millis(50),
464                avg_batch_read_latency: Duration::from_millis(20),
465                write_throughput: 500.0,
466                read_throughput: 1000.0,
467                peak_memory_usage: 100_000_000,
468            },
469            health: HealthMetrics {
470                successful_ops: 9000,
471                failed_ops: 0,
472                success_rate: 1.0,
473                integrity_ok: true,
474                responsive: true,
475            },
476            recommendations: Vec::new(),
477            health_score: 85,
478        };
479
480        StorageAnalysis {
481            backend: "Sled".to_string(),
482            diagnostics,
483            performance_breakdown: HashMap::new(),
484            workload: WorkloadCharacterization {
485                read_write_ratio: 0.7,
486                avg_block_size: 32768,
487                size_distribution: SizeDistribution {
488                    small_pct: 0.3,
489                    medium_pct: 0.5,
490                    large_pct: 0.2,
491                },
492                workload_type,
493            },
494            recommendations: Vec::new(),
495            grade: "B".to_string(),
496        }
497    }
498
499    #[tokio::test]
500    async fn test_auto_tuner_cache_recommendation() {
501        let tuner = AutoTuner::default_config();
502        let analysis = create_test_analysis(WorkloadType::ReadHeavy);
503
504        // Should recommend cache increase for low hit rate
505        let cache_rec = tuner.tune_cache(&analysis);
506        assert!(cache_rec.is_some());
507
508        let rec = cache_rec.unwrap();
509        assert_eq!(rec.parameter, "cache_size");
510        assert!(rec.confidence > 0.0);
511    }
512
513    #[tokio::test]
514    async fn test_auto_tuner_backend_recommendation() {
515        let tuner = AutoTuner::default_config();
516        let mut analysis = create_test_analysis(WorkloadType::WriteHeavy);
517        analysis.backend = "Sled".to_string();
518
519        let backend_rec = tuner.tune_backend_selection(&analysis);
520        assert!(backend_rec.is_some());
521
522        let rec = backend_rec.unwrap();
523        assert_eq!(rec.parameter, "backend");
524        assert_eq!(rec.recommended_value, "ParityDB");
525    }
526
527    #[tokio::test]
528    async fn test_quick_tune() {
529        let tuner = AutoTuner::default_config();
530
531        let read_config = tuner.quick_tune(WorkloadType::ReadHeavy);
532        assert_eq!(read_config.get("backend"), Some(&"Sled".to_string()));
533
534        let write_config = tuner.quick_tune(WorkloadType::WriteHeavy);
535        assert_eq!(write_config.get("backend"), Some(&"ParityDB".to_string()));
536    }
537
538    #[test]
539    fn test_tuning_presets() {
540        let _conservative = TuningPresets::conservative();
541        let _balanced = TuningPresets::balanced();
542        let _aggressive = TuningPresets::aggressive();
543        let _performance = TuningPresets::performance();
544        let _cost = TuningPresets::cost_optimized();
545    }
546}