disc/patch: drop cache-priming, bisect batch failures instead of count=1
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.
This commit is contained in:
+15
-37
@@ -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_BASE: u64 = 32;
|
||||||
const PASSN_SKIP_SECTORS_CAP: u64 = 4096;
|
const PASSN_SKIP_SECTORS_CAP: u64 = 4096;
|
||||||
const PASSN_ESCALATION_RESET_GOOD: u32 = 4;
|
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
|
/// Probe-offset escalation: returns a per-probe skip distance in
|
||||||
/// sectors of `PASSN_SKIP_SECTORS_BASE << (3 × idx)` (i.e. multiplies
|
/// 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<R: SectorSource + ?Sized>(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]`.
|
/// 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)
|
/// On an AACS disc a mid-unit window (start or length not unit-aligned)
|
||||||
@@ -1179,23 +1152,30 @@ pub(super) fn handle_read_failure<R: SectorSource + ?Sized>(
|
|||||||
// Adaptive batching split decision: a batch-read failure
|
// Adaptive batching split decision: a batch-read failure
|
||||||
// (count > 1) is NOT a recorded failure. We don't yet know which
|
// (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
|
// sector in the batch was actually bad — could be one, could be
|
||||||
// many. Drop to count=1 and retry the SAME starting position so
|
// many. BISECT: halve the batch and retry the SAME starting position.
|
||||||
// every sector gets individually probed. Cursor stays put; loop
|
// If the bad sector is in the upper half, the lower half now SUCCEEDS
|
||||||
// continues. Invariants: no good sector ever gets lumped into a
|
// and recovers in BULK (one read, not N single reads); if it's in the
|
||||||
// NonTrimmed mark, no spurious consecutive_failures (which drives
|
// lower half, we halve again. Either way the bad sector is isolated in
|
||||||
// wedge detection), no damage_window pollution from batch-level
|
// O(log n) reads instead of single-stepping all n. Collapsing straight
|
||||||
// signals.
|
// 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 {
|
if count > 1 {
|
||||||
|
let halved = (count / 2).max(1);
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
target: "freemkv::disc",
|
target: "freemkv::disc",
|
||||||
phase = "patch.batch.split",
|
phase = "patch.batch.split",
|
||||||
lba,
|
lba,
|
||||||
count,
|
count,
|
||||||
from_batch = state.current_batch,
|
from_batch = state.current_batch,
|
||||||
|
to_batch = halved,
|
||||||
err_code = err.code(),
|
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;
|
state.consecutive_singles_ok = 0;
|
||||||
return Ok(FailureAction::ContinueInner);
|
return Ok(FailureAction::ContinueInner);
|
||||||
}
|
}
|
||||||
@@ -2040,8 +2020,6 @@ impl<R: SectorSource + ?Sized> PatchCtx<'_, '_, R> {
|
|||||||
"starting sector read"
|
"starting sector read"
|
||||||
);
|
);
|
||||||
|
|
||||||
prime_cache(self.reader, lba, count);
|
|
||||||
|
|
||||||
// Single-shot read (no inline retry — see the historical note
|
// Single-shot read (no inline retry — see the historical note
|
||||||
// in handle_read_failure). `recovery_read` widens a mid-unit
|
// in handle_read_failure). `recovery_read` widens a mid-unit
|
||||||
// AACS window to the aligned unit; otherwise it's a plain read.
|
// AACS window to the aligned unit; otherwise it's a plain read.
|
||||||
|
|||||||
Reference in New Issue
Block a user