use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;
use crate::group::ProcessGroup;
use crate::result::Outcome;
#[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, Eq)]
pub struct RunProfile {
pub outcome: Outcome,
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_cores(&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())
}
#[deprecated(
since = "1.1.0",
note = "renamed to `avg_cpu_cores` (the unit is cores); removed in 2.0"
)]
pub fn avg_cpu(&self) -> Option<f64> {
self.avg_cpu_cores()
}
pub fn code(&self) -> Option<i32> {
self.outcome.code()
}
pub fn signal(&self) -> Option<i32> {
self.outcome.signal()
}
pub fn timed_out(&self) -> bool {
self.outcome.timed_out()
}
}
#[cfg(test)]
mod tests {
use super::{Outcome, 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_cores_is_cpu_time_over_duration() {
let profile = RunProfile {
outcome: Outcome::Exited(0),
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_cores(), Some(0.5));
}
#[test]
fn avg_cpu_cores_is_none_without_cpu_or_duration() {
let no_cpu = RunProfile {
outcome: Outcome::Exited(0),
exit_code: Some(0),
duration: Duration::from_secs(1),
cpu_time: None,
peak_memory_bytes: None,
samples: 0,
};
assert_eq!(no_cpu.avg_cpu_cores(), None);
let no_duration = RunProfile {
outcome: Outcome::Exited(0),
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_cores(), None);
}
#[test]
#[allow(deprecated)]
fn avg_cpu_forwards_to_avg_cpu_cores() {
let profile = RunProfile {
outcome: Outcome::Exited(0),
exit_code: Some(0),
duration: Duration::from_secs(2),
cpu_time: Some(Duration::from_secs(1)),
peak_memory_bytes: None,
samples: 4,
};
assert_eq!(profile.avg_cpu(), profile.avg_cpu_cores());
assert_eq!(profile.avg_cpu(), Some(0.5));
}
#[test]
fn outcome_distinguishes_timeout_from_signal_when_exit_code_is_none() {
let timed_out = RunProfile {
outcome: Outcome::TimedOut,
exit_code: None,
duration: Duration::from_secs(1),
cpu_time: None,
peak_memory_bytes: None,
samples: 0,
};
assert!(timed_out.timed_out());
assert_eq!(timed_out.signal(), None);
let signalled = RunProfile {
outcome: Outcome::Signalled(Some(9)),
exit_code: None,
duration: Duration::from_secs(1),
cpu_time: None,
peak_memory_bytes: None,
samples: 0,
};
assert!(!signalled.timed_out());
assert_eq!(signalled.signal(), Some(9));
assert_eq!(timed_out.exit_code, signalled.exit_code);
}
}