use std::{fmt::Display, time::Instant}; use crate::ui::utils::ByteSpeed; #[derive(Debug, Clone, PartialEq)] pub enum EstimatedTime { Known(f64), Unknown, } #[derive(Debug, Clone, PartialEq)] pub struct ByteSeries { raw: Vec<(f64, u64)>, start: Instant, } impl From for EstimatedTime { fn from(value: f64) -> Self { if value.is_finite() { Self::Known(value) } else { Self::Unknown } } } impl Display for EstimatedTime { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { EstimatedTime::Known(x) => write!(f, "{x:.1}s"), EstimatedTime::Unknown => write!(f, "[unknown]"), } } } impl ByteSeries { pub fn new(start: Instant) -> Self { Self { start, raw: vec![(0.0, 0)], } } pub fn push(&mut self, time: Instant, bytes: u64) { let secs = time.duration_since(self.start).as_secs_f64(); self.raw.push((secs, bytes)); } pub fn last_datapoint(&self) -> (f64, u64) { self.raw.last().copied().unwrap_or((0.0, 0)) } pub fn bytes_encountered(&self) -> u64 { self.last_datapoint().1 } pub fn total_avg_speed(&self) -> ByteSpeed { let s = self.bytes_encountered(); let speed = s as f64 / self.last_datapoint().0; ByteSpeed(if speed.is_nan() { 0.0 } else { speed }) } pub fn estimated_time_left(&self, total_bytes: u64) -> EstimatedTime { let speed = self.total_avg_speed().0; // Saturating subtract is necessary because bytes encountered may be greater // than total bytes, due to the nature of block writing. let bytes_left = total_bytes.saturating_sub(self.bytes_encountered()); let secs_left = bytes_left as f64 / speed; EstimatedTime::from(secs_left) } pub fn start(&self) -> Instant { self.start } pub fn speed(&self, t: f64, window: f64) -> f64 { let b0 = self.interp(t - window); let b1 = self.interp(t); (b1 - b0) / window } /// Returns a series of points representing a timeseries, aggregated by the given window size. pub fn speeds(&self, window: f64) -> impl Iterator + '_ { let bins = (self.last_datapoint().0 / window).ceil() as usize; (0..bins).map(move |i| { let t = i as f64 * window; (t, self.speed(t, window)) }) } /// Returns the index of the sample right before the requested time. fn find_idx_below(&self, t: f64) -> usize { let mut min = 0; if t <= self.raw[min].0 { return min; } let mut max = self.raw.len() - 1; if self.raw[max].0 <= t { return max; } loop { if min >= max - 1 { return min; } let mid = (min + max) / 2; let mid_val = self.raw[mid].0; if t < mid_val { max = mid; } else { min = mid; } } } /// Returns the interpolated number of bytes encountered at the given time. pub fn interp(&self, t: f64) -> f64 { if t < 0.0 { return self.raw[0].1 as f64; } let (last, last_val) = self.last_datapoint(); if t >= last { return last_val as f64; } let i0 = self.find_idx_below(t); let i1 = i0 + 1; let (x0, y0) = self.raw[i0]; let (x1, y1) = self.raw[i1]; (y1 as f64 - y0 as f64) * (t - x0) / (x1 - x0) + y0 as f64 } } #[cfg(test)] mod tests { use std::time::{Duration, Instant}; use approx::assert_relative_eq; use super::ByteSeries; use test_case::test_case; fn example_2s() -> ByteSeries { let now = Instant::now(); let mut s = ByteSeries::new(now); s.push(now.checked_add(Duration::from_secs_f64(0.1)).unwrap(), 10); s.push(now.checked_add(Duration::from_secs_f64(0.2)).unwrap(), 20); s.push(now.checked_add(Duration::from_secs_f64(0.5)).unwrap(), 30); s.push(now.checked_add(Duration::from_secs_f64(1.0)).unwrap(), 40); s.push(now.checked_add(Duration::from_secs_f64(1.5)).unwrap(), 80); s.push(now.checked_add(Duration::from_secs_f64(2.0)).unwrap(), 100); s } #[test_case(0.0 => is eq 0; "zero")] #[test_case(-10.0 => is eq 0; "negative")] #[test_case(0.4 => is eq 2; "between")] #[test_case(0.5 => is eq 3; "exact")] #[test_case(2.0 => is eq 6; "exactly last")] #[test_case(3.0 => is eq 6; "over")] fn find_idx_below(t: f64) -> usize { example_2s().find_idx_below(t) } #[test_case(0.0, 0.0; "zero")] #[test_case(-10.0, 0.0; "negative")] #[test_case(0.75, 35.0; "between")] #[test_case(0.5, 30.0; "exact")] #[test_case(2.0, 100.0; "exactly last")] #[test_case(3.0, 100.0; "over")] fn interp_bytes(t: f64, expected: f64) { let actual = example_2s().interp(t); assert_relative_eq!(actual, expected); } }