io: phase 1 buffering — read-side flatness
Three changes targeting 0.20.9's "muxer never read-stalls on NFS read latency" invariant: A. FileSectorSource gets a 32 MiB internal read-ahead buffer (READAHEAD_BUF_BYTES). Splits out from src/sector/file.rs into src/io/file_sector_source/ with per-OS open hints (Linux posix_fadvise(SEQUENTIAL), macOS fcntl(F_RDADVISE) with 64 MiB cap, Windows TODO stub, BSD/illumos no-op). Backward seeks rebuffer; partial reads at EOF return only the bytes that exist; oversize-request bypass for count > BUF_SECTORS. B. WritebackFile inline #[cfg(target_os = "linux")] blocks split into per-OS files under src/io/writeback_file/. Linux unchanged (fallocate KEEP_SIZE, fsync via bounded_syscall). macOS gets a real F_PREALLOCATE + F_FULLFSYNC impl (was a "skipped (non-linux)" debug log before). Windows is a stub (FlushFileBuffers via std sync_all; TODO for SetFileValidData). BSDs/illumos fall back to std sync_all. C. New byte_channel module — byte-bounded producer/consumer wrapping std sync_channel with Mutex/Condvar byte accounting. Sender blocks when used_bytes + item.byte_size() > capacity. HasByteSize impl for PesFrame. Default cap BYTE_CHANNEL_DEFAULT_CAPACITY = 64 MiB, sized to absorb worst-case NFS read p99 (~2 s × UHD peak compressed ~15 MB/s). The mux call site lives in autorip (out of scope here); this lands the primitive in libfreemkv for autorip to adopt. Test counts: byte_channel +6, file_sector_source +5, sector::file round-trip suite (3) preserved. passn_handler_ab.rs A/B fixture (8 profiles) still green. precommit.sh libfreemkv: fmt + clippy + test all green on Rust 1.86. No version bump; no Cargo.lock changes; no forbidden-file edits (disc/patch.rs, disc/read_error.rs, io/pipeline.rs, tests/passn_handler_ab.rs).
This commit is contained in:
+12
-146
@@ -1,148 +1,19 @@
|
||||
//! File-backed sector I/O — read and write 2048-byte sectors against
|
||||
//! an ISO image on disk.
|
||||
//! File-backed sector sink — write 2048-byte sectors to an ISO image
|
||||
//! on disk.
|
||||
//!
|
||||
//! [`FileSectorSource`] is the read side (open-only). [`FileSectorSink`]
|
||||
//! is the write side (create or open-rw); writes go through
|
||||
//! [`crate::io::WritebackFile`] so big sequential ISO writes share
|
||||
//! the same bounded-cache writeback pipeline used by sweep / patch /
|
||||
//! mux.
|
||||
//! The read-side counterpart ([`crate::io::file_sector_source::FileSectorSource`])
|
||||
//! lives under `io/` because its internals (read-ahead buffer, per-OS
|
||||
//! `fadvise`/`F_RDADVISE` hints) are I/O infrastructure rather than
|
||||
//! sector-trait business logic. Both types remain re-exported at
|
||||
//! [`crate::sector`] for ergonomic imports.
|
||||
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::{Seek, SeekFrom, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
use super::{SectorSink, SectorSource};
|
||||
|
||||
/// Bytes-read threshold per `posix_fadvise(DONTNEED)` drop on the
|
||||
/// read side. Mirrors the writeback chunk size so the read-side
|
||||
/// page cache stays bounded the same way the write side does.
|
||||
#[cfg(target_os = "linux")]
|
||||
const READ_DROP_CHUNK_BYTES: u64 = 32 * 1024 * 1024;
|
||||
|
||||
/// SectorSource backed by a file (ISO image).
|
||||
///
|
||||
/// Seeks to `lba * 2048`, reads `count * 2048` bytes per call. The
|
||||
/// file is held directly: every `read_sectors` call performs an
|
||||
/// absolute seek, so a wrapping `BufReader` would have its buffer
|
||||
/// invalidated on every call (its internal cursor moves with the
|
||||
/// `Seek` impl) — pure overhead. Callers that benefit from buffered
|
||||
/// reads should compose their own `BufReader` at the `read_sectors`
|
||||
/// granularity they care about.
|
||||
pub struct FileSectorSource {
|
||||
file: File,
|
||||
capacity: u32,
|
||||
/// Bytes read since the last `posix_fadvise(DONTNEED)` drop.
|
||||
/// Only updated on Linux; on other targets it stays at 0.
|
||||
#[cfg(target_os = "linux")]
|
||||
bytes_read_since_drop: u64,
|
||||
/// Byte offset at which the current drop window starts (the
|
||||
/// next `posix_fadvise(DONTNEED)` call drops from here).
|
||||
#[cfg(target_os = "linux")]
|
||||
drop_window_start: u64,
|
||||
}
|
||||
|
||||
impl FileSectorSource {
|
||||
/// Open an existing ISO file for reading. Capacity is derived
|
||||
/// from `metadata().len() / 2048`. Returns
|
||||
/// [`Error::IsoTooLarge`] if the file would exceed the 32-bit
|
||||
/// LBA address space (~8 TB).
|
||||
pub fn open(path: &Path) -> std::io::Result<Self> {
|
||||
let file = File::open(path)?;
|
||||
let len = file.metadata()?.len();
|
||||
let sectors = len / 2048;
|
||||
if sectors > u32::MAX as u64 {
|
||||
return Err(Error::IsoTooLarge {
|
||||
path: path.to_string_lossy().into_owned(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
let capacity = sectors as u32;
|
||||
|
||||
// Hint sequential access on Linux so the kernel's readahead
|
||||
// window widens for the ISO sweep. Best-effort: return value
|
||||
// is ignored. On macOS / Windows this is a no-op.
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::os::unix::io::AsRawFd;
|
||||
unsafe {
|
||||
libc::posix_fadvise(file.as_raw_fd(), 0, 0, libc::POSIX_FADV_SEQUENTIAL);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
file,
|
||||
capacity,
|
||||
#[cfg(target_os = "linux")]
|
||||
bytes_read_since_drop: 0,
|
||||
#[cfg(target_os = "linux")]
|
||||
drop_window_start: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Implement the legacy `SectorSource` trait. The blanket impl in
|
||||
// `super` produces the `SectorSource` impl automatically — no need
|
||||
// to write both, and writing both would conflict. This keeps the
|
||||
// 0.17 method-resolution path intact (callers with `SectorSource`
|
||||
// in scope can still write `fsr.read_sectors(..)` against a
|
||||
// `FileSectorSource`).
|
||||
impl SectorSource for FileSectorSource {
|
||||
fn capacity_sectors(&self) -> u32 {
|
||||
self.capacity
|
||||
}
|
||||
|
||||
fn read_sectors(
|
||||
&mut self,
|
||||
lba: u32,
|
||||
count: u16,
|
||||
buf: &mut [u8],
|
||||
_recovery: bool,
|
||||
) -> Result<usize> {
|
||||
let offset = lba as u64 * 2048;
|
||||
let bytes = count as usize * 2048;
|
||||
self.file
|
||||
.seek(SeekFrom::Start(offset))
|
||||
.map_err(|e| Error::IoError { source: e })?;
|
||||
self.file
|
||||
.read_exact(&mut buf[..bytes])
|
||||
.map_err(|e| Error::IoError { source: e })?;
|
||||
|
||||
// On Linux, periodically drop the just-read region from the
|
||||
// page cache to keep cache pressure bounded during multi-GB
|
||||
// sequential ISO reads. Mirrors the write-side pipeline.
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::os::unix::io::AsRawFd;
|
||||
self.bytes_read_since_drop += bytes as u64;
|
||||
if self.bytes_read_since_drop >= READ_DROP_CHUNK_BYTES {
|
||||
let drop_start = self.drop_window_start;
|
||||
let drop_len = self.bytes_read_since_drop;
|
||||
let t0 = std::time::Instant::now();
|
||||
unsafe {
|
||||
libc::posix_fadvise(
|
||||
self.file.as_raw_fd(),
|
||||
drop_start as i64,
|
||||
drop_len as i64,
|
||||
libc::POSIX_FADV_DONTNEED,
|
||||
);
|
||||
}
|
||||
let elapsed_ms = t0.elapsed().as_millis();
|
||||
let start_lba = drop_start / 2048;
|
||||
let end_lba = (drop_start + drop_len) / 2048;
|
||||
tracing::trace!(
|
||||
target: "mux",
|
||||
"FileSectorSource fadvise DONTNEED lba=[{start_lba}..{end_lba}) bytes={drop_len} elapsed_ms={elapsed_ms}"
|
||||
);
|
||||
self.drop_window_start = drop_start + drop_len;
|
||||
self.bytes_read_since_drop = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
use super::SectorSink;
|
||||
|
||||
/// SectorSink backed by a file (ISO image).
|
||||
///
|
||||
@@ -211,13 +82,8 @@ impl SectorSink for FileSectorSink {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// Bring the 0.18 trait into scope (not super::*: the super
|
||||
// module also re-exports the legacy `SectorSource`, and
|
||||
// having both `SectorSource::read_sectors` and
|
||||
// `SectorSource::read_sectors` visible would force every
|
||||
// call site to disambiguate). External consumers see the
|
||||
// same surface this test exercises.
|
||||
use super::{FileSectorSink, FileSectorSource};
|
||||
use super::FileSectorSink;
|
||||
use crate::io::file_sector_source::FileSectorSource;
|
||||
use crate::sector::{SectorSink, SectorSource};
|
||||
use tempfile::tempdir;
|
||||
|
||||
|
||||
+2
-1
@@ -109,5 +109,6 @@ pub trait SectorSink: Send {
|
||||
fn finish(self: Box<Self>) -> Result<()>;
|
||||
}
|
||||
|
||||
pub use crate::io::file_sector_source::FileSectorSource;
|
||||
pub use decrypting::DecryptingSectorSource;
|
||||
pub use file::{FileSectorSink, FileSectorSource};
|
||||
pub use file::FileSectorSink;
|
||||
|
||||
Reference in New Issue
Block a user