use std::borrow::Cow;
use std::fmt;
use super::{Job, JobState, JobSummary, Sample, SampleSeries, Step, Timeline};
use crate::api::{QueuedJob, Record};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutcomeKind {
Succeeded,
Failed,
Running,
Waiting,
Ended,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Outcome {
pub label: Cow<'static, str>,
pub kind: OutcomeKind,
}
impl Outcome {
fn new(label: &'static str, kind: OutcomeKind) -> Outcome {
Outcome {
label: Cow::Borrowed(label),
kind,
}
}
fn concluded(conclusion: &str) -> Outcome {
let kind = match conclusion {
"success" => OutcomeKind::Succeeded,
failed if failed_conclusion(failed) => OutcomeKind::Failed,
_ => OutcomeKind::Ended,
};
Outcome {
label: Cow::Owned(conclusion.replace('_', " ")),
kind,
}
}
}
fn failed_conclusion(conclusion: &str) -> bool {
matches!(conclusion, "failure" | "timed_out")
}
impl JobSummary {
pub fn is_failure(&self) -> bool {
match self.state {
JobState::Lost => true,
JobState::Completed => self.conclusion.as_deref().is_some_and(failed_conclusion),
JobState::Queued | JobState::Running | JobState::Elsewhere => false,
}
}
pub fn outcome(&self) -> Outcome {
let started = self.timeline.started_at.is_some();
match (self.state, self.conclusion.as_deref()) {
(JobState::Queued, _) => Outcome::new("queued", OutcomeKind::Waiting),
(JobState::Running, _) => Outcome::new("running", OutcomeKind::Running),
(JobState::Lost, _) => Outcome::new("lost", OutcomeKind::Failed),
(JobState::Elsewhere, Some("cancelled")) if !started => {
Outcome::new("cancelled in queue", OutcomeKind::Ended)
}
(JobState::Elsewhere, Some(conclusion)) if !started => Outcome {
kind: OutcomeKind::Ended,
..Outcome::concluded(conclusion)
},
(JobState::Elsewhere, _) => Outcome::new("elsewhere", OutcomeKind::Ended),
(JobState::Completed, None) => Outcome::new("completed", OutcomeKind::Ended),
(JobState::Completed, Some(conclusion)) => Outcome::concluded(conclusion),
}
}
pub fn waited(&self, now: u64) -> Option<u64> {
let end = match self.timeline.started_at {
Some(started) => started,
None if !self.state.is_final() => now,
None => return None,
};
Some(end.saturating_sub(self.timeline.queued_at))
}
pub fn ran(&self, now: u64) -> Option<u64> {
let started = self.timeline.started_at?;
let end = match self.timeline.finished_at {
Some(finished) => finished,
None if !self.state.is_final() => now,
None => return None,
};
Some(end.saturating_sub(started))
}
pub fn phases(&self) -> Vec<Phase> {
let timeline = &self.timeline;
let open = !self.state.is_final();
let until = |end: Option<u64>| match end {
Some(end) => Some(Some(end)),
None if open => Some(None),
None => timeline.finished_at.map(Some),
};
let runner = timeline.runner_from();
let mut phases = Vec::new();
if let Some(to) = until(runner.or(timeline.started_at)) {
phases.push(Phase {
kind: PhaseKind::Waiting,
from: timeline.queued_at,
to,
});
}
if let Some(from) = runner
&& let Some(to) = until(timeline.started_at)
{
phases.push(Phase {
kind: PhaseKind::Starting,
from,
to,
});
}
if let Some(from) = timeline.started_at
&& let Some(to) = timeline.finished_at.map(Some).or(open.then_some(None))
{
phases.push(Phase {
kind: PhaseKind::Running,
from,
to,
});
}
if let Some(from) = timeline.finished_at
&& runner.is_some()
{
phases.push(Phase {
kind: PhaseKind::CleaningUp,
from,
to: timeline.cleaned_at,
});
}
phases
}
pub fn title(&self) -> JobTitle<'_> {
job_title(self.workflow.as_deref(), &self.name, Some(self.id))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Phase {
pub kind: PhaseKind,
pub from: u64,
pub to: Option<u64>,
}
impl Phase {
pub fn seconds(&self, now: u64) -> u64 {
self.to.unwrap_or(now).saturating_sub(self.from)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PhaseKind {
Waiting,
Starting,
Running,
CleaningUp,
}
impl PhaseKind {
pub fn label(self) -> &'static str {
match self {
PhaseKind::Waiting => "Waiting for a runner",
PhaseKind::Starting => "Runner starting",
PhaseKind::Running => "Running",
PhaseKind::CleaningUp => "Cleaning up",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Milestone {
pub label: &'static str,
pub at: u64,
}
impl Timeline {
pub fn milestones(&self) -> Vec<Milestone> {
let mut milestones: Vec<Milestone> = [
("Queued", Some(self.queued_at)),
("Container started", self.container_started_at),
("Job started", self.started_at),
("Finished", self.finished_at),
("Cleaned up", self.cleaned_at),
]
.into_iter()
.filter_map(|(label, at)| Some(Milestone { label, at: at? }))
.collect();
milestones.sort_by_key(|milestone| milestone.at);
milestones
}
}
impl Step {
pub fn outcome(&self) -> Outcome {
match (self.conclusion.as_deref(), self.status.as_str()) {
(Some(conclusion), _) => Outcome::concluded(conclusion),
(None, "in_progress") => Outcome::new("running", OutcomeKind::Running),
(None, "completed") => Outcome::new("completed", OutcomeKind::Ended),
(None, _) => Outcome::new("pending", OutcomeKind::Waiting),
}
}
pub fn is_running(&self) -> bool {
self.status == "in_progress" && self.conclusion.is_none()
}
pub fn took(&self, now: u64) -> Option<u64> {
let started = self.started_at?;
let end = match self.completed_at {
Some(completed) => completed,
None if self.is_running() => now,
None => return None,
};
Some(end.saturating_sub(started))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JobTitle<'job> {
pub workflow: Option<&'job str>,
pub name: Cow<'job, str>,
}
pub fn job_title<'job>(
workflow: Option<&'job str>,
name: &'job str,
id: Option<u64>,
) -> JobTitle<'job> {
let name = match (name, id) {
("", Some(id)) => Cow::Owned(format!("job {id}")),
("", None) => Cow::Borrowed("a job"),
(name, _) => Cow::Borrowed(name),
};
JobTitle { workflow, name }
}
impl fmt::Display for JobTitle<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.workflow {
Some(workflow) => write!(f, "{workflow} / {}", self.name),
None => f.write_str(&self.name),
}
}
}
impl QueuedJob {
pub fn title(&self) -> JobTitle<'_> {
job_title(self.workflow.as_deref(), &self.name, Some(self.id))
}
}
impl Record {
pub fn title(&self) -> JobTitle<'_> {
job_title(
self.workflow.as_deref(),
self.name.as_deref().unwrap_or_default(),
Some(self.job),
)
}
}
impl Job {
pub fn observe(&mut self, sample: &Sample) {
let metrics = &mut self.summary.metrics;
if !self.summary.state.is_final() {
metrics.memory_peak = metrics.memory_peak.max(Some(sample.memory));
metrics.cpu_peak = metrics.cpu_peak.max(Some(sample.cpu));
}
}
pub fn reread(&mut self, read: Job) {
let seen = self.summary.metrics.clone();
*self = read;
if !self.summary.state.is_final() {
let metrics = &mut self.summary.metrics;
metrics.memory_peak = metrics.memory_peak.max(seen.memory_peak);
metrics.cpu_peak = metrics.cpu_peak.max(seen.cpu_peak);
}
}
pub fn compare(&self, runs: &[JobSummary], now: u64) -> RunComparison {
let runs: Vec<JobSummary> = runs
.iter()
.map(|run| {
if run.id == self.summary.id {
self.summary.clone()
} else {
run.clone()
}
})
.collect();
let finished: Vec<&JobSummary> = runs
.iter()
.filter(|run| {
matches!(run.state, JobState::Completed | JobState::Lost)
&& run.timeline.started_at.is_some()
})
.collect();
let median = |mut values: Vec<u64>| {
values.sort_unstable();
values.get(values.len().saturating_sub(1) / 2).copied()
};
RunComparison {
median_ran: median(finished.iter().filter_map(|run| run.ran(now)).collect()),
median_waited: median(finished.iter().filter_map(|run| run.waited(now)).collect()),
finished: finished.len(),
failed: finished.iter().filter(|run| run.is_failure()).count(),
largest_peak: runs.iter().filter_map(|run| run.metrics.memory_peak).max(),
runs,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RunComparison {
pub runs: Vec<JobSummary>,
pub median_ran: Option<u64>,
pub median_waited: Option<u64>,
pub finished: usize,
pub failed: usize,
pub largest_peak: Option<u64>,
}
impl SampleSeries {
pub fn usage_between(&self, from: u64, to: u64) -> Option<SampleUsage> {
let inside: Vec<&Sample> = self
.samples
.iter()
.filter(|sample| from < sample.at && sample.at <= to)
.collect();
let count = u64::try_from(inside.len())
.ok()
.filter(|count| *count > 0)?;
Some(SampleUsage {
memory_peak: inside.iter().map(|sample| sample.memory).max()?,
cpu_average: inside.iter().map(|sample| sample.cpu).sum::<u64>() / count,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SampleUsage {
pub memory_peak: u64,
pub cpu_average: u64,
}
impl SampleUsage {
pub fn memory(&self) -> String {
format!("peak {}", crate::config::ByteSize(self.memory_peak))
}
pub fn cpu(&self) -> String {
format!("average {}", super::super::in_cores(self.cpu_average))
}
pub fn description(&self) -> String {
format!("{}, {}", self.memory(), self.cpu())
}
}
#[cfg(test)]
mod tests;