use std::collections::VecDeque;
use std::time::{Duration, Instant};
use dre_core::run::RowProgress;
const WINDOW: Duration = Duration::from_secs(30);
pub const EVERY: Duration = Duration::from_secs(30);
const MIN_ROWS: u64 = 5;
pub struct RowMeter {
samples: VecDeque<(Instant, u64)>,
last_line: Instant,
}
impl RowMeter {
pub fn new(now: Instant) -> RowMeter {
RowMeter {
samples: VecDeque::from([(now, 0)]),
last_line: now,
}
}
pub fn record(&mut self, now: Instant, done: u64) {
self.samples.push_back((now, done));
while self.samples.len() > 2 && now.duration_since(self.samples[1].0) > WINDOW {
self.samples.pop_front();
}
}
pub fn speed(&self, now: Instant) -> Option<f64> {
let &(t0, d0) = self.samples.front()?;
let &(_, d1) = self.samples.back()?;
let secs = now.duration_since(t0).as_secs_f64();
if d1 < MIN_ROWS || secs < 0.5 {
return None;
}
Some((d1 - d0) as f64 / secs)
}
pub fn line_due(&mut self, now: Instant) -> bool {
if now.duration_since(self.last_line) >= EVERY {
self.last_line = now;
true
} else {
false
}
}
}
pub fn describe(p: &RowProgress, speed: Option<f64>, sep: &str) -> String {
let mut parts = vec![format!("{}/{} rows", thousands(p.done), thousands(p.total))];
if p.failed > 0 {
parts.push(format!("{} failed", thousands(p.failed)));
}
let unit = p.rate.map(|r| r.per_seconds);
match (speed, p.rate) {
(Some(s), Some(r)) => parts.push(format!("{} (rate {})", per(s, unit), rate(&r))),
(Some(s), None) => parts.push(per(s, unit)),
(None, Some(r)) => parts.push(format!("rate {}", rate(&r))),
(None, None) => {}
}
if let Some(s) = speed.filter(|s| *s > 0.0)
&& p.done < p.total
{
parts.push(format!(
"~{} left",
duration(((p.total - p.done) as f64 / s).ceil() as u64)
));
}
parts.join(sep)
}
pub fn short_name(label: &str) -> &str {
label.split('`').nth(1).filter(|n| !n.is_empty()).unwrap_or(label)
}
fn per(rows_per_sec: f64, unit: Option<u64>) -> String {
let unit = unit.unwrap_or(if rows_per_sec >= 1.0 {
1
} else if rows_per_sec * 60.0 >= 1.0 {
60
} else {
3600
});
let n = rows_per_sec * unit as f64;
let n = if n >= 10.0 || n == n.trunc() {
format!("{n:.0}")
} else {
format!("{n:.1}")
};
format!("{n}/{}", unit_name(unit))
}
fn rate(r: &dre_core::rate::Rate) -> String {
format!("{}/{}", thousands(r.count), unit_name(r.per_seconds))
}
fn unit_name(per_seconds: u64) -> &'static str {
match per_seconds {
1 => "s",
60 => "min",
_ => "h",
}
}
fn duration(secs: u64) -> String {
match secs {
s if s < 60 => format!("{s}s"),
s if s < 3600 => format!("{}m{:02}s", s / 60, s % 60),
s => format!("{}h{}m", s / 3600, (s % 3600) / 60),
}
}
fn thousands(n: u64) -> String {
let s = n.to_string();
let mut out = String::new();
for (i, c) in s.chars().enumerate() {
if i > 0 && (s.len() - i).is_multiple_of(3) {
out.push(',');
}
out.push(c);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use dre_core::rate::Rate;
fn progress(done: u64, total: u64, failed: u64, rate: Option<&str>) -> RowProgress {
RowProgress {
output: "output `email`".into(),
done,
failed,
total,
rate: rate.map(|r| Rate::parse(r).unwrap()),
finished: false,
}
}
#[test]
fn a_line_every_30_seconds_and_none_for_a_short_job() {
let t0 = Instant::now();
let mut m = RowMeter::new(t0);
assert!(
!m.line_due(t0 + Duration::from_secs(29)),
"a job under 30s prints no line"
);
assert!(m.line_due(t0 + Duration::from_secs(30)));
assert!(!m.line_due(t0 + Duration::from_secs(45)));
assert!(m.line_due(t0 + Duration::from_secs(61)));
}
#[test]
fn speed_is_measured_over_the_recent_window_not_the_rate() {
let t0 = Instant::now();
let mut m = RowMeter::new(t0);
assert_eq!(m.speed(t0), None);
m.record(t0 + Duration::from_secs(1), 3);
assert_eq!(
m.speed(t0 + Duration::from_secs(1)),
None,
"no speed before 5 rows"
);
for s in 1..=10 {
m.record(t0 + Duration::from_secs(s), 36 * s);
}
let now = t0 + Duration::from_secs(10);
let speed = m.speed(now).unwrap();
assert!((speed - 36.0).abs() < 0.01, "{speed}");
assert_eq!(
describe(&progress(360, 400, 0, Some("50/second")), Some(speed), " · "),
"360/400 rows · 36/s (rate 50/s) · ~2s left"
);
for s in 11..=100 {
m.record(t0 + Duration::from_secs(s), 360 + (s - 10));
}
let speed = m.speed(t0 + Duration::from_secs(100)).unwrap();
assert!((speed - 1.0).abs() < 0.1, "{speed}");
}
#[test]
fn descriptions_read_well() {
let p = progress(1200, 20000, 3, Some("100/minute"));
assert_eq!(
describe(&p, Some(98.0 / 60.0), ", "),
"1,200/20,000 rows, 3 failed, 98/min (rate 100/min), ~3h11m left"
);
assert_eq!(describe(&progress(0, 400, 0, None), None, " · "), "0/400 rows");
assert_eq!(
describe(&progress(2, 400, 0, Some("5/second")), None, " · "),
"2/400 rows · rate 5/s"
);
assert_eq!(
describe(&progress(400, 400, 0, None), Some(12.4), " · "),
"400/400 rows · 12/s"
);
assert_eq!(
describe(&progress(10, 400, 0, None), Some(2.5), " · "),
"10/400 rows · 2.5/s · ~2m36s left"
);
assert_eq!(
describe(&progress(10, 400, 0, None), Some(0.5), " · "),
"10/400 rows · 30/min · ~13m00s left"
);
assert_eq!(short_name("output `email`"), "email");
assert_eq!(short_name("output"), "output");
}
}