scirs2_core/logging/progress/
statistics.rs

1//! Progress tracking statistics
2//!
3//! This module provides statistical tracking for progress operations including
4//! throughput analysis, ETA calculation, and performance metrics.
5
6use std::collections::VecDeque;
7use std::time::{Duration, Instant};
8
9/// Progress tracking statistics
10#[derive(Debug, Clone)]
11pub struct ProgressStats {
12    /// Number of items processed
13    pub processed: u64,
14    /// Total number of items to process
15    pub total: u64,
16    /// Percentage complete (0-100)
17    pub percentage: f64,
18    /// Items per second
19    pub items_per_second: f64,
20    /// Estimated time remaining
21    pub eta: Duration,
22    /// Time elapsed
23    pub elapsed: Duration,
24    /// A record of recent processing speeds
25    pub recent_speeds: VecDeque<f64>,
26    /// Highest observed items per second
27    pub max_speed: f64,
28    /// Time of last update
29    pub last_update: Instant,
30    /// Number of updates
31    pub update_count: u64,
32    /// Start time of the tracking
33    pub start_time: Instant,
34}
35
36impl ProgressStats {
37    /// Create new progress statistics
38    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    /// Update statistics based on current processed count
56    pub fn update(&mut self, processed: u64, now: Instant) {
57        let old_processed = self.processed;
58        self.processed = processed.min(self.total);
59
60        // Calculate percentage
61        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        // Calculate elapsed time from start
68        self.elapsed = now.duration_since(self.start_time);
69
70        // Calculate processing speed
71        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            // Keep only the last 20 speed measurements for smoothing
79            if self.recent_speeds.len() > 20 {
80                self.recent_speeds.pop_front();
81            }
82
83            // Calculate average speed from recent measurements
84            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            // Fallback to overall average if no recent measurements
90            self.items_per_second = self.processed as f64 / self.elapsed.as_secs_f64();
91        }
92
93        // Calculate ETA
94        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    /// Get the processing rate in items per second
107    pub fn rate(&self) -> f64 {
108        self.items_per_second
109    }
110
111    /// Get the average processing rate since start
112    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    /// Get the maximum observed processing rate
121    pub fn peak_rate(&self) -> f64 {
122        self.max_speed
123    }
124
125    /// Check if the processing is complete
126    pub fn is_complete(&self) -> bool {
127        self.processed >= self.total && self.total > 0
128    }
129
130    /// Get remaining items to process
131    pub fn remaining(&self) -> u64 {
132        self.total.saturating_sub(self.processed)
133    }
134
135    /// Get a smoothed ETA based on recent performance
136    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        // Use recent speed measurements for more accurate ETA
142        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    /// Get progress efficiency (actual vs expected based on average)
155    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/// Format duration in human-readable format
166#[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/// Format processing rate in human-readable format
195#[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/// Format byte count in human-readable format
207#[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        // Simulate some processing
245        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}