Revert "iter2: restore FileSectorSource readahead buffer (32 MiB)"

This reverts commit 36a2b3af68.
This commit is contained in:
2026-05-17 07:38:34 -07:00
parent 36a2b3af68
commit 6155628693
+29 -35
View File
@@ -103,10 +103,14 @@ pub struct FileSectorSource {
/// Total file size in sectors. Constant after construction; /// Total file size in sectors. Constant after construction;
/// surfaced via [`SectorSource::capacity_sectors`]. /// surfaced via [`SectorSource::capacity_sectors`].
capacity: u32, capacity: u32,
/// 32 MiB application-level readahead buffer. Restored 2026-05-17 /// 0.21.3+: the app-level buffer is no longer touched on the hot
/// after iter1 baseline (18.4 MB/s mean) showed per-sector pread is /// path (every `read_sectors` is a direct pread). The fields are
/// the bottleneck. /// retained so a future per-source-type policy (e.g. a local-disk
/// source where batched reads ARE beneficial) can re-enable
/// buffering cleanly without re-plumbing the struct.
#[allow(dead_code)]
buf: Box<[u8]>, buf: Box<[u8]>,
#[allow(dead_code)]
buf_start_lba: u32, buf_start_lba: u32,
buf_len_sectors: u32, buf_len_sectors: u32,
/// 0.21.6: bytes read since the last DONTNEED drop. Drives the /// 0.21.6: bytes read since the last DONTNEED drop. Drives the
@@ -163,6 +167,7 @@ impl FileSectorSource {
/// True if `[lba, lba + count)` is wholly inside the current /// True if `[lba, lba + count)` is wholly inside the current
/// buffer window. `count == 0` is vacuously true. /// buffer window. `count == 0` is vacuously true.
#[allow(dead_code)]
fn buffer_covers(&self, lba: u32, count: u32) -> bool { fn buffer_covers(&self, lba: u32, count: u32) -> bool {
if self.buf_len_sectors == 0 { if self.buf_len_sectors == 0 {
return false; return false;
@@ -178,6 +183,7 @@ impl FileSectorSource {
/// Refill the buffer so it starts at `lba`. Read as many sectors /// Refill the buffer so it starts at `lba`. Read as many sectors
/// as we have buffer space AND file capacity for. Caller has /// as we have buffer space AND file capacity for. Caller has
/// already checked `lba < capacity`. /// already checked `lba < capacity`.
#[allow(dead_code)]
fn refill(&mut self, lba: u32) -> Result<()> { fn refill(&mut self, lba: u32) -> Result<()> {
debug_assert!(lba < self.capacity, "refill past capacity"); debug_assert!(lba < self.capacity, "refill past capacity");
// Don't read past EOF — clamp the request to remaining // Don't read past EOF — clamp the request to remaining
@@ -220,40 +226,28 @@ impl SectorSource for FileSectorSource {
if count == 0 { if count == 0 {
return Ok(0); return Ok(0);
} }
// Iteration 2 (2026-05-17): restore the 32 MiB readahead buffer. // 0.21.3: bypass the application-level buffer entirely.
// //
// The 0.21.3 bypass was justified by an A/B test taken under the // Empirically the 32 MiB readahead window (0.21.00.21.1) and the
// 0.21.7 producer-side polling cap. Under that cap the producer // 4 MiB shrink (0.21.2) both regressed mux throughput vs the
// couldn't push fast enough to saturate the channel regardless of // pre-Phase-1 0.20.7 baseline on NFS bidirectional workloads
// read strategy, so the comparison "buffer vs no-buffer" measured // (sweep ~25 MB/s OK; mux dropped from 18 → 7-8 → 5-6 MB/s).
// the cap, not the read path. Iter1 baseline with bypass + Phase // Direct pread per call lets the kernel's own readahead policy
// 2.5 + DONTNEED = 18.4 MB/s mean — well below the rig's measured // run, which interleaves naturally with concurrent NFS writes on
// concurrent-r+w ceiling (37 MB/s). Per-sector pread costs ~50 us // the same TCP connection.
// each ≈ 19k syscalls/sec = 38 MB/s ceiling just in syscall
// overhead. The 32 MiB app buffer amortises that to 1 pread per
// 16k sectors and lets the kernel's readahead operate on a wider
// window. DONTNEED below still evicts the page cache so we don't
// pin the ISO in RAM.
// //
// Pathological-large requests (> BUF_SECTORS = 16384) fall back // Buffer fields are retained (currently unused on this path) so
// to direct pread so callers can't deadlock the source. // any future per-source policy can be reintroduced without
if count > BUF_SECTORS { // re-plumbing structure. `refill` / `buffer_covers` are kept too
let offset = lba as u64 * SECTOR_SIZE as u64; // (still exercised by the tests so the API contract is locked).
self.file let offset = lba as u64 * SECTOR_SIZE as u64;
.seek(SeekFrom::Start(offset)) self.file
.map_err(|e| Error::IoError { source: e })?; .seek(SeekFrom::Start(offset))
self.file .map_err(|e| Error::IoError { source: e })?;
.read_exact(&mut out[..bytes]) self.file
.map_err(|e| Error::IoError { source: e })?; .read_exact(&mut out[..bytes])
self.buf_len_sectors = 0; .map_err(|e| Error::IoError { source: e })?;
} else { self.buf_len_sectors = 0;
if !self.buffer_covers(lba, count) {
self.refill(lba)?;
}
let off_sectors = (lba - self.buf_start_lba) as usize;
let off_bytes = off_sectors * SECTOR_SIZE;
out[..bytes].copy_from_slice(&self.buf[off_bytes..off_bytes + bytes]);
}
// 0.21.6: periodic page-cache eviction on the read side. Without // 0.21.6: periodic page-cache eviction on the read side. Without
// this, an 85 GB streaming ISO read pins the entire file in // this, an 85 GB streaming ISO read pins the entire file in