1use std::{
24 collections::HashMap,
25 sync::{
26 Mutex, MutexGuard,
27 atomic::{AtomicU64, Ordering},
28 },
29 time::{Duration, Instant},
30};
31
32const TWO_POW_32_F64: f64 = 4_294_967_296.0;
33
34fn lock_recover<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
35 mutex
36 .lock()
37 .unwrap_or_else(std::sync::PoisonError::into_inner)
38}
39
40fn u64_to_f64(value: u64) -> f64 {
41 let upper = u32::try_from(value >> 32).expect("upper 32 bits always fit in u32");
42 let lower =
43 u32::try_from(value & u64::from(u32::MAX)).expect("lower 32 bits always fit in u32");
44
45 f64::from(upper) * TWO_POW_32_F64 + f64::from(lower)
46}
47
48#[derive(Debug, Clone, Default, PartialEq)]
50pub struct ModelUsage {
51 pub prompt_tokens: u64,
53 pub completion_tokens: u64,
55 pub total_tokens: u64,
57 pub request_count: u64,
59}
60
61impl ModelUsage {
62 #[must_use]
64 pub fn new() -> Self {
65 Self::default()
66 }
67
68 pub fn record(&mut self, prompt_tokens: u64, completion_tokens: u64) {
70 self.prompt_tokens += prompt_tokens;
71 self.completion_tokens += completion_tokens;
72 self.total_tokens += prompt_tokens + completion_tokens;
73 self.request_count += 1;
74 }
75}
76
77#[derive(Debug, Clone, Default, PartialEq)]
81pub struct PipelineStats {
82 pub nodes_processed: u64,
84 pub nodes_failed: u64,
86 pub nodes_stored: u64,
88 pub transformations_applied: u64,
90 pub token_usage: HashMap<String, ModelUsage>,
92 started_at: Option<Instant>,
94 completed_at: Option<Instant>,
96}
97
98impl PipelineStats {
99 #[must_use]
101 pub fn new() -> Self {
102 Self::default()
103 }
104
105 #[must_use]
110 pub fn duration(&self) -> Option<Duration> {
111 match (self.started_at, self.completed_at) {
112 (Some(start), Some(end)) => Some(end.duration_since(start)),
113 (Some(start), None) => Some(start.elapsed()),
114 _ => None,
115 }
116 }
117
118 #[must_use]
122 pub fn nodes_per_second(&self) -> Option<f64> {
123 let duration = self.duration()?;
124 if duration.as_secs_f64() == 0.0 || self.nodes_processed == 0 {
125 return None;
126 }
127 Some(u64_to_f64(self.nodes_processed) / duration.as_secs_f64())
128 }
129
130 #[must_use]
132 pub fn total_tokens(&self) -> u64 {
133 self.token_usage.values().map(|u| u.total_tokens).sum()
134 }
135
136 #[must_use]
138 pub fn total_requests(&self) -> u64 {
139 self.token_usage.values().map(|u| u.request_count).sum()
140 }
141
142 #[must_use]
144 pub fn total_prompt_tokens(&self) -> u64 {
145 self.token_usage.values().map(|u| u.prompt_tokens).sum()
146 }
147
148 #[must_use]
150 pub fn total_completion_tokens(&self) -> u64 {
151 self.token_usage.values().map(|u| u.completion_tokens).sum()
152 }
153}
154
155#[derive(Debug)]
160pub struct StatsCollector {
161 nodes_processed: AtomicU64,
162 nodes_failed: AtomicU64,
163 nodes_stored: AtomicU64,
164 transformations_applied: AtomicU64,
165 token_usage: Mutex<HashMap<String, ModelUsage>>,
166 started_at: Mutex<Option<Instant>>,
167 completed_at: Mutex<Option<Instant>>,
168}
169
170impl Default for StatsCollector {
171 fn default() -> Self {
172 Self::new()
173 }
174}
175
176impl StatsCollector {
177 #[must_use]
179 pub fn new() -> Self {
180 Self {
181 nodes_processed: AtomicU64::new(0),
182 nodes_failed: AtomicU64::new(0),
183 nodes_stored: AtomicU64::new(0),
184 transformations_applied: AtomicU64::new(0),
185 token_usage: Mutex::new(HashMap::new()),
186 started_at: Mutex::new(None),
187 completed_at: Mutex::new(None),
188 }
189 }
190
191 pub fn start(&self) {
193 let mut started = lock_recover(&self.started_at);
194 *started = Some(Instant::now());
195 }
196
197 pub fn complete(&self) {
199 let mut completed = lock_recover(&self.completed_at);
200 *completed = Some(Instant::now());
201 }
202
203 pub fn increment_nodes_processed(&self, count: u64) {
205 self.nodes_processed.fetch_add(count, Ordering::Relaxed);
206 }
207
208 pub fn increment_nodes_failed(&self, count: u64) {
210 self.nodes_failed.fetch_add(count, Ordering::Relaxed);
211 }
212
213 pub fn increment_nodes_stored(&self, count: u64) {
215 self.nodes_stored.fetch_add(count, Ordering::Relaxed);
216 }
217
218 pub fn increment_transformations(&self, count: u64) {
220 self.transformations_applied
221 .fetch_add(count, Ordering::Relaxed);
222 }
223
224 pub fn record_token_usage(
234 &self,
235 model: impl AsRef<str>,
236 prompt_tokens: u64,
237 completion_tokens: u64,
238 ) {
239 let mut usage = lock_recover(&self.token_usage);
240 let model_usage = usage.entry(model.as_ref().to_string()).or_default();
241 model_usage.record(prompt_tokens, completion_tokens);
242 }
243
244 #[must_use]
246 pub fn get_stats(&self) -> PipelineStats {
247 PipelineStats {
248 nodes_processed: self.nodes_processed.load(Ordering::Relaxed),
249 nodes_failed: self.nodes_failed.load(Ordering::Relaxed),
250 nodes_stored: self.nodes_stored.load(Ordering::Relaxed),
251 transformations_applied: self.transformations_applied.load(Ordering::Relaxed),
252 token_usage: lock_recover(&self.token_usage).clone(),
253 started_at: *lock_recover(&self.started_at),
254 completed_at: *lock_recover(&self.completed_at),
255 }
256 }
257}
258
259impl Clone for StatsCollector {
260 fn clone(&self) -> Self {
261 Self {
262 nodes_processed: AtomicU64::new(self.nodes_processed.load(Ordering::Relaxed)),
263 nodes_failed: AtomicU64::new(self.nodes_failed.load(Ordering::Relaxed)),
264 nodes_stored: AtomicU64::new(self.nodes_stored.load(Ordering::Relaxed)),
265 transformations_applied: AtomicU64::new(
266 self.transformations_applied.load(Ordering::Relaxed),
267 ),
268 token_usage: Mutex::new(lock_recover(&self.token_usage).clone()),
269 started_at: Mutex::new(*lock_recover(&self.started_at)),
270 completed_at: Mutex::new(*lock_recover(&self.completed_at)),
271 }
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn test_stats_collector() {
281 let collector = StatsCollector::new();
282
283 collector.start();
284
285 collector.increment_nodes_processed(10);
286 collector.increment_nodes_failed(2);
287 collector.increment_nodes_stored(8);
288 collector.increment_transformations(15);
289
290 collector.complete();
291
292 let stats = collector.get_stats();
293
294 assert_eq!(stats.nodes_processed, 10);
295 assert_eq!(stats.nodes_failed, 2);
296 assert_eq!(stats.nodes_stored, 8);
297 assert_eq!(stats.transformations_applied, 15);
298 assert!(stats.duration().is_some());
299 assert!(stats.nodes_per_second().is_some());
300 }
301
302 #[test]
303 fn test_model_usage() {
304 let mut usage = ModelUsage::new();
305
306 usage.record(100, 50);
307 usage.record(200, 100);
308
309 assert_eq!(usage.prompt_tokens, 300);
310 assert_eq!(usage.completion_tokens, 150);
311 assert_eq!(usage.total_tokens, 450);
312 assert_eq!(usage.request_count, 2);
313 }
314
315 #[test]
316 fn test_record_token_usage() {
317 let collector = StatsCollector::new();
318
319 collector.record_token_usage("gpt-4", 100, 50);
320 collector.record_token_usage("gpt-4", 200, 100);
321 collector.record_token_usage("gpt-3.5", 50, 25);
322
323 let stats = collector.get_stats();
324
325 assert_eq!(stats.token_usage.len(), 2);
326
327 let gpt4_usage = stats.token_usage.get("gpt-4").unwrap();
328 assert_eq!(gpt4_usage.prompt_tokens, 300);
329 assert_eq!(gpt4_usage.completion_tokens, 150);
330 assert_eq!(gpt4_usage.request_count, 2);
331
332 assert_eq!(stats.total_tokens(), 525);
333 assert_eq!(stats.total_requests(), 3);
334 }
335
336 #[test]
337 fn test_empty_stats() {
338 let stats = PipelineStats::new();
339
340 assert_eq!(stats.nodes_processed, 0);
341 assert_eq!(stats.nodes_failed, 0);
342 assert_eq!(stats.total_tokens(), 0);
343 assert!(stats.duration().is_none());
344 assert!(stats.nodes_per_second().is_none());
345 }
346
347 #[test]
348 fn test_stats_collector_clone() {
349 let collector = StatsCollector::new();
350 collector.increment_nodes_processed(5);
351 collector.record_token_usage("model-1", 10, 5);
352
353 let cloned = collector.clone();
354
355 collector.increment_nodes_processed(3);
357
358 let cloned_stats = cloned.get_stats();
360 assert_eq!(cloned_stats.nodes_processed, 5);
361
362 let original_stats = collector.get_stats();
364 assert_eq!(original_stats.nodes_processed, 8);
365 }
366
367 #[test]
368 fn test_pipeline_stats_duration_while_running() {
369 let collector = StatsCollector::new();
370 collector.start();
371
372 let stats = collector.get_stats();
373
374 assert!(stats.duration().is_some());
376 assert_eq!(stats.completed_at, None);
377 }
378}