use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::{broadcast, mpsc, RwLock};
use tokio::task::JoinHandle;
use tracing::debug;
use crate::app::queue::WorkQueue;
use crate::app::worker::WorkerProgress;
use crate::constants::coordinator;
use super::stats::DownloadStats;
pub struct ProgressMonitor {
stats: Arc<RwLock<DownloadStats>>,
queue: Arc<WorkQueue>,
update_interval: Duration,
verbose: bool,
}
impl ProgressMonitor {
pub fn new(
stats: Arc<RwLock<DownloadStats>>,
queue: Arc<WorkQueue>,
update_interval: Duration,
verbose: bool,
) -> Self {
Self {
stats,
queue,
update_interval,
verbose,
}
}
pub fn start_monitoring(
self,
mut progress_rx: mpsc::Receiver<WorkerProgress>,
mut shutdown_rx: broadcast::Receiver<()>,
) -> JoinHandle<()> {
tokio::spawn(async move {
let mut last_update = Instant::now();
let mut bytes_window = Vec::new();
loop {
tokio::select! {
progress = progress_rx.recv() => {
match progress {
Some(update) => {
if self.verbose {
debug!("Worker {} progress: {:?} status, {} files completed",
update.worker_id, update.status, update.files_completed);
}
self.process_worker_update(update, &mut bytes_window).await;
}
None => {
debug!("Progress channel closed");
break;
}
}
}
_ = shutdown_rx.recv() => {
debug!("Progress monitor received shutdown signal");
break;
}
_ = tokio::time::sleep(self.update_interval) => {
self.update_periodic_stats(&mut last_update).await;
}
}
}
})
}
async fn process_worker_update(
&self,
update: WorkerProgress,
bytes_window: &mut Vec<(Instant, u64)>,
) {
let mut stats_guard = self.stats.write().await;
bytes_window.push((Instant::now(), update.bytes_downloaded));
if bytes_window.len() > coordinator::RATE_CALCULATION_WINDOW {
bytes_window.remove(0);
}
if bytes_window.len() >= 2 {
let oldest = &bytes_window[0];
let newest = &bytes_window[bytes_window.len() - 1];
let time_diff = newest.0.duration_since(oldest.0).as_secs_f64();
let bytes_diff = newest.1.saturating_sub(oldest.1);
if time_diff > 0.0 {
stats_guard.download_rate_bps = bytes_diff as f64 / time_diff;
}
}
stats_guard.total_bytes_downloaded = update.total_bytes_downloaded;
}
async fn update_periodic_stats(&self, last_update: &mut Instant) {
if last_update.elapsed() >= self.update_interval {
let queue_stats = self.queue.stats().await;
let mut stats_guard = self.stats.write().await;
stats_guard.files_completed = queue_stats.completed_count as usize;
stats_guard.files_failed = queue_stats.failed_count as usize;
stats_guard.files_in_progress = queue_stats.in_progress_count as usize;
stats_guard.update_duration();
stats_guard.calculate_eta();
*last_update = Instant::now();
}
}
}
pub struct RateCalculator {
window: Vec<(Instant, u64)>,
window_size: usize,
}
impl RateCalculator {
pub fn new(window_size: usize) -> Self {
Self {
window: Vec::new(),
window_size,
}
}
pub fn add_sample(&mut self, bytes: u64) -> f64 {
let now = Instant::now();
self.window.push((now, bytes));
if self.window.len() > self.window_size {
self.window.remove(0);
}
self.calculate_rate()
}
pub fn calculate_rate(&self) -> f64 {
if self.window.len() < 2 {
return 0.0;
}
let oldest = &self.window[0];
let newest = &self.window[self.window.len() - 1];
let time_diff = newest.0.duration_since(oldest.0).as_secs_f64();
let bytes_diff = newest.1.saturating_sub(oldest.1);
if time_diff > 0.0 {
bytes_diff as f64 / time_diff
} else {
0.0
}
}
pub fn sample_count(&self) -> usize {
self.window.len()
}
pub fn clear(&mut self) {
self.window.clear();
}
}
pub struct ProgressAggregator {
total_files_completed: u64,
total_bytes_downloaded: u64,
active_workers: usize,
last_update: Instant,
}
impl ProgressAggregator {
pub fn new() -> Self {
Self {
total_files_completed: 0,
total_bytes_downloaded: 0,
active_workers: 0,
last_update: Instant::now(),
}
}
pub fn update_worker_progress(&mut self, _worker_id: usize, progress: &WorkerProgress) {
self.total_files_completed = self.total_files_completed.max(progress.files_completed);
self.total_bytes_downloaded = self
.total_bytes_downloaded
.max(progress.total_bytes_downloaded);
self.last_update = Instant::now();
}
pub fn set_active_workers(&mut self, count: usize) {
self.active_workers = count;
}
pub fn get_aggregated_stats(&self) -> (u64, u64, usize, Instant) {
(
self.total_files_completed,
self.total_bytes_downloaded,
self.active_workers,
self.last_update,
)
}
pub fn is_stale(&self, max_age: Duration) -> bool {
self.last_update.elapsed() > max_age
}
}
impl Default for ProgressAggregator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::worker::WorkerStatus;
use crate::app::WorkQueue;
use chrono;
#[test]
fn test_rate_calculator() {
let mut calculator = RateCalculator::new(3);
assert_eq!(calculator.calculate_rate(), 0.0);
assert_eq!(calculator.sample_count(), 0);
let _rate1 = calculator.add_sample(1024);
assert_eq!(calculator.sample_count(), 1);
std::thread::sleep(std::time::Duration::from_millis(10));
let rate2 = calculator.add_sample(2048);
assert!(rate2 > 0.0);
assert_eq!(calculator.sample_count(), 2);
calculator.clear();
assert_eq!(calculator.sample_count(), 0);
}
#[test]
fn test_progress_aggregator() {
let mut aggregator = ProgressAggregator::new();
let progress = WorkerProgress {
worker_id: 1,
file_info: None,
bytes_downloaded: 1024,
total_bytes: Some(2048),
download_speed: 1024.0,
eta_seconds: Some(1.0),
status: WorkerStatus::Downloading,
timestamp: chrono::Utc::now(),
files_completed: 10,
total_bytes_downloaded: 10240,
error_message: None,
};
aggregator.update_worker_progress(1, &progress);
aggregator.set_active_workers(4);
let (files, bytes, workers, _) = aggregator.get_aggregated_stats();
assert_eq!(files, 10);
assert_eq!(bytes, 10240);
assert_eq!(workers, 4);
assert!(!aggregator.is_stale(Duration::from_secs(1)));
}
#[tokio::test]
async fn test_progress_monitor_creation() {
let stats = Arc::new(RwLock::new(DownloadStats::default()));
let queue = Arc::new(WorkQueue::new());
let update_interval = Duration::from_millis(100);
let monitor = ProgressMonitor::new(stats.clone(), queue, update_interval, false);
let (_progress_tx, progress_rx) = mpsc::channel(10);
let (shutdown_tx, shutdown_rx) = broadcast::channel(1);
let handle = monitor.start_monitoring(progress_rx, shutdown_rx);
tokio::time::sleep(Duration::from_millis(10)).await;
let _ = shutdown_tx.send(());
let _ = handle.await;
}
#[tokio::test]
async fn test_worker_progress_processing() {
let stats = Arc::new(RwLock::new(DownloadStats::default()));
let queue = Arc::new(WorkQueue::new());
let update_interval = Duration::from_millis(100);
let monitor = ProgressMonitor::new(
stats.clone(),
queue,
update_interval,
true, );
let progress = WorkerProgress {
worker_id: 1,
file_info: None,
bytes_downloaded: 512,
total_bytes: Some(1024),
download_speed: 512.0,
eta_seconds: Some(1.0),
status: WorkerStatus::Downloading,
timestamp: chrono::Utc::now(),
files_completed: 5,
total_bytes_downloaded: 5120,
error_message: None,
};
let mut bytes_window = Vec::new();
monitor
.process_worker_update(progress, &mut bytes_window)
.await;
let stats_guard = stats.read().await;
assert_eq!(stats_guard.total_bytes_downloaded, 5120);
assert_eq!(bytes_window.len(), 1);
}
#[test]
fn test_rate_window_management() {
let mut calculator = RateCalculator::new(3);
for i in 0..5 {
calculator.add_sample((i + 1) * 1024);
std::thread::sleep(std::time::Duration::from_millis(1));
}
assert_eq!(calculator.sample_count(), 3);
let rate = calculator.calculate_rate();
assert!(rate > 0.0);
}
}