use super::{
Advice, AutoPlan, JobSizing, PAGE_CACHE_ROOM, Peaks, ProfileSizing, Sizing, SizingChange,
SizingHold, Starts,
};
use crate::api::{Check, CheckStatus, Tone, Toned};
use crate::config::ByteSize;
pub const GRANULE: u64 = 256 << 20;
pub const SAMPLE_INTERVAL: u64 = 2;
pub fn need(peak: u64, margin: u32) -> u64 {
let room = (peak.saturating_mul(u64::from(margin)) / 100).max(PAGE_CACHE_ROOM);
peak.saturating_add(room)
.div_ceil(GRANULE)
.saturating_mul(GRANULE)
.max(GRANULE)
}
pub fn sampled_note(interval: u64) -> String {
format!("memory peaks are sampled every {interval} s; spikes shorter than that do not show")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Need {
pub memory: u64,
pub reason: String,
}
impl Sizing {
pub fn need(&self, peaks: &Peaks, advice: Advice) -> Need {
let memory = match advice {
Advice::Keep { memory }
| Advice::Lower { memory, .. }
| Advice::Raise { memory, .. }
| Advice::SliceTooSmall { memory } => memory,
Advice::TooFew { .. } => need(peaks.memory.max, self.margin),
};
Need {
memory,
reason: self.need_reason(peaks),
}
}
pub(crate) fn need_reason(&self, peaks: &Peaks) -> String {
if peaks.memory.max * u64::from(self.margin) / 100 >= PAGE_CACHE_ROOM {
format!("its largest peak + {}%", self.margin)
} else {
format!(
"its largest peak + {} for page cache",
ByteSize(PAGE_CACHE_ROOM)
)
}
}
pub fn method_line(&self) -> String {
format!(
"peaks are sampled every {} s, so a shorter spike goes unseen: advice sizes for the largest + {}%, at \
least {} for page cache, rounded up to {}, once a profile ran {} jobs",
self.sample_interval,
self.margin,
ByteSize(PAGE_CACHE_ROOM),
ByteSize(GRANULE),
self.min_jobs
)
}
pub fn method_parts(&self) -> Vec<String> {
vec![
format!("peaks sampled every {} s", self.sample_interval),
format!(
"+{}%, at least {}, in {} steps",
self.margin,
ByteSize(PAGE_CACHE_ROOM),
ByteSize(GRANULE)
),
format!("advice after {} jobs", self.min_jobs),
]
}
pub fn covered(&self) -> u64 {
self.until.saturating_sub(self.since)
}
pub fn doctor_check(&self) -> Check {
let changes: Vec<_> = self
.profiles
.iter()
.filter(|profile| {
profile.advice.change().is_some()
|| matches!(profile.advice, Advice::SliceTooSmall { .. })
})
.collect();
let raise = changes
.iter()
.any(|profile| profile.advice.tone() == Tone::Bad);
let detail = if changes.is_empty() {
let too_few = self
.profiles
.iter()
.any(|profile| matches!(profile.advice, Advice::TooFew { .. }));
if too_few {
format!(
"some profiles ran fewer than {} jobs; runnerctl sizing shows their peaks",
self.min_jobs
)
} else {
format!(
"every profile's memory fits its jobs' largest peak + {}%, at least {} for page cache",
self.margin,
ByteSize(PAGE_CACHE_ROOM)
)
}
} else {
changes
.iter()
.map(|profile| format!("{}: {}", profile.name, self.advice_line(profile)))
.collect::<Vec<_>>()
.join("; ")
};
Check {
name: "sizing".to_owned(),
status: if raise {
CheckStatus::Warn
} else {
CheckStatus::Info
},
detail,
fix: (!changes.is_empty()).then(|| {
changes
.iter()
.map(|profile| match profile.advice {
Advice::SliceTooSmall { .. } => format!(
"raise MemoryMax in the slice's unit file, sudo systemctl daemon-reload, then: \
runnerctl sizing apply {}",
profile.name
),
_ => format!("runnerctl sizing apply {}", profile.name),
})
.collect::<Vec<_>>()
.join("\n")
}),
}
}
}
impl Advice {
pub fn tone(&self) -> Tone {
match self {
Advice::Raise { .. } | Advice::SliceTooSmall { .. } => Tone::Bad,
Advice::Lower { .. } => Tone::Warn,
Advice::Keep { .. } => Tone::Good,
Advice::TooFew { .. } => Tone::Quiet,
}
}
pub fn word(&self) -> String {
match *self {
Advice::TooFew { .. } => "too few".to_owned(),
Advice::Keep { .. } => "keep".to_owned(),
Advice::SliceTooSmall { .. } => "grow slice".to_owned(),
Advice::Lower { memory, slots } | Advice::Raise { memory, slots } => {
format!("{} × {slots}", ByteSize(memory))
}
}
}
}
impl JobSizing {
pub fn needs(&self) -> Toned {
let (text, tone) = match self.advice {
Advice::TooFew { .. } => (
format!("max {}", ByteSize(self.peaks.memory.max)),
Tone::Quiet,
),
Advice::Raise { memory, .. } | Advice::SliceTooSmall { memory } => {
(format!("{} > cap", ByteSize(memory)), Tone::Bad)
}
Advice::Keep { memory } | Advice::Lower { memory, .. } => {
(ByteSize(memory).to_string(), Tone::Neutral)
}
};
Toned { text, tone }
}
}
impl Starts {
pub fn total(&self) -> u32 {
self.warm + self.cold
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProfilePlan<'plan> {
Next {
change: &'plan SizingChange,
blocked: bool,
},
Held {
hold: &'plan SizingHold,
},
LowerHeld {
hold: &'plan SizingHold,
},
Nothing,
}
impl AutoPlan {
pub fn profile_plan(&self, profile: &str, now: u64) -> ProfilePlan<'_> {
if let Some(change) = self.next.iter().find(|change| change.profile == profile) {
return ProfilePlan::Next {
change,
blocked: self.blocked.is_some(),
};
}
match self.held.iter().find(|hold| hold.profile == profile) {
Some(hold) if now < hold.until => ProfilePlan::Held { hold },
Some(hold) if now < hold.lowered_after() => ProfilePlan::LowerHeld { hold },
_ => ProfilePlan::Nothing,
}
}
}
impl ProfilePlan<'_> {
pub fn until(&self) -> Option<u64> {
match self {
ProfilePlan::Held { hold } => Some(hold.until),
ProfilePlan::LowerHeld { hold } => Some(hold.lowered_after()),
ProfilePlan::Next { .. } | ProfilePlan::Nothing => None,
}
}
pub fn headline(&self, plan: &AutoPlan) -> String {
match self {
ProfilePlan::Next {
change,
blocked: true,
} => format!("Once that is fixed: {}", change.line()),
ProfilePlan::Next { change, .. } => format!(
"Next look, within {}: {}",
super::period(plan.every),
change.line()
),
ProfilePlan::Held { .. } => "No change".to_owned(),
ProfilePlan::LowerHeld { .. } => "No lowering".to_owned(),
ProfilePlan::Nothing => "Nothing to change".to_owned(),
}
}
pub fn reason(&self, sizing: &Sizing, profile: &ProfileSizing) -> String {
match self {
ProfilePlan::Next { change, .. } => change.reason.clone(),
ProfilePlan::Held { .. } => "the interval holds it until then".to_owned(),
ProfilePlan::LowerHeld { .. } => "a raise may come before then".to_owned(),
ProfilePlan::Nothing => sizing.advice_line(profile),
}
}
pub fn changed(&self) -> Option<u64> {
match self {
ProfilePlan::Held { hold } | ProfilePlan::LowerHeld { hold } => Some(hold.changed),
ProfilePlan::Next { .. } | ProfilePlan::Nothing => None,
}
}
}
#[cfg(test)]
mod tests;