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 = 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)
128            .map_err(io_err)?
129    });
130
131    Ok(Some(PreparedFile {
132        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 = 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)
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 = std::fs::File::open(f.path()).unwrap();
276                    let mmap_check =
277                        unsafe { memmap2::MmapOptions::new().len(size).map(&file).unwrap() };
278                    let residency: $t = crate::mincore::residency(&mmap_check, size).unwrap();
279                    assert!(
280                        (0..residency.len()).all(|i| residency[i..i + 1].count_filled() == 1),
281                        "expected all pages resident after touch"
282                    );
283                }
284
285                #[test]
286                fn evict_succeeds() {
287                    let (f, _) = create_temp_file(4);
288                    let range = FileRange {
289                        offset: 0,
290                        max_len: None,
291                    };
292                    let result: crate::Result<Option<CountsResult<()>>> =
293                        mode::counts_process_file::<Evict, $t>(&Evict, f.path(), &range);
294                    assert!(result.is_ok());
295                }
296
297                #[test]
298                fn full_residency() {
299                    let (f, _) = create_temp_file(4);
300                    let range = FileRange {
301                        offset: 0,
302                        max_len: None,
303                    };
304                    let result: FullResult<(), $t> =
305                        mode::full_process_file::<Query, $t>(&Query, f.path(), &range, None)
306                            .unwrap()
307                            .unwrap();
308                    assert!(result.residency_after.is_some());
309                    assert_eq!(result.residency_after.unwrap().len(), 4);
310                }
311
312                #[test]
313                fn query_full_reuses_before() {
314                    let (f, _) = create_temp_file(4);
315                    let range = FileRange {
316                        offset: 0,
317                        max_len: None,
318                    };
319                    let result: FullResult<(), $t> =
320                        mode::full_process_file::<Query, $t>(&Query, f.path(), &range, None)
321                            .unwrap()
322                            .unwrap();
323                    assert_eq!(result.pages_in_core_before, result.pages_in_core_after);
324                    assert!(result.residency_before.is_none());
325                    assert!(result.residency_after.is_some());
326                }
327
328                #[test]
329                fn counts_without_bitmap() {
330                    let (f, _) = create_temp_file(4);
331                    let range = FileRange {
332                        offset: 0,
333                        max_len: None,
334                    };
335                    let result = mode::counts_process_file::<Query, $t>(&Query, f.path(), &range)
336                        .unwrap()
337                        .unwrap();
338                    assert_eq!(result.total_pages, 4);
339                }
340
341                #[test]
342                fn nonexistent_returns_error() {
343                    let range = FileRange {
344                        offset: 0,
345                        max_len: None,
346                    };
347                    let result: crate::Result<Option<CountsResult<()>>> =
348                        mode::counts_process_file::<Query, $t>(
349                            &Query,
350                            std::path::Path::new("/nonexistent/file.dat"),
351                            &range,
352                        );
353                    assert!(result.is_err());
354                }
355
356                #[test]
357                fn file_info_pages() {
358                    let (f, _) = create_temp_file(4);
359                    let range = FileRange {
360                        offset: 0,
361                        max_len: None,
362                    };
363                    let info: FileInfo<$t> = file_info(f.path(), &range).unwrap().unwrap();
364                    assert_eq!(info.total_pages, 4);
365                    assert_eq!(info.residency.len(), 4);
366                }
367            }
368        };
369    }
370
371    process_tests!(Vec<bool>, vec_bool_impl);
372
373    #[cfg(feature = "bitvec")]
374    process_tests!(::bitvec::vec::BitVec, bitvec_impl);
375
376    #[test]
377    fn test_stats_default() {
378        let stats = Stats::default();
379        assert_eq!(stats.total_pages.load(Ordering::Relaxed), 0);
380        assert_eq!(stats.initial_pages_in_core.load(Ordering::Relaxed), 0);
381        assert_eq!(stats.action_pages.load(Ordering::Relaxed), 0);
382        assert_eq!(stats.total_files.load(Ordering::Relaxed), 0);
383        assert_eq!(stats.total_dirs.load(Ordering::Relaxed), 0);
384    }
385}