dx_forge/watcher/
cache_warmer.rs

1use anyhow::Result;
2use colored::*;
3use parking_lot::RwLock;
4use rayon::prelude::*;
5use std::collections::HashMap;
6use std::fs;
7use std::fs::File;
8use std::path::{Path, PathBuf};
9use std::sync::atomic::{AtomicUsize, Ordering};
10use std::sync::Arc;
11use std::time::Instant;
12use once_cell::sync::Lazy;
13
14const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024; // 10MB
15
16// Shared file handle pool
17pub static FILE_POOL: Lazy<RwLock<HashMap<PathBuf, Arc<File>>>> = Lazy::new(|| RwLock::new(HashMap::new()));
18
19/// Warm the OS page cache by reading all trackable files
20pub fn warm_cache(repo_root: &Path) -> Result<CacheStats> {
21    let start = Instant::now();
22    
23    // println!("{}", "📦 Warming OS page cache...".bright_cyan());
24    
25    // Collect all trackable files
26    let files = collect_trackable_files(repo_root)?;
27    let total_files = files.len();
28    
29    if total_files == 0 {
30        println!("{} No files to cache", "✓".bright_green());
31        return Ok(CacheStats::default());
32    }
33    
34    // Progress tracking
35    let cached_count = Arc::new(AtomicUsize::new(0));
36    let cached_bytes = Arc::new(AtomicUsize::new(0));
37    
38    // Pre-open file handles and warm cache in parallel
39    // This ensures subsequent reads are instant
40    let handles: Vec<_> = files.par_iter()
41        .filter_map(|path| {
42            // Try to open and read to warm cache
43            if let Ok(file) = File::open(path) {
44                // Read to warm OS cache
45                if let Ok(mmap) = unsafe { memmap2::Mmap::map(&file) } {
46                    let size = mmap.len();
47                    cached_count.fetch_add(1, Ordering::Relaxed);
48                    cached_bytes.fetch_add(size, Ordering::Relaxed);
49                    return Some((path.clone(), Arc::new(file)));
50                }
51            }
52            None
53        })
54        .collect();
55    
56    // Populate pool with all opened handles
57    let mut pool = FILE_POOL.write();
58    for (path, file) in handles {
59        pool.insert(path, file);
60    }
61    drop(pool);
62    
63    let final_count = cached_count.load(Ordering::Relaxed);
64    let final_bytes = cached_bytes.load(Ordering::Relaxed);
65    let elapsed = start.elapsed();
66    
67    // println!(
68    //     "{} Cached {} files ({} KB) in {:?}",
69    //     "✓".bright_green(),
70    //     final_count,
71    //     final_bytes / 1024,
72    //     elapsed
73    // );
74    
75    Ok(CacheStats {
76        files_cached: final_count,
77        bytes_cached: final_bytes,
78        duration_ms: elapsed.as_millis() as u64,
79    })
80}
81
82/// Incrementally warm cache for new files as they're discovered
83pub fn warm_file(path: &Path) -> Result<()> {
84    // Simply read the file to get it into OS cache
85    let _ = fs::read(path)?;
86    Ok(())
87}
88
89/// Collect all files that should be tracked (respecting .gitignore-like rules)
90fn collect_trackable_files(root: &Path) -> Result<Vec<PathBuf>> {
91    use ignore::WalkBuilder;
92    
93    let mut files = Vec::new();
94    
95    let walker = WalkBuilder::new(root)
96        .hidden(false)
97        .git_ignore(true)
98        .git_global(true)
99        .git_exclude(true)
100        .max_depth(None)
101        .follow_links(false)
102        .build();
103    
104    for entry in walker {
105        if let Ok(entry) = entry {
106            let path = entry.path();
107            
108            // Skip if not a file
109            if !path.is_file() {
110                continue;
111            }
112            
113            // Skip if in ignored directories
114            if !is_trackable(path) {
115                continue;
116            }
117            
118            // Skip if too large
119            if let Ok(metadata) = fs::metadata(path) {
120                if metadata.len() > MAX_FILE_SIZE {
121                    continue;
122                }
123            }
124            
125            files.push(path.to_path_buf());
126        }
127    }
128    
129    Ok(files)
130}
131
132fn is_trackable(path: &Path) -> bool {
133    use std::path::Component;
134    
135    const IGNORED_COMPONENTS: [&str; 5] = [".git", ".dx", ".dx_client", "target", "node_modules"];
136    
137    for component in path.components() {
138        if let Component::Normal(seg) = component {
139            if let Some(segment) = seg.to_str() {
140                let lower = segment.to_ascii_lowercase();
141                if IGNORED_COMPONENTS.iter().any(|needle| needle == &lower) {
142                    return false;
143                }
144            }
145        }
146    }
147    
148    true
149}
150
151#[derive(Debug, Default, Clone)]
152#[allow(dead_code)]
153pub struct CacheStats {
154    pub files_cached: usize,
155    pub bytes_cached: usize,
156    pub duration_ms: u64,
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use std::fs;
163    use tempfile::TempDir;
164    
165    #[test]
166    fn test_collect_trackable_files() {
167        let temp_dir = TempDir::new().unwrap();
168        let root = temp_dir.path();
169        
170        // Create test structure
171        fs::create_dir_all(root.join("src")).unwrap();
172        fs::write(root.join("src/main.rs"), "fn main() {}").unwrap();
173        fs::write(root.join("README.md"), "# Test").unwrap();
174        
175        fs::create_dir_all(root.join(".git")).unwrap();
176        fs::write(root.join(".git/config"), "ignored").unwrap();
177        
178        let files = collect_trackable_files(root).unwrap();
179        
180        assert!(files.iter().any(|p| p.ends_with("main.rs")));
181        assert!(files.iter().any(|p| p.ends_with("README.md")));
182        assert!(!files.iter().any(|p| p.to_str().unwrap().contains(".git")));
183    }
184    
185    #[test]
186    fn test_warm_cache() {
187        let temp_dir = TempDir::new().unwrap();
188        let root = temp_dir.path();
189        
190        fs::write(root.join("test.txt"), "test content").unwrap();
191        
192        let stats = warm_cache(root).unwrap();
193        assert!(stats.files_cached > 0);
194        assert!(stats.bytes_cached > 0);
195    }
196}