1use std::fs::File;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5use memmap2::MmapOptions;
6
7use super::{FileInfo, FileRange};
8use crate::Error;
9use crate::mincore::PageMap;
10
11#[derive(Debug, Clone, PartialEq)]
12pub struct FullResult<O, PM> {
13 pub output: O,
14 pub total_pages: usize,
15 pub pages_in_core_before: usize,
16 pub pages_in_core_after: usize,
17 pub residency_before: Option<PM>,
18 pub residency_after: Option<PM>,
19}
20
21#[derive(Debug, Clone, PartialEq)]
22pub struct CountsResult<O> {
23 pub output: O,
24 pub total_pages: usize,
25 pub pages_in_core_before: usize,
26 pub pages_in_core_after: usize,
27}
28
29pub(crate) struct PreparedFile {
30 pub path: PathBuf,
31 pub file: File,
32 pub mmap: Arc<memmap2::Mmap>,
33 range: ResolvedRange,
34}
35
36impl PreparedFile {
37 pub(crate) fn offset(&self) -> u64 {
38 self.range.offset
39 }
40
41 pub(crate) fn len(&self) -> usize {
42 self.range.len
43 }
44
45 pub(crate) fn total_pages(&self) -> usize {
46 self.range.len.div_ceil(*crate::pagesize::PAGE_SIZE)
47 }
48}
49
50struct ResolvedRange {
51 offset: u64,
52 len: usize,
53}
54
55impl ResolvedRange {
56 fn for_file(path: &Path, file_len: u64, range: &FileRange) -> crate::Result<Option<Self>> {
57 range.validate()?;
58 if file_len == 0 {
59 return Ok(None);
60 }
61
62 let offset = range.offset();
63 if offset >= file_len {
64 return Err(Error::OffsetBeyondFile {
65 path: path.to_path_buf(),
66 offset,
67 file_len,
68 });
69 }
70
71 let available = file_len - offset;
72 let len = range.max_len().unwrap_or(available).min(available);
73 Ok(Some(Self {
74 offset,
75 len: len.try_into()?,
76 }))
77 }
78}
79
80pub(crate) fn prepare_file(path: &Path, range: &FileRange) -> crate::Result<Option<PreparedFile>> {
81 let io_err = |e| Error::io(path.display().to_string(), e);
82
83 let file = fs_err::File::open(path).map_err(io_err)?;
84 let file_len = file.metadata().map_err(io_err)?.len();
85
86 let Some(resolved) = ResolvedRange::for_file(path, file_len, range)? else {
87 return Ok(None);
88 };
89
90 let mmap = Arc::new(unsafe {
91 MmapOptions::new()
92 .offset(resolved.offset)
93 .len(resolved.len)
94 .map(file.file())
95 .map_err(io_err)?
96 });
97
98 Ok(Some(PreparedFile {
99 path: path.to_path_buf(),
100 file: file.into_file(),
101 mmap,
102 range: resolved,
103 }))
104}
105
106pub fn file_info<PM: PageMap>(
107 path: &Path,
108 range: &FileRange,
109) -> crate::Result<Option<FileInfo<PM>>> {
110 let Some(prepared) = prepare_file(path, range)? else {
111 return Ok(None);
112 };
113 let residency: PM = crate::mincore::residency(&prepared.mmap, prepared.len())?;
114 Ok(Some(FileInfo {
115 total_pages: prepared.total_pages(),
116 residency,
117 }))
118}
119
120#[cfg(test)]
121mod tests {
122 use crate::mincore::PageMapSlice as _;
123 use crate::mode;
124
125 use super::*;
126 use std::io::Write;
127 use std::sync::atomic::Ordering;
128
129 use super::super::*;
130
131 struct AdvisoryNoop;
132
133 impl Op for AdvisoryNoop {
134 const LABEL: &str = "advisory noop";
135 const EFFECT: ResidencyEffect = ResidencyEffect::EvictAdvisory;
136 type Output = ();
137
138 fn execute<PM: PageMap + Sync>(&self, _ctx: &FileContext<'_, PM>) -> crate::Result<()> {
139 Ok(())
140 }
141 }
142
143 fn create_temp_file(pages: usize) -> (tempfile::NamedTempFile, usize) {
144 let page_size = *crate::pagesize::PAGE_SIZE;
145 let size = page_size * pages;
146 let mut f = tempfile::NamedTempFile::new().unwrap();
147 f.write_all(&vec![0xABu8; size]).unwrap();
148 f.flush().unwrap();
149 (f, size)
150 }
151
152 fn not_cancelled() -> &'static crate::Cancellation {
153 static CANCELLATION: std::sync::LazyLock<crate::Cancellation> =
154 std::sync::LazyLock::new(crate::Cancellation::new);
155 &CANCELLATION
156 }
157
158 macro_rules! process_tests {
159 ($t:ty, $mod:ident) => {
160 mod $mod {
161 use super::*;
162
163 #[test]
164 fn query_counts_pages() {
165 let (f, _) = create_temp_file(4);
166 let range = FileRange {
167 offset: 0,
168 max_len: None,
169 };
170 let result = mode::counts_process_file::<Query, $t>(
171 &Query,
172 f.path(),
173 &range,
174 not_cancelled(),
175 )
176 .unwrap()
177 .unwrap();
178 assert_eq!(result.total_pages, 4);
179 }
180
181 #[test]
182 fn empty_file_returns_none() {
183 let f = tempfile::NamedTempFile::new().unwrap();
184 let range = FileRange {
185 offset: 0,
186 max_len: None,
187 };
188 let result: Option<CountsResult<()>> = mode::counts_process_file::<Query, $t>(
189 &Query,
190 f.path(),
191 &range,
192 not_cancelled(),
193 )
194 .unwrap();
195 assert!(result.is_none());
196 }
197
198 #[test]
199 fn offset_beyond_file() {
200 let (f, _) = create_temp_file(1);
201 let range = FileRange {
202 offset: (*crate::pagesize::PAGE_SIZE * 100) as u64,
203 max_len: None,
204 };
205 let result: crate::Result<Option<CountsResult<()>>> =
206 mode::counts_process_file::<Query, $t>(
207 &Query,
208 f.path(),
209 &range,
210 not_cancelled(),
211 );
212 assert!(result.is_err());
213 let err = result.unwrap_err();
214 assert!(
215 matches!(err, Error::OffsetBeyondFile { .. }),
216 "expected OffsetBeyondFile, got: {err}"
217 );
218 }
219
220 #[test]
221 fn with_max_len() {
222 let (f, _) = create_temp_file(8);
223 let page_size = *crate::pagesize::PAGE_SIZE;
224 let range = FileRange {
225 offset: 0,
226 max_len: Some((page_size * 2) as u64),
227 };
228 let result = mode::counts_process_file::<Query, $t>(
229 &Query,
230 f.path(),
231 &range,
232 not_cancelled(),
233 )
234 .unwrap()
235 .unwrap();
236 assert_eq!(result.total_pages, 2);
237 }
238
239 #[test]
240 fn touch_makes_resident() {
241 let (f, size) = create_temp_file(4);
242 let range = FileRange {
243 offset: 0,
244 max_len: None,
245 };
246
247 mode::counts_process_file::<Evict, $t>(
248 &Evict,
249 f.path(),
250 &range,
251 not_cancelled(),
252 )
253 .unwrap();
254 mode::counts_process_file::<Touch, $t>(
255 &Touch,
256 f.path(),
257 &range,
258 not_cancelled(),
259 )
260 .unwrap();
261
262 let file = fs_err::File::open(f.path()).unwrap();
263 let mmap_check = unsafe {
264 memmap2::MmapOptions::new()
265 .len(size)
266 .map(file.file())
267 .unwrap()
268 };
269 let residency: $t = crate::mincore::residency(&mmap_check, size).unwrap();
270 assert!(
271 (0..residency.len()).all(|i| residency[i..i + 1].count_filled() == 1),
272 "expected all pages resident after touch"
273 );
274 }
275
276 #[test]
277 fn evict_succeeds() {
278 let (f, _) = create_temp_file(4);
279 let range = FileRange {
280 offset: 0,
281 max_len: None,
282 };
283 let result: crate::Result<Option<CountsResult<()>>> =
284 mode::counts_process_file::<Evict, $t>(
285 &Evict,
286 f.path(),
287 &range,
288 not_cancelled(),
289 );
290 assert!(result.is_ok());
291 }
292
293 #[test]
294 fn full_residency() {
295 let (f, _) = create_temp_file(4);
296 let range = FileRange {
297 offset: 0,
298 max_len: None,
299 };
300 let result: FullResult<(), $t> = mode::full_process_file::<Query, $t>(
301 &Query,
302 f.path(),
303 &range,
304 None,
305 None,
306 not_cancelled(),
307 )
308 .unwrap()
309 .unwrap();
310 assert!(result.residency_after.is_some());
311 assert_eq!(result.residency_after.unwrap().len(), 4);
312 }
313
314 #[test]
315 fn query_full_reuses_before() {
316 let (f, _) = create_temp_file(4);
317 let range = FileRange {
318 offset: 0,
319 max_len: None,
320 };
321 let result: FullResult<(), $t> = mode::full_process_file::<Query, $t>(
322 &Query,
323 f.path(),
324 &range,
325 None,
326 None,
327 not_cancelled(),
328 )
329 .unwrap()
330 .unwrap();
331 assert_eq!(result.pages_in_core_before, result.pages_in_core_after);
332 assert!(result.residency_before.is_none());
333 assert!(result.residency_after.is_some());
334 }
335
336 #[test]
337 fn counts_without_bitmap() {
338 let (f, _) = create_temp_file(4);
339 let range = FileRange {
340 offset: 0,
341 max_len: None,
342 };
343 let result = mode::counts_process_file::<Query, $t>(
344 &Query,
345 f.path(),
346 &range,
347 not_cancelled(),
348 )
349 .unwrap()
350 .unwrap();
351 assert_eq!(result.total_pages, 4);
352 }
353
354 #[test]
355 fn nonexistent_returns_error() {
356 let range = FileRange {
357 offset: 0,
358 max_len: None,
359 };
360 let result: crate::Result<Option<CountsResult<()>>> =
361 mode::counts_process_file::<Query, $t>(
362 &Query,
363 std::path::Path::new("/nonexistent/file.dat"),
364 &range,
365 not_cancelled(),
366 );
367 assert!(result.is_err());
368 }
369
370 #[test]
371 fn file_info_pages() {
372 let (f, _) = create_temp_file(4);
373 let range = FileRange {
374 offset: 0,
375 max_len: None,
376 };
377 let info: FileInfo<$t> = file_info(f.path(), &range).unwrap().unwrap();
378 assert_eq!(info.total_pages, 4);
379 assert_eq!(info.residency.len(), 4);
380 }
381 }
382 };
383 }
384
385 process_tests!(Vec<bool>, vec_bool_impl);
386
387 #[cfg(feature = "bitvec")]
388 process_tests!(::bitvec::vec::BitVec, bitvec_impl);
389
390 #[test]
391 fn test_stats_default() {
392 let stats = Stats::default();
393 assert_eq!(stats.total_pages.load(Ordering::Relaxed), 0);
394 assert_eq!(stats.initial_pages_in_core.load(Ordering::Relaxed), 0);
395 assert_eq!(stats.action_pages.load(Ordering::Relaxed), 0);
396 assert_eq!(stats.total_files.load(Ordering::Relaxed), 0);
397 assert_eq!(stats.total_dirs.load(Ordering::Relaxed), 0);
398 }
399
400 #[test]
401 fn zero_length_range_is_rejected() {
402 let (file, _) = create_temp_file(1);
403 let range = FileRange {
404 offset: 0,
405 max_len: Some(0),
406 };
407
408 assert!(matches!(
409 prepare_file(file.path(), &range),
410 Err(Error::EmptyRange)
411 ));
412 }
413
414 #[test]
415 fn unaligned_range_is_rejected() {
416 let (file, _) = create_temp_file(2);
417 let range = FileRange {
418 offset: 1,
419 max_len: Some(*crate::pagesize::PAGE_SIZE as u64),
420 };
421
422 assert!(matches!(
423 prepare_file(file.path(), &range),
424 Err(Error::UnalignedRange { .. })
425 ));
426 }
427
428 #[test]
429 fn overflowing_range_is_rejected_without_panicking() {
430 let (file, _) = create_temp_file(2);
431 let range = FileRange {
432 offset: *crate::pagesize::PAGE_SIZE as u64,
433 max_len: Some(u64::MAX),
434 };
435
436 assert!(matches!(
437 prepare_file(file.path(), &range),
438 Err(Error::RangeOverflow { .. })
439 ));
440 }
441
442 #[test]
443 fn advisory_effect_measures_the_after_state() {
444 let (f, _) = create_temp_file(4);
445 let range = FileRange {
446 offset: 0,
447 max_len: None,
448 };
449
450 let result = mode::counts_process_file::<AdvisoryNoop, Vec<bool>>(
451 &AdvisoryNoop,
452 f.path(),
453 &range,
454 not_cancelled(),
455 )
456 .unwrap()
457 .unwrap();
458
459 assert!(result.pages_in_core_before > 0);
460 assert_eq!(
461 result.pages_in_core_after, result.pages_in_core_before,
462 "an advisory operation must measure rather than assume its after-state"
463 );
464 }
465}