use std::pin::Pin;
use std::sync::{Arc, Weak};
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>,
}
struct SamplerCore {
interval: tokio::time::Interval,
done: bool,
}
impl SamplerCore {
fn new(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);
SamplerCore {
interval,
done: false,
}
}
fn period(&self) -> Duration {
self.interval.period()
}
fn poll_next(
&mut self,
cx: &mut Context<'_>,
take_snapshot: impl FnOnce() -> Option<ProcessGroupStats>,
) -> Poll<Option<ProcessGroupStats>> {
if self.done {
return Poll::Ready(None);
}
std::task::ready!(self.interval.poll_tick(cx));
match take_snapshot() {
Some(snapshot) => Poll::Ready(Some(snapshot)),
None => {
self.done = true;
Poll::Ready(None)
}
}
}
}
pub struct StatsSampler<'a> {
group: &'a ProcessGroup,
core: SamplerCore,
}
impl<'a> StatsSampler<'a> {
pub(crate) fn new(group: &'a ProcessGroup, every: Duration) -> Self {
StatsSampler {
group,
core: SamplerCore::new(every),
}
}
}
impl std::fmt::Debug for StatsSampler<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StatsSampler")
.field("period", &self.core.period())
.field("done", &self.core.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();
let group = this.group;
this.core.poll_next(cx, || group.stats().ok())
}
}
pub struct OwnedStatsSampler {
group: Weak<ProcessGroup>,
core: SamplerCore,
}
impl OwnedStatsSampler {
pub fn new(group: &Arc<ProcessGroup>, every: Duration) -> Self {
OwnedStatsSampler {
group: Arc::downgrade(group),
core: SamplerCore::new(every),
}
}
}
impl std::fmt::Debug for OwnedStatsSampler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OwnedStatsSampler")
.field("period", &self.core.period())
.field("done", &self.core.done)
.field("group_alive", &(self.group.strong_count() > 0))
.finish_non_exhaustive()
}
}
impl tokio_stream::Stream for OwnedStatsSampler {
type Item = ProcessGroupStats;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
let OwnedStatsSampler { group, core } = this;
core.poll_next(cx, || group.upgrade().and_then(|g| g.stats().ok()))
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RunProfile {
pub outcome: Outcome,
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())
}
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()
}
#[doc(hidden)]
pub fn from_parts(
outcome: Outcome,
duration: Duration,
cpu_time: Option<Duration>,
peak_memory_bytes: Option<u64>,
samples: usize,
) -> Self {
RunProfile {
outcome,
duration,
cpu_time,
peak_memory_bytes,
samples,
}
}
}
#[cfg(test)]
mod tests {
use super::{Outcome, OwnedStatsSampler, RunProfile};
use std::sync::Arc;
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 owned_sampler_is_send_and_static() {
fn assert_send_static<T: Send + 'static>() {}
assert_send_static::<OwnedStatsSampler>();
}
#[tokio::test]
async fn owned_sampler_zero_interval_does_not_panic() {
let group = Arc::new(crate::ProcessGroup::new().expect("create group"));
let _sampler = OwnedStatsSampler::new(&group, Duration::ZERO);
}
#[tokio::test]
async fn owned_sampler_ends_when_group_released() {
use tokio_stream::StreamExt;
let group = Arc::new(crate::ProcessGroup::new().expect("create group"));
let mut sampler = OwnedStatsSampler::new(&group, Duration::from_millis(1));
drop(group);
assert!(
sampler.next().await.is_none(),
"a released group must end the owning sampler's series"
);
assert!(
sampler.next().await.is_none(),
"the series must stay ended (fused), not resume"
);
}
#[test]
fn avg_cpu_cores_is_cpu_time_over_duration() {
let profile = RunProfile {
outcome: Outcome::Exited(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),
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),
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]
fn outcome_distinguishes_timeout_from_signal_when_code_is_none() {
let timed_out = RunProfile {
outcome: Outcome::TimedOut,
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)),
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.code(), signalled.code());
}
#[test]
fn run_profile_from_parts_round_trips_every_field() {
let original = RunProfile::from_parts(
Outcome::Exited(0),
Duration::from_secs(2),
Some(Duration::from_secs(1)),
Some(4096),
8,
);
assert_eq!(original.outcome, Outcome::Exited(0));
assert_eq!(original.duration, Duration::from_secs(2));
assert_eq!(original.cpu_time, Some(Duration::from_secs(1)));
assert_eq!(original.peak_memory_bytes, Some(4096));
assert_eq!(original.samples, 8);
assert_eq!(original.avg_cpu_cores(), Some(0.5));
let rebuilt = RunProfile::from_parts(
original.outcome,
original.duration,
original.cpu_time,
original.peak_memory_bytes,
original.samples,
);
assert_eq!(original, rebuilt);
}
}