Skip to main content

ipfrs_storage/
prefetch.rs

1//! Predictive Prefetching for intelligent block preloading
2//!
3//! This module provides predictive prefetching capabilities:
4//! - Access pattern analysis and prediction
5//! - Sequential access detection
6//! - Co-location patterns (blocks accessed together)
7//! - Time-based prediction (blocks accessed at similar times)
8//! - Adaptive prefetch depth based on cache hit rates
9//! - Background prefetching with priority control
10
11use crate::traits::BlockStore;
12use dashmap::DashMap;
13use ipfrs_core::Cid;
14use serde::{Deserialize, Serialize};
15use std::collections::VecDeque;
16use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
17use std::sync::Arc;
18use std::time::{Duration, SystemTime};
19use tokio::sync::Semaphore;
20use tracing::{debug, trace};
21
22/// Access pattern type
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24pub enum AccessPattern {
25    /// Sequential access (e.g., video streaming)
26    Sequential,
27    /// Random access with no clear pattern
28    Random,
29    /// Clustered access (accessing related blocks)
30    Clustered,
31    /// Temporal (accessing at regular intervals)
32    Temporal,
33}
34
35/// Access record for a block
36#[derive(Debug, Clone)]
37struct AccessRecord {
38    /// When the block was accessed
39    #[allow(dead_code)]
40    timestamp: SystemTime,
41    /// Previous block accessed (for pattern detection)
42    #[allow(dead_code)]
43    previous_cid: Option<Cid>,
44    /// Next block accessed (updated retroactively)
45    next_cid: Option<Cid>,
46}
47
48/// Co-location pattern (blocks frequently accessed together)
49#[derive(Debug, Clone)]
50struct CoLocationPattern {
51    /// How many times this pattern was observed
52    count: u64,
53    /// Last time this pattern was seen
54    last_seen: SystemTime,
55    /// Confidence score (0.0 - 1.0)
56    confidence: f64,
57}
58
59/// Prefetch prediction
60#[derive(Debug, Clone)]
61pub struct PrefetchPrediction {
62    /// CID to prefetch
63    pub cid: Cid,
64    /// Confidence score (0.0 - 1.0)
65    pub confidence: f64,
66    /// Predicted access time
67    pub predicted_access: SystemTime,
68    /// Pattern type that generated this prediction
69    pub pattern: AccessPattern,
70}
71
72/// Prefetch configuration
73#[derive(Debug, Clone)]
74pub struct PrefetchConfig {
75    /// Maximum number of blocks to prefetch ahead
76    pub max_prefetch_depth: usize,
77    /// Minimum confidence threshold for prefetching (0.0 - 1.0)
78    pub min_confidence: f64,
79    /// Maximum concurrent prefetch operations
80    pub max_concurrent_prefetch: usize,
81    /// Time window for pattern analysis
82    pub pattern_window: Duration,
83    /// Enable sequential pattern detection
84    pub enable_sequential: bool,
85    /// Enable co-location pattern detection
86    pub enable_colocation: bool,
87    /// Enable temporal pattern detection
88    pub enable_temporal: bool,
89}
90
91impl Default for PrefetchConfig {
92    fn default() -> Self {
93        Self {
94            max_prefetch_depth: 5,
95            min_confidence: 0.6,
96            max_concurrent_prefetch: 3,
97            pattern_window: Duration::from_secs(300), // 5 minutes
98            enable_sequential: true,
99            enable_colocation: true,
100            enable_temporal: true,
101        }
102    }
103}
104
105/// Prefetch statistics
106#[derive(Debug, Default)]
107pub struct PrefetchStats {
108    /// Total prefetch attempts
109    pub prefetch_attempts: AtomicU64,
110    /// Successful prefetches (block was used)
111    pub prefetch_hits: AtomicU64,
112    /// Wasted prefetches (block was not used)
113    pub prefetch_misses: AtomicU64,
114    /// Bytes prefetched
115    pub bytes_prefetched: AtomicU64,
116    /// Average confidence of predictions
117    pub avg_confidence: parking_lot::Mutex<f64>,
118}
119
120impl PrefetchStats {
121    fn record_attempt(&self) {
122        self.prefetch_attempts.fetch_add(1, Ordering::Relaxed);
123    }
124
125    fn record_hit(&self, bytes: u64) {
126        self.prefetch_hits.fetch_add(1, Ordering::Relaxed);
127        self.bytes_prefetched.fetch_add(bytes, Ordering::Relaxed);
128    }
129
130    fn record_miss(&self) {
131        self.prefetch_misses.fetch_add(1, Ordering::Relaxed);
132    }
133
134    /// Get hit rate
135    pub fn hit_rate(&self) -> f64 {
136        let hits = self.prefetch_hits.load(Ordering::Relaxed) as f64;
137        let total = self.prefetch_attempts.load(Ordering::Relaxed) as f64;
138        if total > 0.0 {
139            hits / total
140        } else {
141            0.0
142        }
143    }
144}
145
146/// Predictive prefetcher
147pub struct PredictivePrefetcher<S: BlockStore> {
148    store: Arc<S>,
149    config: parking_lot::RwLock<PrefetchConfig>,
150    /// Access history for each CID
151    access_history: DashMap<Cid, VecDeque<AccessRecord>>,
152    /// Co-location patterns (CID -> related CIDs)
153    colocation_patterns: DashMap<Cid, DashMap<Cid, CoLocationPattern>>,
154    /// Last accessed CID (for detecting sequences)
155    last_accessed: parking_lot::Mutex<Option<Cid>>,
156    /// Prefetch queue
157    #[allow(dead_code)]
158    prefetch_queue: DashMap<Cid, PrefetchPrediction>,
159    /// Prefetch cache (blocks that have been prefetched)
160    prefetch_cache: DashMap<Cid, (Vec<u8>, SystemTime)>,
161    /// Statistics
162    stats: PrefetchStats,
163    /// Semaphore for concurrent prefetch control
164    prefetch_semaphore: Arc<Semaphore>,
165    /// Current prefetch depth (adaptive)
166    current_depth: AtomicUsize,
167}
168
169impl<S: BlockStore + Send + Sync + 'static> PredictivePrefetcher<S> {
170    /// Create a new predictive prefetcher
171    pub fn new(store: Arc<S>, config: PrefetchConfig) -> Self {
172        let max_concurrent = config.max_concurrent_prefetch;
173        let initial_depth = config.max_prefetch_depth;
174
175        Self {
176            store,
177            config: parking_lot::RwLock::new(config),
178            access_history: DashMap::new(),
179            colocation_patterns: DashMap::new(),
180            last_accessed: parking_lot::Mutex::new(None),
181            prefetch_queue: DashMap::new(),
182            prefetch_cache: DashMap::new(),
183            stats: PrefetchStats::default(),
184            prefetch_semaphore: Arc::new(Semaphore::new(max_concurrent)),
185            current_depth: AtomicUsize::new(initial_depth),
186        }
187    }
188
189    /// Record an access and update patterns
190    pub fn record_access(&self, cid: &Cid) {
191        let now = SystemTime::now();
192        let previous = *self.last_accessed.lock();
193
194        // Add to access history (drop guard before accessing prev_history to avoid deadlock)
195        {
196            let mut history = self.access_history.entry(*cid).or_default();
197            history.push_back(AccessRecord {
198                timestamp: now,
199                previous_cid: previous,
200                next_cid: None,
201            });
202
203            // Limit history size
204            if history.len() > 100 {
205                history.pop_front();
206            }
207        } // Guard dropped here
208
209        // Update previous access with next CID (safe now since we dropped the guard above)
210        if let Some(prev_cid) = previous {
211            // Only update if prev_cid is different from cid to avoid unnecessary work
212            if prev_cid != *cid {
213                if let Some(mut prev_history) = self.access_history.get_mut(&prev_cid) {
214                    if let Some(last_record) = prev_history.back_mut() {
215                        last_record.next_cid = Some(*cid);
216                    }
217                }
218            }
219
220            // Update co-location patterns
221            if self.config.read().enable_colocation {
222                self.update_colocation_pattern(&prev_cid, cid);
223            }
224        }
225
226        // Update last accessed
227        *self.last_accessed.lock() = Some(*cid);
228
229        // Check if this was prefetched
230        if let Some(entry) = self.prefetch_cache.get(cid) {
231            let prefetch_time = entry.value().1;
232            let age = now.duration_since(prefetch_time).unwrap_or_default();
233            if age < Duration::from_secs(60) {
234                // Prefetch was used within 60 seconds - count as hit
235                self.stats.record_hit(0); // We don't have size info here
236            } else {
237                self.stats.record_miss();
238            }
239        }
240    }
241
242    /// Update co-location pattern
243    fn update_colocation_pattern(&self, cid1: &Cid, cid2: &Cid) {
244        let patterns = self.colocation_patterns.entry(*cid1).or_default();
245
246        patterns
247            .entry(*cid2)
248            .and_modify(|pattern| {
249                pattern.count += 1;
250                pattern.last_seen = SystemTime::now();
251                // Update confidence based on recency and frequency
252                let recency_factor = 0.9; // Decay factor
253                pattern.confidence = (pattern.confidence * recency_factor + 0.1).min(1.0);
254            })
255            .or_insert_with(|| CoLocationPattern {
256                count: 1,
257                last_seen: SystemTime::now(),
258                confidence: 0.5,
259            });
260    }
261
262    /// Predict next blocks to access
263    pub fn predict_next_blocks(&self, current_cid: &Cid) -> Vec<PrefetchPrediction> {
264        let config = self.config.read();
265        let mut predictions = Vec::new();
266
267        // Sequential pattern prediction
268        if config.enable_sequential {
269            if let Some(seq_predictions) = self.predict_sequential(current_cid) {
270                predictions.extend(seq_predictions);
271            }
272        }
273
274        // Co-location pattern prediction
275        if config.enable_colocation {
276            if let Some(coloc_predictions) = self.predict_colocation(current_cid) {
277                predictions.extend(coloc_predictions);
278            }
279        }
280
281        // Filter by confidence and limit depth
282        predictions.retain(|p| p.confidence >= config.min_confidence);
283        predictions.sort_by(|a, b| {
284            b.confidence
285                .partial_cmp(&a.confidence)
286                .unwrap_or(std::cmp::Ordering::Equal)
287        });
288
289        let depth = self.current_depth.load(Ordering::Relaxed);
290        predictions.truncate(depth);
291
292        predictions
293    }
294
295    /// Predict based on sequential access pattern
296    fn predict_sequential(&self, cid: &Cid) -> Option<Vec<PrefetchPrediction>> {
297        let history = self.access_history.get(cid)?;
298
299        // Check if there's a consistent "next" block
300        let next_counts: DashMap<Cid, u64> = DashMap::new();
301
302        for record in history.iter() {
303            if let Some(next_cid) = record.next_cid {
304                *next_counts.entry(next_cid).or_insert(0) += 1;
305            }
306        }
307
308        if next_counts.is_empty() {
309            return None;
310        }
311
312        // Find most common next block
313        let mut predictions = Vec::new();
314        let total_accesses = history.len() as f64;
315
316        for entry in next_counts.iter() {
317            let count = *entry.value() as f64;
318            let confidence = count / total_accesses;
319
320            if confidence >= 0.3 {
321                predictions.push(PrefetchPrediction {
322                    cid: *entry.key(),
323                    confidence,
324                    predicted_access: SystemTime::now(),
325                    pattern: AccessPattern::Sequential,
326                });
327            }
328        }
329
330        Some(predictions)
331    }
332
333    /// Predict based on co-location patterns
334    fn predict_colocation(&self, cid: &Cid) -> Option<Vec<PrefetchPrediction>> {
335        let patterns = self.colocation_patterns.get(cid)?;
336
337        let mut predictions = Vec::new();
338
339        for entry in patterns.iter() {
340            let pattern = entry.value();
341
342            // Check if pattern is recent
343            let age = SystemTime::now()
344                .duration_since(pattern.last_seen)
345                .unwrap_or_default();
346
347            if age < self.config.read().pattern_window {
348                predictions.push(PrefetchPrediction {
349                    cid: *entry.key(),
350                    confidence: pattern.confidence,
351                    predicted_access: SystemTime::now(),
352                    pattern: AccessPattern::Clustered,
353                });
354            }
355        }
356
357        Some(predictions)
358    }
359
360    /// Prefetch predicted blocks in background
361    pub async fn prefetch_background(&self, predictions: Vec<PrefetchPrediction>) {
362        for prediction in predictions {
363            let store = self.store.clone();
364            let cache = self.prefetch_cache.clone();
365            let stats = &self.stats;
366            let semaphore = self.prefetch_semaphore.clone();
367
368            stats.record_attempt();
369
370            let cid = prediction.cid;
371            trace!(
372                "Prefetching block {} (confidence: {:.2})",
373                cid,
374                prediction.confidence
375            );
376
377            // Spawn prefetch task
378            tokio::spawn(async move {
379                let _permit = semaphore.acquire().await.ok();
380
381                if let Ok(Some(block)) = store.get(&cid).await {
382                    cache.insert(cid, (block.data().to_vec(), SystemTime::now()));
383                    debug!("Prefetched block {}", cid);
384                }
385            });
386        }
387    }
388
389    /// Adapt prefetch depth based on hit rate
390    pub fn adapt_depth(&self) {
391        let hit_rate = self.stats.hit_rate();
392        let current = self.current_depth.load(Ordering::Relaxed);
393        let max_depth = self.config.read().max_prefetch_depth;
394
395        let new_depth = if hit_rate > 0.8 {
396            // High hit rate - increase depth
397            (current + 1).min(max_depth)
398        } else if hit_rate < 0.4 {
399            // Low hit rate - decrease depth
400            (current.saturating_sub(1)).max(1)
401        } else {
402            current
403        };
404
405        if new_depth != current {
406            self.current_depth.store(new_depth, Ordering::Relaxed);
407            debug!(
408                "Adapted prefetch depth: {} -> {} (hit rate: {:.2})",
409                current, new_depth, hit_rate
410            );
411        }
412    }
413
414    /// Get statistics
415    pub fn stats(&self) -> PrefetchStatsSnapshot {
416        PrefetchStatsSnapshot {
417            prefetch_attempts: self.stats.prefetch_attempts.load(Ordering::Relaxed),
418            prefetch_hits: self.stats.prefetch_hits.load(Ordering::Relaxed),
419            prefetch_misses: self.stats.prefetch_misses.load(Ordering::Relaxed),
420            bytes_prefetched: self.stats.bytes_prefetched.load(Ordering::Relaxed),
421            hit_rate: self.stats.hit_rate(),
422            current_depth: self.current_depth.load(Ordering::Relaxed),
423        }
424    }
425
426    /// Clear prefetch cache
427    pub fn clear_cache(&self) {
428        self.prefetch_cache.clear();
429    }
430
431    /// Get cache size
432    pub fn cache_size(&self) -> usize {
433        self.prefetch_cache.len()
434    }
435}
436
437/// Snapshot of prefetch statistics
438#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct PrefetchStatsSnapshot {
440    pub prefetch_attempts: u64,
441    pub prefetch_hits: u64,
442    pub prefetch_misses: u64,
443    pub bytes_prefetched: u64,
444    pub hit_rate: f64,
445    pub current_depth: usize,
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451    use crate::memory::MemoryBlockStore;
452    use ipfrs_core::cid::CidBuilder;
453
454    /// Helper to create a unique CID from an index
455    fn test_cid(index: u64) -> Cid {
456        CidBuilder::new()
457            .build(&index.to_le_bytes())
458            .expect("failed to create test cid")
459    }
460
461    #[tokio::test]
462    async fn test_prefetcher_creation() {
463        let store = Arc::new(MemoryBlockStore::new());
464        let config = PrefetchConfig::default();
465        let prefetcher = PredictivePrefetcher::new(store, config);
466
467        let stats = prefetcher.stats();
468        assert_eq!(stats.prefetch_attempts, 0);
469        assert_eq!(stats.hit_rate, 0.0);
470    }
471
472    #[tokio::test]
473    async fn test_access_recording() {
474        let store = Arc::new(MemoryBlockStore::new());
475        let prefetcher = PredictivePrefetcher::new(store, PrefetchConfig::default());
476
477        let cid1 = test_cid(1);
478        let cid2 = test_cid(2);
479
480        prefetcher.record_access(&cid1);
481        prefetcher.record_access(&cid2);
482
483        // Should have recorded co-location pattern
484        assert!(prefetcher.colocation_patterns.contains_key(&cid1));
485    }
486
487    #[tokio::test]
488    async fn test_sequential_prediction() {
489        let store = Arc::new(MemoryBlockStore::new());
490        let prefetcher = PredictivePrefetcher::new(store, PrefetchConfig::default());
491
492        let cid1 = test_cid(1);
493        let cid2 = test_cid(2);
494
495        // Simulate sequential access pattern
496        for _ in 0..5 {
497            prefetcher.record_access(&cid1);
498            prefetcher.record_access(&cid2);
499        }
500
501        let predictions = prefetcher.predict_next_blocks(&cid1);
502        assert!(!predictions.is_empty());
503
504        // Should predict cid2 after cid1
505        assert!(predictions
506            .iter()
507            .any(|p| p.pattern == AccessPattern::Sequential));
508    }
509}