use std::ops::Range;
use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum AccessPattern {
Random,
Sequential,
WillNeed(Range<usize>),
Normal,
}
pub struct MappedFile {
map: memmap2::Mmap,
}
impl MappedFile {
pub fn open(path: impl AsRef<Path>, pattern: AccessPattern) -> Result<Self, MappedFileError> {
let file = std::fs::File::open(path.as_ref()).map_err(MappedFileError::Io)?;
let map = unsafe { memmap2::MmapOptions::new().map(&file) }.map_err(MappedFileError::Io)?;
let mapped = MappedFile { map };
mapped.apply_hints(&pattern)?;
Ok(mapped)
}
pub fn apply_hints(&self, pattern: &AccessPattern) -> Result<(), MappedFileError> {
#[cfg(unix)]
{
use MadviseAdvice::*;
let advice = match pattern {
AccessPattern::Random => Some(Random),
AccessPattern::Sequential => Some(Sequential),
AccessPattern::Normal => Some(Normal),
AccessPattern::WillNeed(range) => {
let len = self.map.len();
let start = range.start.min(len);
let end = range.end.min(len);
if start < end {
let ptr = unsafe { self.map.as_ptr().add(start) };
raw_madvise(ptr, end - start, libc::MADV_WILLNEED)?;
}
return Ok(());
}
};
if let Some(adv) = advice {
raw_madvise(self.map.as_ptr(), self.map.len(), adv as libc::c_int)?;
}
}
#[cfg(not(unix))]
{
let _ = pattern;
}
Ok(())
}
#[inline]
pub fn as_slice(&self) -> &[u8] {
&self.map
}
#[inline]
pub fn len(&self) -> usize {
self.map.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
}
impl std::ops::Deref for MappedFile {
type Target = [u8];
fn deref(&self) -> &[u8] {
self.as_slice()
}
}
#[derive(thiserror::Error, Debug)]
pub enum MappedFileError {
#[error("mmap i/o error: {0}")]
Io(std::io::Error),
#[error("madvise error (errno {0})")]
Madvise(i32),
}
#[cfg(unix)]
#[allow(dead_code)]
enum MadviseAdvice {
Normal = libc::MADV_NORMAL as isize,
Random = libc::MADV_RANDOM as isize,
Sequential = libc::MADV_SEQUENTIAL as isize,
}
#[cfg(unix)]
fn raw_madvise(ptr: *const u8, len: usize, advice: libc::c_int) -> Result<(), MappedFileError> {
if len == 0 {
return Ok(());
}
let ret = unsafe { libc::madvise(ptr as *mut libc::c_void, len, advice) };
if ret != 0 {
Err(MappedFileError::Madvise(unsafe { *libc::__error() }))
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn write_temp_file(content: &[u8]) -> tempfile::NamedTempFile {
let mut f = tempfile::NamedTempFile::new().unwrap();
f.write_all(content).unwrap();
f.flush().unwrap();
f
}
#[test]
fn open_sequential_no_panic() {
let tmp = write_temp_file(b"hello world");
let mapped = MappedFile::open(tmp.path(), AccessPattern::Sequential).unwrap();
assert_eq!(&*mapped, b"hello world");
}
#[test]
fn open_random_no_panic() {
let tmp = write_temp_file(b"random access data");
let mapped = MappedFile::open(tmp.path(), AccessPattern::Random).unwrap();
assert_eq!(&*mapped, b"random access data");
}
#[test]
fn open_normal_no_panic() {
let tmp = write_temp_file(b"normal hint");
let mapped = MappedFile::open(tmp.path(), AccessPattern::Normal).unwrap();
assert_eq!(&*mapped, b"normal hint");
}
#[test]
fn open_willneed_full_range_no_panic() {
let data = vec![0u8; 4096];
let tmp = write_temp_file(&data);
let mapped = MappedFile::open(tmp.path(), AccessPattern::WillNeed(0..4096)).unwrap();
assert_eq!(mapped.len(), 4096);
}
#[test]
fn open_willneed_partial_range_no_panic() {
let data = vec![1u8; 8192];
let tmp = write_temp_file(&data);
let mapped = MappedFile::open(tmp.path(), AccessPattern::WillNeed(1024..3072)).unwrap();
assert_eq!(mapped.len(), 8192);
}
#[test]
fn open_willneed_overflowing_range_clamped_no_panic() {
let data = vec![2u8; 512];
let tmp = write_temp_file(&data);
let mapped = MappedFile::open(tmp.path(), AccessPattern::WillNeed(0..1_000_000)).unwrap();
assert_eq!(mapped.len(), 512);
}
#[test]
fn reapply_hints_no_panic() {
let tmp = write_temp_file(b"reapply test");
let mapped = MappedFile::open(tmp.path(), AccessPattern::Normal).unwrap();
mapped.apply_hints(&AccessPattern::Sequential).unwrap();
mapped.apply_hints(&AccessPattern::Random).unwrap();
mapped
.apply_hints(&AccessPattern::WillNeed(0..mapped.len()))
.unwrap();
}
#[test]
fn len_and_is_empty() {
let tmp = write_temp_file(b"five!");
let mapped = MappedFile::open(tmp.path(), AccessPattern::Normal).unwrap();
assert_eq!(mapped.len(), 5);
assert!(!mapped.is_empty());
}
}