oar_ocr/pipeline/
stats.rs

1//! Pipeline-wide statistics helpers.
2//!
3//! This module defines the `PipelineStats` structure used to track execution metrics
4//! for OCR pipeline runs and the `StatsManager` helper that coordinates thread-safe
5//! updates to these metrics.
6
7use std::fmt;
8use std::sync::Mutex;
9
10/// Statistics for the OCR pipeline.
11///
12/// Tracks how many images were processed and performance metrics such as average
13/// inference time and success ratios.
14#[derive(Debug, Clone)]
15pub struct PipelineStats {
16    /// The total number of images processed.
17    pub total_processed: usize,
18    /// The number of successful predictions.
19    pub successful_predictions: usize,
20    /// The number of failed predictions.
21    pub failed_predictions: usize,
22    /// The average inference time in milliseconds.
23    pub average_inference_time_ms: f64,
24}
25
26impl PipelineStats {
27    /// Creates a new PipelineStats instance with default values.
28    pub fn new() -> Self {
29        Self {
30            total_processed: 0,
31            successful_predictions: 0,
32            failed_predictions: 0,
33            average_inference_time_ms: 0.0,
34        }
35    }
36
37    /// Returns the success rate as a percentage (0.0 to 100.0).
38    pub fn success_rate(&self) -> f64 {
39        if self.total_processed == 0 {
40            0.0
41        } else {
42            (self.successful_predictions as f64 / self.total_processed as f64) * 100.0
43        }
44    }
45
46    /// Returns the failure rate as a percentage (0.0 to 100.0).
47    pub fn failure_rate(&self) -> f64 {
48        if self.total_processed == 0 {
49            0.0
50        } else {
51            (self.failed_predictions as f64 / self.total_processed as f64) * 100.0
52        }
53    }
54
55    /// Returns the average processing speed in images per second.
56    pub fn images_per_second(&self) -> f64 {
57        if self.average_inference_time_ms == 0.0 {
58            0.0
59        } else {
60            1000.0 / self.average_inference_time_ms
61        }
62    }
63}
64
65impl Default for PipelineStats {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71impl fmt::Display for PipelineStats {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        writeln!(f, "Pipeline Statistics:")?;
74        writeln!(f, "  Total processed: {}", self.total_processed)?;
75        writeln!(
76            f,
77            "  Successful: {} ({:.1}%)",
78            self.successful_predictions,
79            self.success_rate()
80        )?;
81        writeln!(
82            f,
83            "  Failed: {} ({:.1}%)",
84            self.failed_predictions,
85            self.failure_rate()
86        )?;
87        writeln!(
88            f,
89            "  Average inference time: {:.2} ms",
90            self.average_inference_time_ms
91        )?;
92        writeln!(
93            f,
94            "  Processing speed: {:.2} images/sec",
95            self.images_per_second()
96        )?;
97        Ok(())
98    }
99}
100
101/// Thread-safe manager for updating pipeline statistics during OCR execution.
102#[derive(Debug, Default)]
103pub struct StatsManager {
104    /// Shared statistics state guarded by a mutex.
105    stats: Mutex<PipelineStats>,
106}
107
108impl StatsManager {
109    /// Creates a new `StatsManager` instance with zeroed metrics.
110    pub fn new() -> Self {
111        Self::default()
112    }
113
114    /// Returns a copy of the current statistics snapshot.
115    pub fn get_stats(&self) -> PipelineStats {
116        self.stats.lock().unwrap().clone()
117    }
118
119    /// Updates the tracked metrics using the results from a batch run.
120    pub fn update_stats(
121        &self,
122        processed_count: usize,
123        successful_count: usize,
124        failed_count: usize,
125        inference_time_ms: f64,
126    ) {
127        let mut stats = self.stats.lock().unwrap();
128
129        let previous_total = stats.total_processed;
130        let previous_average = stats.average_inference_time_ms;
131        let new_total = previous_total + processed_count;
132
133        stats.total_processed = new_total;
134        stats.successful_predictions += successful_count;
135        stats.failed_predictions += failed_count;
136
137        if new_total > 0 {
138            let accumulated_time = previous_average * previous_total as f64;
139            let new_total_time = accumulated_time + inference_time_ms;
140            stats.average_inference_time_ms = new_total_time / new_total as f64;
141        } else {
142            stats.average_inference_time_ms = 0.0;
143        }
144    }
145
146    /// Resets the tracked statistics to their default state.
147    pub fn reset_stats(&self) {
148        let mut stats = self.stats.lock().unwrap();
149        *stats = PipelineStats::default();
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::{PipelineStats, StatsManager};
156
157    #[test]
158    fn success_rate_handles_zero_processed() {
159        let stats = PipelineStats::default();
160        assert_eq!(stats.success_rate(), 0.0);
161    }
162
163    #[test]
164    fn success_rate_computes_percentage() {
165        let stats = PipelineStats {
166            total_processed: 10,
167            successful_predictions: 7,
168            failed_predictions: 3,
169            average_inference_time_ms: 50.0,
170        };
171        assert_eq!(stats.success_rate(), 70.0);
172    }
173
174    #[test]
175    fn failure_rate_handles_zero_processed() {
176        let stats = PipelineStats::default();
177        assert_eq!(stats.failure_rate(), 0.0);
178    }
179
180    #[test]
181    fn failure_rate_computes_percentage() {
182        let stats = PipelineStats {
183            total_processed: 8,
184            successful_predictions: 6,
185            failed_predictions: 2,
186            average_inference_time_ms: 75.0,
187        };
188        assert_eq!(stats.failure_rate(), 25.0);
189    }
190
191    #[test]
192    fn images_per_second_handles_zero_time() {
193        let stats = PipelineStats {
194            total_processed: 10,
195            successful_predictions: 10,
196            failed_predictions: 0,
197            average_inference_time_ms: 0.0,
198        };
199        assert_eq!(stats.images_per_second(), 0.0);
200    }
201
202    #[test]
203    fn images_per_second_computes_rate() {
204        let stats = PipelineStats {
205            total_processed: 10,
206            successful_predictions: 10,
207            failed_predictions: 0,
208            average_inference_time_ms: 100.0,
209        };
210        assert_eq!(stats.images_per_second(), 10.0);
211    }
212
213    #[test]
214    fn display_formats_metrics() {
215        let stats = PipelineStats {
216            total_processed: 10,
217            successful_predictions: 8,
218            failed_predictions: 2,
219            average_inference_time_ms: 125.0,
220        };
221
222        let display = stats.to_string();
223        assert!(display.contains("Pipeline Statistics:"));
224        assert!(display.contains("Total processed: 10"));
225        assert!(display.contains("Successful: 8 (80.0%)"));
226        assert!(display.contains("Failed: 2 (20.0%)"));
227        assert!(display.contains("Average inference time: 125.00 ms"));
228        assert!(display.contains("Processing speed: 8.00 images/sec"));
229    }
230
231    #[test]
232    fn stats_manager_updates_counters_and_average() {
233        let manager = StatsManager::new();
234
235        manager.update_stats(1, 1, 0, 100.0);
236        let stats = manager.get_stats();
237        assert_eq!(stats.total_processed, 1);
238        assert_eq!(stats.successful_predictions, 1);
239        assert_eq!(stats.failed_predictions, 0);
240        assert_eq!(stats.average_inference_time_ms, 100.0);
241
242        manager.update_stats(1, 0, 1, 200.0);
243        let stats = manager.get_stats();
244        assert_eq!(stats.total_processed, 2);
245        assert_eq!(stats.successful_predictions, 1);
246        assert_eq!(stats.failed_predictions, 1);
247        assert!((stats.average_inference_time_ms - 150.0).abs() < f64::EPSILON);
248    }
249
250    #[test]
251    fn stats_manager_resets_metrics() {
252        let manager = StatsManager::new();
253        manager.update_stats(5, 4, 1, 500.0);
254        manager.reset_stats();
255
256        let stats = manager.get_stats();
257        assert_eq!(stats.total_processed, 0);
258        assert_eq!(stats.successful_predictions, 0);
259        assert_eq!(stats.failed_predictions, 0);
260        assert_eq!(stats.average_inference_time_ms, 0.0);
261    }
262}