From cb7d78ac6af45ee1549e4453b951511484609ba4 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Mon, 29 Jun 2026 10:34:18 -0700 Subject: [PATCH] =?UTF-8?q?disc:=20adaptive=20patch=20speed=20=E2=80=94=20?= =?UTF-8?q?enter=20each=20range=20fast,=20slow=20on=20first=20failure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass N (patch) pinned the slow recovery speed (0x0000) for the whole pass. But Pass 1's damage-jump overshoots, so most of a jumped range is clean data the reverse-walk reads first — grinding it at slow speed wastes minutes per gap. Now each range mirrors Pass 1's model: - Enter at max speed (0xFFFF) + reset to the initial batch (current_batch carries across ranges, so a prior single-sector grind would otherwise start the next range slow). Reads the clean overshoot fast. - First read failure in the range → drop to 0x0000 (once; idempotent SET CD SPEED) for the rest of that range — grind only the genuine damage. - 10 s halt-responsive cooldown between ranges, gated on "this range actually grinded" so a many-small-range pass (100+ ranges) doesn't stall ~20 min on unconditional pauses. No change to good/bad classification, the mapfile, or the abort math — purely read speed + an inter-range cooldown. Synthetic speed-transition test to follow. --- src/disc/patch.rs | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/disc/patch.rs b/src/disc/patch.rs index c0c022e..018c8a0 100644 --- a/src/disc/patch.rs +++ b/src/disc/patch.rs @@ -310,6 +310,10 @@ use crate::sector::SectorSource; // per-LBA NOT_READY retries so a persistently-not-ready disc cannot burn // up to RANGE_BUDGET_CAP_SECS per range on a single LBA. const NOT_READY_MAX_RETRIES_PER_LBA: u32 = 3; +/// Cooldown between patch ranges that actually grinded (dropped to the slow +/// recovery speed). Lets the drive settle before the next range re-enters at max +/// speed. Gated on "grinded" so a many-small-range pass doesn't stall on it. +const INTER_RANGE_COOLDOWN_SECS: u64 = 10; const BRIDGE_DEGRADATION_PAUSE_SECS: u64 = 10; const POST_FAILURE_PAUSE_SECS: u64 = 1; const CONSECUTIVE_FAIL_LONG_PAUSE: u64 = 5; @@ -1838,7 +1842,15 @@ impl Disc { ); let mut buf = vec![0u8; initial_batch as usize * 2048]; - reader.set_speed(0x0000); + // Adaptive patch speed (replaces the old pass-wide slow pin): each range + // now ENTERS at max speed + the initial batch and only drops to the slow + // recovery speed on its FIRST read failure (set per-range below). Pass 1's + // damage-jump overshoots, so most of a jumped range is clean data the + // reverse-walk reads first — reading it at max speed instead of grinding + // the whole range slow is the win. `cooldown_pending` arms the inter-range + // pause, but ONLY after a range that actually grinded (dropped to slow) — + // an unconditional pause would stall 20+ min on a many-small-range pass. + let mut cooldown_pending = false; log_patch_start_snapshot(&initial_entries, &initial_stats, bytes_good_before); @@ -1864,6 +1876,18 @@ impl Disc { ); 'outer: for (range_idx, (range_pos, range_size)) in bad_ranges.iter().enumerate() { + // Inter-range cooldown — only after a range that grinded (dropped to + // slow). Halt-responsive so a stop request interrupts the wait. + if cooldown_pending { + tracing::info!( + target: "freemkv::disc", + phase = "patch_inter_range_cooldown", + secs = INTER_RANGE_COOLDOWN_SECS, + "Cooldown before next range (previous range grinded at slow speed)" + ); + super::sleep_secs_or_halt(INTER_RANGE_COOLDOWN_SECS, opts.halt.as_ref()); + cooldown_pending = false; + } tracing::info!( target: "freemkv::disc", phase = "patch_range_start", @@ -1926,6 +1950,15 @@ impl Disc { range_budget_secs, "Per-range time budget computed" ); + // Enter the range at MAX speed + the full initial batch: read the + // clean overshoot fast. `current_batch` carries across ranges, so + // reset it here or a prior range's single-sector grind would start + // this range slow. `range_slowed` flips on the first read failure + // (below), dropping to the slow recovery speed for the rest of the + // range and arming the inter-range cooldown. + reader.set_speed(0xFFFF); + state.current_batch = initial_batch; + let mut range_slowed = false; loop { if let Some(ref h) = opts.halt { if h.load(std::sync::atomic::Ordering::Relaxed) { @@ -2043,6 +2076,15 @@ impl Disc { } } Err(err) => { + // First failure in this range: the fast-batched pass over + // the clean overshoot is done; drop to the slow recovery + // speed for the rest of the range and arm the cooldown. + // Idempotent — only the first failure issues SET CD SPEED. + if !range_slowed { + reader.set_speed(0x0000); + range_slowed = true; + cooldown_pending = true; + } match handle_read_failure( &mut state, &frame,