docker_image_pusher/registry/
stats.rs

1//! Upload statistics and progress reporting
2
3use crate::logging::Logger;
4use std::time::{Duration, Instant};
5
6/// Upload statistics for tracking progress and performance
7#[derive(Debug, Clone)]
8pub struct UploadStats {
9    pub total_bytes: u64,
10    pub uploaded_bytes: u64,
11    pub total_layers: usize,
12    pub uploaded_layers: usize,
13    pub successful_layers: usize,
14    pub skipped_layers: usize,
15    pub failed_layers: usize,
16    pub start_time: Instant,
17    pub current_layer_start: Option<Instant>,
18}
19
20impl UploadStats {
21    pub fn new(total_bytes: u64, total_layers: usize) -> Self {
22        Self {
23            total_bytes,
24            uploaded_bytes: 0,
25            total_layers,
26            uploaded_layers: 0,
27            successful_layers: 0,
28            skipped_layers: 0,
29            failed_layers: 0,
30            start_time: Instant::now(),
31            current_layer_start: None,
32        }
33    }
34
35    /// Start tracking upload statistics
36    pub fn start(&mut self) {
37        self.start_time = Instant::now();
38    }
39
40    /// Set total number of layers
41    pub fn set_total_layers(&mut self, count: usize) {
42        self.total_layers = count;
43    }
44
45    /// Legacy method for compatibility with ProgressReporter
46    pub fn start_layer(&mut self) {
47        self.current_layer_start = Some(Instant::now());
48    }
49
50    /// Begin tracking a specific layer by digest and size
51    pub fn begin_layer_upload(&mut self, _digest: &str, _size: u64) {
52        self.current_layer_start = Some(Instant::now());
53    }
54
55    /// Mark a layer as completed successfully
56    pub fn mark_layer_completed(&mut self, _digest: &str) {
57        self.successful_layers += 1;
58        self.uploaded_layers += 1;
59        self.current_layer_start = None;
60    }
61
62    /// Mark a layer as skipped (already exists)
63    pub fn mark_layer_skipped(&mut self, _digest: &str) {
64        self.skipped_layers += 1;
65        self.uploaded_layers += 1;
66    }
67
68    /// Mark a layer as failed
69    pub fn mark_layer_failed(&mut self, _digest: &str, _error: String) {
70        self.failed_layers += 1;
71    }
72
73    /// Get total duration of upload
74    pub fn total_duration(&self) -> Option<Duration> {
75        Some(self.start_time.elapsed())
76    }
77
78    pub fn finish_layer(&mut self, layer_bytes: u64) {
79        self.uploaded_bytes += layer_bytes;
80        self.uploaded_layers += 1;
81        self.current_layer_start = None;
82    }
83
84    pub fn get_progress_percentage(&self) -> f64 {
85        if self.total_bytes == 0 {
86            100.0
87        } else {
88            (self.uploaded_bytes as f64 / self.total_bytes as f64) * 100.0
89        }
90    }
91
92    pub fn get_average_speed(&self) -> u64 {
93        let elapsed = self.start_time.elapsed().as_secs();
94        if elapsed > 0 {
95            self.uploaded_bytes / elapsed
96        } else {
97            0
98        }
99    }
100
101    pub fn get_eta(&self) -> Option<Duration> {
102        if self.uploaded_bytes == 0 {
103            return None;
104        }
105
106        let _elapsed = self.start_time.elapsed();
107        let remaining_bytes = self.total_bytes.saturating_sub(self.uploaded_bytes);
108        let speed = self.get_average_speed();
109
110        if speed > 0 {
111            Some(Duration::from_secs(remaining_bytes / speed))
112        } else {
113            None
114        }
115    }
116}
117
118/// Layer-specific upload statistics
119#[derive(Debug, Clone)]
120pub struct LayerUploadStats {
121    pub digest: String,
122    pub size: u64,
123    pub uploaded: u64,
124    pub start_time: Instant,
125    pub status: LayerUploadStatus,
126}
127
128#[derive(Debug, Clone, PartialEq)]
129pub enum LayerUploadStatus {
130    Pending,
131    InProgress,
132    Completed,
133    Skipped,
134    Failed(String),
135}
136
137impl LayerUploadStats {
138    pub fn new(digest: String, size: u64) -> Self {
139        Self {
140            digest,
141            size,
142            uploaded: 0,
143            start_time: Instant::now(),
144            status: LayerUploadStatus::Pending,
145        }
146    }
147
148    pub fn start(&mut self) {
149        self.status = LayerUploadStatus::InProgress;
150        self.start_time = Instant::now();
151    }
152
153    pub fn update_progress(&mut self, uploaded: u64) {
154        self.uploaded = uploaded;
155    }
156
157    pub fn complete(&mut self) {
158        self.status = LayerUploadStatus::Completed;
159        self.uploaded = self.size;
160    }
161
162    pub fn skip(&mut self) {
163        self.status = LayerUploadStatus::Skipped;
164    }
165
166    pub fn fail(&mut self, error: String) {
167        self.status = LayerUploadStatus::Failed(error);
168    }
169
170    pub fn get_progress_percentage(&self) -> f64 {
171        if self.size == 0 {
172            100.0
173        } else {
174            (self.uploaded as f64 / self.size as f64) * 100.0
175        }
176    }
177
178    pub fn get_speed(&self) -> u64 {
179        let elapsed = self.start_time.elapsed().as_secs();
180        if elapsed > 0 && self.uploaded > 0 {
181            self.uploaded / elapsed
182        } else {
183            0
184        }
185    }
186}
187
188/// Progress reporter for upload operations
189pub struct ProgressReporter {
190    output: Logger,
191    stats: UploadStats,
192    layer_stats: Vec<LayerUploadStats>,
193}
194
195impl ProgressReporter {
196    pub fn new(output: Logger, total_bytes: u64, total_layers: usize) -> Self {
197        Self {
198            output,
199            stats: UploadStats::new(total_bytes, total_layers),
200            layer_stats: Vec::new(),
201        }
202    }
203
204    pub fn add_layer(&mut self, digest: String, size: u64) {
205        self.layer_stats.push(LayerUploadStats::new(digest, size));
206    }
207
208    pub fn start_layer(&mut self, digest: &str) {
209        self.stats.start_layer();
210        if let Some(layer_stat) = self.layer_stats.iter_mut().find(|l| l.digest == digest) {
211            layer_stat.start();
212        }
213    }
214
215    pub fn update_layer_progress(&mut self, digest: &str, uploaded: u64) {
216        if let Some(layer_stat) = self.layer_stats.iter_mut().find(|l| l.digest == digest) {
217            layer_stat.update_progress(uploaded);
218        }
219    }
220
221    pub fn finish_layer(&mut self, digest: &str, size: u64) {
222        self.stats.finish_layer(size);
223        if let Some(layer_stat) = self.layer_stats.iter_mut().find(|l| l.digest == digest) {
224            layer_stat.complete();
225        }
226    }
227
228    pub fn skip_layer(&mut self, digest: &str) {
229        if let Some(layer_stat) = self.layer_stats.iter_mut().find(|l| l.digest == digest) {
230            layer_stat.skip();
231        }
232    }
233
234    pub fn fail_layer(&mut self, digest: &str, error: String) {
235        if let Some(layer_stat) = self.layer_stats.iter_mut().find(|l| l.digest == digest) {
236            layer_stat.fail(error);
237        }
238    }
239
240    pub fn report_progress(&self) {
241        let progress = self.stats.get_progress_percentage();
242        let speed = self.stats.get_average_speed();
243
244        self.output.info(&format!(
245            "Upload progress: {:.1}% ({}/{} layers, avg speed: {})",
246            progress,
247            self.stats.uploaded_layers,
248            self.stats.total_layers,
249            self.output.format_speed(speed)
250        ));
251
252        if let Some(eta) = self.stats.get_eta() {
253            self.output.detail(&format!(
254                "Estimated time remaining: {}",
255                self.output.format_duration(eta)
256            ));
257        }
258    }
259
260    pub fn report_final_stats(&self) {
261        let total_time = self.stats.start_time.elapsed();
262        let avg_speed = self.stats.get_average_speed();
263
264        let completed = self
265            .layer_stats
266            .iter()
267            .filter(|l| l.status == LayerUploadStatus::Completed)
268            .count();
269        let skipped = self
270            .layer_stats
271            .iter()
272            .filter(|l| l.status == LayerUploadStatus::Skipped)
273            .count();
274        let failed = self
275            .layer_stats
276            .iter()
277            .filter(|l| matches!(l.status, LayerUploadStatus::Failed(_)))
278            .count();
279
280        self.output.success(&format!(
281            "Upload completed in {} - {} uploaded, {} skipped, {} failed (avg speed: {})",
282            self.output.format_duration(total_time),
283            completed,
284            skipped,
285            failed,
286            self.output.format_speed(avg_speed)
287        ));
288    }
289}