patch: add Jump handler (lead fast tier) + recovery-based progress %

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.
This commit is contained in:
Matthew Jackson
2026-06-30 20:40:22 -07:00
parent bc07011bcb
commit 8d775cd341
2 changed files with 97 additions and 2 deletions
+16 -2
View File
@@ -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<Box<dyn SectionHandler>> = 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,