#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PassKind {
Sweep,
Trim { reverse: bool },
Scrape { reverse: bool },
Mux,
Verify,
}
#[derive(Debug, Clone, Copy)]
pub struct PassProgress {
pub kind: PassKind,
pub work_done: u64,
pub work_total: u64,
pub bytes_good_total: u64,
pub bytes_unreadable_total: u64,
pub bytes_pending_total: u64,
pub bytes_total_disc: u64,
pub disc_duration_secs: Option<f64>,
pub bytes_bad_in_main_title: u64,
pub main_title_duration_secs: Option<f64>,
pub main_title_size_bytes: Option<u64>,
}
impl PassProgress {
pub fn work_pct(&self) -> f64 {
if self.work_total == 0 {
return 100.0;
}
(self.work_done as f64 / self.work_total as f64 * 100.0).clamp(0.0, 100.0)
}
pub fn good_pct(&self) -> f64 {
if self.bytes_total_disc == 0 {
return 100.0;
}
(self.bytes_good_total as f64 / self.bytes_total_disc as f64 * 100.0).clamp(0.0, 100.0)
}
pub fn bad_pct(&self) -> f64 {
if self.bytes_total_disc == 0 {
return 0.0;
}
(self.bytes_unreadable_total as f64 / self.bytes_total_disc as f64 * 100.0)
.clamp(0.0, 100.0)
}
pub fn pending_pct(&self) -> f64 {
if self.bytes_total_disc == 0 {
return 0.0;
}
(self.bytes_pending_total as f64 / self.bytes_total_disc as f64 * 100.0).clamp(0.0, 100.0)
}
}
#[derive(Debug)]
pub struct Heartbeat {
phase: &'static str,
interval: std::time::Duration,
start: std::time::Instant,
last: std::time::Instant,
cpu_counter: u32,
}
impl Heartbeat {
pub const DEFAULT_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5);
pub fn new(phase: &'static str) -> Self {
Self::with_interval(phase, Self::DEFAULT_INTERVAL)
}
pub fn with_interval(phase: &'static str, interval: std::time::Duration) -> Self {
let now = std::time::Instant::now();
Self {
phase,
interval,
start: now,
last: now,
cpu_counter: 0,
}
}
pub fn tick(&mut self, pos: u64, total: u64) -> bool {
let now = std::time::Instant::now();
if now.duration_since(self.last) < self.interval {
return false;
}
self.last = now;
self.emit(pos, total, now);
true
}
pub fn tick_cpu(&mut self, pos: u64, total: u64) -> bool {
self.cpu_counter = self.cpu_counter.wrapping_add(1);
if self.cpu_counter % 256 != 0 {
return false;
}
self.tick(pos, total)
}
fn emit(&self, pos: u64, total: u64, now: std::time::Instant) {
let pct = if total == 0 {
0.0
} else {
(pos as f64 / total as f64 * 100.0).clamp(0.0, 100.0)
};
let elapsed_ms = now.duration_since(self.start).as_millis() as u64;
tracing::debug!(
target: "freemkv::heartbeat",
phase = self.phase,
pos,
total,
pct,
elapsed_ms,
"alive"
);
}
}
pub trait Progress {
fn report(&self, p: &PassProgress) -> bool;
}
impl<F: Fn(&PassProgress) -> bool> Progress for F {
fn report(&self, p: &PassProgress) -> bool {
(self)(p)
}
}
#[cfg(test)]
mod heartbeat_tests {
use super::Heartbeat;
use std::time::Duration;
#[test]
fn first_tick_does_not_beat() {
let mut hb = Heartbeat::with_interval("test", Duration::from_secs(60));
assert!(!hb.tick(0, 100));
assert!(!hb.tick(50, 100));
}
#[test]
fn beats_once_per_interval() {
let mut hb = Heartbeat::with_interval("test", Duration::from_millis(10));
assert!(!hb.tick(1, 100));
std::thread::sleep(Duration::from_millis(15));
assert!(hb.tick(2, 100), "should beat after interval elapsed");
assert!(!hb.tick(3, 100));
}
#[test]
fn tick_cpu_throttles_clock_reads() {
let mut hb = Heartbeat::with_interval("test", Duration::from_nanos(0));
for _ in 0..255 {
assert!(!hb.tick_cpu(0, 100));
}
assert!(hb.tick_cpu(0, 100));
}
}