Skip to main content

laminar_core/budget/
mod.rs

1//! # Task Budget Enforcement
2//!
3//! Implements task budget enforcement to ensure Ring 0 latency guarantees are met.
4//! Each operation has a time budget, and exceeding it triggers metrics/alerts.
5//! Ring 1 background tasks cooperatively yield when Ring 0 has pending work or
6//! when their budget is exhausted.
7//!
8//! ## Budget Constants
9//!
10//! | Ring | Operation | Budget | Notes |
11//! |------|-----------|--------|-------|
12//! | 0 | Single event | 500ns | p99 latency target |
13//! | 0 | Event batch | 5μs | Up to 10 events |
14//! | 0 | State lookup | 200ns | Fast path |
15//! | 0 | Window trigger | 10μs | Aggregation emission |
16//! | 1 | Background chunk | 1ms | Cooperative yielding |
17//! | 1 | Checkpoint prep | 10ms | Async operation |
18//! | 1 | WAL flush | 100μs | Group commit |
19//!
20//! ## Usage
21//!
22//! ```rust,ignore
23//! use laminar_core::budget::TaskBudget;
24//!
25//! // Ring 0: Track single event processing
26//! fn process_event(event: &Event) {
27//!     let _budget = TaskBudget::ring0_event();
28//!     // Process event - metrics recorded automatically on drop
29//!     process(event);
30//! }
31//!
32//! // Ring 1: Cooperative yielding
33//! fn process_background_work(&mut self) -> YieldReason {
34//!     let budget = TaskBudget::ring1_chunk();
35//!
36//!     while !budget.exceeded() {
37//!         if self.ring0_has_pending() {
38//!             return YieldReason::Ring0Priority;
39//!         }
40//!         // Process work...
41//!     }
42//!
43//!     YieldReason::BudgetExceeded
44//! }
45//! ```
46//!
47//! ## Metrics
48//!
49//! The module tracks:
50//! - Task duration histograms (per task type, per ring)
51//! - Budget violation counts
52//! - Amount exceeded (for capacity planning)
53//!
54//! Access budget status via `TaskBudget` methods.
55
56mod task_budget;
57mod yield_reason;
58
59pub use task_budget::TaskBudget;
60pub use yield_reason::YieldReason;
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use std::thread;
66    use std::time::Duration;
67
68    #[test]
69    fn test_budget_exceeded_detection() {
70        let budget = TaskBudget::ring0_event();
71
72        // Simulate work that takes longer than 500ns
73        thread::sleep(Duration::from_micros(10));
74
75        assert!(budget.exceeded());
76        assert!(budget.remaining_ns() < 0);
77    }
78
79    #[test]
80    fn test_budget_not_exceeded() {
81        let budget = TaskBudget::ring1_chunk(); // 1ms budget
82
83        // Very quick operation
84        let _ = 1 + 1;
85
86        assert!(!budget.exceeded());
87        assert!(budget.remaining_ns() > 0);
88    }
89
90    #[test]
91    fn test_budget_almost_exceeded() {
92        let budget = TaskBudget::custom("test", 0, 1_000_000); // 1ms
93
94        // Sleep for 900μs (90% of budget)
95        thread::sleep(Duration::from_micros(900));
96
97        // Should be almost exceeded (>80% used)
98        assert!(budget.almost_exceeded());
99        // But not fully exceeded yet
100        // Note: timing can be imprecise, so we don't assert !exceeded()
101    }
102
103    #[test]
104    fn test_yield_reason_display() {
105        assert_eq!(format!("{}", YieldReason::BudgetExceeded), "BudgetExceeded");
106        assert_eq!(format!("{}", YieldReason::Ring0Priority), "Ring0Priority");
107        assert_eq!(format!("{}", YieldReason::QueueEmpty), "QueueEmpty");
108        assert_eq!(
109            format!("{}", YieldReason::ShutdownRequested),
110            "ShutdownRequested"
111        );
112    }
113
114    #[test]
115    fn test_custom_budget() {
116        let budget = TaskBudget::custom("my_task", 2, 50_000); // 50μs
117
118        assert_eq!(budget.name(), "my_task");
119        assert_eq!(budget.ring(), 2);
120        assert_eq!(budget.budget_ns(), 50_000);
121    }
122
123    #[test]
124    fn test_ring0_budgets() {
125        let event = TaskBudget::ring0_event();
126        assert_eq!(event.budget_ns(), TaskBudget::RING0_EVENT_NS);
127        assert_eq!(event.ring(), 0);
128
129        let batch = TaskBudget::ring0_batch();
130        assert_eq!(batch.budget_ns(), TaskBudget::RING0_BATCH_NS);
131
132        let lookup = TaskBudget::ring0_lookup();
133        assert_eq!(lookup.budget_ns(), TaskBudget::RING0_LOOKUP_NS);
134
135        let window = TaskBudget::ring0_window();
136        assert_eq!(window.budget_ns(), TaskBudget::RING0_WINDOW_NS);
137    }
138
139    #[test]
140    fn test_ring1_budgets() {
141        let chunk = TaskBudget::ring1_chunk();
142        assert_eq!(chunk.budget_ns(), TaskBudget::RING1_CHUNK_NS);
143        assert_eq!(chunk.ring(), 1);
144
145        let checkpoint = TaskBudget::ring1_checkpoint();
146        assert_eq!(checkpoint.budget_ns(), TaskBudget::RING1_CHECKPOINT_NS);
147
148        let wal_flush = TaskBudget::ring1_wal_flush();
149        assert_eq!(wal_flush.budget_ns(), TaskBudget::RING1_WAL_FLUSH_NS);
150    }
151
152    #[test]
153    fn test_budget_overhead() {
154        // Measure overhead of creating/dropping budgets
155        // This should be < 100ns on modern hardware (we aim for < 10ns)
156        let iterations = 10_000;
157
158        let start = std::time::Instant::now();
159        for _ in 0..iterations {
160            let _budget = TaskBudget::ring0_event();
161            // Budget dropped here
162        }
163        let elapsed = start.elapsed();
164
165        #[allow(clippy::cast_sign_loss)]
166        let overhead_ns = elapsed.as_nanos() / iterations as u128;
167        // Debug builds disable inlining and add extra checks, so the threshold
168        // must be relaxed. Release builds should stay under 200ns.
169        let threshold = if cfg!(debug_assertions) { 2_000 } else { 200 };
170        assert!(
171            overhead_ns < threshold,
172            "Budget overhead {overhead_ns} ns is too high (target < {threshold}ns)",
173        );
174    }
175}