use crate::error::TdbError;
use crate::storage::{BufferPool, BufferPoolStats};
use scirs2_core::metrics::{Counter, Gauge, Histogram, Timer};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct BufferPoolTunerConfig {
pub min_size: usize,
pub max_size: usize,
pub target_hit_rate: f64,
pub tuning_interval_secs: u64,
pub adjustment_step: f64,
pub aggressive_tuning: bool,
}
impl Default for BufferPoolTunerConfig {
fn default() -> Self {
Self {
min_size: 64, max_size: 65536, target_hit_rate: 0.9, tuning_interval_secs: 60,
adjustment_step: 0.1, aggressive_tuning: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccessPattern {
Sequential,
Random,
Mixed,
ScanHeavy,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EvictionPolicy {
#[default]
LRU,
LFU,
ARC,
Clock,
}
#[derive(Debug, Clone)]
pub struct TuningRecommendation {
pub recommended_size: usize,
pub recommended_policy: EvictionPolicy,
pub confidence: f64,
pub reason: String,
}
#[derive(Debug, Clone)]
pub struct PerformanceReport {
pub current_size: usize,
pub hit_rate: f64,
pub avg_latency_us: f64,
pub access_pattern: AccessPattern,
pub tuning_iterations: u64,
pub bytes_saved: u64,
}
pub struct BufferPoolTuner {
config: BufferPoolTunerConfig,
last_tuning: Instant,
access_history: Vec<AccessPattern>,
tuning_iterations: u64,
hit_rate_gauge: Gauge,
miss_rate_gauge: Gauge,
pool_size_gauge: Gauge,
eviction_counter: Counter,
access_counter: Counter,
access_latency_histogram: Histogram,
tuning_adjustments: Counter,
}
impl BufferPoolTuner {
pub fn new(config: BufferPoolTunerConfig) -> Self {
Self {
config,
last_tuning: Instant::now(),
access_history: Vec::new(),
tuning_iterations: 0,
hit_rate_gauge: Gauge::new("buffer_pool_hit_rate".to_string()),
miss_rate_gauge: Gauge::new("buffer_pool_miss_rate".to_string()),
pool_size_gauge: Gauge::new("buffer_pool_size".to_string()),
eviction_counter: Counter::new("buffer_pool_evictions".to_string()),
access_counter: Counter::new("buffer_pool_accesses".to_string()),
access_latency_histogram: Histogram::new("buffer_pool_access_latency".to_string()),
tuning_adjustments: Counter::new("buffer_pool_tuning_adjustments".to_string()),
}
}
pub fn update_metrics(
&mut self,
stats: &BufferPoolStats,
pool_size: usize,
cached_pages: usize,
) {
use std::sync::atomic::Ordering;
let total_fetches = stats.total_fetches.load(Ordering::Relaxed);
let cache_hits = stats.cache_hits.load(Ordering::Relaxed);
let cache_misses = stats.cache_misses.load(Ordering::Relaxed);
let evictions = stats.evictions.load(Ordering::Relaxed);
if total_fetches > 0 {
let hit_rate = cache_hits as f64 / total_fetches as f64;
let miss_rate = cache_misses as f64 / total_fetches as f64;
self.hit_rate_gauge.set(hit_rate);
self.miss_rate_gauge.set(miss_rate);
}
self.pool_size_gauge.set(pool_size as f64);
self.eviction_counter.add(evictions);
self.access_counter.add(total_fetches);
}
pub fn record_access_latency(&self, latency: Duration) {
self.access_latency_histogram
.observe(latency.as_secs_f64() * 1_000_000.0); }
pub fn detect_access_pattern(&mut self, stats: &BufferPoolStats) -> AccessPattern {
use std::sync::atomic::Ordering;
let total_fetches = stats.total_fetches.load(Ordering::Relaxed);
let cache_hits = stats.cache_hits.load(Ordering::Relaxed);
let evictions = stats.evictions.load(Ordering::Relaxed);
if total_fetches == 0 {
return AccessPattern::Unknown;
}
let hit_rate = cache_hits as f64 / total_fetches as f64;
let pattern = if hit_rate > 0.95 {
AccessPattern::Sequential } else if hit_rate < 0.5 {
AccessPattern::Random } else if evictions > total_fetches / 2 {
AccessPattern::ScanHeavy } else {
AccessPattern::Mixed
};
self.access_history.push(pattern);
if self.access_history.len() > 10 {
self.access_history.remove(0);
}
pattern
}
pub fn recommend_tuning(
&mut self,
stats: &BufferPoolStats,
current_pool_size: usize,
) -> Option<TuningRecommendation> {
use std::sync::atomic::Ordering;
let total_fetches = stats.total_fetches.load(Ordering::Relaxed);
let cache_hits = stats.cache_hits.load(Ordering::Relaxed);
if total_fetches == 0 {
return None;
}
let current_hit_rate = cache_hits as f64 / total_fetches as f64;
let pattern = self.detect_access_pattern(stats);
if current_hit_rate >= self.config.target_hit_rate {
return None;
}
let hit_rate_deficit = self.config.target_hit_rate - current_hit_rate;
let adjustment_multiplier = if self.config.aggressive_tuning {
1.5
} else {
1.0
};
let size_increase_factor =
1.0 + (hit_rate_deficit * self.config.adjustment_step * adjustment_multiplier).min(0.5);
let recommended_size = ((current_pool_size as f64 * size_increase_factor) as usize)
.clamp(self.config.min_size, self.config.max_size);
let recommended_policy = match pattern {
AccessPattern::Sequential => EvictionPolicy::LRU,
AccessPattern::Random => EvictionPolicy::ARC,
AccessPattern::ScanHeavy => EvictionPolicy::Clock,
AccessPattern::Mixed => EvictionPolicy::LFU,
AccessPattern::Unknown => EvictionPolicy::LRU,
};
let confidence = if self.access_history.len() >= 5 {
0.8
} else {
0.5
};
Some(TuningRecommendation {
recommended_size,
recommended_policy,
confidence,
reason: format!(
"Hit rate {:.2}% is below target {:.2}%. Detected {:?} access pattern.",
current_hit_rate * 100.0,
self.config.target_hit_rate * 100.0,
pattern
),
})
}
pub fn apply_tuning(&mut self, recommendation: &TuningRecommendation) -> bool {
let elapsed = self.last_tuning.elapsed();
if elapsed < Duration::from_secs(self.config.tuning_interval_secs) {
return false;
}
if recommendation.confidence < 0.6 {
return false;
}
self.last_tuning = Instant::now();
self.tuning_iterations += 1;
self.tuning_adjustments.inc();
true
}
pub fn generate_report(
&self,
stats: &BufferPoolStats,
current_pool_size: usize,
) -> PerformanceReport {
use std::sync::atomic::Ordering;
let total_fetches = stats.total_fetches.load(Ordering::Relaxed);
let cache_hits = stats.cache_hits.load(Ordering::Relaxed);
let hit_rate = if total_fetches > 0 {
cache_hits as f64 / total_fetches as f64
} else {
0.0
};
let latency_stats = self.access_latency_histogram.get_stats();
let avg_latency_us = latency_stats.mean;
let access_pattern = if let Some(&last_pattern) = self.access_history.last() {
last_pattern
} else {
AccessPattern::Unknown
};
PerformanceReport {
current_size: current_pool_size,
hit_rate,
avg_latency_us,
access_pattern,
tuning_iterations: self.tuning_iterations,
bytes_saved: 0, }
}
pub fn get_hit_rate(&self) -> f64 {
self.hit_rate_gauge.get()
}
pub fn get_total_accesses(&self) -> u64 {
self.access_counter.get()
}
pub fn get_total_evictions(&self) -> u64 {
self.eviction_counter.get()
}
pub fn get_tuning_iterations(&self) -> u64 {
self.tuning_iterations
}
pub fn reset_metrics(&mut self) {
self.hit_rate_gauge = Gauge::new("buffer_pool_hit_rate".to_string());
self.miss_rate_gauge = Gauge::new("buffer_pool_miss_rate".to_string());
self.pool_size_gauge = Gauge::new("buffer_pool_size".to_string());
self.eviction_counter = Counter::new("buffer_pool_evictions".to_string());
self.access_counter = Counter::new("buffer_pool_accesses".to_string());
self.access_latency_histogram = Histogram::new("buffer_pool_access_latency".to_string());
self.tuning_adjustments = Counter::new("buffer_pool_tuning_adjustments".to_string());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_buffer_pool_tuner_creation() {
let config = BufferPoolTunerConfig::default();
let tuner = BufferPoolTuner::new(config.clone());
assert_eq!(tuner.get_tuning_iterations(), 0);
assert_eq!(tuner.get_total_accesses(), 0);
assert_eq!(tuner.get_total_evictions(), 0);
}
#[test]
fn test_update_metrics() {
use std::sync::atomic::{AtomicU64, Ordering};
let config = BufferPoolTunerConfig::default();
let mut tuner = BufferPoolTuner::new(config);
let stats = BufferPoolStats {
total_fetches: AtomicU64::new(1000),
cache_hits: AtomicU64::new(900),
cache_misses: AtomicU64::new(100),
evictions: AtomicU64::new(50),
writes: AtomicU64::new(10),
};
tuner.update_metrics(&stats, 1024, 500);
let hit_rate = tuner.get_hit_rate();
assert!((hit_rate - 0.9).abs() < 0.01);
}
#[test]
fn test_access_pattern_detection() {
use std::sync::atomic::AtomicU64;
let config = BufferPoolTunerConfig::default();
let mut tuner = BufferPoolTuner::new(config);
let sequential_stats = BufferPoolStats {
total_fetches: AtomicU64::new(1000),
cache_hits: AtomicU64::new(980),
cache_misses: AtomicU64::new(20),
evictions: AtomicU64::new(5),
writes: AtomicU64::new(10),
};
let pattern = tuner.detect_access_pattern(&sequential_stats);
assert_eq!(pattern, AccessPattern::Sequential);
let random_stats = BufferPoolStats {
total_fetches: AtomicU64::new(1000),
cache_hits: AtomicU64::new(300),
cache_misses: AtomicU64::new(700),
evictions: AtomicU64::new(200),
writes: AtomicU64::new(50),
};
let pattern = tuner.detect_access_pattern(&random_stats);
assert_eq!(pattern, AccessPattern::Random);
}
#[test]
fn test_tuning_recommendation() {
use std::sync::atomic::AtomicU64;
let config = BufferPoolTunerConfig {
target_hit_rate: 0.9,
..Default::default()
};
let mut tuner = BufferPoolTuner::new(config);
let stats = BufferPoolStats {
total_fetches: AtomicU64::new(1000),
cache_hits: AtomicU64::new(600),
cache_misses: AtomicU64::new(400),
evictions: AtomicU64::new(100),
writes: AtomicU64::new(50),
};
let current_pool_size = 1024;
let recommendation = tuner.recommend_tuning(&stats, current_pool_size);
assert!(recommendation.is_some());
let rec = recommendation.unwrap();
assert!(rec.recommended_size > current_pool_size);
assert!(rec.confidence > 0.0);
}
#[test]
fn test_no_tuning_when_hit_rate_good() {
use std::sync::atomic::AtomicU64;
let config = BufferPoolTunerConfig {
target_hit_rate: 0.9,
..Default::default()
};
let mut tuner = BufferPoolTuner::new(config);
let stats = BufferPoolStats {
total_fetches: AtomicU64::new(1000),
cache_hits: AtomicU64::new(950),
cache_misses: AtomicU64::new(50),
evictions: AtomicU64::new(10),
writes: AtomicU64::new(5),
};
let recommendation = tuner.recommend_tuning(&stats, 1024);
assert!(recommendation.is_none());
}
#[test]
fn test_performance_report() {
use std::sync::atomic::AtomicU64;
let config = BufferPoolTunerConfig::default();
let mut tuner = BufferPoolTuner::new(config);
let stats = BufferPoolStats {
total_fetches: AtomicU64::new(1000),
cache_hits: AtomicU64::new(800),
cache_misses: AtomicU64::new(200),
evictions: AtomicU64::new(50),
writes: AtomicU64::new(25),
};
tuner.update_metrics(&stats, 1024, 500);
let report = tuner.generate_report(&stats, 1024);
assert_eq!(report.current_size, 1024);
assert!((report.hit_rate - 0.8).abs() < 0.01);
}
#[test]
fn test_access_latency_recording() {
let config = BufferPoolTunerConfig::default();
let tuner = BufferPoolTuner::new(config);
tuner.record_access_latency(Duration::from_micros(100));
tuner.record_access_latency(Duration::from_micros(200));
tuner.record_access_latency(Duration::from_micros(150));
let stats = tuner.access_latency_histogram.get_stats();
assert_eq!(stats.count, 3);
assert!(stats.mean > 0.0);
}
#[test]
fn test_aggressive_tuning() {
use std::sync::atomic::AtomicU64;
let config = BufferPoolTunerConfig {
target_hit_rate: 0.9,
aggressive_tuning: true,
..Default::default()
};
let mut tuner_aggressive = BufferPoolTuner::new(config.clone());
let config_normal = BufferPoolTunerConfig {
target_hit_rate: 0.9,
aggressive_tuning: false,
..Default::default()
};
let mut tuner_normal = BufferPoolTuner::new(config_normal);
let stats = BufferPoolStats {
total_fetches: AtomicU64::new(1000),
cache_hits: AtomicU64::new(600),
cache_misses: AtomicU64::new(400),
evictions: AtomicU64::new(100),
writes: AtomicU64::new(50),
};
let current_pool_size = 1000;
let rec_aggressive = tuner_aggressive
.recommend_tuning(&stats, current_pool_size)
.unwrap();
let rec_normal = tuner_normal
.recommend_tuning(&stats, current_pool_size)
.unwrap();
assert!(rec_aggressive.recommended_size >= rec_normal.recommended_size);
}
#[test]
fn test_eviction_policy_recommendation() {
use std::sync::atomic::AtomicU64;
let config = BufferPoolTunerConfig::default();
let mut tuner = BufferPoolTuner::new(config);
let sequential_stats = BufferPoolStats {
total_fetches: AtomicU64::new(1000),
cache_hits: AtomicU64::new(980),
cache_misses: AtomicU64::new(20),
evictions: AtomicU64::new(5),
writes: AtomicU64::new(3),
};
tuner.detect_access_pattern(&sequential_stats);
if let Some(rec) = tuner.recommend_tuning(&sequential_stats, 500) {
assert_eq!(rec.recommended_policy, EvictionPolicy::LRU);
}
let random_stats = BufferPoolStats {
total_fetches: AtomicU64::new(1000),
cache_hits: AtomicU64::new(300),
cache_misses: AtomicU64::new(700),
evictions: AtomicU64::new(200),
writes: AtomicU64::new(100),
};
tuner.detect_access_pattern(&random_stats);
if let Some(rec) = tuner.recommend_tuning(&random_stats, 500) {
assert_eq!(rec.recommended_policy, EvictionPolicy::ARC);
}
}
#[test]
fn test_reset_metrics() {
use std::sync::atomic::AtomicU64;
let config = BufferPoolTunerConfig::default();
let mut tuner = BufferPoolTuner::new(config);
let stats = BufferPoolStats {
total_fetches: AtomicU64::new(1000),
cache_hits: AtomicU64::new(900),
cache_misses: AtomicU64::new(100),
evictions: AtomicU64::new(50),
writes: AtomicU64::new(25),
};
tuner.update_metrics(&stats, 1024, 500);
assert!(tuner.get_hit_rate() > 0.0);
tuner.reset_metrics();
assert_eq!(tuner.get_hit_rate(), 0.0);
assert_eq!(tuner.get_total_accesses(), 0);
}
}