1use std::collections::VecDeque;
7use std::time::{Instant, SystemTime, UNIX_EPOCH};
8
9use crate::config::FetchPhase;
10use crate::progress::PerformanceMetrics;
11use crate::progress::Progress;
12
13#[derive(Debug, Clone)]
15pub struct ExtendedProgress {
16 pub base: Progress,
18
19 pub rate_bps: Option<f64>,
21
22 pub eta_seconds: Option<u64>,
24
25 pub history: VecDeque<ProgressSnapshot>,
27
28 pub start_time: Instant,
30
31 pub last_update: Instant,
33
34 pub performance_metrics: PerformanceMetrics,
36}
37
38#[derive(Debug, Clone)]
40pub struct ProgressSnapshot {
41 pub timestamp: u64,
43 pub bytes_downloaded: u64,
45}
46
47impl ExtendedProgress {
48 pub fn new(mut base: Progress) -> Self {
50 let now = Instant::now();
51 let mut history = VecDeque::with_capacity(100);
52
53 history.push_back(ProgressSnapshot {
55 timestamp: SystemTime::now()
56 .duration_since(UNIX_EPOCH)
57 .unwrap_or_default()
58 .as_millis() as u64,
59 bytes_downloaded: base.bytes_downloaded,
60 });
61
62 let performance_metrics = base.performance_metrics.take().unwrap_or_default();
63
64 Self {
65 base,
66 rate_bps: None,
67 eta_seconds: None,
68 history,
69 start_time: now,
70 last_update: now,
71 performance_metrics,
72 }
73 }
74
75 pub fn update(&mut self, progress: Progress) {
77 let now = Instant::now();
78
79 self.history.push_back(ProgressSnapshot {
81 timestamp: SystemTime::now()
82 .duration_since(UNIX_EPOCH)
83 .unwrap_or_default()
84 .as_millis() as u64,
85 bytes_downloaded: progress.bytes_downloaded,
86 });
87
88 if self.history.len() > 100 {
90 self.history.pop_front();
91 }
92
93 self.rate_bps = self.calculate_rate();
95 self.eta_seconds = self.calculate_eta();
96
97 self.rate_bps = self.calculate_rate();
99
100 self.eta_seconds = self.calculate_eta();
102
103 self.base = progress;
105 self.last_update = now;
106 }
107
108 fn calculate_rate(&self) -> Option<f64> {
110 if self.history.len() < 2 {
111 return None;
112 }
113
114 let recent = &self.history;
115 let time_diff = recent.back().unwrap().timestamp - recent.front().unwrap().timestamp;
116
117 if time_diff == 0 {
118 return None;
119 }
120
121 let bytes_diff =
122 recent.back().unwrap().bytes_downloaded - recent.front().unwrap().bytes_downloaded;
123 let rate = bytes_diff as f64 / (time_diff as f64 / 1000.0);
124
125 Some(rate)
127 }
128
129 fn calculate_eta(&self) -> Option<u64> {
131 if let (Some(rate), Some(total)) = (self.rate_bps, self.base.total_bytes) {
132 if rate > 0.0 && self.base.bytes_downloaded < total {
133 let remaining = total - self.base.bytes_downloaded;
134 Some((remaining as f64 / rate) as u64)
135 } else {
136 None
137 }
138 } else {
139 None
140 }
141 }
142
143 pub fn speed_string(&self) -> String {
145 if let Some(rate) = self.rate_bps {
146 if rate >= 1_000_000.0 {
147 format!("{:.1} MB/s", rate / 1_000_000.0)
148 } else if rate >= 1000.0 {
149 format!("{:.1} kB/s", rate / 1000.0)
150 } else {
151 format!("{:.0} B/s", rate)
152 }
153 } else {
154 "Unknown".to_string()
155 }
156 }
157
158 pub fn eta_string(&self) -> String {
160 if let Some(eta) = self.eta_seconds {
161 if eta >= 3600 {
162 let hours = eta / 3600;
163 let minutes = (eta % 3600) / 60;
164 format!("{}h {}m", hours, minutes)
165 } else if eta >= 60 {
166 format!("{}m", eta / 60)
167 } else {
168 format!("{}s", eta)
169 }
170 } else {
171 "Unknown".to_string()
172 }
173 }
174
175 pub fn elapsed_seconds(&self) -> u64 {
177 self.start_time.elapsed().as_secs()
178 }
179
180 pub fn elapsed_string(&self) -> String {
182 let elapsed = self.elapsed_seconds();
183 if elapsed >= 3600 {
184 let hours = elapsed / 3600;
185 let minutes = (elapsed % 3600) / 60;
186 format!("{}h {}m", hours, minutes)
187 } else if elapsed >= 60 {
188 format!("{}m", elapsed / 60)
189 } else {
190 format!("{}s", elapsed)
191 }
192 }
193}
194
195pub struct ProgressReporter {
197 pub trackers: Vec<ExtendedProgress>,
199}
200
201impl Default for ProgressReporter {
202 fn default() -> Self {
203 Self::new()
204 }
205}
206
207impl ProgressReporter {
208 pub fn new() -> Self {
210 Self {
211 trackers: Vec::new(),
212 }
213 }
214
215 pub fn add_tracker(&mut self, progress: Progress) -> usize {
217 let extended = ExtendedProgress::new(progress);
218 self.trackers.push(extended);
219 self.trackers.len() - 1
220 }
221
222 pub fn update_tracker(&mut self, index: usize, progress: Progress) {
224 if let Some(tracker) = self.trackers.get_mut(index) {
225 tracker.update(progress);
226 }
227 }
228
229 pub fn get_tracker(&self, index: usize) -> Option<&ExtendedProgress> {
231 self.trackers.get(index)
232 }
233
234 pub fn total_progress(&self) -> Progress {
236 let total_bytes: u64 = self.trackers.iter().map(|t| t.base.bytes_downloaded).sum();
237 let total_estimated: Option<u64> = self
238 .trackers
239 .iter()
240 .filter_map(|t| t.base.total_bytes)
241 .reduce(|acc, x| acc + x);
242
243 Progress {
244 phase: if self.trackers.iter().all(|t| t.base.is_completed()) {
245 FetchPhase::Completed
246 } else if self.trackers.iter().any(|t| t.base.is_retrying()) {
247 FetchPhase::Connecting
248 } else {
249 FetchPhase::Downloading
250 },
251 bytes_downloaded: total_bytes,
252 total_bytes: total_estimated,
253 retry_count: self
254 .trackers
255 .iter()
256 .map(|t| t.base.retry_count)
257 .max()
258 .unwrap_or(0),
259 performance_metrics: None,
260 }
261 }
262
263 pub fn total_rate(&self) -> Option<f64> {
265 let total_rate: f64 = self.trackers.iter().filter_map(|t| t.rate_bps).sum();
266
267 if total_rate > 0.0 {
268 Some(total_rate)
269 } else {
270 None
271 }
272 }
273
274 pub fn total_eta(&self) -> Option<u64> {
276 let total_remaining: u64 = self
277 .trackers
278 .iter()
279 .filter_map(|t| {
280 if let (Some(total), Some(downloaded)) =
281 (t.base.total_bytes, Some(t.base.bytes_downloaded))
282 {
283 if downloaded < total {
284 Some(total - downloaded)
285 } else {
286 None
287 }
288 } else {
289 None
290 }
291 })
292 .sum();
293
294 if let (Some(total_rate), _) = (self.total_rate(), Some(total_remaining)) {
295 if total_rate > 0.0 {
296 Some((total_remaining as f64 / total_rate) as u64)
297 } else {
298 None
299 }
300 } else {
301 None
302 }
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use std::time::Duration;
310 use tokio::time::sleep;
311
312 #[test]
313 fn test_extended_progress_creation() {
314 let base = Progress {
315 phase: FetchPhase::Downloading,
316 bytes_downloaded: 512,
317 total_bytes: Some(1024),
318 retry_count: 0,
319 performance_metrics: None,
320 };
321
322 let extended = ExtendedProgress::new(base);
323
324 assert_eq!(extended.base.bytes_downloaded, 512);
325 assert_eq!(extended.base.total_bytes, Some(1024));
326 assert_eq!(extended.rate_bps, None);
327 assert_eq!(extended.eta_seconds, None);
328 assert_eq!(extended.history.len(), 1); }
330
331 #[tokio::test]
332 async fn test_rate_calculation() {
333 let mut extended = ExtendedProgress::new(Progress {
334 phase: FetchPhase::Downloading,
335 bytes_downloaded: 0,
336 total_bytes: Some(1000),
337 retry_count: 0,
338 performance_metrics: None,
339 });
340
341 let _start = SystemTime::now()
343 .duration_since(UNIX_EPOCH)
344 .unwrap()
345 .as_secs();
346
347 extended.update(Progress {
349 phase: FetchPhase::Downloading,
350 bytes_downloaded: 100,
351 total_bytes: Some(1000),
352 retry_count: 0,
353 performance_metrics: None,
354 });
355
356 tokio::time::sleep(Duration::from_secs(1)).await;
358 extended.update(Progress {
359 phase: FetchPhase::Downloading,
360 bytes_downloaded: 200,
361 total_bytes: Some(1000),
362 retry_count: 0,
363 performance_metrics: None,
364 });
365
366 assert!(extended.rate_bps.is_some());
368 assert!(extended.rate_bps.unwrap() > 0.0);
369
370 let rate = extended.rate_bps.unwrap();
373 assert!(rate > 10.0 && rate < 500.0); }
375
376 #[tokio::test]
377 async fn test_eta_calculation() {
378 let mut extended = ExtendedProgress::new(Progress {
379 phase: FetchPhase::Downloading,
380 bytes_downloaded: 0,
381 total_bytes: Some(1000),
382 retry_count: 0,
383 performance_metrics: None,
384 });
385
386 for i in 0..5 {
388 let progress = Progress {
389 phase: FetchPhase::Downloading,
390 bytes_downloaded: (i + 1) * 200,
391 total_bytes: Some(1000),
392 retry_count: 0,
393 performance_metrics: None,
394 };
395 extended.update(progress);
396 sleep(Duration::from_millis(100)).await;
397 }
398
399 assert!(extended.eta_seconds.is_some());
401 let eta = extended.eta_seconds.unwrap();
402
403 assert!(eta <= 10); }
406
407 #[test]
408 fn test_speed_string() {
409 let mut extended = ExtendedProgress::new(Progress {
410 phase: FetchPhase::Downloading,
411 bytes_downloaded: 0,
412 total_bytes: Some(1024),
413 retry_count: 0,
414 performance_metrics: None,
415 });
416
417 assert_eq!(extended.speed_string(), "Unknown");
419
420 extended.rate_bps = Some(1024.0);
422 assert_eq!(extended.speed_string(), "1.0 kB/s");
423
424 extended.rate_bps = Some(2_048_000.0);
426 assert_eq!(extended.speed_string(), "2.0 MB/s");
427
428 extended.rate_bps = Some(512.0);
430 assert_eq!(extended.speed_string(), "512 B/s");
431 }
432
433 #[test]
434 fn test_eta_string() {
435 let mut extended = ExtendedProgress::new(Progress {
436 phase: FetchPhase::Downloading,
437 bytes_downloaded: 0,
438 total_bytes: Some(1024),
439 retry_count: 0,
440 performance_metrics: None,
441 });
442
443 assert_eq!(extended.eta_string(), "Unknown");
445
446 extended.eta_seconds = Some(30);
448 assert_eq!(extended.eta_string(), "30s");
449
450 extended.eta_seconds = Some(90);
452 assert_eq!(extended.eta_string(), "1m");
453
454 extended.eta_seconds = Some(3661);
456 assert_eq!(extended.eta_string(), "1h 1m");
457
458 extended.eta_seconds = Some(7200);
460 assert_eq!(extended.eta_string(), "2h 0m");
461 }
462
463 #[tokio::test]
464 async fn test_elapsed_time() {
465 let extended = ExtendedProgress::new(Progress {
466 phase: FetchPhase::Downloading,
467 bytes_downloaded: 0,
468 total_bytes: Some(1024),
469 retry_count: 0,
470 performance_metrics: None,
471 });
472
473 assert_eq!(extended.elapsed_seconds(), 0);
475
476 sleep(Duration::from_millis(1500)).await;
478 assert!(extended.elapsed_seconds() >= 1);
479
480 assert_eq!(extended.elapsed_string(), "1s");
482
483 sleep(Duration::from_secs(90)).await;
485 assert!(extended.elapsed_seconds() >= 91);
486 assert_eq!(extended.elapsed_string(), "1m");
487 }
488
489 #[test]
490 fn test_progress_reporter() {
491 let mut reporter = ProgressReporter::new();
492
493 let progress1 = Progress {
495 phase: FetchPhase::Downloading,
496 bytes_downloaded: 256,
497 total_bytes: Some(512),
498 retry_count: 0,
499 performance_metrics: None,
500 };
501 let progress2 = Progress {
502 phase: FetchPhase::Downloading,
503 bytes_downloaded: 128,
504 total_bytes: Some(256),
505 retry_count: 1,
506 performance_metrics: None,
507 };
508
509 let id1 = reporter.add_tracker(progress1);
510 let id2 = reporter.add_tracker(progress2);
511
512 assert_eq!(id1, 0);
513 assert_eq!(id2, 1);
514 assert_eq!(reporter.trackers.len(), 2);
515
516 let updated1 = Progress {
518 phase: FetchPhase::Downloading,
519 bytes_downloaded: 512,
520 total_bytes: Some(512),
521 retry_count: 0,
522 performance_metrics: None,
523 };
524 reporter.update_tracker(0, updated1);
525
526 assert_eq!(reporter.get_tracker(0).unwrap().base.bytes_downloaded, 512);
527
528 let total = reporter.total_progress();
530 assert_eq!(total.bytes_downloaded, 640); assert_eq!(total.total_bytes, Some(768)); }
533
534 #[test]
535 fn test_total_metrics() {
536 let mut reporter = ProgressReporter::new();
537
538 let progress1 = Progress {
540 phase: FetchPhase::Downloading,
541 bytes_downloaded: 0,
542 total_bytes: Some(1000),
543 retry_count: 0,
544 performance_metrics: None,
545 };
546 let progress2 = Progress {
547 phase: FetchPhase::Downloading,
548 bytes_downloaded: 0,
549 total_bytes: Some(2000),
550 retry_count: 0,
551 performance_metrics: None,
552 };
553
554 reporter.add_tracker(progress1);
555 reporter.add_tracker(progress2);
556
557 let updated1 = Progress {
559 phase: FetchPhase::Downloading,
560 bytes_downloaded: 500,
561 total_bytes: Some(1000),
562 retry_count: 0,
563 performance_metrics: None,
564 };
565 let updated2 = Progress {
566 phase: FetchPhase::Downloading,
567 bytes_downloaded: 1000,
568 total_bytes: Some(2000),
569 retry_count: 0,
570 performance_metrics: None,
571 };
572
573 reporter.update_tracker(0, updated1);
574 reporter.update_tracker(1, updated2);
575
576 let total = reporter.total_progress();
579 assert_eq!(total.bytes_downloaded, 1500); assert_eq!(total.total_bytes, Some(3000)); }
582}