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:
+16
-2
@@ -59,7 +59,7 @@ use crate::io::pipeline::{Flow, Sink};
|
|||||||
|
|
||||||
use super::mapfile::{self, MapStats, Mapfile, SectorStatus};
|
use super::mapfile::{self, MapStats, Mapfile, SectorStatus};
|
||||||
use super::section_recover::{
|
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
|
/// 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).
|
// 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 {
|
let mut handlers: Vec<Box<dyn SectionHandler>> = if tier == 0 {
|
||||||
vec![
|
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 {
|
Box::new(Linear {
|
||||||
reverse: true,
|
reverse: true,
|
||||||
fast: true,
|
fast: true,
|
||||||
@@ -974,9 +981,16 @@ impl Disc {
|
|||||||
.map(|t| bytes_bad_in_title(t, &bad_ranges_now))
|
.map(|t| bytes_bad_in_title(t, &bad_ranges_now))
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let main_title = self.titles.first();
|
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 {
|
let pp = crate::progress::PassProgress {
|
||||||
kind,
|
kind,
|
||||||
work_done: state.work_done,
|
work_done: recovered,
|
||||||
work_total: state.work_total,
|
work_total: state.work_total,
|
||||||
bytes_good_total: s.bytes_good,
|
bytes_good_total: s.bytes_good,
|
||||||
bytes_unreadable_total: s.bytes_unreadable,
|
bytes_unreadable_total: s.bytes_unreadable,
|
||||||
|
|||||||
@@ -42,6 +42,14 @@ const SECTOR: u64 = 2048;
|
|||||||
/// spans against granularity on dead ones.
|
/// spans against granularity on dead ones.
|
||||||
const BATCH_SECTORS: u64 = 32;
|
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.
|
/// Where a handler left the section after its bounded attempt.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub(super) enum HandlerOutcome {
|
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
|
/// Run the handler chain over one section's still-bad set. This is the
|
||||||
/// never-hang guarantee: each handler is bounded by the deadline
|
/// never-hang guarantee: each handler is bounded by the deadline
|
||||||
/// `section_deadline_for(bad)` returns, and the loop always drains to
|
/// `section_deadline_for(bad)` returns, and the loop always drains to
|
||||||
|
|||||||
Reference in New Issue
Block a user