use core::time::Duration;
use crate::history::{
ContributorMetric, HistoricalSample, HistoryMetric, HistoryRing, HistoryView,
};
use crate::model::{MeasuredValue, ProcessIdentity};
use super::TimeWindow;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct Counted {
pub matched: usize,
pub considered: usize,
pub unavailable: usize,
pub span: Duration,
}
impl Counted {
#[must_use]
pub const fn visited(&self) -> usize {
self.considered.saturating_add(self.unavailable)
}
#[must_use]
pub const fn window(&self) -> TimeWindow {
TimeWindow::new(self.span, self.considered)
}
#[must_use]
pub const fn sustained(&self, required: usize, minimum: usize) -> bool {
self.considered >= minimum && self.matched >= required
}
}
#[derive(Clone, Copy, Debug)]
pub struct HistoryWindow<'a> {
ring: &'a HistoryRing,
view: HistoryView,
}
impl<'a> HistoryWindow<'a> {
#[must_use]
pub const fn new(ring: &'a HistoryRing, view: HistoryView) -> Self {
Self { ring, view }
}
#[must_use]
pub const fn live(ring: &'a HistoryRing) -> Self {
Self::new(ring, HistoryView::live())
}
#[must_use]
pub const fn view(&self) -> HistoryView {
self.view
}
#[must_use]
pub const fn ring(&self) -> &'a HistoryRing {
self.ring
}
#[must_use]
pub fn expected_interval(&self) -> Duration {
self.ring.limits().interval()
}
#[must_use]
pub fn selected(&self) -> Option<&'a HistoricalSample> {
self.view.selected(self.ring)
}
#[must_use]
pub fn len(&self) -> usize {
self.ring.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.ring.is_empty()
}
pub fn recent(&self, count: usize) -> impl DoubleEndedIterator<Item = &'a HistoricalSample> {
let ring = self.ring;
self.bounds(count)
.into_iter()
.flat_map(move |(first, last)| {
(first..=last).filter_map(move |at| ring.get_absolute(at))
})
}
#[must_use]
pub fn previous_sample(&self, sequence: u64) -> Option<&'a HistoricalSample> {
self.recent(2).rfind(|sample| sample.sequence < sequence)
}
#[must_use]
pub fn count_where(
&self,
metric: HistoryMetric,
count: usize,
predicate: impl Fn(f64) -> bool,
) -> Counted {
let mut counted = Counted::default();
let mut oldest: Option<Duration> = None;
let mut newest = Duration::ZERO;
for sample in self.recent(count) {
if oldest.is_none() {
oldest = Some(sample.monotonic_offset);
}
newest = sample.monotonic_offset;
match sample.system.scalar(metric) {
Some(value) => {
counted.considered = counted.considered.saturating_add(1);
if predicate(value) {
counted.matched = counted.matched.saturating_add(1);
}
}
None => counted.unavailable = counted.unavailable.saturating_add(1),
}
}
counted.span = newest.saturating_sub(oldest.unwrap_or(newest));
counted
}
#[must_use]
pub fn count_at_least(&self, metric: HistoryMetric, count: usize, threshold: f64) -> Counted {
self.count_where(metric, count, |value| value >= threshold)
}
#[must_use]
pub fn trend(&self, metric: HistoryMetric, count: usize) -> Option<(f64, f64, Duration)> {
let mut first: Option<(f64, Duration)> = None;
let mut last: Option<(f64, Duration)> = None;
for sample in self.recent(count) {
if let Some(value) = sample.system.scalar(metric) {
if first.is_none() {
first = Some((value, sample.monotonic_offset));
}
last = Some((value, sample.monotonic_offset));
}
}
let (start, start_at) = first?;
let (end, end_at) = last?;
Some((start, end, end_at.saturating_sub(start_at)))
}
fn bounds(&self, count: usize) -> Option<(u64, u64)> {
if count == 0 {
return None;
}
let last = self.view.selected_absolute(self.ring)?;
let span = u64::try_from(count).unwrap_or(u64::MAX).saturating_sub(1);
let first = last.saturating_sub(span).max(self.ring.first_absolute());
Some((first, last))
}
}
#[must_use]
pub fn contributor_value(
sample: &HistoricalSample,
metric: ContributorMetric,
identity: ProcessIdentity,
) -> Option<f64> {
sample
.contributors
.metric(metric)
.entries()
.iter()
.find(|entry| entry.identity == identity)
.map(|entry| measured_scalar(entry.value))
}
pub(crate) fn measured_scalar(value: MeasuredValue) -> f64 {
match value {
MeasuredValue::Bytes(bytes) | MeasuredValue::Count(bytes) => bytes as f64,
MeasuredValue::ByteRate(rate) | MeasuredValue::EventRate(rate) => rate.per_second(),
MeasuredValue::Percent(percent) => f64::from(percent.value()),
MeasuredValue::Duration(duration) => duration.as_secs_f64(),
MeasuredValue::Load(load) => f64::from(load),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diagnostics::fixtures::{Timeline, set_cpu, set_memory};
use crate::history::HistoryMetric;
use crate::model::{MetricState, UnavailableReason};
#[test]
fn counting_an_empty_ring_reports_nothing_considered() {
let timeline = Timeline::new(Duration::from_secs(1));
let window = timeline.window();
let counted = window.count_at_least(HistoryMetric::CpuBusy, 15, 80.0);
assert!(window.is_empty());
assert_eq!(counted, Counted::default());
assert!(!counted.sustained(1, 1));
assert_eq!(counted.window().samples, 0);
}
#[test]
fn the_expected_interval_comes_from_history_rather_than_an_assumption() {
for interval in [Duration::from_millis(500), Duration::from_secs(5)] {
let timeline = Timeline::new(interval);
assert_eq!(timeline.window().expected_interval(), timeline.interval());
assert_eq!(timeline.window().expected_interval(), interval);
}
}
#[test]
fn counting_is_limited_to_the_requested_window() {
let mut timeline = Timeline::new(Duration::from_secs(1));
for _ in 0..10 {
timeline.push(|snapshot| set_cpu(snapshot, 10.0));
}
for _ in 0..5 {
timeline.push(|snapshot| set_cpu(snapshot, 90.0));
}
let window = timeline.window();
let counted = window.count_at_least(HistoryMetric::CpuBusy, 5, 80.0);
assert_eq!(counted.matched, 5);
assert_eq!(counted.considered, 5);
assert_eq!(counted.span, Duration::from_secs(4));
let wider = window.count_at_least(HistoryMetric::CpuBusy, 15, 80.0);
assert_eq!(wider.matched, 5);
assert_eq!(wider.considered, 15);
}
#[test]
fn a_window_larger_than_the_ring_counts_only_what_exists() {
let mut timeline = Timeline::new(Duration::from_secs(1));
for _ in 0..3 {
timeline.push(|snapshot| set_cpu(snapshot, 99.0));
}
let counted = timeline
.window()
.count_at_least(HistoryMetric::CpuBusy, 100, 80.0);
assert_eq!(counted.visited(), 3);
assert_eq!(counted.matched, 3);
}
#[test]
fn an_unavailable_sample_is_neither_a_match_nor_a_considered_reading() {
let mut timeline = Timeline::new(Duration::from_secs(1));
for _ in 0..5 {
timeline.push(|snapshot| set_cpu(snapshot, 99.0));
}
for _ in 0..5 {
timeline.push(|snapshot| {
snapshot.cpu.total =
MetricState::TemporarilyUnavailable(UnavailableReason::CounterReset);
});
}
let counted = timeline
.window()
.count_at_least(HistoryMetric::CpuBusy, 10, 80.0);
assert_eq!(counted.matched, 5);
assert_eq!(counted.considered, 5);
assert_eq!(counted.unavailable, 5);
assert_eq!(counted.visited(), 10);
assert!(
!counted.sustained(10, 10),
"five readings cannot support a ten-sample claim"
);
}
#[test]
fn the_cursor_decides_which_window_is_counted() {
let mut timeline = Timeline::new(Duration::from_secs(1));
for _ in 0..10 {
timeline.push(|snapshot| set_cpu(snapshot, 95.0));
}
for _ in 0..10 {
timeline.push(|snapshot| set_cpu(snapshot, 1.0));
}
let live = timeline.window();
assert_eq!(
live.count_at_least(HistoryMetric::CpuBusy, 10, 80.0)
.matched,
0
);
let mut view = HistoryView::live();
view.step_back(timeline.ring(), 10);
let historical = HistoryWindow::new(timeline.ring(), view);
assert_eq!(
historical
.count_at_least(HistoryMetric::CpuBusy, 10, 80.0)
.matched,
10,
"a rule evaluated over a selected sample must see that sample's past"
);
assert_eq!(historical.view(), view);
}
#[test]
fn a_trend_needs_both_endpoints_measured() {
let mut timeline = Timeline::new(Duration::from_secs(1));
timeline.push(|snapshot| set_memory(snapshot, 1_000, 800));
timeline.push(|snapshot| {
snapshot.memory.usage = MetricState::PermissionDenied;
});
assert!(
timeline
.window()
.trend(HistoryMetric::MemoryUsedShare, 2)
.is_some(),
"the older endpoint is still measured, so the trend spans one sample"
);
let mut only_unavailable = Timeline::new(Duration::from_secs(1));
only_unavailable.push(|snapshot| {
snapshot.memory.usage = MetricState::PermissionDenied;
});
assert!(
only_unavailable
.window()
.trend(HistoryMetric::MemoryUsedShare, 2)
.is_none()
);
}
#[test]
fn a_trend_reports_the_real_span_between_the_endpoints() {
let mut timeline = Timeline::new(Duration::from_millis(500));
for used_share in [10u64, 20, 30] {
timeline.push(move |snapshot| set_memory(snapshot, 1_000, 1_000 - used_share * 10));
}
let (start, end, span) = timeline
.window()
.trend(HistoryMetric::MemoryUsedShare, 3)
.expect("three measured samples");
assert!((start - 10.0).abs() < 0.01, "{start}");
assert!((end - 30.0).abs() < 0.01, "{end}");
assert_eq!(span, Duration::from_secs(1), "two 500ms intervals");
}
#[test]
fn the_previous_sample_is_the_newest_one_older_than_the_snapshot() {
let mut timeline = Timeline::new(Duration::from_secs(1));
timeline.push(|snapshot| set_cpu(snapshot, 1.0));
timeline.push(|snapshot| set_cpu(snapshot, 2.0));
let current = timeline.push(|snapshot| set_cpu(snapshot, 3.0));
let window = timeline.window();
let previous = window
.previous_sample(current.sequence)
.expect("a previous sample exists");
assert_eq!(previous.sequence, current.sequence - 1);
assert!(
window.previous_sample(0).is_none(),
"nothing precedes the first sample"
);
}
#[test]
fn a_zero_length_window_reads_nothing_instead_of_panicking() {
let mut timeline = Timeline::new(Duration::from_secs(1));
timeline.push(|snapshot| set_cpu(snapshot, 50.0));
assert_eq!(timeline.window().recent(0).count(), 0);
assert_eq!(
timeline
.window()
.count_at_least(HistoryMetric::CpuBusy, 0, 1.0)
.visited(),
0
);
}
#[test]
fn every_measured_value_kind_has_a_comparable_scalar() {
use crate::units::{Percent, Rate};
let rate = Rate::new(1_024.0).expect("valid rate");
let cases = [
(MeasuredValue::Bytes(4_096), 4_096.0),
(MeasuredValue::Count(7), 7.0),
(MeasuredValue::ByteRate(rate), 1_024.0),
(MeasuredValue::EventRate(rate), 1_024.0),
(
MeasuredValue::Percent(Percent::new(37.5).expect("valid")),
37.5,
),
(MeasuredValue::Duration(Duration::from_secs(2)), 2.0),
(MeasuredValue::Load(4.25), 4.25),
];
for (value, expected) in cases {
let scalar = measured_scalar(value);
assert!((scalar - expected).abs() < 0.001, "{value:?} -> {scalar}");
}
}
}