pagers-core 0.2.2

Core library for page cache diagnostics and control on Linux and macOS
Documentation
mod evict;
mod lock;
mod lockall;
mod process;
mod query;
mod touch;

use std::fs::File;
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;

use memmap2::Mmap;

use crate::Cancellation;
use crate::mincore::{DefaultPageMap, PageMap};

pub use evict::Evict;
pub use lock::{Lock, LockedFile};
pub use lockall::Lockall;
pub use process::{CountsResult, FullResult, file_info};
pub(crate) use process::{PreparedFile, prepare_file};
pub use query::Query;
pub use touch::Touch;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ResidencyEffect {
    Preserve,
    Populate,
    EvictAdvisory,
}

impl ResidencyEffect {
    pub const fn action_sign(self) -> isize {
        match self {
            Self::Preserve => 0,
            Self::Populate => 1,
            Self::EvictAdvisory => -1,
        }
    }

    pub const fn has_action(self) -> bool {
        !matches!(self, Self::Preserve)
    }

    pub const fn progress_resident(self) -> Option<bool> {
        match self {
            Self::Preserve => None,
            Self::Populate => Some(true),
            Self::EvictAdvisory => Some(false),
        }
    }

    pub fn action_pages(self, before: usize, after: usize) -> usize {
        match self {
            Self::Preserve => 0,
            Self::Populate => after.saturating_sub(before),
            Self::EvictAdvisory => before.saturating_sub(after),
        }
    }
}

pub trait Op: Sync {
    const LABEL: &str;
    const EFFECT: ResidencyEffect;

    type Output: Send;
    fn execute<PM: PageMap + Sync>(&self, ctx: &FileContext<'_, PM>)
    -> crate::Result<Self::Output>;

    fn finish(&self) -> crate::Result<()> {
        Ok(())
    }
}

pub struct FileContext<'a, PM: PageMap = DefaultPageMap> {
    prepared: PreparedFile,
    cancellation: Cancellation,
    on_progress: Option<&'a (dyn Fn(usize, usize) + Sync)>,
    residency: Option<&'a PM>,
}

impl<'a, PM: PageMap> FileContext<'a, PM> {
    pub(crate) fn new(prepared: PreparedFile, cancellation: Cancellation) -> Self {
        Self {
            prepared,
            cancellation,
            on_progress: None,
            residency: None,
        }
    }

    pub(crate) fn with_progress(
        mut self,
        on_progress: Option<&'a (dyn Fn(usize, usize) + Sync)>,
    ) -> Self {
        self.on_progress = on_progress;
        self
    }

    pub(crate) fn with_residency(mut self, residency: Option<&'a PM>) -> Self {
        self.residency = residency;
        self
    }

    pub fn file(&self) -> &File {
        &self.prepared.file
    }

    pub fn path(&self) -> &Path {
        &self.prepared.path
    }

    pub fn mmap(&self) -> &Mmap {
        &self.prepared.mmap
    }

    pub fn mapping(&self) -> Arc<Mmap> {
        Arc::clone(&self.prepared.mmap)
    }

    pub fn offset(&self) -> u64 {
        self.prepared.offset()
    }

    pub fn len(&self) -> usize {
        self.prepared.len()
    }

    pub fn is_empty(&self) -> bool {
        self.prepared.mmap.is_empty()
    }

    pub fn total_pages(&self) -> usize {
        self.prepared.total_pages()
    }

    pub fn residency(&self) -> Option<&PM> {
        self.residency
    }

    pub fn report_progress(&self, pages_walked: usize, action_count: usize) {
        if let Some(on_progress) = self.on_progress {
            on_progress(pages_walked, action_count);
        }
    }

    pub fn check_cancelled(&self) -> crate::Result<()> {
        self.cancellation.check()
    }
}

impl<PM: PageMap> std::fmt::Debug for FileContext<'_, PM> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("FileContext")
            .field("path", &self.path())
            .field("offset", &self.offset())
            .field("len", &self.len())
            .finish()
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FileRange {
    offset: u64,
    max_len: Option<u64>,
}

