From 9cc422bd003eb7fed01c3f123512b7b0b1fe7525 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Tue, 30 Jun 2026 11:20:44 -0700 Subject: [PATCH] disc/patch: drop cache-priming, bisect batch failures instead of count=1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two patch-recovery speed fixes, grounded in live rip1 telemetry on the Dune UHD bad regions. 1. Pull prime_cache. In the reverse walk it issued throwaway reads of lba-3..lba-1 — i.e. straight into the not-yet-visited dead L-EC core — grinding each ~7-9s at the fast timeout, then discarding the result. The live trace showed a good target read (read_duration_ms=1) taking 9s wall-clock purely from the prime grind ahead of it. The dead-zone skip (Tier 1) + scatter's recalibration read (read_good_sectors) cover the cache-warm intent without grinding the dead zone, so prime is redundant harm — removed (function + CACHE_PRIME_SECTORS + call). 2. Bisect on batch failure. handle_read_failure used to collapse a failed count>1 batch straight to count=1 and single-walk the entire batch. Now it halves (count/2) and retries the same start: a good half recovers in BULK, the bad sector is isolated in O(log n) reads instead of O(n). Invariant preserved — only a count==1 failure marks NonTrimmed, so no good sector is lumped into a bad mark. These are Tier 2 (bisect) of the progressive recovery model (fast-mass-skip -> bisect -> scatter); composable so order can be tuned. 25 lib + 8 AB fixture tests green; precommit clean on Rust 1.86. --- src/disc/patch.rs | 52 ++++++++++++++--------------------------------- 1 file changed, 15 insertions(+), 37 deletions(-) diff --git a/src/disc/patch.rs b/src/disc/patch.rs index f361d23..cf4b49b 100644 --- a/src/disc/patch.rs +++ b/src/disc/patch.rs @@ -351,9 +351,6 @@ const PASSN_DAMAGE_THRESHOLD_PCT: usize = crate::disc::read_error::PATCH_DAMAGE_ const PASSN_SKIP_SECTORS_BASE: u64 = 32; const PASSN_SKIP_SECTORS_CAP: u64 = 4096; const PASSN_ESCALATION_RESET_GOOD: u32 = 4; -// Cache-prime: number of single-sector throwaway reads issued at LBAs -// immediately preceding the target before a count==1 recovery read. -const CACHE_PRIME_SECTORS: u32 = 3; /// Probe-offset escalation: returns a per-probe skip distance in /// sectors of `PASSN_SKIP_SECTORS_BASE << (3 × idx)` (i.e. multiplies @@ -436,30 +433,6 @@ pub(super) fn compute_initial_state( )) } -/// Cache priming: before the recovery read, issue `CACHE_PRIME_SECTORS` -/// throwaway single-sector reads at the LBAs immediately preceding -/// `lba`. The drive's read-ahead cache prefetches forward on -/// sequential reads — by the time the caller asks for `lba` it may -/// already be cached, even if a cold read of the same LBA would fail. -/// Proven 2026-05-07 with dd-as-oracle: 8/8 sectors recoverable when -/// primed vs 6/8 cold. Failures are best-effort: we already have these -/// bytes Finished from a prior pass, so prime failures don't update -/// mapfile state. Only runs on count==1 reads (the genuine recovery -/// path) and when `lba >= CACHE_PRIME_SECTORS` so the subtraction -/// doesn't underflow. -pub(super) fn prime_cache(reader: &mut R, lba: u32, count: u16) { - if !(lba >= CACHE_PRIME_SECTORS && count == 1) { - return; - } - let mut prime_buf = [0u8; 2048]; - for i in 0..CACHE_PRIME_SECTORS { - let prime_lba = lba - CACHE_PRIME_SECTORS + i; - // Best-effort; ignore errors. Recovery=false is intentional: - // a fast 1.5s timeout is fine because we don't need the data. - let _ = reader.read_sectors(prime_lba, 1, &mut prime_buf[..], false); - } -} - /// One recovery read of `[lba, lba+count)` into `buf[..count*2048]`. /// /// On an AACS disc a mid-unit window (start or length not unit-aligned) @@ -1179,23 +1152,30 @@ pub(super) fn handle_read_failure( // Adaptive batching split decision: a batch-read failure // (count > 1) is NOT a recorded failure. We don't yet know which // sector in the batch was actually bad — could be one, could be - // many. Drop to count=1 and retry the SAME starting position so - // every sector gets individually probed. Cursor stays put; loop - // continues. Invariants: no good sector ever gets lumped into a - // NonTrimmed mark, no spurious consecutive_failures (which drives - // wedge detection), no damage_window pollution from batch-level - // signals. + // many. BISECT: halve the batch and retry the SAME starting position. + // If the bad sector is in the upper half, the lower half now SUCCEEDS + // and recovers in BULK (one read, not N single reads); if it's in the + // lower half, we halve again. Either way the bad sector is isolated in + // O(log n) reads instead of single-stepping all n. Collapsing straight + // to count=1 (the pre-bisect behavior) single-walked the entire failed + // batch — on a mostly-good NonTrimmed range that is the dominant cost. + // Cursor stays put; loop continues. Invariants preserved: only a + // count==1 failure ever marks NonTrimmed, so no good sector is lumped + // into a bad mark; a batch failure still doesn't touch + // consecutive_failures / the damage window. if count > 1 { + let halved = (count / 2).max(1); tracing::info!( target: "freemkv::disc", phase = "patch.batch.split", lba, count, from_batch = state.current_batch, + to_batch = halved, err_code = err.code(), - "adaptive batching: batch read failed, dropping to count=1 to probe individually" + "adaptive batching: batch read failed, bisecting (halving) to isolate the bad sector" ); - state.current_batch = 1; + state.current_batch = halved; state.consecutive_singles_ok = 0; return Ok(FailureAction::ContinueInner); } @@ -2040,8 +2020,6 @@ impl PatchCtx<'_, '_, R> { "starting sector read" ); - prime_cache(self.reader, lba, count); - // Single-shot read (no inline retry — see the historical note // in handle_read_failure). `recovery_read` widens a mid-unit // AACS window to the aligned unit; otherwise it's a plain read.