Skip to main content

pagers_core/ops/
process.rs

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}