1use core::time::Duration;
14use std::time::Instant;
15
16use crate::model::{CpuNormalization, MetricState, UnavailableReason};
17use crate::rates::keyed::DeltaTracker;
18use crate::units::Percent;
19
20fn percent_of_duration(part: Duration, whole: Duration) -> Option<Percent> {
27 let part = u64::try_from(part.as_nanos()).ok()?;
28 let whole = u64::try_from(whole.as_nanos()).ok()?;
29 Percent::ratio(part, whole)
30}
31
32#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
40pub struct CpuTimeTotals {
41 pub busy: Duration,
43 pub idle: Duration,
49}
50
51impl CpuTimeTotals {
52 #[must_use]
54 pub const fn new(busy: Duration, idle: Duration) -> Self {
55 Self { busy, idle }
56 }
57
58 #[must_use]
63 pub const fn total(self) -> Duration {
64 self.busy.saturating_add(self.idle)
65 }
66}
67
68#[derive(Clone, Copy, Debug, Default)]
80pub struct SystemCpuTracker {
81 last: Option<(CpuTimeTotals, Instant)>,
82}
83
84impl SystemCpuTracker {
85 #[must_use]
87 pub const fn new() -> Self {
88 Self { last: None }
89 }
90
91 #[must_use]
93 pub const fn is_warming_up(&self) -> bool {
94 self.last.is_none()
95 }
96
97 #[must_use]
99 pub const fn last_observed_at(&self) -> Option<Instant> {
100 match self.last {
101 Some((_, at)) => Some(at),
102 None => None,
103 }
104 }
105
106 pub fn forget_baseline(&mut self) {
108 self.last = None;
109 }
110
111 pub fn observe(&mut self, totals: CpuTimeTotals, at: Instant) -> MetricState<Percent> {
113 let Some((previous, _)) = self.last.replace((totals, at)) else {
116 return MetricState::WarmingUp;
117 };
118 let (Some(busy), Some(idle)) = (
119 totals.busy.checked_sub(previous.busy),
120 totals.idle.checked_sub(previous.idle),
121 ) else {
122 return MetricState::TemporarilyUnavailable(UnavailableReason::CounterReset);
126 };
127 match percent_of_duration(busy, busy.saturating_add(idle)) {
128 Some(percent) => MetricState::Available(percent.clamped_to_100()),
131 None => MetricState::WarmingUp,
134 }
135 }
136}
137
138impl DeltaTracker for SystemCpuTracker {
139 type Config = ();
140 type Reading = CpuTimeTotals;
141 type Value = Percent;
142
143 fn with_config(_config: Self::Config) -> Self {
144 Self::new()
145 }
146
147 fn observe_reading(&mut self, reading: Self::Reading, at: Instant) -> MetricState<Self::Value> {
148 self.observe(reading, at)
149 }
150
151 fn last_observed_at(&self) -> Option<Instant> {
154 self.last.map(|(_, at)| at)
155 }
156
157 fn forget_baseline(&mut self) {
158 self.last = None;
159 }
160}
161
162#[derive(Clone, Copy, Debug, Default)]
186pub struct ProcessCpuTracker {
187 last: Option<(Duration, Instant)>,
188}
189
190impl ProcessCpuTracker {
191 #[must_use]
193 pub const fn new() -> Self {
194 Self { last: None }
195 }
196
197 #[must_use]
199 pub const fn is_warming_up(&self) -> bool {
200 self.last.is_none()
201 }
202
203 #[must_use]
205 pub const fn last_observed_at(&self) -> Option<Instant> {
206 match self.last {
207 Some((_, at)) => Some(at),
208 None => None,
209 }
210 }
211
212 pub fn forget_baseline(&mut self) {
214 self.last = None;
215 }
216
217 pub fn observe(&mut self, cpu_time: Duration, at: Instant) -> MetricState<Percent> {
220 let Some((previous_cpu, previous_at)) = self.last.replace((cpu_time, at)) else {
221 return MetricState::WarmingUp;
222 };
223 let Some(cpu_delta) = cpu_time.checked_sub(previous_cpu) else {
224 return MetricState::TemporarilyUnavailable(UnavailableReason::CounterReset);
229 };
230 match percent_of_duration(cpu_delta, at.saturating_duration_since(previous_at)) {
231 Some(percent) => MetricState::Available(percent),
234 None => MetricState::WarmingUp,
235 }
236 }
237
238 pub fn observe_normalized(
245 &mut self,
246 cpu_time: Duration,
247 at: Instant,
248 normalization: CpuNormalization,
249 logical_cpus: u16,
250 ) -> MetricState<Percent> {
251 let core_normalized = self.observe(cpu_time, at);
252 let Some(percent) = core_normalized.fresh().copied() else {
253 return core_normalized;
254 };
255 match normalization.apply(percent, logical_cpus) {
256 Some(scaled) => MetricState::Available(scaled),
257 None => MetricState::TemporarilyUnavailable(UnavailableReason::ReadFailed),
258 }
259 }
260}
261
262impl DeltaTracker for ProcessCpuTracker {
263 type Config = ();
264 type Reading = Duration;
265 type Value = Percent;
266
267 fn with_config(_config: Self::Config) -> Self {
268 Self::new()
269 }
270
271 fn observe_reading(&mut self, reading: Self::Reading, at: Instant) -> MetricState<Self::Value> {
272 self.observe(reading, at)
273 }
274
275 fn last_observed_at(&self) -> Option<Instant> {
278 self.last.map(|(_, at)| at)
279 }
280
281 fn forget_baseline(&mut self) {
282 self.last = None;
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 fn origin() -> Instant {
291 Instant::now()
292 }
293
294 fn percent(state: &MetricState<Percent>) -> f32 {
295 state
296 .fresh()
297 .expect("expected a measured percentage")
298 .value()
299 }
300
301 fn secs(seconds: u64) -> Duration {
302 Duration::from_secs(seconds)
303 }
304
305 #[test]
306 fn totals_sum_busy_and_idle_without_overflowing() {
307 let totals = CpuTimeTotals::new(secs(3), secs(5));
308 assert_eq!(totals.total(), secs(8));
309 let extreme = CpuTimeTotals::new(Duration::MAX, secs(1));
310 assert_eq!(extreme.total(), Duration::MAX);
311 }
312
313 #[test]
314 fn a_first_system_sample_is_warming_up_and_not_zero_percent() {
315 let mut tracker = SystemCpuTracker::new();
316 assert!(tracker.is_warming_up());
317 let state = tracker.observe(CpuTimeTotals::new(secs(100), secs(900)), origin());
318 assert!(state.is_warming_up());
319 assert_eq!(state.fresh(), None);
320 assert!(!tracker.is_warming_up());
321 }
322
323 #[test]
324 fn system_cpu_is_the_busy_share_of_total_cpu_time() {
325 let t0 = origin();
327 let mut tracker = SystemCpuTracker::new();
328 tracker.observe(CpuTimeTotals::new(secs(0), secs(0)), t0);
329 let state = tracker.observe(CpuTimeTotals::new(secs(1), secs(7)), t0 + secs(1));
330 assert!((percent(&state) - 12.5).abs() < f32::EPSILON);
331 }
332
333 #[test]
334 fn system_cpu_is_an_aggregate_capped_at_one_hundred_percent() {
335 let t0 = origin();
338 let mut tracker = SystemCpuTracker::new();
339 tracker.observe(CpuTimeTotals::new(secs(0), secs(0)), t0);
340 let state = tracker.observe(CpuTimeTotals::new(secs(8), secs(0)), t0 + secs(1));
341 assert!((percent(&state) - 100.0).abs() < f32::EPSILON);
342 }
343
344 #[test]
345 fn system_cpu_does_not_depend_on_the_sample_interval_length() {
346 let t0 = origin();
349 let before = CpuTimeTotals::new(secs(10), secs(70));
350 let after = CpuTimeTotals::new(secs(11), secs(77));
351
352 let mut quick = SystemCpuTracker::new();
353 quick.observe(before, t0);
354 let quick_state = quick.observe(after, t0 + Duration::from_millis(250));
355
356 let mut slow = SystemCpuTracker::new();
357 slow.observe(before, t0);
358 let slow_state = slow.observe(after, t0 + secs(4));
359
360 assert!((percent(&quick_state) - percent(&slow_state)).abs() < f32::EPSILON);
361 }
362
363 #[test]
364 fn a_stalled_system_counter_is_warming_up_not_zero_percent() {
365 let t0 = origin();
366 let totals = CpuTimeTotals::new(secs(10), secs(70));
367 let mut tracker = SystemCpuTracker::new();
368 tracker.observe(totals, t0);
369 let state = tracker.observe(totals, t0 + secs(1));
370 assert!(state.is_warming_up());
371 assert_ne!(state, MetricState::Available(Percent::ZERO));
372 }
373
374 #[test]
375 fn system_cpu_time_going_backwards_is_a_reset_and_recovers_next_sample() {
376 let t0 = origin();
377 let mut tracker = SystemCpuTracker::new();
378 tracker.observe(CpuTimeTotals::new(secs(100), secs(700)), t0);
379
380 let reset = tracker.observe(CpuTimeTotals::new(secs(2), secs(6)), t0 + secs(1));
381 assert_eq!(
382 reset,
383 MetricState::TemporarilyUnavailable(UnavailableReason::CounterReset)
384 );
385
386 let recovered = tracker.observe(CpuTimeTotals::new(secs(3), secs(13)), t0 + secs(2));
387 assert!((percent(&recovered) - 12.5).abs() < f32::EPSILON);
388 }
389
390 #[test]
391 fn an_idle_only_reset_is_detected_as_well_as_a_busy_only_one() {
392 let t0 = origin();
393 let mut tracker = SystemCpuTracker::new();
394 tracker.observe(CpuTimeTotals::new(secs(100), secs(700)), t0);
395 let reset = tracker.observe(CpuTimeTotals::new(secs(101), secs(1)), t0 + secs(1));
396 assert_eq!(
397 reset,
398 MetricState::TemporarilyUnavailable(UnavailableReason::CounterReset)
399 );
400 }
401
402 #[test]
403 fn a_first_process_sample_is_warming_up_and_not_zero_percent() {
404 let mut tracker = ProcessCpuTracker::new();
405 let state = tracker.observe(secs(42), origin());
406 assert!(state.is_warming_up());
407 assert_ne!(state, MetricState::Available(Percent::ZERO));
408 }
409
410 #[test]
411 fn a_fully_busy_single_core_reads_one_hundred_percent_on_an_eight_cpu_machine() {
412 let t0 = origin();
415 let mut tracker = ProcessCpuTracker::new();
416 tracker.observe(secs(0), t0);
417 let state = tracker.observe(secs(1), t0 + secs(1));
418 assert!((percent(&state) - 100.0).abs() < f32::EPSILON);
419
420 let machine =
421 CpuNormalization::Machine.apply(Percent::new(percent(&state)).expect("valid"), 8);
422 assert!((machine.expect("valid").value() - 12.5).abs() < f32::EPSILON);
423 }
424
425 #[test]
426 fn a_process_on_four_cores_reads_four_hundred_percent_under_core_normalization() {
427 let t0 = origin();
428 let mut tracker = ProcessCpuTracker::new();
429 tracker.observe(secs(0), t0);
430 let state = tracker.observe(secs(4), t0 + secs(1));
431 assert!((percent(&state) - 400.0).abs() < f32::EPSILON);
432 }
433
434 #[test]
435 fn both_normalizations_are_reachable_from_one_reading() {
436 let t0 = origin();
437 let mut core = ProcessCpuTracker::new();
438 core.observe(secs(0), t0);
439 let core_state = core.observe_normalized(secs(4), t0 + secs(1), CpuNormalization::Core, 8);
440 assert!((percent(&core_state) - 400.0).abs() < f32::EPSILON);
441
442 let mut machine = ProcessCpuTracker::new();
443 machine.observe(secs(0), t0);
444 let machine_state =
445 machine.observe_normalized(secs(4), t0 + secs(1), CpuNormalization::Machine, 8);
446 assert!((percent(&machine_state) - 50.0).abs() < f32::EPSILON);
447 }
448
449 #[test]
450 fn machine_normalization_without_a_cpu_count_is_unavailable_not_zero() {
451 let t0 = origin();
452 let mut tracker = ProcessCpuTracker::new();
453 tracker.observe(secs(0), t0);
454 let state = tracker.observe_normalized(secs(1), t0 + secs(1), CpuNormalization::Machine, 0);
455 assert_eq!(
456 state,
457 MetricState::TemporarilyUnavailable(UnavailableReason::ReadFailed)
458 );
459 assert_eq!(state.fresh(), None);
460 }
461
462 #[test]
463 fn normalization_preserves_an_unavailable_state_rather_than_scaling_it() {
464 let mut tracker = ProcessCpuTracker::new();
465 let first = tracker.observe_normalized(secs(5), origin(), CpuNormalization::Machine, 8);
466 assert!(first.is_warming_up());
467 }
468
469 #[test]
470 fn process_cpu_uses_the_actual_elapsed_interval() {
471 let t0 = origin();
475 let cpu = Duration::from_millis(500);
476
477 let mut quick = ProcessCpuTracker::new();
478 quick.observe(Duration::ZERO, t0);
479 let quick_state = quick.observe(cpu, t0 + Duration::from_millis(500));
480
481 let mut slow = ProcessCpuTracker::new();
482 slow.observe(Duration::ZERO, t0);
483 let slow_state = slow.observe(cpu, t0 + secs(2));
484
485 assert!((percent(&quick_state) - 100.0).abs() < f32::EPSILON);
486 assert!((percent(&slow_state) - 25.0).abs() < f32::EPSILON);
487 }
488
489 #[test]
490 fn a_process_that_used_no_cpu_reads_a_real_zero_percent() {
491 let t0 = origin();
492 let mut tracker = ProcessCpuTracker::new();
493 tracker.observe(secs(9), t0);
494 let state = tracker.observe(secs(9), t0 + secs(1));
495 assert_eq!(state, MetricState::Available(Percent::ZERO));
496 }
497
498 #[test]
499 fn zero_elapsed_process_sample_is_warming_up_not_a_division_by_zero() {
500 let t0 = origin();
501 let mut tracker = ProcessCpuTracker::new();
502 tracker.observe(secs(1), t0);
503 let state = tracker.observe(secs(2), t0);
504 assert!(state.is_warming_up());
505 }
506
507 #[test]
508 fn a_reversed_instant_cannot_make_a_process_percentage_negative_or_huge() {
509 let t0 = origin();
510 let mut tracker = ProcessCpuTracker::new();
511 tracker.observe(secs(0), t0 + secs(10));
512 let state = tracker.observe(secs(5), t0);
513 assert!(state.is_warming_up());
514 }
515
516 #[test]
517 fn process_cpu_time_going_backwards_is_a_reset_and_recovers_next_sample() {
518 let t0 = origin();
519 let mut tracker = ProcessCpuTracker::new();
520 tracker.observe(secs(30), t0);
521
522 let reset = tracker.observe(secs(1), t0 + secs(1));
523 assert_eq!(
524 reset,
525 MetricState::TemporarilyUnavailable(UnavailableReason::CounterReset)
526 );
527
528 let recovered = tracker.observe(secs(2), t0 + secs(2));
529 assert!((percent(&recovered) - 100.0).abs() < f32::EPSILON);
530 }
531
532 #[test]
533 fn forgetting_a_process_baseline_prevents_a_delta_across_the_gap() {
534 let t0 = origin();
535 let mut tracker = ProcessCpuTracker::new();
536 tracker.observe(secs(0), t0);
537 tracker.forget_baseline();
538 assert!(tracker.is_warming_up());
539 assert!(tracker.observe(secs(600), t0 + secs(1)).is_warming_up());
540 }
541
542 #[test]
543 fn trackers_report_when_they_last_saw_a_reading() {
544 let t0 = origin();
545 let at = t0 + secs(5);
546 let mut system = SystemCpuTracker::new();
547 assert_eq!(system.last_observed_at(), None);
548 system.observe(CpuTimeTotals::default(), at);
549 assert_eq!(system.last_observed_at(), Some(at));
550
551 let mut process = ProcessCpuTracker::new();
552 assert_eq!(process.last_observed_at(), None);
553 process.observe(Duration::ZERO, at);
554 assert_eq!(process.last_observed_at(), Some(at));
555 }
556}