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<f64> 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<Item = (f64, f64)> + '_ { 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); }}
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<f64> 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<Item = (f64, f64)> + '_ {
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);
}
}