1use std::collections::HashSet;
2use std::fs::File;
3use std::io::{self, Read};
4use std::path::{Path, PathBuf};
5use std::sync::atomic::{AtomicUsize, Ordering};
6use std::sync::{Mutex, OnceLock};
7
8const LARGE_FILE_CACHE_ADVICE_MIN_BYTES: u64 = 64 * 1024 * 1024;
9
10static EVICTION_DEFERRAL_DEPTH: AtomicUsize = AtomicUsize::new(0);
20
21fn deferred_evictions() -> &'static Mutex<HashSet<PathBuf>> {
22 static DEFERRED: OnceLock<Mutex<HashSet<PathBuf>>> = OnceLock::new();
23 DEFERRED.get_or_init(|| Mutex::new(HashSet::new()))
24}
25
26pub struct CacheEvictionDeferral(());
33
34impl CacheEvictionDeferral {
35 pub fn acquire() -> Self {
43 EVICTION_DEFERRAL_DEPTH.fetch_add(1, Ordering::SeqCst);
44 Self(())
45 }
46}
47
48impl Drop for CacheEvictionDeferral {
49 fn drop(&mut self) {
50 if EVICTION_DEFERRAL_DEPTH.fetch_sub(1, Ordering::SeqCst) != 1 {
51 return;
52 }
53 let drained: Vec<PathBuf> = {
54 let mut deferred = deferred_evictions()
55 .lock()
56 .unwrap_or_else(std::sync::PoisonError::into_inner);
57 deferred.drain().collect()
58 };
59 for path in drained {
60 evict_path_now(&path);
61 }
62 }
63}
64
65fn eviction_deferred(path: &Path) -> bool {
66 if EVICTION_DEFERRAL_DEPTH.load(Ordering::SeqCst) == 0 {
67 return false;
68 }
69 deferred_evictions()
70 .lock()
71 .unwrap_or_else(std::sync::PoisonError::into_inner)
72 .insert(path.to_path_buf());
73 true
74}
75
76fn evict_path_now(path: &Path) {
77 #[cfg(unix)]
84 let opened = {
85 use std::os::unix::fs::OpenOptionsExt;
86 std::fs::OpenOptions::new()
87 .read(true)
88 .custom_flags(libc::O_NONBLOCK | libc::O_NOFOLLOW)
89 .open(path)
90 };
91 #[cfg(not(unix))]
92 let opened = File::open(path);
93 let Ok(file) = opened else {
94 return;
95 };
96 let Ok(metadata) = file.metadata() else {
97 return;
98 };
99 if !metadata.is_file() || metadata.len() < LARGE_FILE_CACHE_ADVICE_MIN_BYTES {
100 return;
101 }
102 log_cache_advice(
103 "dontneed",
104 path,
105 raw_cache_advice(&file, 0, metadata.len(), CacheAdvice::DontNeed),
106 );
107}
108
109pub(crate) struct CacheAdvisedReader {
110 file: File,
111 path: PathBuf,
112 file_len: u64,
113 touched: u64,
114}
115
116impl CacheAdvisedReader {
117 pub(crate) fn open(path: &Path) -> io::Result<Self> {
118 let file = File::open(path)?;
119 let file_len = file.metadata()?.len();
120 advise_sequential(&file, path, file_len);
121 Ok(Self {
122 file,
123 path: path.to_path_buf(),
124 file_len,
125 touched: 0,
126 })
127 }
128}
129
130impl Read for CacheAdvisedReader {
131 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
132 let read = self.file.read(buf)?;
133 self.touched = self.touched.saturating_add(read as u64);
134 Ok(read)
135 }
136}
137
138impl Drop for CacheAdvisedReader {
139 fn drop(&mut self) {
140 drop_touched_file_cache(&self.file, &self.path, self.file_len, 0, self.touched);
141 }
142}
143
144pub(crate) fn read_to_vec(path: &Path) -> io::Result<Vec<u8>> {
145 let mut reader = CacheAdvisedReader::open(path)?;
146 let mut data = Vec::new();
147 reader.read_to_end(&mut data)?;
148 Ok(data)
149}
150
151pub(crate) fn advise_sequential(file: &File, path: &Path, len: u64) {
152 advise_range_sequential(file, path, 0, len);
153}
154
155pub(crate) fn advise_range_sequential(file: &File, path: &Path, offset: u64, len: u64) {
156 if len < LARGE_FILE_CACHE_ADVICE_MIN_BYTES {
157 return;
158 }
159 log_cache_advice(
160 "sequential",
161 path,
162 raw_cache_advice(file, offset, len, CacheAdvice::Sequential),
163 );
164}
165
166pub(crate) fn drop_file_cache(file: &File, path: &Path, offset: u64, len: u64) {
167 if len < LARGE_FILE_CACHE_ADVICE_MIN_BYTES {
168 return;
169 }
170 if eviction_deferred(path) {
171 return;
172 }
173 log_cache_advice(
174 "dontneed",
175 path,
176 raw_cache_advice(file, offset, len, CacheAdvice::DontNeed),
177 );
178}
179
180pub(crate) fn drop_touched_file_cache(
181 file: &File,
182 path: &Path,
183 file_len: u64,
184 offset: u64,
185 touched: u64,
186) {
187 if file_len < LARGE_FILE_CACHE_ADVICE_MIN_BYTES || touched == 0 {
188 return;
189 }
190 if eviction_deferred(path) {
191 return;
192 }
193 log_cache_advice(
194 "dontneed",
195 path,
196 raw_cache_advice(file, offset, touched, CacheAdvice::DontNeed),
197 );
198}
199
200pub(crate) fn drop_path_cache(path: &Path) {
201 let Ok(file) = File::open(path) else {
202 return;
203 };
204 let len = file.metadata().ok().map_or(0, |metadata| metadata.len());
205 drop_file_cache(&file, path, 0, len);
206}
207
208fn log_cache_advice(operation: &'static str, path: &Path, result: io::Result<()>) {
209 match result {
210 Ok(()) => tracing::trace!(operation, path = %path.display(), "file cache advice applied"),
211 Err(error) => {
212 tracing::debug!(operation, path = %path.display(), error = %error, "file cache advice failed")
213 }
214 }
215}
216
217#[derive(Clone, Copy)]
218enum CacheAdvice {
219 Sequential,
220 DontNeed,
221}
222
223#[cfg(any(target_os = "linux", target_os = "android"))]
224fn raw_cache_advice(file: &File, offset: u64, len: u64, advice: CacheAdvice) -> io::Result<()> {
225 use std::os::fd::AsRawFd;
226
227 let offset: libc::off_t = offset
228 .try_into()
229 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "cache advice offset overflow"))?;
230 let len: libc::off_t = len
231 .try_into()
232 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "cache advice length overflow"))?;
233 let advice = match advice {
234 CacheAdvice::Sequential => libc::POSIX_FADV_SEQUENTIAL,
235 CacheAdvice::DontNeed => libc::POSIX_FADV_DONTNEED,
236 };
237 let rc = unsafe { libc::posix_fadvise(file.as_raw_fd(), offset, len, advice) };
238 if rc == 0 {
239 Ok(())
240 } else {
241 Err(io::Error::from_raw_os_error(rc))
242 }
243}
244
245#[cfg(not(any(target_os = "linux", target_os = "android")))]
246fn raw_cache_advice(_file: &File, _offset: u64, _len: u64, _advice: CacheAdvice) -> io::Result<()> {
247 Ok(())
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn read_to_vec_preserves_contents() {
256 let temp = tempfile::NamedTempFile::new().unwrap();
257 std::fs::write(temp.path(), b"cache-advised payload").unwrap();
258
259 assert_eq!(read_to_vec(temp.path()).unwrap(), b"cache-advised payload");
260 }
261
262 #[test]
263 fn path_drop_swallows_missing_file() {
264 drop_path_cache(Path::new("/definitely/missing/weaver/par2-cache.bin"));
265 }
266
267 static DEFERRAL_TEST_LOCK: Mutex<()> = Mutex::new(());
270
271 #[test]
272 fn eviction_deferral_records_and_drains() {
273 let _serial = DEFERRAL_TEST_LOCK
274 .lock()
275 .unwrap_or_else(std::sync::PoisonError::into_inner);
276 let temp = tempfile::NamedTempFile::new().unwrap();
277 let file = File::open(temp.path()).unwrap();
278 {
279 let _outer = CacheEvictionDeferral::acquire();
280 let _inner = CacheEvictionDeferral::acquire();
281 assert!(EVICTION_DEFERRAL_DEPTH.load(Ordering::SeqCst) >= 2);
282 drop_file_cache(&file, temp.path(), 0, LARGE_FILE_CACHE_ADVICE_MIN_BYTES + 1);
284 assert!(
285 deferred_evictions()
286 .lock()
287 .unwrap_or_else(std::sync::PoisonError::into_inner)
288 .contains(temp.path())
289 );
290 }
291 if EVICTION_DEFERRAL_DEPTH.load(Ordering::SeqCst) == 0 {
295 assert!(
296 !deferred_evictions()
297 .lock()
298 .unwrap_or_else(std::sync::PoisonError::into_inner)
299 .contains(temp.path())
300 );
301 }
302 }
303
304 #[test]
305 fn below_threshold_drops_are_never_deferred() {
306 let _serial = DEFERRAL_TEST_LOCK
307 .lock()
308 .unwrap_or_else(std::sync::PoisonError::into_inner);
309 let temp = tempfile::NamedTempFile::new().unwrap();
310 let file = File::open(temp.path()).unwrap();
311 let _scope = CacheEvictionDeferral::acquire();
312 drop_file_cache(&file, temp.path(), 0, 1024);
313 drop_touched_file_cache(&file, temp.path(), 1024, 0, 1024);
314 assert!(
315 !deferred_evictions()
316 .lock()
317 .unwrap_or_else(std::sync::PoisonError::into_inner)
318 .contains(temp.path())
319 );
320 }
321
322 #[cfg(not(any(target_os = "linux", target_os = "android")))]
323 #[test]
324 fn cache_advice_noops_on_unsupported_platforms() {
325 let temp = tempfile::NamedTempFile::new().unwrap();
326 let file = File::open(temp.path()).unwrap();
327
328 assert!(raw_cache_advice(&file, 0, 0, CacheAdvice::Sequential).is_ok());
329 assert!(raw_cache_advice(&file, 0, 0, CacheAdvice::DontNeed).is_ok());
330 }
331}