use std::collections::VecDeque;
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use crate::config::FetchPhase;
use crate::progress::PerformanceMetrics;
use crate::progress::Progress;
#[derive(Debug, Clone)]
pub struct ExtendedProgress {
pub base: Progress,
pub rate_bps: Option<f64>,
pub eta_seconds: Option<u64>,
pub history: VecDeque<ProgressSnapshot>,
pub start_time: Instant,
pub last_update: Instant,
pub performance_metrics: PerformanceMetrics,
}
#[derive(Debug, Clone)]
pub struct ProgressSnapshot {
pub timestamp: u64,
pub bytes_downloaded: u64,
}
impl ExtendedProgress {
pub fn new(mut base: Progress) -> Self {
let now = Instant::now();
let mut history = VecDeque::with_capacity(100);
history.push_back(ProgressSnapshot {
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64,
bytes_downloaded: base.bytes_downloaded,
});
let performance_metrics = base.performance_metrics.take().unwrap_or_default();
Self {
base,
rate_bps: None,
eta_seconds: None,
history,
start_time: now,
last_update: now,
performance_metrics,
}
}
pub fn update(&mut self, progress: Progress) {
let now = Instant::now();
self.history.push_back(ProgressSnapshot {
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64,
bytes_downloaded: progress.bytes_downloaded,
});
if self.history.len() > 100 {
self.history.pop_front();
}
self.rate_bps = self.calculate_rate();
self.eta_seconds = self.calculate_eta();
self.rate_bps = self.calculate_rate();
self.eta_seconds = self.calculate_eta();
self.base = progress;
self.last_update = now;
}
fn calculate_rate(&self) -> Option<f64> {
if self.history.len() < 2 {
return None;
}
let recent = &self.history;
let time_diff = recent.back().unwrap().timestamp - recent.front().unwrap().timestamp;
if time_diff == 0 {
return None;
}
let bytes_diff =
recent.back().unwrap().bytes_downloaded - recent.front().unwrap().bytes_downloaded;
let rate = bytes_diff as f64 / (time_diff as f64 / 1000.0);
Some(rate)
}
fn calculate_eta(&self) -> Option<u64> {
if let (Some(rate), Some(total)) = (self.rate_bps, self.base.total_bytes) {
if rate > 0.0 && self.base.bytes_downloaded < total {
let remaining = total - self.base.bytes_downloaded;
Some((remaining as f64 / rate) as u64)
} else {
None
}
} else {
None
}
}
pub fn speed_string(&self) -> String {
if let Some(rate) = self.rate_bps {
if rate >= 1_000_000.0 {
format!("{:.1} MB/s", rate / 1_000_000.0)
} else if rate >= 1000.0 {
format!("{:.1} kB/s", rate / 1000.0)
} else {
format!("{:.0} B/s", rate)
}
} else {
"Unknown".to_string()
}
}
pub fn eta_string(&self) -> String {
if let Some(eta) = self.eta_seconds {
if eta >= 3600 {
let hours = eta / 3600;
let minutes = (eta % 3600) / 60;
format!("{}h {}m", hours, minutes)
} else if eta >= 60 {
format!("{}m", eta / 60)
} else {
format!("{}s", eta)
}
} else {
"Unknown".to_string()
}
}
pub fn elapsed_seconds(&self) -> u64 {
self.start_time.elapsed().as_secs()
}
pub fn elapsed_string(&self) -> String {
let elapsed = self.elapsed_seconds();
if elapsed >= 3600 {
let hours = elapsed / 3600;
let minutes = (elapsed % 3600) / 60;
format!("{}h {}m", hours, minutes)
} else if elapsed >= 60 {
format!("{}m", elapsed / 60)
} else {
format!("{}s", elapsed)
}
}
}
pub struct ProgressReporter {
pub trackers: Vec<ExtendedProgress>,
}
impl Default for ProgressReporter {
fn default() -> Self {
Self::new()
}
}
impl ProgressReporter {
pub fn new() -> Self {
Self {
trackers: Vec::new(),
}
}
pub fn add_tracker(&mut self, progress: Progress) -> usize {
let extended = ExtendedProgress::new(progress);
self.trackers.push(extended);
self.trackers.len() - 1
}
pub fn update_tracker(&mut self, index: usize, progress: Progress) {
if let Some(tracker) = self.trackers.get_mut(index) {
tracker.update(progress);
}
}
pub fn get_tracker(&self, index: usize) -> Option<&ExtendedProgress> {
self.trackers.get(index)
}
pub fn total_progress(&self) -> Progress {
let total_bytes: u64 = self.trackers.iter().map(|t| t.base.bytes_downloaded).sum();
let total_estimated: Option<u64> = self
.trackers
.iter()
.filter_map(|t| t.base.total_bytes)
.reduce(|acc, x| acc + x);
Progress {
phase: if self.trackers.iter().all(|t| t.base.is_completed()) {
FetchPhase::Completed
} else if self.trackers.iter().any(|t| t.base.is_retrying()) {
FetchPhase::Connecting
} else {
FetchPhase::Downloading
},
bytes_downloaded: total_bytes,
total_bytes: total_estimated,
retry_count: self
.trackers
.iter()
.map(|t| t.base.retry_count)
.max()
.unwrap_or(0),
performance_metrics: None,
}
}
pub fn total_rate(&self) -> Option<f64> {
let total_rate: f64 = self.trackers.iter().filter_map(|t| t.rate_bps).sum();
if total_rate > 0.0 {
Some(total_rate)
} else {
None
}
}
pub fn total_eta(&self) -> Option<u64> {
let total_remaining: u64 = self
.trackers
.iter()
.filter_map(|t| {
if let (Some(total), Some(downloaded)) =
(t.base.total_bytes, Some(t.base.bytes_downloaded))
{
if downloaded < total {
Some(total - downloaded)
} else {
None
}
} else {
None
}
})
.sum();
if let (Some(total_rate), _) = (self.total_rate(), Some(total_remaining)) {
if total_rate > 0.0 {
Some((total_remaining as f64 / total_rate) as u64)
} else {
None
}
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
use tokio::time::sleep;
#[test]
fn test_extended_progress_creation() {
let base = Progress {
phase: FetchPhase::Downloading,
bytes_downloaded: 512,
total_bytes: Some(1024),
retry_count: 0,
performance_metrics: None,
};
let extended = ExtendedProgress::new(base);
assert_eq!(extended.base.bytes_downloaded, 512);
assert_eq!(extended.base.total_bytes, Some(1024));
assert_eq!(extended.rate_bps, None);
assert_eq!(extended.eta_seconds, None);
assert_eq!(extended.history.len(), 1); }
#[tokio::test]
async fn test_rate_calculation() {
let mut extended = ExtendedProgress::new(Progress {
phase: FetchPhase::Downloading,
bytes_downloaded: 0,
total_bytes: Some(1000),
retry_count: 0,
performance_metrics: None,
});
let _start = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
extended.update(Progress {
phase: FetchPhase::Downloading,
bytes_downloaded: 100,
total_bytes: Some(1000),
retry_count: 0,
performance_metrics: None,
});
tokio::time::sleep(Duration::from_secs(1)).await;
extended.update(Progress {
phase: FetchPhase::Downloading,
bytes_downloaded: 200,
total_bytes: Some(1000),
retry_count: 0,
performance_metrics: None,
});
assert!(extended.rate_bps.is_some());
assert!(extended.rate_bps.unwrap() > 0.0);
let rate = extended.rate_bps.unwrap();
assert!(rate > 10.0 && rate < 500.0); }
#[tokio::test]
async fn test_eta_calculation() {
let mut extended = ExtendedProgress::new(Progress {
phase: FetchPhase::Downloading,
bytes_downloaded: 0,
total_bytes: Some(1000),
retry_count: 0,
performance_metrics: None,
});
for i in 0..5 {
let progress = Progress {
phase: FetchPhase::Downloading,
bytes_downloaded: (i + 1) * 200,
total_bytes: Some(1000),
retry_count: 0,
performance_metrics: None,
};
extended.update(progress);
sleep(Duration::from_millis(100)).await;
}
assert!(extended.eta_seconds.is_some());
let eta = extended.eta_seconds.unwrap();
assert!(eta <= 10); }
#[test]
fn test_speed_string() {
let mut extended = ExtendedProgress::new(Progress {
phase: FetchPhase::Downloading,
bytes_downloaded: 0,
total_bytes: Some(1024),
retry_count: 0,
performance_metrics: None,
});
assert_eq!(extended.speed_string(), "Unknown");
extended.rate_bps = Some(1024.0);
assert_eq!(extended.speed_string(), "1.0 kB/s");
extended.rate_bps = Some(2_048_000.0);
assert_eq!(extended.speed_string(), "2.0 MB/s");
extended.rate_bps = Some(512.0);
assert_eq!(extended.speed_string(), "512 B/s");
}
#[test]
fn test_eta_string() {
let mut extended = ExtendedProgress::new(Progress {
phase: FetchPhase::Downloading,
bytes_downloaded: 0,
total_bytes: Some(1024),
retry_count: 0,
performance_metrics: None,
});
assert_eq!(extended.eta_string(), "Unknown");
extended.eta_seconds = Some(30);
assert_eq!(extended.eta_string(), "30s");
extended.eta_seconds = Some(90);
assert_eq!(extended.eta_string(), "1m");
extended.eta_seconds = Some(3661);
assert_eq!(extended.eta_string(), "1h 1m");
extended.eta_seconds = Some(7200);
assert_eq!(extended.eta_string(), "2h 0m");
}
#[tokio::test]
async fn test_elapsed_time() {
let extended = ExtendedProgress::new(Progress {
phase: FetchPhase::Downloading,
bytes_downloaded: 0,
total_bytes: Some(1024),
retry_count: 0,
performance_metrics: None,
});
assert_eq!(extended.elapsed_seconds(), 0);
sleep(Duration::from_millis(1500)).await;
assert!(extended.elapsed_seconds() >= 1);
assert_eq!(extended.elapsed_string(), "1s");
sleep(Duration::from_secs(90)).await;
assert!(extended.elapsed_seconds() >= 91);
assert_eq!(extended.elapsed_string(), "1m");
}
#[test]
fn test_progress_reporter() {
let mut reporter = ProgressReporter::new();
let progress1 = Progress {
phase: FetchPhase::Downloading,
bytes_downloaded: 256,
total_bytes: Some(512),
retry_count: 0,
performance_metrics: None,
};
let progress2 = Progress {
phase: FetchPhase::Downloading,
bytes_downloaded: 128,
total_bytes: Some(256),
retry_count: 1,
performance_metrics: None,
};
let id1 = reporter.add_tracker(progress1);
let id2 = reporter.add_tracker(progress2);
assert_eq!(id1, 0);
assert_eq!(id2, 1);
assert_eq!(reporter.trackers.len(), 2);
let updated1 = Progress {
phase: FetchPhase::Downloading,
bytes_downloaded: 512,
total_bytes: Some(512),
retry_count: 0,
performance_metrics: None,
};
reporter.update_tracker(0, updated1);
assert_eq!(reporter.get_tracker(0).unwrap().base.bytes_downloaded, 512);
let total = reporter.total_progress();
assert_eq!(total.bytes_downloaded, 640); assert_eq!(total.total_bytes, Some(768)); }
#[test]
fn test_total_metrics() {
let mut reporter = ProgressReporter::new();
let progress1 = Progress {
phase: FetchPhase::Downloading,
bytes_downloaded: 0,
total_bytes: Some(1000),
retry_count: 0,
performance_metrics: None,
};
let progress2 = Progress {
phase: FetchPhase::Downloading,
bytes_downloaded: 0,
total_bytes: Some(2000),
retry_count: 0,
performance_metrics: None,
};
reporter.add_tracker(progress1);
reporter.add_tracker(progress2);
let updated1 = Progress {
phase: FetchPhase::Downloading,
bytes_downloaded: 500,
total_bytes: Some(1000),
retry_count: 0,
performance_metrics: None,
};
let updated2 = Progress {
phase: FetchPhase::Downloading,
bytes_downloaded: 1000,
total_bytes: Some(2000),
retry_count: 0,
performance_metrics: None,
};
reporter.update_tracker(0, updated1);
reporter.update_tracker(1, updated2);
let total = reporter.total_progress();
assert_eq!(total.bytes_downloaded, 1500); assert_eq!(total.total_bytes, Some(3000)); }
}