scirs2_core/logging/progress/
statistics.rs1use std::collections::VecDeque;
7use std::time::{Duration, Instant};
8
9#[derive(Debug, Clone)]
11pub struct ProgressStats {
12 pub processed: u64,
14 pub total: u64,
16 pub percentage: f64,
18 pub items_per_second: f64,
20 pub eta: Duration,
22 pub elapsed: Duration,
24 pub recent_speeds: VecDeque<f64>,
26 pub max_speed: f64,
28 pub last_update: Instant,
30 pub update_count: u64,
32 pub start_time: Instant,
34}
35
36impl ProgressStats {
37 pub fn new(total: u64) -> Self {
39 let now = Instant::now();
40 Self {
41 processed: 0,
42 total,
43 percentage: 0.0,
44 items_per_second: 0.0,
45 eta: Duration::from_secs(0),
46 elapsed: Duration::from_secs(0),
47 recent_speeds: VecDeque::with_capacity(20),
48 max_speed: 0.0,
49 last_update: now,
50 update_count: 0,
51 start_time: now,
52 }
53 }
54
55 pub fn update(&mut self, processed: u64, now: Instant) {
57 let old_processed = self.processed;
58 self.processed = processed.min(self.total);
59
60 if self.total > 0 {
62 self.percentage = (self.processed as f64 / self.total as f64) * 100.0;
63 } else {
64 self.percentage = 0.0;
65 }
66
67 self.elapsed = now.duration_since(self.start_time);
69
70 let time_diff = now.duration_since(self.last_update);
72 let items_diff = self.processed.saturating_sub(old_processed);
73
74 if items_diff > 0 && !time_diff.is_zero() {
75 let speed = items_diff as f64 / time_diff.as_secs_f64();
76 self.recent_speeds.push_back(speed);
77
78 if self.recent_speeds.len() > 20 {
80 self.recent_speeds.pop_front();
81 }
82
83 let avg_speed: f64 =
85 self.recent_speeds.iter().sum::<f64>() / self.recent_speeds.len() as f64;
86 self.items_per_second = avg_speed;
87 self.max_speed = self.max_speed.max(avg_speed);
88 } else if self.elapsed.as_secs_f64() > 0.0 && self.processed > 0 {
89 self.items_per_second = self.processed as f64 / self.elapsed.as_secs_f64();
91 }
92
93 if self.items_per_second > 0.0 && self.processed < self.total {
95 let remaining_items = self.total - self.processed;
96 let remaining_seconds = remaining_items as f64 / self.items_per_second;
97 self.eta = Duration::from_secs_f64(remaining_seconds.max(0.0));
98 } else {
99 self.eta = Duration::from_secs(0);
100 }
101
102 self.last_update = now;
103 self.update_count += 1;
104 }
105
106 pub fn rate(&self) -> f64 {
108 self.items_per_second
109 }
110
111 pub fn average_rate(&self) -> f64 {
113 if self.elapsed.as_secs_f64() > 0.0 && self.processed > 0 {
114 self.processed as f64 / self.elapsed.as_secs_f64()
115 } else {
116 0.0
117 }
118 }
119
120 pub fn peak_rate(&self) -> f64 {
122 self.max_speed
123 }
124
125 pub fn is_complete(&self) -> bool {
127 self.processed >= self.total && self.total > 0
128 }
129
130 pub fn remaining(&self) -> u64 {
132 self.total.saturating_sub(self.processed)
133 }
134
135 pub fn smoothed_eta(&self) -> Duration {
137 if self.recent_speeds.is_empty() || self.processed >= self.total {
138 return Duration::from_secs(0);
139 }
140
141 let recent_avg: f64 =
143 self.recent_speeds.iter().sum::<f64>() / self.recent_speeds.len() as f64;
144
145 if recent_avg > 0.0 {
146 let remaining_items = self.total - self.processed;
147 let remaining_seconds = remaining_items as f64 / recent_avg;
148 Duration::from_secs_f64(remaining_seconds.max(0.0))
149 } else {
150 self.eta
151 }
152 }
153
154 pub fn efficiency(&self) -> f64 {
156 let avg_rate = self.average_rate();
157 if avg_rate > 0.0 && self.items_per_second > 0.0 {
158 self.items_per_second / avg_rate
159 } else {
160 1.0
161 }
162 }
163}
164
165#[allow(dead_code)]
167pub fn format_duration(duration: &Duration) -> String {
168 let total_secs = duration.as_secs();
169
170 if total_secs < 60 {
171 return format!("{total_secs}s");
172 }
173
174 let mins = total_secs / 60;
175 let secs = total_secs % 60;
176
177 if mins < 60 {
178 return format!("{mins}m {secs}s");
179 }
180
181 let hours = mins / 60;
182 let mins = mins % 60;
183
184 if hours < 24 {
185 return format!("{hours}h {mins}m {secs}s");
186 }
187
188 let days = hours / 24;
189 let hours = hours % 24;
190
191 format!("{days}d {hours}h {mins}m {secs}s")
192}
193
194#[allow(dead_code)]
196pub fn format_rate(rate: f64) -> String {
197 if rate >= 1000000.0 {
198 format!("{:.1}M it/s", rate / 1000000.0)
199 } else if rate >= 1000.0 {
200 format!("{:.1}k it/s", rate / 1000.0)
201 } else {
202 format!("{rate:.1} it/s")
203 }
204}
205
206#[allow(dead_code)]
208pub fn format_bytes(bytes: u64) -> String {
209 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
210 let mut size = bytes as f64;
211 let mut unit_index = 0;
212
213 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
214 size /= 1024.0;
215 unit_index += 1;
216 }
217
218 if unit_index == 0 {
219 format!("{:.0} {}", size, UNITS[unit_index])
220 } else {
221 format!("{:.1} {}", size, UNITS[unit_index])
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use std::thread;
229 use std::time::Duration;
230
231 #[test]
232 fn test_progress_stats_creation() {
233 let stats = ProgressStats::new(100);
234 assert_eq!(stats.total, 100);
235 assert_eq!(stats.processed, 0);
236 assert_eq!(stats.percentage, 0.0);
237 }
238
239 #[test]
240 fn test_progress_stats_update() {
241 let mut stats = ProgressStats::new(100);
242 let now = Instant::now();
243
244 thread::sleep(Duration::from_millis(10));
246 stats.update(25, now + Duration::from_millis(10));
247
248 assert_eq!(stats.processed, 25);
249 assert_eq!(stats.percentage, 25.0);
250 assert!(stats.items_per_second > 0.0);
251 }
252
253 #[test]
254 fn test_format_duration() {
255 assert_eq!(format_duration(&Duration::from_secs(30)), "30s");
256 assert_eq!(format_duration(&Duration::from_secs(90)), "1m 30s");
257 assert_eq!(format_duration(&Duration::from_secs(3665)), "1h 1m 5s");
258 }
259
260 #[test]
261 fn test_format_rate() {
262 assert_eq!(format_rate(10.5), "10.5 it/s");
263 assert_eq!(format_rate(1500.0), "1.5k it/s");
264 assert_eq!(format_rate(2500000.0), "2.5M it/s");
265 }
266
267 #[test]
268 fn test_format_bytes() {
269 assert_eq!(format_bytes(512), "512 B");
270 assert_eq!(format_bytes(1536), "1.5 KB");
271 assert_eq!(format_bytes(2097152), "2.0 MB");
272 }
273}