v0.13.22 — replace bisect-on-fail with hysteresis Block↔Single
The v0.13.21 bisect-on-fail recovery was correct (100% of recoverable
sectors picked up) but slow on dense damage clusters. Live test on
Dune 2 v0.13.21 burned ~30 s per damaged 60-block — paying a ~5 s
kernel ABORT/timeout at every level of a log₂(60) ≈ 6 deep DFS, on
the failing branch each time.
Replaced with a two-state hysteresis machine in Disc::copy:
Block(batch):
read(batch) ok → write, advance, stay Block
read(batch) fail → switch to Single, retry SAME range at bpt=1
Single:
read(1) ok → write, consecutive_good++
if consecutive_good >= BPT1_EXIT_THRESHOLD:
switch to Block, reset counter
read(1) fail → mark NonTrimmed, consecutive_good = 0
BPT1_EXIT_THRESHOLD = 10_000 sectors (= 20 MB clean run). Calibrated
from the 2026-04-26 BU40N empirical probe data; tunable.
Per-block math on a damaged 60-block with 1 truly bad sector:
Bisect (v0.13.21): ~30 s (5 s × 6 levels)
Hysteresis (v0.13.22): ~10 s (5 s bpt=batch fail
+ 59 × 1 ms good
+ 1 × 5 s bad)
Inside a damaged cluster spanning many 60-blocks the win compounds:
hysteresis pays the bpt=batch fail cost ONCE on entry, then stays at
bpt=1 across the cluster; bisection re-paid it every 60 sectors. For
Dune 2's ~1248-sector boundary cluster that's ~21 fewer 5-sec
kernel timeouts ≈ 100 s saved per pass.
Telemetry: new phase=mode_change trace event with from, to, lba, and
consecutive_good. Replaces v0.13.21's phase=bisect. Worklist DFS is
gone — single iterative for s in 0..count on the failure path.
Test rename, same fixture and same 100% recovery expectation:
test_disc_copy_bisect_recovers_via_single_sector_reads
→ test_disc_copy_hysteresis_recovers_via_single_sector_reads
Also adds DamageSeverity (Clean / Cosmetic / Moderate / Serious) +
classify_damage(bad_sectors, lost_ms), re-exported from libfreemkv,
so applications can render structured severity instead of formatting
their own from raw counters.
This commit is contained in:
@@ -538,16 +538,18 @@ fn test_disc_copy_halts_promptly_on_failing_reader() {
|
||||
);
|
||||
}
|
||||
|
||||
// ── 8. Bisect-on-fail recovers data the drive can read individually ──────
|
||||
// ── 8. Hysteresis recovers data the drive can read individually ──────────
|
||||
//
|
||||
// Empirically observed on the LG BU40N: in damaged regions the drive fails
|
||||
// multi-sector READ commands but reads each sector cleanly when asked one
|
||||
// at a time. Disc::copy's bisect-on-fail must recover those sectors
|
||||
// without bailing or skip-forwarding past clean territory.
|
||||
// at a time. Disc::copy's hysteresis state machine (0.13.22, replaces the
|
||||
// 0.13.21 bisect-on-fail) drops to bpt=1 on the first multi-sector failure
|
||||
// and stays there until BPT1_EXIT_THRESHOLD consecutive good single-sector
|
||||
// reads, then returns to bpt=batch.
|
||||
//
|
||||
// Fixture: a reader that returns Err for any read with count > 1, and Ok
|
||||
// for count == 1. With bisect-on-fail, we must observe a 100 % bytes_good
|
||||
// outcome — every sector recovered via the bisection.
|
||||
// for count == 1. The full disc must recover via the bpt=1 path with
|
||||
// 100 % bytes_good outcome.
|
||||
|
||||
struct BlockSizeFailingReader {
|
||||
capacity: u32,
|
||||
@@ -578,11 +580,13 @@ impl SectorReader for BlockSizeFailingReader {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disc_copy_bisect_recovers_via_single_sector_reads() {
|
||||
fn test_disc_copy_hysteresis_recovers_via_single_sector_reads() {
|
||||
// 256 sectors = 0.5 MB. Reader fails any multi-sector read but
|
||||
// succeeds on bpt=1. Bisection must descend log2(batch) levels and
|
||||
// recover every sector. This is the BU40N bad-zone pattern in
|
||||
// miniature.
|
||||
// succeeds on bpt=1. The hysteresis path must drop to Single mode on
|
||||
// the first multi-sector failure and recover every sector at bpt=1.
|
||||
// Stays in Single mode until BPT1_EXIT_THRESHOLD reached (10 000
|
||||
// sectors); since this disc is only 256 sectors we never re-enter
|
||||
// Block mode, which is fine — every sector still recovers.
|
||||
let capacity_sectors: u32 = 256;
|
||||
let total_bytes: u64 = capacity_sectors as u64 * SECTOR_SIZE as u64;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user