use core::time::Duration;
use std::{fmt::Debug, time::Instant};
use super::Error;
#[derive(Debug)]
pub enum Progress<RE: Debug, FE: Debug> {
Incomplete {
download: ProgressDetails,
unzip: ProgressDetails,
delete: ProgressDetails,
},
Successful,
Errored(Error<RE, FE>),
}
#[derive(Debug, Clone)]
pub struct ProgressDetails {
total_bytes: u64,
processed_bytes: u64,
last_rate_check: Instant,
processed_since_last_check: u64,
bytes_per_sec: u64,
}
impl<RE: Debug, FE: Debug> Clone for Progress<RE, FE>
where
Error<RE, FE>: Clone,
{
fn clone(&self) -> Self {
match self {
Self::Incomplete {
download,
unzip,
delete,
} => Self::Incomplete {
download: download.clone(),
unzip: unzip.clone(),
delete: delete.clone(),
},
Self::Successful => Self::Successful,
Self::Errored(e) => Self::Errored(e.clone()),
}
}
}
impl ProgressDetails {
pub(crate) fn new(total_bytes: u64) -> Self {
Self {
total_bytes,
processed_bytes: 0,
last_rate_check: Instant::now(),
processed_since_last_check: 0,
bytes_per_sec: 0,
}
}
pub(crate) fn add_chunk(&mut self, data: u64) {
self.processed_bytes += data;
self.processed_since_last_check += data;
if self.processed_bytes > self.total_bytes {
let process = &self;
tracing::warn!(
?process,
"Processed Bytes is larger than Total Bytes, something seems off"
);
}
let current_time = Instant::now();
let since_last_check = current_time - self.last_rate_check;
let since_last_check_f32 = since_last_check.as_secs_f32();
if since_last_check >= Duration::from_millis(500) || (since_last_check_f32 > 0.0 && self.bytes_per_sec == 0) {
let bytes_per_sec = (self.processed_since_last_check as f32 / since_last_check_f32) as u64;
self.processed_since_last_check = 0;
self.last_rate_check = current_time;
if self.bytes_per_sec == 0 {
self.bytes_per_sec = bytes_per_sec;
} else {
self.bytes_per_sec = (self.bytes_per_sec * 3 + bytes_per_sec) / 4;
}
}
}
pub fn total_bytes(&self) -> u64 { self.total_bytes }
pub fn processed_bytes(&self) -> u64 { self.processed_bytes }
pub fn is_finished(&self) -> bool { self.processed_bytes >= self.total_bytes }
pub fn bytes_per_sec(&self) -> u64 { self.bytes_per_sec }
pub fn percent_complete(&self) -> u64 {
(self.processed_bytes * 100)
.checked_div(self.total_bytes)
.unwrap_or(100)
}
pub fn time_remaining(&self) -> Duration {
Duration::from_secs_f32(
(self.total_bytes.saturating_sub(self.processed_bytes)) as f32 / self.bytes_per_sec.max(1) as f32,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_chunk() {
let mut details = ProgressDetails::new(1024);
assert_eq!(details.total_bytes, 1024);
assert_eq!(details.processed_bytes, 0);
const QUARTER_TIME: Duration = Duration::from_millis(100);
std::thread::sleep(QUARTER_TIME);
details.add_chunk(256);
assert_eq!(details.processed_bytes, 256);
assert_ne!(details.bytes_per_sec, 0);
assert!(!details.is_finished());
let time_remaining = details.time_remaining();
assert!(time_remaining > QUARTER_TIME * 2);
assert!(time_remaining < QUARTER_TIME * 4);
details.add_chunk(768);
assert_eq!(details.processed_bytes, 1024);
assert_ne!(details.bytes_per_sec, 0);
assert!(details.is_finished());
}
#[test]
fn test_progress_clone_variants() {
let progress1 = Progress::<(), ()>::Incomplete {
download: ProgressDetails::new(0),
unzip: ProgressDetails::new(0),
delete: ProgressDetails::new(0),
};
let progress2 = Progress::<(), ()>::Successful;
let progress3 = Progress::<(), ()>::Errored(Error::JoinError);
assert!(matches!(progress1.clone(), Progress::Incomplete { .. }));
assert!(matches!(progress2.clone(), Progress::Successful));
assert!(matches!(
progress3.clone(),
Progress::<(), ()>::Errored(Error::JoinError)
));
}
#[test]
fn test_progress_details() {
let mut progress1 = ProgressDetails::new(1000);
assert_eq!(progress1.total_bytes(), 1000);
assert_eq!(progress1.processed_bytes(), 0);
assert!(!progress1.is_finished());
assert_eq!(progress1.percent_complete(), 0);
progress1.add_chunk(500);
assert_eq!(progress1.processed_bytes(), 500);
assert_eq!(progress1.percent_complete(), 50);
progress1.add_chunk(500);
assert_eq!(progress1.processed_bytes(), 1000);
assert_eq!(progress1.percent_complete(), 100);
assert!(progress1.is_finished());
let progress2 = ProgressDetails::new(0);
assert_eq!(progress2.percent_complete(), 100);
assert!(progress2.is_finished());
}
}