Skip to main content

oxirs_tdb/storage/
buffer_pool_tuner.rs

1//! # Buffer Pool Tuner
2//!
3//! Provides adaptive tuning of buffer pool parameters based on access patterns
4//! and performance metrics. Uses scirs2-core metrics API for production-grade monitoring.
5
6use crate::error::TdbError;
7use crate::storage::{BufferPool, BufferPoolStats};
8use scirs2_core::metrics::{Counter, Gauge, Histogram, Timer};
9use std::collections::HashMap;
10use std::sync::Arc;
11use std::time::{Duration, Instant};
12
13/// Configuration for buffer pool tuner
14#[derive(Debug, Clone)]
15pub struct BufferPoolTunerConfig {
16    /// Minimum buffer pool size
17    pub min_size: usize,
18    /// Maximum buffer pool size
19    pub max_size: usize,
20    /// Target hit rate (0.0 - 1.0)
21    pub target_hit_rate: f64,
22    /// Tuning interval in seconds
23    pub tuning_interval_secs: u64,
24    /// Adjustment step size (percentage of current size)
25    pub adjustment_step: f64,
26    /// Enable aggressive tuning
27    pub aggressive_tuning: bool,
28}
29
30impl Default for BufferPoolTunerConfig {
31    fn default() -> Self {
32        Self {
33            min_size: 64,         // 64 pages minimum (256 KB)
34            max_size: 65536,      // 65536 pages maximum (256 MB)
35            target_hit_rate: 0.9, // 90% target hit rate
36            tuning_interval_secs: 60,
37            adjustment_step: 0.1, // 10% adjustment steps
38            aggressive_tuning: false,
39        }
40    }
41}
42
43/// Access pattern classification
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum AccessPattern {
46    /// Sequential access pattern (high locality)
47    Sequential,
48    /// Random access pattern (low locality)
49    Random,
50    /// Mixed access pattern
51    Mixed,
52    /// Scan-heavy workload
53    ScanHeavy,
54    /// Unknown pattern
55    Unknown,
56}
57
58/// Eviction policy for buffer pool
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
60pub enum EvictionPolicy {
61    /// Least Recently Used
62    #[default]
63    LRU,
64    /// Least Frequently Used
65    LFU,
66    /// Adaptive Replacement Cache
67    ARC,
68    /// Clock/Second Chance
69    Clock,
70}
71
72/// Tuning recommendation
73#[derive(Debug, Clone)]
74pub struct TuningRecommendation {
75    /// Recommended buffer pool size
76    pub recommended_size: usize,
77    /// Recommended eviction policy
78    pub recommended_policy: EvictionPolicy,
79    /// Confidence level (0.0 - 1.0)
80    pub confidence: f64,
81    /// Reasoning behind the recommendation
82    pub reason: String,
83}
84
85/// Performance report from tuner
86#[derive(Debug, Clone)]
87pub struct PerformanceReport {
88    /// Current buffer pool size
89    pub current_size: usize,
90    /// Current hit rate
91    pub hit_rate: f64,
92    /// Average access latency
93    pub avg_latency_us: f64,
94    /// Detected access pattern
95    pub access_pattern: AccessPattern,
96    /// Number of tuning iterations
97    pub tuning_iterations: u64,
98    /// Total bytes saved through optimization
99    pub bytes_saved: u64,
100}
101
102/// Buffer pool tuner with adaptive optimization
103pub struct BufferPoolTuner {
104    /// Configuration
105    config: BufferPoolTunerConfig,
106    /// Last tuning timestamp
107    last_tuning: Instant,
108    /// Access pattern history
109    access_history: Vec<AccessPattern>,
110    /// Tuning iterations counter
111    tuning_iterations: u64,
112
113    // Metrics (scirs2-core v0.1.0+ API)
114    /// Hit rate gauge
115    hit_rate_gauge: Gauge,
116    /// Miss rate gauge
117    miss_rate_gauge: Gauge,
118    /// Pool size gauge
119    pool_size_gauge: Gauge,
120    /// Eviction counter
121    eviction_counter: Counter,
122    /// Access counter
123    access_counter: Counter,
124    /// Access latency histogram
125    access_latency_histogram: Histogram,
126    /// Tuning adjustments counter
127    tuning_adjustments: Counter,
128}
129
130impl BufferPoolTuner {
131    /// Create a new buffer pool tuner
132    pub fn new(config: BufferPoolTunerConfig) -> Self {
133        Self {
134            config,
135            last_tuning: Instant::now(),
136            access_history: Vec::new(),
137            tuning_iterations: 0,
138
139            // Initialize metrics
140            hit_rate_gauge: Gauge::new("buffer_pool_hit_rate".to_string()),
141            miss_rate_gauge: Gauge::new("buffer_pool_miss_rate".to_string()),
142            pool_size_gauge: Gauge::new("buffer_pool_size".to_string()),
143            eviction_counter: Counter::new("buffer_pool_evictions".to_string()),
144            access_counter: Counter::new("buffer_pool_accesses".to_string()),
145            access_latency_histogram: Histogram::new("buffer_pool_access_latency".to_string()),
146            tuning_adjustments: Counter::new("buffer_pool_tuning_adjustments".to_string()),
147        }
148    }
149
150    /// Update metrics from buffer pool stats
151    pub fn update_metrics(
152        &mut self,
153        stats: &BufferPoolStats,
154        pool_size: usize,
155        cached_pages: usize,
156    ) {
157        use std::sync::atomic::Ordering;
158
159        let total_fetches = stats.total_fetches.load(Ordering::Relaxed);
160        let cache_hits = stats.cache_hits.load(Ordering::Relaxed);
161        let cache_misses = stats.cache_misses.load(Ordering::Relaxed);
162        let evictions = stats.evictions.load(Ordering::Relaxed);
163
164        if total_fetches > 0 {
165            let hit_rate = cache_hits as f64 / total_fetches as f64;
166            let miss_rate = cache_misses as f64 / total_fetches as f64;
167
168            self.hit_rate_gauge.set(hit_rate);
169            self.miss_rate_gauge.set(miss_rate);
170        }
171
172        self.pool_size_gauge.set(pool_size as f64);
173        self.eviction_counter.add(evictions);
174        self.access_counter.add(total_fetches);
175    }
176
177    /// Record an access latency
178    pub fn record_access_latency(&self, latency: Duration) {
179        self.access_latency_histogram
180            .observe(latency.as_secs_f64() * 1_000_000.0); // Convert to microseconds
181    }
182
183    /// Detect access pattern from buffer pool statistics
184    pub fn detect_access_pattern(&mut self, stats: &BufferPoolStats) -> AccessPattern {
185        use std::sync::atomic::Ordering;
186
187        let total_fetches = stats.total_fetches.load(Ordering::Relaxed);
188        let cache_hits = stats.cache_hits.load(Ordering::Relaxed);
189        let evictions = stats.evictions.load(Ordering::Relaxed);
190
191        if total_fetches == 0 {
192            return AccessPattern::Unknown;
193        }
194
195        let hit_rate = cache_hits as f64 / total_fetches as f64;
196
197        // Simple heuristic-based pattern detection
198        let pattern = if hit_rate > 0.95 {
199            AccessPattern::Sequential // Very high hit rate suggests sequential access
200        } else if hit_rate < 0.5 {
201            AccessPattern::Random // Low hit rate suggests random access
202        } else if evictions > total_fetches / 2 {
203            AccessPattern::ScanHeavy // Many evictions suggest scan operations
204        } else {
205            AccessPattern::Mixed
206        };
207
208        // Update access history (keep last 10 patterns)
209        self.access_history.push(pattern);
210        if self.access_history.len() > 10 {
211            self.access_history.remove(0);
212        }
213
214        pattern
215    }
216
217    /// Generate tuning recommendation
218    pub fn recommend_tuning(
219        &mut self,
220        stats: &BufferPoolStats,
221        current_pool_size: usize,
222    ) -> Option<TuningRecommendation> {
223        use std::sync::atomic::Ordering;
224
225        let total_fetches = stats.total_fetches.load(Ordering::Relaxed);
226        let cache_hits = stats.cache_hits.load(Ordering::Relaxed);
227
228        if total_fetches == 0 {
229            return None;
230        }
231
232        let current_hit_rate = cache_hits as f64 / total_fetches as f64;
233        let pattern = self.detect_access_pattern(stats);
234
235        // Determine if tuning is needed
236        if current_hit_rate >= self.config.target_hit_rate {
237            // Hit rate is good, no tuning needed
238            return None;
239        }
240
241        // Calculate recommended size adjustment
242        let hit_rate_deficit = self.config.target_hit_rate - current_hit_rate;
243        let adjustment_multiplier = if self.config.aggressive_tuning {
244            1.5
245        } else {
246            1.0
247        };
248
249        let size_increase_factor =
250            1.0 + (hit_rate_deficit * self.config.adjustment_step * adjustment_multiplier).min(0.5);
251
252        let recommended_size = ((current_pool_size as f64 * size_increase_factor) as usize)
253            .clamp(self.config.min_size, self.config.max_size);
254
255        // Recommend eviction policy based on access pattern
256        let recommended_policy = match pattern {
257            AccessPattern::Sequential => EvictionPolicy::LRU,
258            AccessPattern::Random => EvictionPolicy::ARC,
259            AccessPattern::ScanHeavy => EvictionPolicy::Clock,
260            AccessPattern::Mixed => EvictionPolicy::LFU,
261            AccessPattern::Unknown => EvictionPolicy::LRU,
262        };
263
264        let confidence = if self.access_history.len() >= 5 {
265            0.8
266        } else {
267            0.5
268        };
269
270        Some(TuningRecommendation {
271            recommended_size,
272            recommended_policy,
273            confidence,
274            reason: format!(
275                "Hit rate {:.2}% is below target {:.2}%. Detected {:?} access pattern.",
276                current_hit_rate * 100.0,
277                self.config.target_hit_rate * 100.0,
278                pattern
279            ),
280        })
281    }
282
283    /// Apply tuning recommendation (returns true if applied)
284    pub fn apply_tuning(&mut self, recommendation: &TuningRecommendation) -> bool {
285        // Check if enough time has elapsed since last tuning
286        let elapsed = self.last_tuning.elapsed();
287        if elapsed < Duration::from_secs(self.config.tuning_interval_secs) {
288            return false;
289        }
290
291        // Only apply high-confidence recommendations
292        if recommendation.confidence < 0.6 {
293            return false;
294        }
295
296        self.last_tuning = Instant::now();
297        self.tuning_iterations += 1;
298        self.tuning_adjustments.inc();
299
300        true
301    }
302
303    /// Generate performance report
304    pub fn generate_report(
305        &self,
306        stats: &BufferPoolStats,
307        current_pool_size: usize,
308    ) -> PerformanceReport {
309        use std::sync::atomic::Ordering;
310
311        let total_fetches = stats.total_fetches.load(Ordering::Relaxed);
312        let cache_hits = stats.cache_hits.load(Ordering::Relaxed);
313
314        let hit_rate = if total_fetches > 0 {
315            cache_hits as f64 / total_fetches as f64
316        } else {
317            0.0
318        };
319
320        let latency_stats = self.access_latency_histogram.get_stats();
321        let avg_latency_us = latency_stats.mean;
322
323        let access_pattern = if let Some(&last_pattern) = self.access_history.last() {
324            last_pattern
325        } else {
326            AccessPattern::Unknown
327        };
328
329        PerformanceReport {
330            current_size: current_pool_size,
331            hit_rate,
332            avg_latency_us,
333            access_pattern,
334            tuning_iterations: self.tuning_iterations,
335            bytes_saved: 0, // Could be calculated based on tuning history
336        }
337    }
338
339    /// Get current hit rate from metrics
340    pub fn get_hit_rate(&self) -> f64 {
341        self.hit_rate_gauge.get()
342    }
343
344    /// Get total accesses from metrics
345    pub fn get_total_accesses(&self) -> u64 {
346        self.access_counter.get()
347    }
348
349    /// Get total evictions from metrics
350    pub fn get_total_evictions(&self) -> u64 {
351        self.eviction_counter.get()
352    }
353
354    /// Get tuning iterations count
355    pub fn get_tuning_iterations(&self) -> u64 {
356        self.tuning_iterations
357    }
358
359    /// Reset metrics
360    pub fn reset_metrics(&mut self) {
361        // Note: scirs2-core metrics don't have a reset method, so we recreate them
362        self.hit_rate_gauge = Gauge::new("buffer_pool_hit_rate".to_string());
363        self.miss_rate_gauge = Gauge::new("buffer_pool_miss_rate".to_string());
364        self.pool_size_gauge = Gauge::new("buffer_pool_size".to_string());
365        self.eviction_counter = Counter::new("buffer_pool_evictions".to_string());
366        self.access_counter = Counter::new("buffer_pool_accesses".to_string());
367        self.access_latency_histogram = Histogram::new("buffer_pool_access_latency".to_string());
368        self.tuning_adjustments = Counter::new("buffer_pool_tuning_adjustments".to_string());
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    #[test]
377    fn test_buffer_pool_tuner_creation() {
378        let config = BufferPoolTunerConfig::default();
379        let tuner = BufferPoolTuner::new(config.clone());
380
381        assert_eq!(tuner.get_tuning_iterations(), 0);
382        assert_eq!(tuner.get_total_accesses(), 0);
383        assert_eq!(tuner.get_total_evictions(), 0);
384    }
385
386    #[test]
387    fn test_update_metrics() {
388        use std::sync::atomic::{AtomicU64, Ordering};
389
390        let config = BufferPoolTunerConfig::default();
391        let mut tuner = BufferPoolTuner::new(config);
392
393        let stats = BufferPoolStats {
394            total_fetches: AtomicU64::new(1000),
395            cache_hits: AtomicU64::new(900),
396            cache_misses: AtomicU64::new(100),
397            evictions: AtomicU64::new(50),
398            writes: AtomicU64::new(10),
399        };
400
401        tuner.update_metrics(&stats, 1024, 500);
402
403        let hit_rate = tuner.get_hit_rate();
404        assert!((hit_rate - 0.9).abs() < 0.01);
405    }
406
407    #[test]
408    fn test_access_pattern_detection() {
409        use std::sync::atomic::AtomicU64;
410
411        let config = BufferPoolTunerConfig::default();
412        let mut tuner = BufferPoolTuner::new(config);
413
414        // High hit rate -> Sequential
415        let sequential_stats = BufferPoolStats {
416            total_fetches: AtomicU64::new(1000),
417            cache_hits: AtomicU64::new(980),
418            cache_misses: AtomicU64::new(20),
419            evictions: AtomicU64::new(5),
420            writes: AtomicU64::new(10),
421        };
422        let pattern = tuner.detect_access_pattern(&sequential_stats);
423        assert_eq!(pattern, AccessPattern::Sequential);
424
425        // Low hit rate -> Random
426        let random_stats = BufferPoolStats {
427            total_fetches: AtomicU64::new(1000),
428            cache_hits: AtomicU64::new(300),
429            cache_misses: AtomicU64::new(700),
430            evictions: AtomicU64::new(200),
431            writes: AtomicU64::new(50),
432        };
433        let pattern = tuner.detect_access_pattern(&random_stats);
434        assert_eq!(pattern, AccessPattern::Random);
435    }
436
437    #[test]
438    fn test_tuning_recommendation() {
439        use std::sync::atomic::AtomicU64;
440
441        let config = BufferPoolTunerConfig {
442            target_hit_rate: 0.9,
443            ..Default::default()
444        };
445        let mut tuner = BufferPoolTuner::new(config);
446
447        // Low hit rate should trigger recommendation
448        let stats = BufferPoolStats {
449            total_fetches: AtomicU64::new(1000),
450            cache_hits: AtomicU64::new(600),
451            cache_misses: AtomicU64::new(400),
452            evictions: AtomicU64::new(100),
453            writes: AtomicU64::new(50),
454        };
455
456        let current_pool_size = 1024;
457        let recommendation = tuner.recommend_tuning(&stats, current_pool_size);
458        assert!(recommendation.is_some());
459
460        let rec = recommendation.unwrap();
461        assert!(rec.recommended_size > current_pool_size);
462        assert!(rec.confidence > 0.0);
463    }
464
465    #[test]
466    fn test_no_tuning_when_hit_rate_good() {
467        use std::sync::atomic::AtomicU64;
468
469        let config = BufferPoolTunerConfig {
470            target_hit_rate: 0.9,
471            ..Default::default()
472        };
473        let mut tuner = BufferPoolTuner::new(config);
474
475        // Good hit rate should not trigger recommendation
476        let stats = BufferPoolStats {
477            total_fetches: AtomicU64::new(1000),
478            cache_hits: AtomicU64::new(950),
479            cache_misses: AtomicU64::new(50),
480            evictions: AtomicU64::new(10),
481            writes: AtomicU64::new(5),
482        };
483
484        let recommendation = tuner.recommend_tuning(&stats, 1024);
485        assert!(recommendation.is_none());
486    }
487
488    #[test]
489    fn test_performance_report() {
490        use std::sync::atomic::AtomicU64;
491
492        let config = BufferPoolTunerConfig::default();
493        let mut tuner = BufferPoolTuner::new(config);
494
495        let stats = BufferPoolStats {
496            total_fetches: AtomicU64::new(1000),
497            cache_hits: AtomicU64::new(800),
498            cache_misses: AtomicU64::new(200),
499            evictions: AtomicU64::new(50),
500            writes: AtomicU64::new(25),
501        };
502
503        tuner.update_metrics(&stats, 1024, 500);
504        let report = tuner.generate_report(&stats, 1024);
505
506        assert_eq!(report.current_size, 1024);
507        assert!((report.hit_rate - 0.8).abs() < 0.01);
508    }
509
510    #[test]
511    fn test_access_latency_recording() {
512        let config = BufferPoolTunerConfig::default();
513        let tuner = BufferPoolTuner::new(config);
514
515        tuner.record_access_latency(Duration::from_micros(100));
516        tuner.record_access_latency(Duration::from_micros(200));
517        tuner.record_access_latency(Duration::from_micros(150));
518
519        let stats = tuner.access_latency_histogram.get_stats();
520        assert_eq!(stats.count, 3);
521        assert!(stats.mean > 0.0);
522    }
523
524    #[test]
525    fn test_aggressive_tuning() {
526        use std::sync::atomic::AtomicU64;
527
528        let config = BufferPoolTunerConfig {
529            target_hit_rate: 0.9,
530            aggressive_tuning: true,
531            ..Default::default()
532        };
533        let mut tuner_aggressive = BufferPoolTuner::new(config.clone());
534
535        let config_normal = BufferPoolTunerConfig {
536            target_hit_rate: 0.9,
537            aggressive_tuning: false,
538            ..Default::default()
539        };
540        let mut tuner_normal = BufferPoolTuner::new(config_normal);
541
542        let stats = BufferPoolStats {
543            total_fetches: AtomicU64::new(1000),
544            cache_hits: AtomicU64::new(600),
545            cache_misses: AtomicU64::new(400),
546            evictions: AtomicU64::new(100),
547            writes: AtomicU64::new(50),
548        };
549
550        let current_pool_size = 1000;
551        let rec_aggressive = tuner_aggressive
552            .recommend_tuning(&stats, current_pool_size)
553            .unwrap();
554        let rec_normal = tuner_normal
555            .recommend_tuning(&stats, current_pool_size)
556            .unwrap();
557
558        // Aggressive should recommend larger increase
559        assert!(rec_aggressive.recommended_size >= rec_normal.recommended_size);
560    }
561
562    #[test]
563    fn test_eviction_policy_recommendation() {
564        use std::sync::atomic::AtomicU64;
565
566        let config = BufferPoolTunerConfig::default();
567        let mut tuner = BufferPoolTuner::new(config);
568
569        // Sequential pattern should recommend LRU
570        let sequential_stats = BufferPoolStats {
571            total_fetches: AtomicU64::new(1000),
572            cache_hits: AtomicU64::new(980),
573            cache_misses: AtomicU64::new(20),
574            evictions: AtomicU64::new(5),
575            writes: AtomicU64::new(3),
576        };
577        tuner.detect_access_pattern(&sequential_stats);
578        if let Some(rec) = tuner.recommend_tuning(&sequential_stats, 500) {
579            assert_eq!(rec.recommended_policy, EvictionPolicy::LRU);
580        }
581
582        // Random pattern should recommend ARC
583        let random_stats = BufferPoolStats {
584            total_fetches: AtomicU64::new(1000),
585            cache_hits: AtomicU64::new(300),
586            cache_misses: AtomicU64::new(700),
587            evictions: AtomicU64::new(200),
588            writes: AtomicU64::new(100),
589        };
590        tuner.detect_access_pattern(&random_stats);
591        if let Some(rec) = tuner.recommend_tuning(&random_stats, 500) {
592            assert_eq!(rec.recommended_policy, EvictionPolicy::ARC);
593        }
594    }
595
596    #[test]
597    fn test_reset_metrics() {
598        use std::sync::atomic::AtomicU64;
599
600        let config = BufferPoolTunerConfig::default();
601        let mut tuner = BufferPoolTuner::new(config);
602
603        let stats = BufferPoolStats {
604            total_fetches: AtomicU64::new(1000),
605            cache_hits: AtomicU64::new(900),
606            cache_misses: AtomicU64::new(100),
607            evictions: AtomicU64::new(50),
608            writes: AtomicU64::new(25),
609        };
610
611        tuner.update_metrics(&stats, 1024, 500);
612        assert!(tuner.get_hit_rate() > 0.0);
613
614        tuner.reset_metrics();
615        assert_eq!(tuner.get_hit_rate(), 0.0);
616        assert_eq!(tuner.get_total_accesses(), 0);
617    }
618}