use serde::{Deserialize, Serialize};
use crate::api::AdmissionPolicy;
use crate::config::ByteSize;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SizingQuery {
pub since: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Sizing {
pub since: u64,
pub until: u64,
pub margin: u32,
pub sample_interval: u64,
pub min_jobs: u32,
pub memory_max: Option<u64>,
#[serde(default)]
pub admission: AdmissionPolicy,
pub profiles: Vec<ProfileSizing>,
#[serde(default)]
pub auto: bool,
#[serde(default)]
pub changes: Vec<SizingChange>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SizingChange {
pub at: u64,
pub profile: String,
pub from: Resize,
pub to: Resize,
pub reason: String,
#[serde(default)]
pub uncertain: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProfileSizing {
pub name: String,
pub memory: u64,
pub cpus: u32,
pub slots: u32,
pub peaks: Peaks,
pub wait: Spread,
pub busy: u32,
#[serde(default)]
pub wants_slots: bool,
pub advice: Advice,
pub jobs: Vec<JobSizing>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JobSizing {
pub repository: String,
pub workflow: Option<String>,
pub name: String,
pub peaks: Peaks,
pub wait: Spread,
pub advice: Advice,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Peaks {
pub jobs: u64,
pub at_cap: u64,
pub memory: Spread,
pub cpu: Spread,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Spread {
pub p50: u64,
pub p95: u64,
pub p99: u64,
pub max: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", tag = "kind")]
pub enum Advice {
TooFew {
jobs: u64,
},
Keep {
memory: u64,
},
Lower {
memory: u64,
slots: u32,
},
Raise {
memory: u64,
slots: u32,
},
SliceTooSmall {
memory: u64,
},
}
impl Advice {
pub fn change(&self) -> Option<Resize> {
match *self {
Advice::Lower { memory, slots } | Advice::Raise { memory, slots } => {
Some(Resize { memory, slots })
}
Advice::TooFew { .. } | Advice::Keep { .. } | Advice::SliceTooSmall { .. } => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Resize {
pub memory: u64,
pub slots: u32,
}
impl SizingChange {
pub fn line(&self) -> String {
let memory = if self.from.memory == self.to.memory {
format!("memory {}", ByteSize(self.to.memory))
} else {
format!(
"memory {} → {}",
ByteSize(self.from.memory),
ByteSize(self.to.memory)
)
};
let slots = if self.from.slots == self.to.slots {
format!("slots {}", self.to.slots)
} else {
format!("slots {} → {}", self.from.slots, self.to.slots)
};
if self.uncertain {
format!("{memory}, {slots} (may not have been written)")
} else {
format!("{memory}, {slots}")
}
}
}
impl Sizing {
pub fn advice_line(&self, profile: &ProfileSizing) -> String {
let line = self.memory_line(profile);
if self.admission == AdmissionPolicy::Measured
&& profile.wants_slots
&& !matches!(profile.advice, Advice::TooFew { .. })
{
format!(
"{line}; jobs waited {}s for a slot (p95) and slots were busy {}%: measured admission reserves \
predicted peaks, not caps, so more slots may fit: raise slots by hand",
profile.wait.p95, profile.busy
)
} else {
line
}
}
pub(crate) fn memory_line(&self, profile: &ProfileSizing) -> String {
let size = |bytes: u64| ByteSize(bytes).to_string();
let peaks = &profile.peaks;
let need = format!("its largest peak + {}%", self.margin);
let at_cap = format!(
"{} of {} jobs ran out of memory at their cap",
peaks.at_cap, peaks.jobs
);
match profile.advice {
Advice::TooFew { jobs } => format!(
"{jobs} jobs, too few to advise on (it takes {}); largest peak {} of a {} cap",
self.min_jobs,
size(peaks.memory.max),
size(profile.memory)
),
Advice::Keep { .. } if peaks.at_cap > 0 => format!(
"keep {}: {at_cap}, so they may need more than they show",
size(profile.memory)
),
Advice::Keep { memory } => {
format!("keep {}: {need} is {}", size(profile.memory), size(memory))
}
Advice::Lower { memory, slots } => {
let share = profile.memory * u64::from(profile.slots);
if self.admission == AdmissionPolicy::Measured {
return format!(
"lower memory to {} from {}, {need}",
size(memory),
size(profile.memory)
);
}
let then = if slots > profile.slots {
format!("{slots} slots fit in the same {}", size(share))
} else {
format!(
"frees {} of the budget",
size(share - memory * u64::from(slots))
)
};
format!(
"lower memory to {} from {}, {need}; {then}",
size(memory),
size(profile.memory)
)
}
Advice::Raise { memory, slots } => {
let why = if peaks.at_cap > 0 {
at_cap
} else {
format!("{need} is above the {} cap", size(profile.memory))
};
let slots = if slots < profile.slots {
format!("; {slots} of its {} slots fit the slice", profile.slots)
} else {
String::new()
};
format!("raise memory to {}: {why}{slots}", size(memory))
}
Advice::SliceTooSmall { memory } => format!(
"needs {memory_needed} ({need}), more than the slice holds for one slot: raise the slice's \
MemoryMax first",
memory_needed = size(memory)
),
}
}
}
#[cfg(all(test, any(feature = "host", feature = "client")))]
mod tests;