use std::path::Path;
use std::sync::Arc;
use crate::array::Data;
use crate::buffer::Buffer;
use crate::{Array, DType, Error, Result, shape};
#[derive(Clone)]
pub struct MappedFile {
buffer: Arc<Buffer>,
}
impl std::fmt::Debug for MappedFile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MappedFile")
.field("len", &self.len())
.finish()
}
}
impl MappedFile {
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
Ok(Self {
buffer: Arc::new(Buffer::map_file(path.as_ref())?),
})
}
pub fn mapping_size(len: u64) -> u64 {
nuvec_gpu::mapping_size(len as usize) as u64
}
pub fn copy_size(len: u64) -> u64 {
nuvec_gpu::allocation_size(len as usize).map_or(u64::MAX, |n| n as u64)
}
pub fn headroom() -> Option<u64> {
let m = nuvec_gpu::Device::shared()?.memory();
Some(m.memory_limit.saturating_sub(m.active_with_mapped) as u64)
}
pub fn len(&self) -> usize {
self.buffer.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn bytes(&self) -> &[u8] {
self.buffer.bytes()
}
pub fn read_ranges(
path: impl AsRef<Path>,
ranges: &[(u64, usize, usize)],
len: usize,
) -> Result<Self> {
let path = path.as_ref();
let mut at = 0;
for &(_, offset, n) in ranges {
if offset < at || offset.checked_add(n).is_none_or(|end| end > len) {
return Err(Error::InvalidArgument(format!(
"{}: read ranges must be increasing, disjoint and within {len} bytes",
path.display()
)));
}
at = offset + n;
}
let io = |e: std::io::Error| Error::Io(format!("{}: {e}", path.display()));
let file = std::fs::File::open(path).map_err(io)?;
let mut buffer = Buffer::try_alloc(len)?;
read_parallel(&file, buffer.bytes_mut(), ranges).map_err(io)?;
Ok(Self {
buffer: Arc::new(buffer),
})
}
pub fn array(&self, offset: usize, dtype: DType, shape: &[usize]) -> Result<Array> {
let nbytes = shape::checked_nbytes(shape, dtype.size())?;
if offset
.checked_add(nbytes)
.is_none_or(|end| end > self.len())
{
return Err(Error::Io(format!(
"{nbytes} bytes at offset {offset} exceed the file's {} bytes",
self.len()
)));
}
if offset.is_multiple_of(dtype.size()) {
let data = Data {
buffer: self.buffer.clone(),
offset: (offset / dtype.size()) as isize,
};
return Array::leaf(Ok(data), dtype, shape.into());
}
let mut copy = Buffer::try_alloc(nbytes)?;
copy.bytes_mut()
.copy_from_slice(&self.bytes()[offset..offset + nbytes]);
Array::from_buffer(copy, dtype, shape)
}
}
#[derive(Clone)]
pub struct MappedRanges {
runs: Vec<(usize, Arc<Buffer>)>,
}
impl std::fmt::Debug for MappedRanges {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let runs: Vec<(usize, usize)> = self.runs.iter().map(|(o, b)| (*o, b.len())).collect();
f.debug_struct("MappedRanges").field("runs", &runs).finish()
}
}
impl MappedRanges {
pub fn page_size() -> usize {
nuvec_gpu::page_size()
}
pub fn open(path: impl AsRef<Path>, ranges: &[(u64, u64)]) -> Result<Self> {
let path = path.as_ref();
let mut end = 0;
let mut runs = Vec::with_capacity(ranges.len());
for &(offset, len) in ranges {
if offset < end {
return Err(Error::InvalidArgument(format!(
"{}: mapped ranges must be increasing and disjoint",
path.display()
)));
}
end = offset + len;
let (offset, len) = (offset as usize, len as usize);
runs.push((offset, Arc::new(Buffer::map_file_range(path, offset, len)?)));
}
Ok(Self { runs })
}
pub fn mapped_size(&self) -> u64 {
self.runs
.iter()
.map(|(_, b)| MappedFile::mapping_size(b.len() as u64))
.sum()
}
pub fn array(&self, offset: usize, dtype: DType, shape: &[usize]) -> Result<Array> {
let nbytes = shape::checked_nbytes(shape, dtype.size())?;
let run = self.runs.iter().find(|(start, b)| {
*start <= offset
&& offset
.checked_add(nbytes)
.is_some_and(|end| end <= start + b.len())
});
let Some((start, buffer)) = run else {
return Err(Error::Io(format!(
"{nbytes} bytes at offset {offset} are not inside a mapped range"
)));
};
let at = offset - start;
if !at.is_multiple_of(dtype.size()) {
return Err(Error::Io(format!(
"offset {offset} is not aligned to {} bytes",
dtype.size()
)));
}
let data = Data {
buffer: buffer.clone(),
offset: (at / dtype.size()) as isize,
};
Array::leaf(Ok(data), dtype, shape.into())
}
}
const CHUNK: usize = 16 << 20;
const PER_THREAD: usize = 64 << 20;
const MAX_THREADS: usize = 8;
fn read_parallel(
file: &std::fs::File,
dest: &mut [u8],
ranges: &[(u64, usize, usize)],
) -> std::io::Result<()> {
let mut chunks: Vec<(u64, &mut [u8])> = Vec::new();
let mut rest = dest;
let mut consumed = 0;
for &(file_offset, offset, n) in ranges {
let (_, tail) = std::mem::take(&mut rest).split_at_mut(offset - consumed);
let (mut range, tail) = tail.split_at_mut(n);
rest = tail;
consumed = offset + n;
let mut at = file_offset;
while !range.is_empty() {
let (chunk, tail) = range.split_at_mut(range.len().min(CHUNK));
let n = chunk.len() as u64;
chunks.push((at, chunk));
at += n;
range = tail;
}
}
let total: usize = ranges.iter().map(|r| r.2).sum();
let threads = std::thread::available_parallelism()
.map_or(1, usize::from)
.min(MAX_THREADS)
.min(total.div_ceil(PER_THREAD))
.max(1);
if threads == 1 {
return chunks
.into_iter()
.try_for_each(|(at, chunk)| read_exact_at(file, chunk, at));
}
let work = std::sync::Mutex::new(chunks.into_iter());
std::thread::scope(|s| {
let workers: Vec<_> = (0..threads)
.map(|_| {
s.spawn(|| {
loop {
let next = work
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.next();
let Some((at, chunk)) = next else {
return Ok(());
};
read_exact_at(file, chunk, at)?;
}
})
})
.collect();
workers.into_iter().try_for_each(|w| {
w.join()
.unwrap_or_else(|_| Err(std::io::Error::other("a reading thread panicked")))
})
})
}
#[cfg(unix)]
fn read_exact_at(file: &std::fs::File, buf: &mut [u8], offset: u64) -> std::io::Result<()> {
std::os::unix::fs::FileExt::read_exact_at(file, buf, offset)
}
#[cfg(windows)]
fn read_exact_at(file: &std::fs::File, mut buf: &mut [u8], mut offset: u64) -> std::io::Result<()> {
use std::os::windows::fs::FileExt;
while !buf.is_empty() {
match file.seek_read(buf, offset) {
Ok(0) => return Err(std::io::ErrorKind::UnexpectedEof.into()),
Ok(n) => {
buf = &mut buf[n..];
offset += n as u64;
}
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
Err(e) => return Err(e),
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg_attr(miri, ignore)] fn read_ranges_places_every_byte() {
let path =
std::env::temp_dir().join(format!("nuvec-read-ranges-{}.bin", std::process::id()));
let bytes: Vec<u8> = (0..(CHUNK * 2 + 12_345))
.map(|i| (i * 31 % 251) as u8)
.collect();
std::fs::write(&path, &bytes).unwrap();
let big = CHUNK * 2 + 1000;
let ranges = [
(3u64, 0usize, 10usize),
(17, 16, big),
(bytes.len() as u64 - 5, big + 32, 5),
];
let file = MappedFile::read_ranges(&path, &ranges, big + 37).unwrap();
for &(at, offset, n) in &ranges {
assert_eq!(
&file.bytes()[offset..offset + n],
&bytes[at as usize..at as usize + n]
);
}
assert!(MappedFile::read_ranges(&path, &[(bytes.len() as u64 - 2, 0, 4)], 4).is_err());
assert!(MappedFile::read_ranges(&path, &[(0, 8, 4), (0, 0, 4)], 12).is_err());
assert!(MappedFile::read_ranges(&path, &[(0, 0, 8)], 4).is_err());
let empty = MappedFile::read_ranges(&path, &[], 0).unwrap();
assert!(empty.is_empty());
std::fs::remove_file(&path).unwrap();
}
}