1use dashmap::DashMap;
4use parking_lot::Mutex;
5use std::cell::RefCell;
6use std::collections::HashMap;
7use std::hash::{Hash, Hasher};
8use std::path::PathBuf;
9use std::sync::Arc;
10use std::time::SystemTime;
11use twox_hash::XxHash64;
12
13#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
14pub struct FileState {
15 pub hash: u64,
16 pub mtime_secs: u64,
17 pub mtime_nanos: u32,
18}
19
20impl FileState {
21 pub fn new(hash: u64, mtime: SystemTime) -> Self {
22 let duration = mtime
23 .duration_since(SystemTime::UNIX_EPOCH)
24 .unwrap_or_default();
25 Self {
26 hash,
27 mtime_secs: duration.as_secs(),
28 mtime_nanos: duration.subsec_nanos(),
29 }
30 }
31
32 pub fn mtime(&self) -> SystemTime {
33 SystemTime::UNIX_EPOCH + std::time::Duration::new(self.mtime_secs, self.mtime_nanos)
34 }
35}
36
37thread_local! {
38 static LOCAL_READS: RefCell<Vec<(PathBuf, FileState)>> = RefCell::default();
39 static LOCAL_WRITES: RefCell<Vec<(PathBuf, FileState)>> = RefCell::default();
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Hash)]
43pub struct MemoKey {
44 pub function: &'static str,
45 pub input_hash: u64,
46}
47
48#[derive(Debug)]
49pub struct BuildTracker {
50 reads: Mutex<HashMap<PathBuf, FileState>>,
51 writes: Mutex<HashMap<PathBuf, FileState>>,
52 memo: DashMap<MemoKey, Vec<u8>>,
53 enabled: bool,
54}
55
56impl Default for BuildTracker {
57 fn default() -> Self {
58 Self::new()
59 }
60}
61
62impl BuildTracker {
63 pub fn new() -> Self {
64 Self {
65 reads: Mutex::new(HashMap::new()),
66 writes: Mutex::new(HashMap::new()),
67 memo: DashMap::new(),
68 enabled: true,
69 }
70 }
71
72 pub fn disabled() -> Self {
73 Self {
74 reads: Mutex::new(HashMap::new()),
75 writes: Mutex::new(HashMap::new()),
76 memo: DashMap::new(),
77 enabled: false,
78 }
79 }
80
81 pub fn is_enabled(&self) -> bool {
82 self.enabled
83 }
84
85 pub fn record_read(&self, path: PathBuf, content: &[u8]) {
86 if !self.enabled {
87 return;
88 }
89 let hash = hash_content(content);
90 let mtime = std::fs::metadata(&path)
91 .and_then(|m| m.modified())
92 .unwrap_or(SystemTime::UNIX_EPOCH);
93 LOCAL_READS.with(|reads| {
94 reads.borrow_mut().push((path, FileState::new(hash, mtime)));
95 });
96 }
97
98 pub fn record_read_with_hash(&self, path: PathBuf, hash: u64, mtime: SystemTime) {
99 if !self.enabled {
100 return;
101 }
102 LOCAL_READS.with(|reads| {
103 reads.borrow_mut().push((path, FileState::new(hash, mtime)));
104 });
105 }
106
107 pub fn record_write(&self, path: PathBuf, content: &[u8]) {
108 if !self.enabled {
109 return;
110 }
111 let hash = hash_content(content);
112 let mtime = std::fs::metadata(&path)
113 .and_then(|m| m.modified())
114 .unwrap_or(SystemTime::now());
115 LOCAL_WRITES.with(|writes| {
116 writes
117 .borrow_mut()
118 .push((path, FileState::new(hash, mtime)));
119 });
120 }
121
122 pub fn merge_thread_locals(&self) {
123 if !self.enabled {
124 return;
125 }
126 LOCAL_READS.with(|reads| {
127 let mut local = reads.borrow_mut();
128 if !local.is_empty() {
129 let mut main = self.reads.lock();
130 for (path, state) in local.drain(..) {
131 main.insert(path, state);
132 }
133 }
134 });
135 LOCAL_WRITES.with(|writes| {
136 let mut local = writes.borrow_mut();
137 if !local.is_empty() {
138 let mut main = self.writes.lock();
139 for (path, state) in local.drain(..) {
140 main.insert(path, state);
141 }
142 }
143 });
144 }
145
146 pub fn merge_all_threads(&self) {
147 if !self.enabled {
148 return;
149 }
150 self.merge_thread_locals();
151 rayon::broadcast(|_| {
152 self.merge_thread_locals();
153 });
154 }
155
156 pub fn memo_get(&self, function: &'static str, input_hash: u64) -> Option<Vec<u8>> {
157 if !self.enabled {
158 return None;
159 }
160 let key = MemoKey {
161 function,
162 input_hash,
163 };
164 self.memo.get(&key).map(|v| v.clone())
165 }
166
167 pub fn memo_set(&self, function: &'static str, input_hash: u64, output: Vec<u8>) {
168 if !self.enabled {
169 return;
170 }
171 let key = MemoKey {
172 function,
173 input_hash,
174 };
175 self.memo.insert(key, output);
176 }
177
178 pub fn get_reads(&self) -> HashMap<PathBuf, FileState> {
179 self.merge_thread_locals();
180 self.reads.lock().clone()
181 }
182
183 pub fn get_writes(&self) -> HashMap<PathBuf, FileState> {
184 self.merge_thread_locals();
185 self.writes.lock().clone()
186 }
187
188 pub fn clear(&self) {
189 LOCAL_READS.with(|r| r.borrow_mut().clear());
190 LOCAL_WRITES.with(|w| w.borrow_mut().clear());
191 self.reads.lock().clear();
192 self.writes.lock().clear();
193 self.memo.clear();
194 }
195
196 pub fn get_changed_files(&self, cached: &CachedDeps) -> Vec<PathBuf> {
197 let mut changed = Vec::new();
198 for (path, old_state) in &cached.reads {
199 if let Ok(metadata) = std::fs::metadata(path) {
200 if let Ok(mtime) = metadata.modified() {
201 if mtime != old_state.mtime() {
202 if let Ok(content) = std::fs::read(path) {
203 if hash_content(&content) != old_state.hash {
204 changed.push(path.clone());
205 }
206 } else {
207 changed.push(path.clone());
208 }
209 }
210 } else {
211 changed.push(path.clone());
212 }
213 } else {
214 changed.push(path.clone());
215 }
216 }
217 changed
218 }
219}
220
221#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
222pub struct CachedDeps {
223 pub reads: HashMap<PathBuf, FileState>,
224 pub writes: HashMap<PathBuf, FileState>,
225}
226
227impl CachedDeps {
228 pub fn from_tracker(tracker: &BuildTracker) -> Self {
229 Self {
230 reads: tracker.get_reads(),
231 writes: tracker.get_writes(),
232 }
233 }
234
235 pub fn load(path: &std::path::Path) -> Option<Self> {
236 let content = std::fs::read(path).ok()?;
237 postcard::from_bytes(&content).ok()
238 }
239
240 pub fn save(&self, path: &std::path::Path) -> std::io::Result<()> {
241 if let Some(parent) = path.parent() {
242 std::fs::create_dir_all(parent)?;
243 }
244 let encoded = postcard::to_allocvec(self).map_err(std::io::Error::other)?;
245 std::fs::write(path, encoded)
246 }
247}
248
249pub fn hash_content(content: &[u8]) -> u64 {
250 let mut hasher = XxHash64::with_seed(0);
251 hasher.write(content);
252 hasher.finish()
253}
254
255pub fn hash_str(s: &str) -> u64 {
256 hash_content(s.as_bytes())
257}
258
259pub type SharedTracker = Arc<BuildTracker>;
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn test_hash_content() {
267 let content = b"hello world";
268 let hash1 = hash_content(content);
269 let hash2 = hash_content(content);
270 assert_eq!(hash1, hash2);
271
272 let different = b"hello world!";
273 let hash3 = hash_content(different);
274 assert_ne!(hash1, hash3);
275 }
276
277 #[test]
278 fn test_tracker_read_write() {
279 let tracker = BuildTracker::new();
280
281 tracker.record_read(PathBuf::from("test.txt"), b"content");
282 tracker.record_write(PathBuf::from("output.txt"), b"output");
283
284 tracker.merge_thread_locals();
285
286 let reads = tracker.get_reads();
287 let writes = tracker.get_writes();
288
289 assert_eq!(reads.len(), 1);
290 assert_eq!(writes.len(), 1);
291 }
292
293 #[test]
294 fn test_memo() {
295 let tracker = BuildTracker::new();
296
297 tracker.memo_set("render_markdown", 12345, b"cached".to_vec());
298
299 let cached = tracker.memo_get("render_markdown", 12345);
300 assert_eq!(cached, Some(b"cached".to_vec()));
301
302 let miss = tracker.memo_get("render_markdown", 99999);
303 assert_eq!(miss, None);
304 }
305}