v0.13.46: damage-jump algorithm replaces probe, bridge degradation detection, ecc_sectors()
This commit is contained in:
+92
-43
@@ -1288,8 +1288,15 @@ impl Disc {
|
|||||||
let mut last_log_iter: u64 = 0;
|
let mut last_log_iter: u64 = 0;
|
||||||
let mut not_ready_retries: u32 = 0;
|
let mut not_ready_retries: u32 = 0;
|
||||||
const NOT_READY_MAX_RETRIES: u32 = 3;
|
const NOT_READY_MAX_RETRIES: u32 = 3;
|
||||||
let mut error_count_in_zone: u64 = 0;
|
let mut bridge_degradation_count: u32 = 0;
|
||||||
let mut probe_buf = vec![0u8; 2048];
|
const BRIDGE_DEGRADATION_MAX: u32 = 5;
|
||||||
|
const BRIDGE_DEGRADATION_COOLDOWN_SECS: u64 = 10;
|
||||||
|
const DAMAGE_WINDOW: usize = 50;
|
||||||
|
const DAMAGE_THRESHOLD_PCT: usize = 25;
|
||||||
|
const JUMP_SECTORS_FACTOR: u64 = 256;
|
||||||
|
let mut damage_window: Vec<bool> = Vec::with_capacity(DAMAGE_WINDOW);
|
||||||
|
let mut jump_multiplier: u64 = 1;
|
||||||
|
let mut consecutive_good: u64 = 0;
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
target: "freemkv::disc",
|
target: "freemkv::disc",
|
||||||
phase = "copy_start",
|
phase = "copy_start",
|
||||||
@@ -1353,7 +1360,15 @@ impl Disc {
|
|||||||
|
|
||||||
if read_result.is_ok() {
|
if read_result.is_ok() {
|
||||||
read_ok_count += 1;
|
read_ok_count += 1;
|
||||||
error_count_in_zone = 0;
|
damage_window.push(true);
|
||||||
|
if damage_window.len() > DAMAGE_WINDOW {
|
||||||
|
damage_window.remove(0);
|
||||||
|
}
|
||||||
|
consecutive_good += 1;
|
||||||
|
if consecutive_good >= DAMAGE_WINDOW as u64 {
|
||||||
|
jump_multiplier = 1;
|
||||||
|
}
|
||||||
|
bridge_degradation_count = 0;
|
||||||
|
|
||||||
if opts.decrypt {
|
if opts.decrypt {
|
||||||
crate::decrypt::decrypt_sectors(
|
crate::decrypt::decrypt_sectors(
|
||||||
@@ -1383,6 +1398,7 @@ impl Disc {
|
|||||||
} else {
|
} else {
|
||||||
let err = read_result.err().unwrap();
|
let err = read_result.err().unwrap();
|
||||||
read_err_count += 1;
|
read_err_count += 1;
|
||||||
|
consecutive_good = 0;
|
||||||
|
|
||||||
if err.is_scsi_transport_failure() {
|
if err.is_scsi_transport_failure() {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
@@ -1395,6 +1411,28 @@ impl Disc {
|
|||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err.is_bridge_degradation() {
|
||||||
|
if bridge_degradation_count < BRIDGE_DEGRADATION_MAX {
|
||||||
|
bridge_degradation_count += 1;
|
||||||
|
tracing::warn!(
|
||||||
|
target: "freemkv::disc",
|
||||||
|
phase = "bridge_degradation",
|
||||||
|
lba = block_lba,
|
||||||
|
degradation_count = bridge_degradation_count,
|
||||||
|
error = %err,
|
||||||
|
"bridge degradation; cooling down 10s"
|
||||||
|
);
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(BRIDGE_DEGRADATION_COOLDOWN_SECS));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tracing::warn!(
|
||||||
|
target: "freemkv::disc",
|
||||||
|
phase = "bridge_degradation_exhausted",
|
||||||
|
lba = block_lba,
|
||||||
|
"bridge degradation retries exhausted; treating as bad sector"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let sense = err.scsi_sense();
|
let sense = err.scsi_sense();
|
||||||
let sense_key = sense.map(|s| s.sense_key).unwrap_or(0);
|
let sense_key = sense.map(|s| s.sense_key).unwrap_or(0);
|
||||||
let asc = sense.map(|s| s.asc).unwrap_or(0);
|
let asc = sense.map(|s| s.asc).unwrap_or(0);
|
||||||
@@ -1437,48 +1475,51 @@ impl Disc {
|
|||||||
.map_err(|e| Error::IoError { source: e })?;
|
.map_err(|e| Error::IoError { source: e })?;
|
||||||
bytes_done = bytes_done.saturating_add(block_bytes);
|
bytes_done = bytes_done.saturating_add(block_bytes);
|
||||||
|
|
||||||
error_count_in_zone += 1;
|
damage_window.push(false);
|
||||||
if error_count_in_zone >= 4 {
|
if damage_window.len() > DAMAGE_WINDOW {
|
||||||
let probe_offset_sectors = 256 * batch as u64;
|
damage_window.remove(0);
|
||||||
let probe_lba = ((pos / 2048) + probe_offset_sectors) as u32;
|
}
|
||||||
let probe_end_lba = (region_end / 2048) as u32;
|
|
||||||
if probe_lba < probe_end_lba {
|
let bad_count = damage_window.iter().filter(|&&b| !b).count();
|
||||||
match reader.read_sectors(probe_lba, 1, &mut probe_buf, false) {
|
if damage_window.len() >= DAMAGE_WINDOW
|
||||||
Ok(_) => {
|
&& bad_count * 100 / damage_window.len() >= DAMAGE_THRESHOLD_PCT
|
||||||
let jump_pos = probe_lba as u64 * 2048;
|
{
|
||||||
let gap_start = pos + block_bytes;
|
let jump_sectors = JUMP_SECTORS_FACTOR * batch as u64 * jump_multiplier;
|
||||||
let gap_bytes = jump_pos.saturating_sub(gap_start);
|
let jump_lba = ((pos / 2048) + jump_sectors) as u32;
|
||||||
let jump_mb = gap_bytes / 1_048_576;
|
let region_end_lba = (region_end / 2048) as u32;
|
||||||
if gap_bytes > 0 {
|
if jump_lba < region_end_lba {
|
||||||
tracing::warn!(
|
let jump_pos = jump_lba as u64 * 2048;
|
||||||
target: "freemkv::disc",
|
let gap_start = pos + block_bytes;
|
||||||
phase = "probe_jump",
|
let gap_bytes = jump_pos.saturating_sub(gap_start);
|
||||||
from_lba = block_lba,
|
let jump_mb = gap_bytes / 1_048_576;
|
||||||
to_lba = probe_lba,
|
tracing::warn!(
|
||||||
jump_mb,
|
target: "freemkv::disc",
|
||||||
errors = error_count_in_zone,
|
phase = "damage_jump",
|
||||||
"probe found good media; jumping ahead"
|
from_lba = block_lba,
|
||||||
);
|
to_lba = jump_lba,
|
||||||
let zero = vec![0u8; 65536];
|
jump_mb,
|
||||||
let mut filled: u64 = 0;
|
bad_pct = bad_count * 100 / damage_window.len(),
|
||||||
while filled < gap_bytes {
|
multiplier = jump_multiplier,
|
||||||
let chunk = (gap_bytes - filled).min(zero.len() as u64);
|
"25%+ failures in last 50 blocks; jumping ahead"
|
||||||
file.seek(SeekFrom::Start(gap_start + filled))
|
);
|
||||||
.map_err(|e| Error::IoError { source: e })?;
|
if gap_bytes > 0 {
|
||||||
file.write_all(&zero[..chunk as usize])
|
let zero = vec![0u8; 65536];
|
||||||
.map_err(|e| Error::IoError { source: e })?;
|
let mut filled: u64 = 0;
|
||||||
filled += chunk;
|
while filled < gap_bytes {
|
||||||
}
|
let chunk = (gap_bytes - filled).min(zero.len() as u64);
|
||||||
map.record(gap_start, gap_bytes, mapfile::SectorStatus::NonTrimmed)
|
file.seek(SeekFrom::Start(gap_start + filled))
|
||||||
.map_err(|e| Error::IoError { source: e })?;
|
.map_err(|e| Error::IoError { source: e })?;
|
||||||
bytes_done = bytes_done.saturating_add(gap_bytes);
|
file.write_all(&zero[..chunk as usize])
|
||||||
}
|
.map_err(|e| Error::IoError { source: e })?;
|
||||||
pos = jump_pos;
|
filled += chunk;
|
||||||
did_skip_ahead = true;
|
|
||||||
error_count_in_zone = 0;
|
|
||||||
}
|
}
|
||||||
Err(_) => {}
|
map.record(gap_start, gap_bytes, mapfile::SectorStatus::NonTrimmed)
|
||||||
|
.map_err(|e| Error::IoError { source: e })?;
|
||||||
|
bytes_done = bytes_done.saturating_add(gap_bytes);
|
||||||
}
|
}
|
||||||
|
pos = jump_pos;
|
||||||
|
jump_multiplier *= 2;
|
||||||
|
did_skip_ahead = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1861,6 +1902,14 @@ const MAX_BATCH_SECTORS: u16 = 510;
|
|||||||
const DEFAULT_BATCH_SECTORS: u16 = 60;
|
const DEFAULT_BATCH_SECTORS: u16 = 60;
|
||||||
const MIN_BATCH_SECTORS: u16 = 3;
|
const MIN_BATCH_SECTORS: u16 = 3;
|
||||||
|
|
||||||
|
pub fn ecc_sectors(format: DiscFormat) -> u16 {
|
||||||
|
match format {
|
||||||
|
DiscFormat::Uhd | DiscFormat::BluRay => 32,
|
||||||
|
DiscFormat::Dvd => 16,
|
||||||
|
DiscFormat::Unknown => 32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Coarse damage tier for a finished or in-progress rip. Maps the
|
/// Coarse damage tier for a finished or in-progress rip. Maps the
|
||||||
/// observable signals (bad sector count + lost wallclock playback time)
|
/// observable signals (bad sector count + lost wallclock playback time)
|
||||||
/// onto a small discrete classification so UIs can render a colored badge
|
/// onto a small discrete classification so UIs can render a colored badge
|
||||||
|
|||||||
@@ -516,6 +516,23 @@ impl Error {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True if this error indicates bridge degradation — the SCSI status
|
||||||
|
/// is neither GOOD (0x00), CHECK CONDITION (0x02), nor transport failure
|
||||||
|
/// (0xFF). Observed on the Initio INIC-1618L USB bridge preceding a full
|
||||||
|
/// crash: the bridge firmware returns non-standard status bytes (e.g.
|
||||||
|
/// 0x04, 0x05) with empty sense data. The caller should cool down
|
||||||
|
/// (10 s pause) and retry rather than hammering the bridge.
|
||||||
|
pub fn is_bridge_degradation(&self) -> bool {
|
||||||
|
let status = match self {
|
||||||
|
Error::ScsiError { status, .. } => *status,
|
||||||
|
Error::DiscRead { status, .. } => status.unwrap_or(0),
|
||||||
|
_ => return false,
|
||||||
|
};
|
||||||
|
status != crate::scsi::SCSI_STATUS_GOOD
|
||||||
|
&& status != crate::scsi::SCSI_STATUS_CHECK_CONDITION
|
||||||
|
&& status != crate::scsi::SCSI_STATUS_TRANSPORT_FAILURE
|
||||||
|
}
|
||||||
|
|
||||||
/// True if the underlying SCSI failure is a *marginal read* — the
|
/// True if the underlying SCSI failure is a *marginal read* — the
|
||||||
/// drive returned an error category in which smaller-granularity
|
/// drive returned an error category in which smaller-granularity
|
||||||
/// retries can sometimes recover the data:
|
/// retries can sometimes recover the data:
|
||||||
|
|||||||
Reference in New Issue
Block a user