impl FileRange {
    pub fn full() -> Self {
        Self {
            offset: 0,
            max_len: None,
        }
    }

    pub fn new(offset: u64, max_len: Option<u64>) -> crate::Result<Self> {
        let range = Self { offset, max_len };
        range.validate()?;
        Ok(range)
    }

    pub fn offset(&self) -> u64 {
        self.offset
    }

    pub fn max_len(&self) -> Option<u64> {
        self.max_len
    }

    pub(crate) fn validate(&self) -> crate::Result<()> {
        let page_size = *crate::pagesize::PAGE_SIZE as u64;
        if !self.offset.is_multiple_of(page_size) {
            return Err(crate::Error::UnalignedRange {
                offset: self.offset,
                page_size,
            });
        }
        if self.max_len == Some(0) {
            return Err(crate::Error::EmptyRange);
        }
        if let Some(max_len) = self.max_len
            && self.offset.checked_add(max_len).is_none()
        {
            return Err(crate::Error::RangeOverflow {
                offset: self.offset,
                max_len,
            });
        }
        Ok(())
    }
}

#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FileInfo<PM = DefaultPageMap> {
    pub total_pages: usize,
    pub residency: PM,
}

#[derive(Debug, Default)]
pub struct Stats {
    pub total_pages: AtomicUsize,
    pub initial_pages_in_core: AtomicUsize,
    pub action_pages: AtomicUsize,
    pub total_files: AtomicUsize,
    pub total_dirs: AtomicUsize,
}

impl Stats {
    pub fn new() -> Self {
        Self::default()
    }
}

#[cfg(test)]
mod tests {
    use std::io::Write;
    use std::sync::Arc;

    use crate::Cancellation;

    use super::{FileContext, FileRange, Op, ResidencyEffect, Touch, prepare_file};

    #[test]
    fn residency_effect_derives_sign_and_delta() {
        assert_eq!(ResidencyEffect::Preserve.action_sign(), 0);
        assert_eq!(ResidencyEffect::Preserve.action_pages(3, 7), 0);

        assert_eq!(ResidencyEffect::Populate.action_sign(), 1);
        assert_eq!(ResidencyEffect::Populate.action_pages(3, 7), 4);

        assert_eq!(ResidencyEffect::EvictAdvisory.action_sign(), -1);
        assert_eq!(ResidencyEffect::EvictAdvisory.action_pages(7, 3), 4);
        assert_eq!(ResidencyEffect::EvictAdvisory.action_pages(3, 7), 0);
    }

    #[test]
    fn file_context_from_prepared_file_preserves_mapping() {
        let mut file = tempfile::NamedTempFile::new().unwrap();
        file.write_all(&[0; 64]).unwrap();
        let prepared = prepare_file(
            file.path(),
            &FileRange {
                offset: 0,
                max_len: None,
            },
        )
        .unwrap()
        .unwrap();
        let mmap = Arc::clone(&prepared.mmap);

        let context: FileContext<'_, Vec<bool>> = FileContext::new(prepared, Cancellation::new());

        assert_eq!(context.len(), mmap.len());
        assert_eq!(context.path(), file.path());
        assert_eq!(context.total_pages(), 1);
    }

    #[test]
    fn touch_stops_when_cancelled_during_page_walk() {
        let mut file = tempfile::NamedTempFile::new().unwrap();
        file.write_all(&vec![0; *crate::pagesize::PAGE_SIZE * 512])
            .unwrap();
        let prepared = prepare_file(file.path(), &FileRange::full())
            .unwrap()
            .unwrap();
        let cancellation = Cancellation::new();
        let context_cancellation = cancellation.clone();
        let cancel_after_progress = |_: usize, _: usize| {
            cancellation.cancel();
        };
        let context: FileContext<'_, Vec<bool>> = FileContext::new(prepared, context_cancellation)
            .with_progress(Some(&cancel_after_progress));

        let result = Touch.execute(&context);

        assert!(matches!(result, Err(crate::Error::Cancelled)));
    }
}