rs_web/lua/
tracker.rs

1//! Build dependency tracker for incremental builds
2
3use 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}