Skip to main content

laminar_core/budget/
stats.rs

1//! Budget metrics tracking with lock-free counters.
2//!
3//! Metrics intentionally use f64 for rate calculations, accepting precision loss
4//! for very large counts (>2^52). This is acceptable for monitoring purposes.
5
6#![allow(clippy::cast_precision_loss)] // Metrics don't need full u64 precision
7
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::sync::OnceLock;
10
11/// Global budget metrics instance.
12static GLOBAL_METRICS: OnceLock<BudgetMetrics> = OnceLock::new();
13
14/// Aggregated metrics for task budget tracking.
15///
16/// Uses atomic counters for lock-free updates from multiple threads.
17/// Metrics are aggregated globally and can be queried via `snapshot()`.
18///
19/// # Thread Safety
20///
21/// All operations are lock-free and safe to call from any thread.
22/// The metrics are eventually consistent - reads may not reflect
23/// the most recent writes from other threads.
24#[derive(Debug)]
25pub struct BudgetMetrics {
26    // Global counters
27    total_tasks: AtomicU64,
28    total_violations: AtomicU64,
29    total_exceeded_ns: AtomicU64,
30    total_duration_ns: AtomicU64,
31
32    // Ring 0 specific
33    ring0_tasks: AtomicU64,
34    ring0_violations: AtomicU64,
35    ring0_exceeded_ns: AtomicU64,
36
37    // Ring 1 specific
38    ring1_tasks: AtomicU64,
39    ring1_violations: AtomicU64,
40    ring1_exceeded_ns: AtomicU64,
41
42    // Yield counters
43    yields_budget: AtomicU64,
44    yields_priority: AtomicU64,
45    yields_empty: AtomicU64,
46    yields_other: AtomicU64,
47}
48
49impl BudgetMetrics {
50    /// Create a new metrics instance.
51    #[must_use]
52    pub fn new() -> Self {
53        Self {
54            total_tasks: AtomicU64::new(0),
55            total_violations: AtomicU64::new(0),
56            total_exceeded_ns: AtomicU64::new(0),
57            total_duration_ns: AtomicU64::new(0),
58            ring0_tasks: AtomicU64::new(0),
59            ring0_violations: AtomicU64::new(0),
60            ring0_exceeded_ns: AtomicU64::new(0),
61            ring1_tasks: AtomicU64::new(0),
62            ring1_violations: AtomicU64::new(0),
63            ring1_exceeded_ns: AtomicU64::new(0),
64            yields_budget: AtomicU64::new(0),
65            yields_priority: AtomicU64::new(0),
66            yields_empty: AtomicU64::new(0),
67            yields_other: AtomicU64::new(0),
68        }
69    }
70
71    /// Get the global metrics instance.
72    #[must_use]
73    pub fn global() -> &'static Self {
74        GLOBAL_METRICS.get_or_init(Self::new)
75    }
76
77    /// Record a completed task.
78    ///
79    /// # Arguments
80    ///
81    /// * `name` - Task name (for future per-task metrics)
82    /// * `ring` - Ring number (0, 1, or 2)
83    /// * `budget_ns` - Budget in nanoseconds
84    /// * `elapsed_ns` - Actual elapsed time in nanoseconds
85    pub fn record_task(&self, _name: &'static str, ring: u8, budget_ns: u64, elapsed_ns: u64) {
86        // Update global counters
87        self.total_tasks.fetch_add(1, Ordering::Relaxed);
88        self.total_duration_ns
89            .fetch_add(elapsed_ns, Ordering::Relaxed);
90
91        // Check for violation
92        if elapsed_ns > budget_ns {
93            let exceeded_by = elapsed_ns - budget_ns;
94            self.total_violations.fetch_add(1, Ordering::Relaxed);
95            self.total_exceeded_ns
96                .fetch_add(exceeded_by, Ordering::Relaxed);
97
98            // Update ring-specific counters
99            match ring {
100                0 => {
101                    self.ring0_violations.fetch_add(1, Ordering::Relaxed);
102                    self.ring0_exceeded_ns
103                        .fetch_add(exceeded_by, Ordering::Relaxed);
104                }
105                1 => {
106                    self.ring1_violations.fetch_add(1, Ordering::Relaxed);
107                    self.ring1_exceeded_ns
108                        .fetch_add(exceeded_by, Ordering::Relaxed);
109                }
110                _ => {}
111            }
112        }
113
114        // Update ring task counts
115        match ring {
116            0 => {
117                self.ring0_tasks.fetch_add(1, Ordering::Relaxed);
118            }
119            1 => {
120                self.ring1_tasks.fetch_add(1, Ordering::Relaxed);
121            }
122            _ => {}
123        }
124    }
125
126    /// Record a yield event.
127    ///
128    /// # Arguments
129    ///
130    /// * `reason` - The yield reason
131    pub fn record_yield(&self, reason: &super::YieldReason) {
132        use super::YieldReason;
133
134        match reason {
135            YieldReason::BudgetExceeded => {
136                self.yields_budget.fetch_add(1, Ordering::Relaxed);
137            }
138            YieldReason::Ring0Priority => {
139                self.yields_priority.fetch_add(1, Ordering::Relaxed);
140            }
141            YieldReason::QueueEmpty => {
142                self.yields_empty.fetch_add(1, Ordering::Relaxed);
143            }
144            _ => {
145                self.yields_other.fetch_add(1, Ordering::Relaxed);
146            }
147        }
148    }
149
150    /// Get a snapshot of current metrics.
151    #[must_use]
152    pub fn snapshot(&self) -> BudgetMetricsSnapshot {
153        BudgetMetricsSnapshot {
154            total_tasks: self.total_tasks.load(Ordering::Relaxed),
155            total_violations: self.total_violations.load(Ordering::Relaxed),
156            total_exceeded_ns: self.total_exceeded_ns.load(Ordering::Relaxed),
157            total_duration_ns: self.total_duration_ns.load(Ordering::Relaxed),
158            ring0_tasks: self.ring0_tasks.load(Ordering::Relaxed),
159            ring0_violations: self.ring0_violations.load(Ordering::Relaxed),
160            ring0_exceeded_ns: self.ring0_exceeded_ns.load(Ordering::Relaxed),
161            ring1_tasks: self.ring1_tasks.load(Ordering::Relaxed),
162            ring1_violations: self.ring1_violations.load(Ordering::Relaxed),
163            ring1_exceeded_ns: self.ring1_exceeded_ns.load(Ordering::Relaxed),
164            yields_budget: self.yields_budget.load(Ordering::Relaxed),
165            yields_priority: self.yields_priority.load(Ordering::Relaxed),
166            yields_empty: self.yields_empty.load(Ordering::Relaxed),
167            yields_other: self.yields_other.load(Ordering::Relaxed),
168        }
169    }
170
171    /// Reset all metrics to zero.
172    ///
173    /// Useful for testing or periodic metric collection.
174    pub fn reset(&self) {
175        self.total_tasks.store(0, Ordering::Relaxed);
176        self.total_violations.store(0, Ordering::Relaxed);
177        self.total_exceeded_ns.store(0, Ordering::Relaxed);
178        self.total_duration_ns.store(0, Ordering::Relaxed);
179        self.ring0_tasks.store(0, Ordering::Relaxed);
180        self.ring0_violations.store(0, Ordering::Relaxed);
181        self.ring0_exceeded_ns.store(0, Ordering::Relaxed);
182        self.ring1_tasks.store(0, Ordering::Relaxed);
183        self.ring1_violations.store(0, Ordering::Relaxed);
184        self.ring1_exceeded_ns.store(0, Ordering::Relaxed);
185        self.yields_budget.store(0, Ordering::Relaxed);
186        self.yields_priority.store(0, Ordering::Relaxed);
187        self.yields_empty.store(0, Ordering::Relaxed);
188        self.yields_other.store(0, Ordering::Relaxed);
189    }
190}
191
192impl Default for BudgetMetrics {
193    fn default() -> Self {
194        Self::new()
195    }
196}
197
198/// Snapshot of budget metrics at a point in time.
199///
200/// Use this for reporting, alerting, or exporting to external systems.
201#[derive(Debug, Clone, PartialEq, Eq)]
202pub struct BudgetMetricsSnapshot {
203    /// Total number of tasks tracked
204    pub total_tasks: u64,
205    /// Total number of budget violations
206    pub total_violations: u64,
207    /// Total nanoseconds exceeded across all violations
208    pub total_exceeded_ns: u64,
209    /// Total duration of all tasks in nanoseconds
210    pub total_duration_ns: u64,
211
212    /// Ring 0 task count
213    pub ring0_tasks: u64,
214    /// Ring 0 violation count
215    pub ring0_violations: u64,
216    /// Ring 0 total exceeded nanoseconds
217    pub ring0_exceeded_ns: u64,
218
219    /// Ring 1 task count
220    pub ring1_tasks: u64,
221    /// Ring 1 violation count
222    pub ring1_violations: u64,
223    /// Ring 1 total exceeded nanoseconds
224    pub ring1_exceeded_ns: u64,
225
226    /// Yields due to budget exceeded
227    pub yields_budget: u64,
228    /// Yields due to Ring 0 priority
229    pub yields_priority: u64,
230    /// Yields due to empty queue
231    pub yields_empty: u64,
232    /// Yields due to other reasons
233    pub yields_other: u64,
234}
235
236impl BudgetMetricsSnapshot {
237    /// Calculate violation rate (violations per 1000 tasks).
238    #[must_use]
239    pub fn violation_rate_per_1000(&self) -> f64 {
240        if self.total_tasks == 0 {
241            return 0.0;
242        }
243        (self.total_violations as f64 / self.total_tasks as f64) * 1000.0
244    }
245
246    /// Calculate average exceeded time in nanoseconds.
247    #[must_use]
248    pub fn avg_exceeded_ns(&self) -> u64 {
249        if self.total_violations == 0 {
250            return 0;
251        }
252        self.total_exceeded_ns / self.total_violations
253    }
254
255    /// Calculate average task duration in nanoseconds.
256    #[must_use]
257    pub fn avg_duration_ns(&self) -> u64 {
258        if self.total_tasks == 0 {
259            return 0;
260        }
261        self.total_duration_ns / self.total_tasks
262    }
263
264    /// Calculate Ring 0 violation rate (violations per 1000 tasks).
265    #[must_use]
266    pub fn ring0_violation_rate_per_1000(&self) -> f64 {
267        if self.ring0_tasks == 0 {
268            return 0.0;
269        }
270        (self.ring0_violations as f64 / self.ring0_tasks as f64) * 1000.0
271    }
272
273    /// Calculate Ring 1 violation rate (violations per 1000 tasks).
274    #[must_use]
275    pub fn ring1_violation_rate_per_1000(&self) -> f64 {
276        if self.ring1_tasks == 0 {
277            return 0.0;
278        }
279        (self.ring1_violations as f64 / self.ring1_tasks as f64) * 1000.0
280    }
281
282    /// Total yield count across all reasons.
283    #[must_use]
284    pub fn total_yields(&self) -> u64 {
285        self.yields_budget + self.yields_priority + self.yields_empty + self.yields_other
286    }
287
288    /// Calculate priority yield percentage (of all yields).
289    #[must_use]
290    pub fn priority_yield_percentage(&self) -> f64 {
291        let total = self.total_yields();
292        if total == 0 {
293            return 0.0;
294        }
295        (self.yields_priority as f64 / total as f64) * 100.0
296    }
297}
298
299/// Statistics for a specific task type.
300///
301/// Used for per-task tracking in the monitor.
302#[derive(Debug, Clone, Default)]
303pub struct TaskStats {
304    /// Number of times this task has run
305    pub count: u64,
306    /// Number of budget violations
307    pub violations: u64,
308    /// Total exceeded time in nanoseconds
309    pub total_exceeded_ns: u64,
310    /// Minimum duration seen
311    pub min_duration_ns: u64,
312    /// Maximum duration seen
313    pub max_duration_ns: u64,
314    /// Sum of durations (for average calculation)
315    pub total_duration_ns: u64,
316}
317
318impl TaskStats {
319    /// Create new empty stats.
320    #[must_use]
321    pub fn new() -> Self {
322        Self {
323            count: 0,
324            violations: 0,
325            total_exceeded_ns: 0,
326            min_duration_ns: u64::MAX,
327            max_duration_ns: 0,
328            total_duration_ns: 0,
329        }
330    }
331
332    /// Record a task execution.
333    pub fn record(&mut self, budget_ns: u64, elapsed_ns: u64) {
334        self.count += 1;
335        self.total_duration_ns += elapsed_ns;
336
337        if elapsed_ns < self.min_duration_ns {
338            self.min_duration_ns = elapsed_ns;
339        }
340        if elapsed_ns > self.max_duration_ns {
341            self.max_duration_ns = elapsed_ns;
342        }
343
344        if elapsed_ns > budget_ns {
345            self.violations += 1;
346            self.total_exceeded_ns += elapsed_ns - budget_ns;
347        }
348    }
349
350    /// Calculate average duration.
351    #[must_use]
352    pub fn avg_duration_ns(&self) -> u64 {
353        if self.count == 0 {
354            return 0;
355        }
356        self.total_duration_ns / self.count
357    }
358
359    /// Calculate average exceeded time (only for violations).
360    #[must_use]
361    pub fn avg_exceeded_ns(&self) -> u64 {
362        if self.violations == 0 {
363            return 0;
364        }
365        self.total_exceeded_ns / self.violations
366    }
367
368    /// Calculate violation rate as percentage.
369    #[must_use]
370    pub fn violation_rate_percent(&self) -> f64 {
371        if self.count == 0 {
372            return 0.0;
373        }
374        (self.violations as f64 / self.count as f64) * 100.0
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381
382    #[test]
383    fn test_metrics_new() {
384        let metrics = BudgetMetrics::new();
385        let snapshot = metrics.snapshot();
386        assert_eq!(snapshot.total_tasks, 0);
387        assert_eq!(snapshot.total_violations, 0);
388    }
389
390    #[test]
391    fn test_record_task_no_violation() {
392        let metrics = BudgetMetrics::new();
393        metrics.record_task("test", 0, 1000, 500); // Under budget
394
395        let snapshot = metrics.snapshot();
396        assert_eq!(snapshot.total_tasks, 1);
397        assert_eq!(snapshot.total_violations, 0);
398        assert_eq!(snapshot.total_duration_ns, 500);
399        assert_eq!(snapshot.ring0_tasks, 1);
400    }
401
402    #[test]
403    fn test_record_task_with_violation() {
404        let metrics = BudgetMetrics::new();
405        metrics.record_task("test", 0, 1000, 1500); // Over budget by 500
406
407        let snapshot = metrics.snapshot();
408        assert_eq!(snapshot.total_tasks, 1);
409        assert_eq!(snapshot.total_violations, 1);
410        assert_eq!(snapshot.total_exceeded_ns, 500);
411        assert_eq!(snapshot.ring0_violations, 1);
412        assert_eq!(snapshot.ring0_exceeded_ns, 500);
413    }
414
415    #[test]
416    fn test_record_ring1_task() {
417        let metrics = BudgetMetrics::new();
418        metrics.record_task("test", 1, 1000, 1500);
419
420        let snapshot = metrics.snapshot();
421        assert_eq!(snapshot.ring1_tasks, 1);
422        assert_eq!(snapshot.ring1_violations, 1);
423        assert_eq!(snapshot.ring1_exceeded_ns, 500);
424    }
425
426    #[test]
427    fn test_record_yield() {
428        let metrics = BudgetMetrics::new();
429        metrics.record_yield(&super::super::YieldReason::BudgetExceeded);
430        metrics.record_yield(&super::super::YieldReason::Ring0Priority);
431        metrics.record_yield(&super::super::YieldReason::QueueEmpty);
432        metrics.record_yield(&super::super::YieldReason::ShutdownRequested);
433
434        let snapshot = metrics.snapshot();
435        assert_eq!(snapshot.yields_budget, 1);
436        assert_eq!(snapshot.yields_priority, 1);
437        assert_eq!(snapshot.yields_empty, 1);
438        assert_eq!(snapshot.yields_other, 1);
439    }
440
441    #[test]
442    fn test_metrics_reset() {
443        let metrics = BudgetMetrics::new();
444        metrics.record_task("test", 0, 1000, 1500);
445        metrics.record_yield(&super::super::YieldReason::BudgetExceeded);
446
447        metrics.reset();
448
449        let snapshot = metrics.snapshot();
450        assert_eq!(snapshot.total_tasks, 0);
451        assert_eq!(snapshot.total_violations, 0);
452        assert_eq!(snapshot.yields_budget, 0);
453    }
454
455    #[test]
456    fn test_snapshot_calculations() {
457        let metrics = BudgetMetrics::new();
458
459        // Record 10 tasks, 2 violations
460        for i in 0..10 {
461            let elapsed = if i < 2 { 1500 } else { 500 };
462            metrics.record_task("test", 0, 1000, elapsed);
463        }
464
465        let snapshot = metrics.snapshot();
466        assert_eq!(snapshot.total_tasks, 10);
467        assert_eq!(snapshot.total_violations, 2);
468
469        // 2 violations per 10 tasks = 200 per 1000
470        assert!((snapshot.violation_rate_per_1000() - 200.0).abs() < 0.01);
471
472        // Each violation exceeded by 500ns, average = 500
473        assert_eq!(snapshot.avg_exceeded_ns(), 500);
474    }
475
476    #[test]
477    fn test_task_stats() {
478        let mut stats = TaskStats::new();
479
480        stats.record(1000, 500); // Under budget
481        stats.record(1000, 1200); // Over by 200
482        stats.record(1000, 800); // Under budget
483
484        assert_eq!(stats.count, 3);
485        assert_eq!(stats.violations, 1);
486        assert_eq!(stats.total_exceeded_ns, 200);
487        assert_eq!(stats.min_duration_ns, 500);
488        assert_eq!(stats.max_duration_ns, 1200);
489        assert_eq!(stats.avg_duration_ns(), (500 + 1200 + 800) / 3);
490    }
491
492    #[test]
493    fn test_priority_yield_percentage() {
494        let metrics = BudgetMetrics::new();
495        metrics.record_yield(&super::super::YieldReason::Ring0Priority);
496        metrics.record_yield(&super::super::YieldReason::Ring0Priority);
497        metrics.record_yield(&super::super::YieldReason::BudgetExceeded);
498        metrics.record_yield(&super::super::YieldReason::QueueEmpty);
499
500        let snapshot = metrics.snapshot();
501        assert_eq!(snapshot.total_yields(), 4);
502        // 2 priority out of 4 total = 50%
503        assert!((snapshot.priority_yield_percentage() - 50.0).abs() < 0.01);
504    }
505
506    #[test]
507    fn test_global_metrics() {
508        // Access global metrics
509        let global = BudgetMetrics::global();
510
511        // Should be the same instance on subsequent calls
512        let global2 = BudgetMetrics::global();
513        assert!(std::ptr::eq(global, global2));
514    }
515}