use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;
use crate::group::ProcessGroup;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ProcessGroupStats {
pub active_process_count: usize,
pub total_cpu_time: Option<Duration>,
pub peak_memory_bytes: Option<u64>,
}
pub struct StatsSampler<'a> {
group: &'a ProcessGroup,
interval: tokio::time::Interval,
done: bool,
}
impl<'a> StatsSampler<'a> {
pub(crate) fn new(group: &'a ProcessGroup, every: Duration) -> Self {
let every = every.max(Duration::from_millis(1));
let mut interval = tokio::time::interval(every);
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
StatsSampler {
group,
interval,
done: false,
}
}
}
impl std::fmt::Debug for StatsSampler<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StatsSampler")
.field("period", &self.interval.period())
.field("done", &self.done)
.finish_non_exhaustive()
}
}
impl tokio_stream::Stream for StatsSampler<'_> {
type Item = ProcessGroupStats;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
if this.done {
return Poll::Ready(None);
}
std::task::ready!(this.interval.poll_tick(cx));
match this.group.stats() {
Ok(snapshot) => Poll::Ready(Some(snapshot)),
Err(_) => {
this.done = true;
Poll::Ready(None)
}
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RunProfile {
pub exit_code: Option<i32>,
pub duration: Duration,
pub cpu_time: Option<Duration>,
pub peak_memory_bytes: Option<u64>,
pub samples: usize,
}
impl RunProfile {
pub fn avg_cpu(&self) -> Option<f64> {
let cpu = self.cpu_time?;
if self.duration.is_zero() {
return None;
}
Some(cpu.as_secs_f64() / self.duration.as_secs_f64())
}
}
#[cfg(test)]
mod tests {
use super::RunProfile;
use std::time::Duration;
#[tokio::test]
async fn zero_interval_sampler_does_not_panic() {
let group = crate::ProcessGroup::new().expect("create group");
let _sampler = group.sample_stats(Duration::ZERO);
}
#[test]
fn avg_cpu_is_cpu_time_over_duration() {
let profile = RunProfile {
exit_code: Some(0),
duration: Duration::from_secs(2),
cpu_time: Some(Duration::from_secs(1)),
peak_memory_bytes: None,
samples: 8,
};
assert_eq!(profile.avg_cpu(), Some(0.5));
}
#[test]
fn avg_cpu_is_none_without_cpu_or_duration() {
let no_cpu = RunProfile {
exit_code: Some(0),
duration: Duration::from_secs(1),
cpu_time: None,
peak_memory_bytes: None,
samples: 0,
};
assert_eq!(no_cpu.avg_cpu(), None);
let no_duration = RunProfile {
exit_code: Some(0),
duration: Duration::ZERO,
cpu_time: Some(Duration::from_secs(1)),
peak_memory_bytes: None,
samples: 1,
};
assert_eq!(no_duration.avg_cpu(), None);
}
}