patch: single Jump-scout tier 0, expand-Bisect, live in-handler progress
Tier 0 is now a single fast Jump scout: it streams the big readable ranges back and skips dead runs in seconds, so the pass reaches every section fast and converges to the small genuine-dead residue instead of grinding three handlers x 60s on each dead fragment. Tier 1 (fast mop-up + slow deep reads + Bisect) works only that residue. Bisect now expands: on a good probe it reads outward forward and backward in full batches until a read fails, recovering the whole readable island in large reads; the two failing ends become smaller bad sub-ranges it bisects again. One huge bad range becomes many precisely located small dead clusters. Progress heartbeat: HandlerCtx gains a throttled tick (250ms) called from every read, pushing a fresh snapshot to the reporter DURING a handler. The bar and speed now move continuously as recovery happens instead of jumping once per section (the reason speed read 0 B/s and the % looked frozen between range boundaries).
This commit is contained in:
+38
-10
@@ -70,6 +70,11 @@ use super::section_recover::{
|
|||||||
/// Replaces the old 1800 s/range + 3600 s/pass grind budgets on the live path.
|
/// Replaces the old 1800 s/range + 3600 s/pass grind budgets on the live path.
|
||||||
const PER_HANDLER_BUDGET_SECS: u64 = 60;
|
const PER_HANDLER_BUDGET_SECS: u64 = 60;
|
||||||
|
|
||||||
|
/// Minimum interval between progress heartbeats pushed from inside a handler, so
|
||||||
|
/// the UI's bar/speed move continuously during a long section without flooding
|
||||||
|
/// the reporter (see the tick closure in `recover_section`).
|
||||||
|
const PROGRESS_TICK_MS: u64 = 250;
|
||||||
|
|
||||||
/// Bridges the decoupled [`RecoverySink`] a handler writes to onto the live
|
/// Bridges the decoupled [`RecoverySink`] a handler writes to onto the live
|
||||||
/// patch consumer pipe: each recovered span becomes a [`PatchItem::Recovered`]
|
/// patch consumer pipe: each recovered span becomes a [`PatchItem::Recovered`]
|
||||||
/// the consumer thread seeks + writes + records `Finished`. `recovered` can't
|
/// the consumer thread seeks + writes + records `Finished`. `recovered` can't
|
||||||
@@ -833,14 +838,18 @@ impl PatchCtx<'_, '_> {
|
|||||||
// before any slow grind. Tier 1: slow deep-recovery + bisect on what tier
|
// before any slow grind. Tier 1: slow deep-recovery + bisect on what tier
|
||||||
// 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 {
|
||||||
|
// Tier 0 = a SINGLE fast scout (Jump only). It streams the big
|
||||||
|
// readable wins back and skips dead runs in seconds — so the pass
|
||||||
|
// sweeps every range fast, recovers the recoverable bulk largest
|
||||||
|
// first, and converges to the small genuine-dead residue instead of
|
||||||
|
// spending 3 handlers × 60 s grinding every dead fragment. Order of
|
||||||
|
// recovery is exactly big-wins → smaller → smallest, then grind.
|
||||||
|
vec![Box::new(Jump)]
|
||||||
|
} else {
|
||||||
|
// Tier 1 = deep recovery on the (now small) residue: fast full-batch
|
||||||
|
// mop-up of anything Jump stepped over, then slow deep-recovery reads,
|
||||||
|
// then Bisect for readable islands inside a mostly-dead chunk.
|
||||||
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,
|
||||||
@@ -849,9 +858,6 @@ impl PatchCtx<'_, '_> {
|
|||||||
reverse: false,
|
reverse: false,
|
||||||
fast: true,
|
fast: true,
|
||||||
}),
|
}),
|
||||||
]
|
|
||||||
} else {
|
|
||||||
vec![
|
|
||||||
Box::new(Linear {
|
Box::new(Linear {
|
||||||
reverse: true,
|
reverse: true,
|
||||||
fast: false,
|
fast: false,
|
||||||
@@ -876,12 +882,34 @@ impl PatchCtx<'_, '_> {
|
|||||||
|
|
||||||
let bad_before = bad.total_len();
|
let bad_before = bad.total_len();
|
||||||
let outcome = {
|
let outcome = {
|
||||||
|
// Progress heartbeat: a throttled closure that pushes a fresh
|
||||||
|
// snapshot to the reporter as recovery happens (called from every
|
||||||
|
// read via `HandlerCtx::progress`), so the bar and speed move DURING
|
||||||
|
// a handler instead of only when a section finishes. Scoped to this
|
||||||
|
// block so its borrow of `self.state` ends before the post-tier
|
||||||
|
// accounting below.
|
||||||
|
let disc = self.disc;
|
||||||
|
let opts = self.opts;
|
||||||
|
let shared = self.shared;
|
||||||
|
let total_bytes = self.total_bytes;
|
||||||
|
let state = &self.state;
|
||||||
|
let last_tick = std::cell::Cell::new(now_ptr());
|
||||||
|
let mut tick = move || {
|
||||||
|
let t = now_ptr();
|
||||||
|
if t.duration_since(last_tick.get())
|
||||||
|
>= std::time::Duration::from_millis(PROGRESS_TICK_MS)
|
||||||
|
{
|
||||||
|
last_tick.set(t);
|
||||||
|
let _ = disc.report_patch_progress(state, opts, total_bytes, shared);
|
||||||
|
}
|
||||||
|
};
|
||||||
let mut ctx = HandlerCtx {
|
let mut ctx = HandlerCtx {
|
||||||
reader: &mut *self.reader,
|
reader: &mut *self.reader,
|
||||||
sink: &mut sink,
|
sink: &mut sink,
|
||||||
now: &now_fn,
|
now: &now_fn,
|
||||||
halt: self.opts.halt.as_deref(),
|
halt: self.opts.halt.as_deref(),
|
||||||
decrypt_is_aacs: self.decrypt_is_aacs,
|
decrypt_is_aacs: self.decrypt_is_aacs,
|
||||||
|
tick: Some(&mut tick),
|
||||||
};
|
};
|
||||||
run_handlers(&mut ctx, &mut handlers, bad, |_bad| {
|
run_handlers(&mut ctx, &mut handlers, bad, |_bad| {
|
||||||
now_ptr() + std::time::Duration::from_secs(PER_HANDLER_BUDGET_SECS)
|
now_ptr() + std::time::Duration::from_secs(PER_HANDLER_BUDGET_SECS)
|
||||||
|
|||||||
+93
-20
@@ -88,6 +88,12 @@ pub(super) struct HandlerCtx<'a> {
|
|||||||
pub halt: Option<&'a AtomicBool>,
|
pub halt: Option<&'a AtomicBool>,
|
||||||
/// Widen mid-unit reads to the aligned AACS unit (see [`recovery_read`]).
|
/// Widen mid-unit reads to the aligned AACS unit (see [`recovery_read`]).
|
||||||
pub decrypt_is_aacs: bool,
|
pub decrypt_is_aacs: bool,
|
||||||
|
/// Progress heartbeat. Handlers call [`HandlerCtx::progress`] frequently (it
|
||||||
|
/// is internally throttled); this pushes a fresh progress snapshot to the
|
||||||
|
/// caller's reporter DURING a handler, not just at range boundaries — so the
|
||||||
|
/// bar and speed move as recovery happens instead of jumping once per
|
||||||
|
/// section. `None` in tests (no reporter).
|
||||||
|
pub tick: Option<&'a mut dyn FnMut()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HandlerCtx<'_> {
|
impl HandlerCtx<'_> {
|
||||||
@@ -98,6 +104,13 @@ impl HandlerCtx<'_> {
|
|||||||
fn past(&self, deadline: Instant) -> bool {
|
fn past(&self, deadline: Instant) -> bool {
|
||||||
(self.now)() >= deadline
|
(self.now)() >= deadline
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Emit a progress heartbeat (throttling lives in the tick closure).
|
||||||
|
fn progress(&mut self) {
|
||||||
|
if let Some(t) = self.tick.as_mut() {
|
||||||
|
t();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Outcome of one physical read attempt, before the caller decides what to do
|
/// Outcome of one physical read attempt, before the caller decides what to do
|
||||||
@@ -124,14 +137,18 @@ fn read_span(
|
|||||||
) -> ReadHit {
|
) -> ReadHit {
|
||||||
let lba = (pos / SECTOR) as u32;
|
let lba = (pos / SECTOR) as u32;
|
||||||
let bytes = count as usize * SECTOR as usize;
|
let bytes = count as usize * SECTOR as usize;
|
||||||
match recovery_read(ctx.reader, ctx.decrypt_is_aacs, lba, count, buf, recovery) {
|
let hit = match recovery_read(ctx.reader, ctx.decrypt_is_aacs, lba, count, buf, recovery) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
ctx.sink.recovered(pos, &buf[..bytes]);
|
ctx.sink.recovered(pos, &buf[..bytes]);
|
||||||
ReadHit::Good
|
ReadHit::Good
|
||||||
}
|
}
|
||||||
Err(e) if e.is_scsi_transport_failure() => ReadHit::Transport,
|
Err(e) if e.is_scsi_transport_failure() => ReadHit::Transport,
|
||||||
Err(_) => ReadHit::Bad,
|
Err(_) => ReadHit::Bad,
|
||||||
}
|
};
|
||||||
|
// Heartbeat after every read (the tick closure throttles to ~250 ms) so the
|
||||||
|
// UI's bar/speed move DURING a handler, not just when the section finishes.
|
||||||
|
ctx.progress();
|
||||||
|
hit
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One recovery idea, given a bounded shot at the section's still-bad set.
|
/// One recovery idea, given a bounded shot at the section's still-bad set.
|
||||||
@@ -262,11 +279,14 @@ impl SectionHandler for Linear {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Probe the MIDDLE sector of each bad sub-range; if it reads, remove it and
|
/// Bisect + expand. Probe the middle sector of a bad sub-range; when it reads,
|
||||||
/// recurse on the two halves to converge on good centers. If the middle is dead,
|
/// EXPAND outward from it — forward and backward in full batches — until a read
|
||||||
/// leave that chunk for another handler / pass. Finds islands of readable data
|
/// fails, recovering the whole readable island around the good centre in large
|
||||||
/// inside a mostly-dead range that a linear sweep would tar with one failing
|
/// reads. The two failing ends become smaller bad sub-ranges, pushed back to be
|
||||||
/// batch.
|
/// bisected again. A dead middle just splits into halves. This shreds one huge
|
||||||
|
/// bad range into precisely-located small dead clusters (a handful of sectors)
|
||||||
|
/// instead of leaving the whole thing bad. Uses fast reads: it LOCATES readable
|
||||||
|
/// data; deep-recovering the dead sectors is the slow linear handlers' job.
|
||||||
pub(super) struct Bisect;
|
pub(super) struct Bisect;
|
||||||
|
|
||||||
impl SectionHandler for Bisect {
|
impl SectionHandler for Bisect {
|
||||||
@@ -280,10 +300,13 @@ impl SectionHandler for Bisect {
|
|||||||
bad: &mut SubRanges,
|
bad: &mut SubRanges,
|
||||||
deadline: Instant,
|
deadline: Instant,
|
||||||
) -> HandlerOutcome {
|
) -> HandlerOutcome {
|
||||||
let mut buf = [0u8; SECTOR as usize];
|
let batch = BATCH_SECTORS * SECTOR;
|
||||||
// Explicit work stack of (pos, len) chunks still to probe. Each good
|
let mut buf = vec![0u8; batch as usize];
|
||||||
// probe removes one sector and pushes its two halves; each read consumes
|
let mut probe = [0u8; SECTOR as usize];
|
||||||
// a sector, so the stack drains in bounded steps.
|
// Work stack of still-bad chunks. A good probe recovers the readable
|
||||||
|
// island around it and pushes the two (smaller) failing ends; a dead
|
||||||
|
// probe pushes the two halves. Either way the stack shrinks toward small
|
||||||
|
// bad clusters, so it drains in bounded steps.
|
||||||
let mut stack: Vec<(u64, u64)> = bad.ranges().to_vec();
|
let mut stack: Vec<(u64, u64)> = bad.ranges().to_vec();
|
||||||
while let Some((rp, rl)) = stack.pop() {
|
while let Some((rp, rl)) = stack.pop() {
|
||||||
if rl == 0 {
|
if rl == 0 {
|
||||||
@@ -295,23 +318,65 @@ impl SectionHandler for Bisect {
|
|||||||
if ctx.past(deadline) {
|
if ctx.past(deadline) {
|
||||||
return HandlerOutcome::Remaining;
|
return HandlerOutcome::Remaining;
|
||||||
}
|
}
|
||||||
// Middle sector, floored to a sector boundary.
|
let end = rp + rl;
|
||||||
let sectors = rl / SECTOR;
|
let mid = rp + (rl / SECTOR / 2) * SECTOR;
|
||||||
let mid = rp + (sectors / 2) * SECTOR;
|
match read_span(ctx, &mut probe, mid, 1, false) {
|
||||||
match read_span(ctx, &mut buf, mid, 1, true) {
|
|
||||||
ReadHit::Good => {
|
ReadHit::Good => {
|
||||||
bad.remove(mid, SECTOR);
|
bad.remove(mid, SECTOR);
|
||||||
// Left half [rp, mid), right half [mid+SECTOR, rp+rl).
|
// Expand FORWARD from mid+1 in batches until a read fails.
|
||||||
|
let mut fwd = mid + SECTOR;
|
||||||
|
while fwd < end {
|
||||||
|
if ctx.past(deadline) {
|
||||||
|
return HandlerOutcome::Remaining;
|
||||||
|
}
|
||||||
|
let span = batch.min(end - fwd);
|
||||||
|
let count = (span / SECTOR) as u16;
|
||||||
|
match read_span(ctx, &mut buf[..span as usize], fwd, count, false) {
|
||||||
|
ReadHit::Good => {
|
||||||
|
bad.remove(fwd, span);
|
||||||
|
fwd += span;
|
||||||
|
}
|
||||||
|
ReadHit::Bad => break,
|
||||||
|
ReadHit::Transport => return HandlerOutcome::TransportFault,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Expand BACKWARD from mid toward rp until a read fails.
|
||||||
|
let mut bwd = mid;
|
||||||
|
while bwd > rp {
|
||||||
|
if ctx.past(deadline) {
|
||||||
|
return HandlerOutcome::Remaining;
|
||||||
|
}
|
||||||
|
let span = batch.min(bwd - rp);
|
||||||
|
let pos = bwd - span;
|
||||||
|
let count = (span / SECTOR) as u16;
|
||||||
|
match read_span(ctx, &mut buf[..span as usize], pos, count, false) {
|
||||||
|
ReadHit::Good => {
|
||||||
|
bad.remove(pos, span);
|
||||||
|
bwd = pos;
|
||||||
|
}
|
||||||
|
ReadHit::Bad => break,
|
||||||
|
ReadHit::Transport => return HandlerOutcome::TransportFault,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The two failing ends stay bad — bisect them again to pin
|
||||||
|
// the exact dead sectors.
|
||||||
|
if bwd > rp {
|
||||||
|
stack.push((rp, bwd - rp));
|
||||||
|
}
|
||||||
|
if fwd < end {
|
||||||
|
stack.push((fwd, end - fwd));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ReadHit::Bad => {
|
||||||
|
// Dead middle: split and keep hunting for a good centre.
|
||||||
if mid > rp {
|
if mid > rp {
|
||||||
stack.push((rp, mid - rp));
|
stack.push((rp, mid - rp));
|
||||||
}
|
}
|
||||||
let right = mid + SECTOR;
|
let right = mid + SECTOR;
|
||||||
if right < rp + rl {
|
if right < end {
|
||||||
stack.push((right, rp + rl - right));
|
stack.push((right, end - right));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Dead middle: leave the chunk bad and move on.
|
|
||||||
ReadHit::Bad => {}
|
|
||||||
ReadHit::Transport => return HandlerOutcome::TransportFault,
|
ReadHit::Transport => return HandlerOutcome::TransportFault,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -570,6 +635,7 @@ mod tests {
|
|||||||
now: &now,
|
now: &now,
|
||||||
halt: None,
|
halt: None,
|
||||||
decrypt_is_aacs: false,
|
decrypt_is_aacs: false,
|
||||||
|
tick: None,
|
||||||
};
|
};
|
||||||
let mut bad = SubRanges::from_section(0, 10 * SECTOR);
|
let mut bad = SubRanges::from_section(0, 10 * SECTOR);
|
||||||
// Generous deadline: 10 s from start.
|
// Generous deadline: 10 s from start.
|
||||||
@@ -610,6 +676,7 @@ mod tests {
|
|||||||
now: &now,
|
now: &now,
|
||||||
halt: None,
|
halt: None,
|
||||||
decrypt_is_aacs: false,
|
decrypt_is_aacs: false,
|
||||||
|
tick: None,
|
||||||
};
|
};
|
||||||
let mut bad = SubRanges::from_section(0, 40 * SECTOR);
|
let mut bad = SubRanges::from_section(0, 40 * SECTOR);
|
||||||
let deadline = (ctx.now)() + Duration::from_secs(10);
|
let deadline = (ctx.now)() + Duration::from_secs(10);
|
||||||
@@ -645,6 +712,7 @@ mod tests {
|
|||||||
now: &now,
|
now: &now,
|
||||||
halt: None,
|
halt: None,
|
||||||
decrypt_is_aacs: false,
|
decrypt_is_aacs: false,
|
||||||
|
tick: None,
|
||||||
};
|
};
|
||||||
let mut bad = SubRanges::from_section(0, 1000 * SECTOR);
|
let mut bad = SubRanges::from_section(0, 1000 * SECTOR);
|
||||||
let deadline = (ctx.now)() + Duration::from_secs(3);
|
let deadline = (ctx.now)() + Duration::from_secs(3);
|
||||||
@@ -678,6 +746,7 @@ mod tests {
|
|||||||
now: &now,
|
now: &now,
|
||||||
halt: None,
|
halt: None,
|
||||||
decrypt_is_aacs: false,
|
decrypt_is_aacs: false,
|
||||||
|
tick: None,
|
||||||
};
|
};
|
||||||
let mut bad = SubRanges::from_section(0, 9 * SECTOR);
|
let mut bad = SubRanges::from_section(0, 9 * SECTOR);
|
||||||
let deadline = (ctx.now)() + Duration::from_secs(10);
|
let deadline = (ctx.now)() + Duration::from_secs(10);
|
||||||
@@ -712,6 +781,7 @@ mod tests {
|
|||||||
now: &now,
|
now: &now,
|
||||||
halt: None,
|
halt: None,
|
||||||
decrypt_is_aacs: false,
|
decrypt_is_aacs: false,
|
||||||
|
tick: None,
|
||||||
};
|
};
|
||||||
let mut bad = SubRanges::from_section(0, 16 * SECTOR);
|
let mut bad = SubRanges::from_section(0, 16 * SECTOR);
|
||||||
let mut handlers: Vec<Box<dyn SectionHandler>> = vec![
|
let mut handlers: Vec<Box<dyn SectionHandler>> = vec![
|
||||||
@@ -750,6 +820,7 @@ mod tests {
|
|||||||
now: &now,
|
now: &now,
|
||||||
halt: None,
|
halt: None,
|
||||||
decrypt_is_aacs: false,
|
decrypt_is_aacs: false,
|
||||||
|
tick: None,
|
||||||
};
|
};
|
||||||
let mut bad = SubRanges::from_section(0, 64 * SECTOR);
|
let mut bad = SubRanges::from_section(0, 64 * SECTOR);
|
||||||
let mut handlers: Vec<Box<dyn SectionHandler>> = vec![Box::new(Linear {
|
let mut handlers: Vec<Box<dyn SectionHandler>> = vec![Box::new(Linear {
|
||||||
@@ -778,6 +849,7 @@ mod tests {
|
|||||||
now: &now,
|
now: &now,
|
||||||
halt: None,
|
halt: None,
|
||||||
decrypt_is_aacs: false,
|
decrypt_is_aacs: false,
|
||||||
|
tick: None,
|
||||||
};
|
};
|
||||||
// Single-sector batches so the transport LBA is hit directly.
|
// Single-sector batches so the transport LBA is hit directly.
|
||||||
let mut bad = SubRanges::from_section(0, 8 * SECTOR);
|
let mut bad = SubRanges::from_section(0, 8 * SECTOR);
|
||||||
@@ -805,6 +877,7 @@ mod tests {
|
|||||||
now: &now,
|
now: &now,
|
||||||
halt: Some(&halt),
|
halt: Some(&halt),
|
||||||
decrypt_is_aacs: false,
|
decrypt_is_aacs: false,
|
||||||
|
tick: None,
|
||||||
};
|
};
|
||||||
let mut bad = SubRanges::from_section(0, 100 * SECTOR);
|
let mut bad = SubRanges::from_section(0, 100 * SECTOR);
|
||||||
let deadline = (ctx.now)() + Duration::from_secs(10);
|
let deadline = (ctx.now)() + Duration::from_secs(10);
|
||||||
|
|||||||
Reference in New Issue
Block a user