use std::fmt::Write as _;
use serde::{Deserialize, Serialize};
use crate::config::ByteSize;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum AdmissionPolicy {
#[default]
Caps,
Measured,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", tag = "kind")]
pub enum Basis {
Job {
runs: u64,
#[serde(default, skip_serializing_if = "JobPeak::is_p95")]
peak: JobPeak,
},
Profile {
runs: u64,
},
ProfileMax {
runs: u64,
},
Overran {
until: u64,
},
Cap,
Unpredictable {
until: u64,
},
Unmeasured,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum JobPeak {
#[default]
P95,
Largest,
}
impl JobPeak {
#[expect(
clippy::trivially_copy_pass_by_ref,
reason = "serde's skip_serializing_if passes a reference"
)]
fn is_p95(&self) -> bool {
*self == JobPeak::P95
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Admission {
pub policy: AdmissionPolicy,
pub reserved: u64,
pub basis: Basis,
pub free: Option<u64>,
pub slice: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cpu: Option<CpuAdmission>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct CpuAdmission {
pub reserved: u64,
pub basis: Basis,
pub free: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub waited: Option<u64>,
}
impl Admission {
pub fn line(&self) -> String {
let size = |bytes: u64| ByteSize(bytes).to_string();
let reserved = size(self.reserved);
let why = match self.basis {
Basis::Job {
runs,
peak: JobPeak::P95,
} => {
format!("reserved {reserved}, the p95 peak of {runs} runs of its job + the margin")
}
Basis::Job {
runs,
peak: JobPeak::Largest,
} => {
format!(
"reserved {reserved}, the largest peak of {runs} runs of its job + the margin"
)
}
Basis::Profile { runs } => format!(
"reserved {reserved}, from {runs} runs of its profile: its job had too few of its own"
),
Basis::ProfileMax { runs } => format!(
"reserved {reserved}, the largest peak of {runs} runs of its profile + the margin: its job had no \
runs of its own"
),
Basis::Overran { .. } => format!(
"reserved {reserved}, its job's largest peak + the margin: a run of it went past what it reserved \
lately"
),
Basis::Cap if self.policy == AdmissionPolicy::Caps => {
format!("reserved its {reserved} cap")
}
Basis::Cap => format!("reserved its {reserved} cap: too few runs to predict from"),
Basis::Unpredictable { .. } => format!(
"reserved its {reserved} cap: a run of its job ran out of memory at its cap or was killed for memory lately"
),
Basis::Unmeasured => {
format!("reserved its {reserved} cap: no recent sample of the slice's memory")
}
};
let policy = match self.policy {
AdmissionPolicy::Caps => "caps",
AdmissionPolicy::Measured => "measured peaks",
};
let mut line = format!("admitted by {policy}: {why}");
if let Some(free) = self.free {
let _ = write!(line, "; {} of the budget was free", size(free));
}
if let Some(slice) = self.slice {
let _ = write!(line, ", the slice at {}", size(slice));
}
if let Some(cpu) = self.cpu {
let _ = write!(line, ". {}", cpu.line());
}
line
}
}
impl CpuAdmission {
pub fn line(&self) -> String {
let reserved = cpus(self.reserved);
let why = match self.basis {
Basis::Job {
runs,
peak: JobPeak::P95,
} => format!("the p95 average of {runs} runs of its job"),
Basis::Job {
runs,
peak: JobPeak::Largest,
} => format!("the largest average of {runs} runs of its job"),
Basis::Profile { runs } => {
format!("from {runs} runs of its profile: its job had too few of its own")
}
Basis::ProfileMax { runs } => format!(
"the largest average of {runs} runs of its profile: its job had no runs of its own"
),
Basis::Cap
| Basis::Overran { .. }
| Basis::Unpredictable { .. }
| Basis::Unmeasured => "its profile's cpus".to_owned(),
};
let mut line = format!(
"Reserved {reserved}, {why}; {} of the budget were free",
cpus(self.free)
);
if let Some(waited) = self.waited {
let _ = write!(line, "; it waited {} for CPU", wait(waited));
}
line
}
}
pub fn cores(millicores: u64) -> String {
tenths(millicores.div_ceil(100))
}
pub(super) fn free_cores(millicores: u64) -> String {
tenths(millicores / 100)
}
fn tenths(tenths: u64) -> String {
if tenths.is_multiple_of(10) {
(tenths / 10).to_string()
} else {
format!("{}.{}", tenths / 10, tenths % 10)
}
}
pub fn cpus(millicores: u64) -> String {
match cores(millicores).as_str() {
"1" => "1 CPU".to_owned(),
number => format!("{number} CPUs"),
}
}
pub(super) fn wait(secs: u64) -> String {
match secs {
0..60 => format!("{secs}s"),
60..3600 => format!("{}m{:02}s", secs / 60, secs % 60),
_ => format!("{}h{:02}m", secs / 3600, secs % 3600 / 60),
}
}
#[cfg(all(test, any(feature = "host", feature = "client")))]
mod tests;