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}