Skip to main content

pagers_core/ops/
process.rs

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