dx_forge/watcher/
cache_warmer.rs1use 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; pub static FILE_POOL: Lazy<RwLock<HashMap<PathBuf, Arc<File>>>> = Lazy::new(|| RwLock::new(HashMap::new()));
18
19pub fn warm_cache(repo_root: &Path) -> Result<CacheStats> {
21 let start = Instant::now();
22
23 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 let cached_count = Arc::new(AtomicUsize::new(0));
36 let cached_bytes = Arc::new(AtomicUsize::new(0));
37
38 let handles: Vec<_> = files.par_iter()
41 .filter_map(|path| {
42 if let Ok(file) = File::open(path) {
44 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 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 Ok(CacheStats {
76 files_cached: final_count,
77 bytes_cached: final_bytes,
78 duration_ms: elapsed.as_millis() as u64,
79 })
80}
81
82pub fn warm_file(path: &Path) -> Result<()> {
84 let _ = fs::read(path)?;
86 Ok(())
87}
88
89fn 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 if !path.is_file() {
110 continue;
111 }
112
113 if !is_trackable(path) {
115 continue;
116 }
117
118 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 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}