From 8d775cd341d787edfdccb82cad4eb09c65ad97b4 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Tue, 30 Jun 2026 20:40:22 -0700 Subject: [PATCH] patch: add Jump handler (lead fast tier) + recovery-based progress % MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Jump: on sustained batch failures skip ahead an escalating distance (1 MiB doubling to 256 MiB) to find where readable data resumes, leaving the skipped span for Bisect to pin — mirrors the Pass-1 damage-jump. It leads the fast tier so a large dead run is skipped in seconds instead of the linear sweeps grinding every dead batch (10 s each) first; on a readable range it just streams it back. Recovers readable data buried behind a big dead front (the 192 MB Dune range). Progress %: report bytes RECOVERED (initial-bad minus still-pending) instead of a per-range counter that only advanced on the final tier — so the bar reflects the readable bulk recovered during tier 0 the instant it lands, matching the 'MB remaining' number. --- src/disc/patch.rs | 18 ++++++++- src/disc/section_recover.rs | 81 +++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/src/disc/patch.rs b/src/disc/patch.rs index 907fe11..3ce5f28 100644 --- a/src/disc/patch.rs +++ b/src/disc/patch.rs @@ -59,7 +59,7 @@ use crate::io::pipeline::{Flow, Sink}; use super::mapfile::{self, MapStats, Mapfile, SectorStatus}; use super::section_recover::{ - Bisect, HandlerCtx, HandlerOutcome, Linear, RecoverySink, SectionHandler, run_handlers, + Bisect, HandlerCtx, HandlerOutcome, Jump, Linear, RecoverySink, SectionHandler, run_handlers, }; /// Wall-clock budget one recovery handler gets on a section before the chain @@ -834,6 +834,13 @@ impl PatchCtx<'_, '_> { // 0 left. Adding a recovery idea is one more entry in the right tier (#55). let mut handlers: Vec> = if tier == 0 { vec![ + // Jump LEADS the fast tier: it recovers readable data and skips + // ahead past dead runs, so a mostly-dead range is confirmed and + // left in seconds instead of the linear sweeps grinding every + // dead batch (10 s each) first. On a readable range it just + // streams it back like a linear read. The linear sweeps then + // mop up the spans Jump stepped over. + Box::new(Jump), Box::new(Linear { reverse: true, fast: true, @@ -974,9 +981,16 @@ impl Disc { .map(|t| bytes_bad_in_title(t, &bad_ranges_now)) .unwrap_or(0); let main_title = self.titles.first(); + // Progress = bytes RECOVERED so far (initial bad − still-pending), not a + // per-range counter. With breadth-first tiers the readable bulk comes + // back during tier 0 before any range is "finished", so a range-counter + // sits at 0% while hundreds of MB are actually recovered. Deriving it + // from the live pending count makes the bar (and the speed the client + // computes from its delta) reflect real recovery the instant it happens. + let recovered = state.work_total.saturating_sub(s.bytes_pending); let pp = crate::progress::PassProgress { kind, - work_done: state.work_done, + work_done: recovered, work_total: state.work_total, bytes_good_total: s.bytes_good, bytes_unreadable_total: s.bytes_unreadable, diff --git a/src/disc/section_recover.rs b/src/disc/section_recover.rs index ec75186..4b8ede3 100644 --- a/src/disc/section_recover.rs +++ b/src/disc/section_recover.rs @@ -42,6 +42,14 @@ const SECTOR: u64 = 2048; /// spans against granularity on dead ones. const BATCH_SECTORS: u64 = 32; +/// `Jump` handler: after this many consecutive failed batches, skip ahead to +/// find where readable data resumes rather than reading every dead sector. +const JUMP_AFTER_FAILS: u32 = 2; +/// `Jump` initial skip distance; doubles after each jump, capped at +/// [`JUMP_CAP_BYTES`]. Mirrors the escalating Pass-1 damage-jump. +const JUMP_BASE_BYTES: u64 = 1 << 20; // 1 MiB +const JUMP_CAP_BYTES: u64 = 256 << 20; // 256 MiB + /// Where a handler left the section after its bounded attempt. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(super) enum HandlerOutcome { @@ -316,6 +324,79 @@ impl SectionHandler for Bisect { } } +/// Blow through a LARGE dead run fast. Reads forward in batches; after +/// [`JUMP_AFTER_FAILS`] consecutive failed batches it SKIPS AHEAD an escalating +/// distance (1 MiB → 2 → 4 … capped at [`JUMP_CAP_BYTES`]), leaving the skipped +/// span bad, to find where readable data RESUMES — mirroring the Pass-1 +/// damage-jump. A later handler / `Bisect` pins the exact good/bad boundary the +/// jump stepped over. Uses fast reads (this is a scout, not a deep-recovery +/// pass). Without it a linear walk pays one up-to-10 s read per dead batch +/// across the whole run, so a deadline-bounded pass never reaches readable data +/// buried behind a big dead front (exactly the 192 MB range on Dune). +pub(super) struct Jump; + +impl SectionHandler for Jump { + fn name(&self) -> &'static str { + "jump" + } + + fn recover( + &mut self, + ctx: &mut HandlerCtx, + bad: &mut SubRanges, + deadline: Instant, + ) -> HandlerOutcome { + let batch = BATCH_SECTORS * SECTOR; + let mut buf = vec![0u8; batch as usize]; + let snapshot: Vec<(u64, u64)> = bad.ranges().to_vec(); + for (rp, rl) in snapshot { + let mut off = 0u64; + let mut consec_fail = 0u32; + let mut jump = JUMP_BASE_BYTES; + while off < rl { + if ctx.halted() { + return HandlerOutcome::Halted; + } + if ctx.past(deadline) { + return HandlerOutcome::Remaining; + } + let span = batch.min(rl - off); + let pos = rp + off; + let count = (span / SECTOR) as u16; + match read_span(ctx, &mut buf[..span as usize], pos, count, false) { + ReadHit::Good => { + bad.remove(pos, span); + consec_fail = 0; + jump = JUMP_BASE_BYTES; + off += span; + } + ReadHit::Bad => { + consec_fail += 1; + if consec_fail >= JUMP_AFTER_FAILS { + // Sustained dead run — skip ahead (sector-aligned so + // the walk stays batch-aligned) and escalate the next + // jump. The skipped span stays bad for Bisect / a + // later handler to pin the boundary. + let step = (jump / SECTOR).max(1) * SECTOR; + off = (off + step).min(rl); + jump = jump.saturating_mul(2).min(JUMP_CAP_BYTES); + consec_fail = 0; + } else { + off += span; + } + } + ReadHit::Transport => return HandlerOutcome::TransportFault, + } + } + } + if bad.is_empty() { + HandlerOutcome::Complete + } else { + HandlerOutcome::Remaining + } + } +} + /// Run the handler chain over one section's still-bad set. This is the /// never-hang guarantee: each handler is bounded by the deadline /// `section_deadline_for(bad)` returns, and the loop always drains to