1#![allow(clippy::cast_precision_loss)] use std::sync::atomic::{AtomicU64, Ordering};
9use std::sync::OnceLock;
10
11static GLOBAL_METRICS: OnceLock<BudgetMetrics> = OnceLock::new();
13
14#[derive(Debug)]
25pub struct BudgetMetrics {
26 total_tasks: AtomicU64,
28 total_violations: AtomicU64,
29 total_exceeded_ns: AtomicU64,
30 total_duration_ns: AtomicU64,
31
32 ring0_tasks: AtomicU64,
34 ring0_violations: AtomicU64,
35 ring0_exceeded_ns: AtomicU64,
36
37 ring1_tasks: AtomicU64,
39 ring1_violations: AtomicU64,
40 ring1_exceeded_ns: AtomicU64,
41
42 yields_budget: AtomicU64,
44 yields_priority: AtomicU64,
45 yields_empty: AtomicU64,
46 yields_other: AtomicU64,
47}
48
49impl BudgetMetrics {
50 #[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 #[must_use]
73 pub fn global() -> &'static Self {
74 GLOBAL_METRICS.get_or_init(Self::new)
75 }
76
77 pub fn record_task(&self, _name: &'static str, ring: u8, budget_ns: u64, elapsed_ns: u64) {
86 self.total_tasks.fetch_add(1, Ordering::Relaxed);
88 self.total_duration_ns
89 .fetch_add(elapsed_ns, Ordering::Relaxed);
90
91 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 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 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 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 #[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 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#[derive(Debug, Clone, PartialEq, Eq)]
202pub struct BudgetMetricsSnapshot {
203 pub total_tasks: u64,
205 pub total_violations: u64,
207 pub total_exceeded_ns: u64,
209 pub total_duration_ns: u64,
211
212 pub ring0_tasks: u64,
214 pub ring0_violations: u64,
216 pub ring0_exceeded_ns: u64,
218
219 pub ring1_tasks: u64,
221 pub ring1_violations: u64,
223 pub ring1_exceeded_ns: u64,
225
226 pub yields_budget: u64,
228 pub yields_priority: u64,
230 pub yields_empty: u64,
232 pub yields_other: u64,
234}
235
236impl BudgetMetricsSnapshot {
237 #[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 #[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 #[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 #[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 #[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 #[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 #[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#[derive(Debug, Clone, Default)]
303pub struct TaskStats {
304 pub count: u64,
306 pub violations: u64,
308 pub total_exceeded_ns: u64,
310 pub min_duration_ns: u64,
312 pub max_duration_ns: u64,
314 pub total_duration_ns: u64,
316}
317
318impl TaskStats {
319 #[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 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 #[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 #[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 #[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); 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); 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 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 assert!((snapshot.violation_rate_per_1000() - 200.0).abs() < 0.01);
471
472 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); stats.record(1000, 1200); stats.record(1000, 800); 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 assert!((snapshot.priority_yield_percentage() - 50.0).abs() < 0.01);
504 }
505
506 #[test]
507 fn test_global_metrics() {
508 let global = BudgetMetrics::global();
510
511 let global2 = BudgetMetrics::global();
513 assert!(std::ptr::eq(global, global2));
514 }
515}