use std::panic::{AssertUnwindSafe, catch_unwind};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Condvar, Mutex, PoisonError};
use crate::parallel::{Panicked, Threads};
const ALIGN: u64 = 4096;
const ALIGN_BYTES: usize = 4096;
const ALIGN_DOWN: u64 = !(ALIGN - 1);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Extent {
pub file: usize,
pub offset: u64,
pub len: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Options {
pub threads: Threads,
pub window: u64,
pub max_gap: u64,
}
impl Options {
pub fn new(threads: Threads) -> Options {
Options {
threads,
window: 16 << 20,
max_gap: 1 << 20,
}
}
}
#[derive(Clone, Debug, thiserror::Error)]
pub enum ExtentError {
#[error("{path}: {source}")]
Io {
path: PathBuf,
source: Arc<std::io::Error>,
},
#[error("{path}: {len} bytes at {offset:#x} run past the end of the file at {end:#x}")]
PastEnd {
path: PathBuf,
offset: u64,
len: usize,
end: u64,
},
#[error("file {file} of {files}")]
NoFile {
file: usize,
files: usize,
},
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Scanned {
pub windows: u64,
pub bytes: u64,
}
#[derive(Debug)]
struct Window {
file: usize,
start: u64,
end: u64,
extents: Vec<usize>,
}
#[derive(Debug, Default)]
struct Plan {
ordinary: Vec<Window>,
large: Vec<Window>,
bad: Vec<(usize, ExtentError)>,
}
impl Window {
fn span(&self) -> u64 {
self.end.saturating_sub(self.start)
}
}
fn windows(files: usize, extents: &[Extent], options: &Options) -> Plan {
let mut bad = Vec::new();
let mut order: Vec<(usize, u64, u64, usize)> = Vec::with_capacity(extents.len());
for (i, e) in extents.iter().enumerate() {
if e.file >= files {
bad.push((i, ExtentError::NoFile { file: e.file, files }));
continue;
}
let end = u64::try_from(e.len)
.ok()
.and_then(|len| e.offset.checked_add(len));
let aligned = end.and_then(|end| end.checked_next_multiple_of(ALIGN));
match aligned {
Some(end) => order.push((e.file, e.offset & ALIGN_DOWN, end, i)),
None => bad.push((
i,
ExtentError::PastEnd {
path: PathBuf::new(),
offset: e.offset,
len: e.len,
end: u64::MAX,
},
)),
}
}
order.sort_unstable();
let mut out: Vec<Window> = Vec::new();
for (file, start, end, i) in order {
if let Some(w) = out.last_mut()
&& w.file == file
&& start <= w.end.saturating_add(options.max_gap)
&& end.max(w.end).saturating_sub(w.start) <= options.window
{
w.end = w.end.max(end);
w.extents.push(i);
continue;
}
out.push(Window {
file,
start,
end,
extents: vec![i],
});
}
let (mut large, ordinary): (Vec<Window>, Vec<Window>) =
out.into_iter().partition(|w| w.span() > options.window);
large.sort_by_key(|w| (std::cmp::Reverse(w.span()), w.file, w.start));
Plan { ordinary, large, bad }
}
#[derive(Debug, Default)]
struct Held {
next: usize,
bytes: u64,
}
enum Job<'w> {
Ordinary(&'w Window),
Large(&'w Window),
}
#[derive(Debug, Default)]
struct Aligned(Vec<u8>);
impl Aligned {
fn get(&mut self, len: usize) -> Option<&mut [u8]> {
let total = len.checked_add(ALIGN_BYTES)?;
if self.0.len() < total {
self.0 = vec![0; total];
}
let off = self.0.as_ptr().align_offset(ALIGN_BYTES);
self.0.get_mut(off..off.checked_add(len)?)
}
}
#[derive(Debug)]
#[allow(
clippy::disallowed_types,
reason = "toolkit I/O: scan reads the files its caller names (module documentation)"
)]
struct Open {
file: std::fs::File,
unbuffered: bool,
}
#[allow(
clippy::disallowed_types,
reason = "toolkit I/O: scan reads the files its caller names (module documentation)"
)]
fn open(path: &Path, unbuffered: bool) -> std::io::Result<Open> {
let mut o = std::fs::OpenOptions::new();
o.read(true);
#[cfg(windows)]
if unbuffered {
use std::os::windows::fs::OpenOptionsExt;
const FILE_FLAG_NO_BUFFERING: u32 = 0x2000_0000;
o.custom_flags(FILE_FLAG_NO_BUFFERING);
}
Ok(Open {
file: o.open(path)?,
unbuffered: cfg!(windows) && unbuffered,
})
}
#[allow(
clippy::disallowed_types,
reason = "toolkit I/O: scan reads the files its caller names (module documentation)"
)]
fn read_at(file: &std::fs::File, buf: &mut [u8], at: u64) -> std::io::Result<usize> {
#[cfg(windows)]
{
std::os::windows::fs::FileExt::seek_read(file, buf, at)
}
#[cfg(unix)]
{
std::os::unix::fs::FileExt::read_at(file, buf, at)
}
}
fn fill(f: &Open, buf: &mut [u8], at: u64) -> std::io::Result<usize> {
let mut got = 0usize;
while let Some(rest) = buf.get_mut(got..) {
if rest.is_empty() {
break;
}
let pos = u64::try_from(got).ok().and_then(|g| at.checked_add(g));
let Some(pos) = pos else { break };
let n = read_at(&f.file, rest, pos)?;
if n == 0 {
break;
}
got = got.saturating_add(n);
if f.unbuffered && !(n as u64).is_multiple_of(ALIGN) {
break;
}
}
Ok(got)
}
pub fn scan(
files: &[PathBuf],
extents: &[Extent],
options: &Options,
visit: impl Fn(usize, Result<&mut [u8], ExtentError>) + Sync,
) -> Result<Scanned, Panicked> {
let Plan { ordinary, large, bad } = windows(files.len(), extents, options);
let budget = options
.window
.saturating_mul(4)
.saturating_mul(options.threads.get() as u64);
let call = |i: usize, r: Result<&mut [u8], ExtentError>| {
catch_unwind(AssertUnwindSafe(|| visit(i, r))).map_err(|p| crate::parallel::panicked(i, &*p))
};
for (i, e) in bad {
let e = match e {
ExtentError::PastEnd { offset, len, end, .. } => ExtentError::PastEnd {
path: extents
.get(i)
.and_then(|x| files.get(x.file))
.cloned()
.unwrap_or_default(),
offset,
len,
end,
},
e => e,
};
call(i, Err(e))?;
}
let next = AtomicUsize::new(0);
let held = Mutex::new(Held::default());
let released = Condvar::new();
let failed = AtomicBool::new(false);
let (windows_read, bytes_read) = (AtomicU64::new(0), AtomicU64::new(0));
let take = || {
let mut h = held.lock().unwrap_or_else(PoisonError::into_inner);
loop {
if failed.load(Ordering::Relaxed) {
return None;
}
if let Some(w) = large.get(h.next)
&& (h.bytes == 0 || h.bytes.saturating_add(w.span()) <= budget)
{
h.next = h.next.saturating_add(1);
h.bytes = h.bytes.saturating_add(w.span());
return Some(Job::Large(w));
}
drop(h);
if let Some(w) = ordinary.get(next.fetch_add(1, Ordering::Relaxed)) {
return Some(Job::Ordinary(w));
}
h = held.lock().unwrap_or_else(PoisonError::into_inner);
if h.next >= large.len() {
return None;
}
let fits = large
.get(h.next)
.is_some_and(|w| h.bytes == 0 || h.bytes.saturating_add(w.span()) <= budget);
if !fits {
h = released.wait(h).unwrap_or_else(PoisonError::into_inner);
}
}
};
let release = |w: &Window| {
let mut h = held.lock().unwrap_or_else(PoisonError::into_inner);
h.bytes = h.bytes.saturating_sub(w.span());
drop(h);
released.notify_all();
};
let read_and_visit = |w: &Window,
buf: &mut Aligned,
opened: &mut Vec<Option<Result<Open, Arc<std::io::Error>>>>|
-> Result<(), Panicked> {
let path = files.get(w.file).map(PathBuf::as_path).unwrap_or(Path::new(""));
let slot = opened.get_mut(w.file);
let f = match slot {
Some(s) => s.get_or_insert_with(|| open(path, true).map_err(Arc::new)),
None => return Ok(()),
};
let span = usize::try_from(w.end.saturating_sub(w.start))
.ok()
.and_then(|n| buf.get(n));
let read = match (f.as_ref(), span) {
(Ok(f), Some(span)) => match fill(f, span, w.start) {
Err(e) if f.unbuffered && e.kind() == std::io::ErrorKind::InvalidInput => {
match open(path, false) {
Ok(b) => {
let got = fill(&b, span, w.start);
if let Some(s) = opened.get_mut(w.file) {
*s = Some(Ok(b));
}
got.map(|n| (n, span))
}
Err(e) => Err(e),
}
}
r => r.map(|n| (n, span)),
}
.map_err(Arc::new),
(Err(e), _) => Err(Arc::clone(e)),
(Ok(_), None) => Err(Arc::new(std::io::Error::other(
"a window larger than memory can address",
))),
};
match read {
Ok((got, span)) => {
windows_read.fetch_add(1, Ordering::Relaxed);
bytes_read.fetch_add(got as u64, Ordering::Relaxed);
let file_end = w.start.saturating_add(got as u64);
for &i in &w.extents {
let Some(e) = extents.get(i) else { continue };
let at = usize::try_from(e.offset.saturating_sub(w.start)).unwrap_or(usize::MAX);
let bytes = at
.checked_add(e.len)
.filter(|&end| end <= got)
.and_then(|end| span.get_mut(at..end));
let r = bytes.ok_or_else(|| ExtentError::PastEnd {
path: path.to_path_buf(),
offset: e.offset,
len: e.len,
end: file_end,
});
call(i, r)?;
}
}
Err(source) => {
for &i in &w.extents {
let r = Err(ExtentError::Io {
path: path.to_path_buf(),
source: Arc::clone(&source),
});
call(i, r)?;
}
}
}
Ok(())
};
let worker = || -> Result<(), Panicked> {
let mut kept = Aligned::default();
let mut opened: Vec<Option<Result<Open, Arc<std::io::Error>>>> = Vec::new();
opened.resize_with(files.len(), || None);
while let Some(job) = take() {
let result = match job {
Job::Ordinary(w) => read_and_visit(w, &mut kept, &mut opened),
Job::Large(w) => {
let r = read_and_visit(w, &mut Aligned::default(), &mut opened);
release(w);
r
}
};
if let Err(p) = result {
failed.store(true, Ordering::Relaxed);
released.notify_all();
return Err(p);
}
}
Ok(())
};
let workers = options
.threads
.get()
.min(ordinary.len().saturating_add(large.len()))
.max(1);
let results: Vec<Result<(), Panicked>> = if workers == 1 {
vec![worker()]
} else {
std::thread::scope(|s| {
let handles: Vec<_> = (0..workers).map(|_| s.spawn(worker)).collect();
handles.into_iter().map(|h| h.join().unwrap_or(Ok(()))).collect()
})
};
if let Some(p) = results.into_iter().filter_map(Result::err).min_by_key(|p| p.item) {
return Err(p);
}
Ok(Scanned {
windows: windows_read.load(Ordering::Relaxed),
bytes: bytes_read.load(Ordering::Relaxed),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn opts(window: u64, max_gap: u64) -> Options {
Options {
threads: Threads::ONE,
window,
max_gap,
}
}
#[test]
fn neighbours_join_and_a_gap_splits() {
let e = |offset, len| Extent { file: 0, offset, len };
let extents = [e(5000, 100), e(0, 100), e(1 << 20, 10)];
let plan = windows(1, &extents, &opts(16 << 20, 8192));
let w = plan.ordinary;
assert!(plan.bad.is_empty() && plan.large.is_empty());
assert_eq!(w.len(), 2);
assert_eq!(
(w[0].start, w[0].end, w[0].extents.clone()),
(0, 8192, vec![1, 0])
);
assert_eq!((w[1].start, w[1].end), (1 << 20, (1 << 20) + 4096));
}
#[test]
fn a_window_never_exceeds_its_size_unless_one_extent_does() {
let e = |offset, len| Extent { file: 0, offset, len };
let extents = [e(0, 4096), e(4096, 4096), e(8192, 4096), e(12288, 40000)];
let plan = windows(1, &extents, &opts(8192, 0));
let spans = |w: &[Window]| w.iter().map(|w| (w.start, w.end)).collect::<Vec<_>>();
assert_eq!(spans(&plan.ordinary), [(0, 8192), (8192, 12288)]);
assert_eq!(spans(&plan.large), [(12288, 53248)]);
}
#[test]
fn large_reads_go_longest_first() {
let e = |file, offset, len| Extent { file, offset, len };
let extents = [
e(0, 0, 20000),
e(0, 1 << 20, 90000),
e(1, 0, 90000),
e(1, 1 << 20, 50000),
e(0, 1 << 21, 100),
];
let plan = windows(2, &extents, &opts(8192, 0));
let large: Vec<_> = plan.large.iter().map(|w| (w.file, w.start, w.span())).collect();
assert_eq!(
large,
[
(0, 1 << 20, 90112),
(1, 0, 90112),
(1, 1 << 20, 53248),
(0, 0, 20480)
]
);
assert_eq!(plan.ordinary.len(), 1);
}
#[test]
fn files_do_not_join_and_unknown_files_are_refused() {
let extents = [
Extent {
file: 1,
offset: 0,
len: 10,
},
Extent {
file: 0,
offset: 0,
len: 10,
},
Extent {
file: 2,
offset: 0,
len: 10,
},
Extent {
file: 0,
offset: u64::MAX - 1,
len: 10,
},
];
let plan = windows(2, &extents, &opts(16 << 20, 1 << 20));
let (w, bad) = (plan.ordinary, plan.bad);
assert_eq!(w.iter().map(|w| w.file).collect::<Vec<_>>(), [0, 1]);
assert!(matches!(bad[0], (2, ExtentError::NoFile { file: 2, files: 2 })));
assert!(matches!(bad[1], (3, ExtentError::PastEnd { .. })));
}
}