1use 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#[derive(Debug, Clone)]
15pub struct BufferPoolTunerConfig {
16 pub min_size: usize,
18 pub max_size: usize,
20 pub target_hit_rate: f64,
22 pub tuning_interval_secs: u64,
24 pub adjustment_step: f64,
26 pub aggressive_tuning: bool,
28}
29
30impl Default for BufferPoolTunerConfig {
31 fn default() -> Self {
32 Self {
33 min_size: 64, max_size: 65536, target_hit_rate: 0.9, tuning_interval_secs: 60,
37 adjustment_step: 0.1, aggressive_tuning: false,
39 }
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum AccessPattern {
46 Sequential,
48 Random,
50 Mixed,
52 ScanHeavy,
54 Unknown,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
60pub enum EvictionPolicy {
61 #[default]
63 LRU,
64 LFU,
66 ARC,
68 Clock,
70}
71
72#[derive(Debug, Clone)]
74pub struct TuningRecommendation {
75 pub recommended_size: usize,
77 pub recommended_policy: EvictionPolicy,
79 pub confidence: f64,
81 pub reason: String,
83}
84
85#[derive(Debug, Clone)]
87pub struct PerformanceReport {
88 pub current_size: usize,
90 pub hit_rate: f64,
92 pub avg_latency_us: f64,
94 pub access_pattern: AccessPattern,
96 pub tuning_iterations: u64,
98 pub bytes_saved: u64,
100}
101
102pub struct BufferPoolTuner {
104 config: BufferPoolTunerConfig,
106 last_tuning: Instant,
108 access_history: Vec<AccessPattern>,
110 tuning_iterations: u64,
112
113 hit_rate_gauge: Gauge,
116 miss_rate_gauge: Gauge,
118 pool_size_gauge: Gauge,
120 eviction_counter: Counter,
122 access_counter: Counter,
124 access_latency_histogram: Histogram,
126 tuning_adjustments: Counter,
128}
129
130impl BufferPoolTuner {
131 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 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 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 pub fn record_access_latency(&self, latency: Duration) {
179 self.access_latency_histogram
180 .observe(latency.as_secs_f64() * 1_000_000.0); }
182
183 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 let pattern = if hit_rate > 0.95 {
199 AccessPattern::Sequential } else if hit_rate < 0.5 {
201 AccessPattern::Random } else if evictions > total_fetches / 2 {
203 AccessPattern::ScanHeavy } else {
205 AccessPattern::Mixed
206 };
207
208 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 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 if current_hit_rate >= self.config.target_hit_rate {
237 return None;
239 }
240
241 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 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 pub fn apply_tuning(&mut self, recommendation: &TuningRecommendation) -> bool {
285 let elapsed = self.last_tuning.elapsed();
287 if elapsed < Duration::from_secs(self.config.tuning_interval_secs) {
288 return false;
289 }
290
291 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 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, }
337 }
338
339 pub fn get_hit_rate(&self) -> f64 {
341 self.hit_rate_gauge.get()
342 }
343
344 pub fn get_total_accesses(&self) -> u64 {
346 self.access_counter.get()
347 }
348
349 pub fn get_total_evictions(&self) -> u64 {
351 self.eviction_counter.get()
352 }
353
354 pub fn get_tuning_iterations(&self) -> u64 {
356 self.tuning_iterations
357 }
358
359 pub fn reset_metrics(&mut self) {
361 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 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 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 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 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 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 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 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}