1use std::collections::BTreeSet;
7
8use crate::admission::Key;
9use crate::admission::recent::{self, Counted, Ending, JobRuns, RECENT_RUNS, Recent, SIZING_TIMES};
10use crate::api::{self, Peaks, Resize, RevertCause, SizingRevert, job_title};
11use crate::config::{Admission, Config};
12
13use super::{Capped, History, MemorySet, Ran, spread};
14
15pub const HISTORY: u64 = 30 * 24 * 3600 * 1000;
17
18pub const CHANGES_READ: u64 = 2 * HISTORY;
21
22pub const FAILED_RUNS: usize = 3;
24
25pub(super) fn windows(config: &Config, history: &History, now: u64) -> Recent {
27 let size = SIZING_TIMES.saturating_mul(RECENT_RUNS.max(u64::from(config.sizing.min_jobs)));
28 let mut recent = Recent::new(size);
29 let start = now.saturating_sub(HISTORY);
30 for (run, ending) in recent::rows(history) {
31 if run.queued_at < start {
32 continue;
33 }
34 recent.read(
35 run,
36 &run.profile,
37 ending,
38 cap_of(config, run),
39 recent::finished(run),
40 );
41 }
42 recent
43}
44
45fn cap_of(config: &Config, run: &Ran) -> u64 {
47 run.memory_cap
48 .or_else(|| config.profiles.get(&run.profile).map(|p| p.memory.0))
49 .unwrap_or(u64::MAX)
50}
51
52pub(super) fn peaks<'run>(runs: impl Iterator<Item = &'run Counted>, on_cap: u64) -> Peaks {
54 let runs: Vec<&Counted> = runs.collect();
55 Peaks {
56 jobs: runs.len() as u64,
57 at_cap: runs.iter().filter(|run| run.capped.met()).count() as u64,
58 on_cap,
59 memory: spread(runs.iter().map(|run| run.peak).collect()),
60 cpu: spread(runs.iter().map(|run| run.cpu_peak).collect()),
61 }
62}
63
64fn run_need(run: &Counted, margin: u32) -> u64 {
67 if run.capped.met() {
68 api::need(run.peak.max(run.cap), margin)
69 } else {
70 api::need(run.peak, margin).min(run.cap)
71 }
72}
73
74pub(super) fn floor(runs: &JobRuns, margin: u32) -> u64 {
76 runs.window()
77 .map(|run| run_need(run, margin))
78 .max()
79 .unwrap_or(0)
80}
81
82fn larger_caps(runs: &JobRuns, margin: u32, above: u64) -> u64 {
86 runs.window()
87 .filter(|run| run.cap > above && run.capped != Capped::Assumed)
88 .map(|run| run_need(run, margin).min(run.cap))
89 .max()
90 .unwrap_or(0)
91}
92
93pub(super) fn on_cap(runs: &JobRuns, now: u64) -> bool {
95 runs.held(now, None).is_some()
96}
97
98pub(super) fn took(set: &[MemorySet], profile: &str, memory: u64, now: u64) -> u64 {
101 let mut since = None;
102 let mut seen = false;
103 for event in set.iter().filter(|event| event.profile == profile) {
104 seen = true;
105 if event.memory != memory {
106 since = None;
107 } else if since.is_none() {
108 since = Some(event.at);
109 }
110 }
111 since.unwrap_or(if seen { now } else { 0 })
112}
113
114pub(super) fn raise_need(
119 history: &History,
120 recent: &Recent,
121 profile: &str,
122 cap: u64,
123 took: u64,
124 margin: u32,
125 key: Option<&Key>,
126) -> u64 {
127 history
128 .jobs
129 .iter()
130 .chain(&history.cancelled)
131 .filter(|run| {
132 run.profile == profile
133 && run.memory_cap == Some(cap)
134 && recent::finished(run) >= took
135 && run.capped(cap) == Capped::Met
136 && key.is_none_or(|key| Key::of(run) == *key)
137 })
138 .map(|run| {
139 let larger = recent
140 .job(&Key::of(run), profile)
141 .map_or(0, |runs| larger_caps(runs, margin, cap));
142 api::need(run.memory_peak.max(cap), margin).max(larger)
143 })
144 .max()
145 .unwrap_or(0)
146}
147
148fn too_far<'change>(
151 history: &'change History,
152 profile: &str,
153) -> impl Iterator<Item = (&'change api::SizingChange, SizingRevert)> {
154 history
155 .changes
156 .iter()
157 .filter(move |change| change.profile == profile && change.to.memory < change.from.memory)
158 .filter_map(move |change| {
159 cause(history, profile, change).map(|(job, cause)| {
160 (
161 change,
162 SizingRevert {
163 from: change.to.memory,
164 to: change.from.memory,
165 job,
166 cause,
167 },
168 )
169 })
170 })
171}
172
173fn cause(
175 history: &History,
176 profile: &str,
177 lowering: &api::SizingChange,
178) -> Option<(String, RevertCause)> {
179 let (lowered, from) = (lowering.to.memory, lowering.from.memory);
180 let since = lowering.at.saturating_mul(1000);
181 let after: Vec<(&Ran, Ending)> = recent::rows(history)
182 .into_iter()
183 .filter(|(run, _)| {
184 run.profile == profile
185 && run.memory_cap == Some(lowered)
186 && recent::finished(run) >= since
187 })
188 .collect();
189 let named = |run: &Ran| {
190 format!(
191 "{} {}",
192 run.repository,
193 job_title(run.workflow.as_deref(), &run.name, None)
194 )
195 };
196 if let Some((run, _)) = after
197 .iter()
198 .find(|(run, _)| run.capped(lowered) == Capped::Met)
199 {
200 return Some((named(run), RevertCause::Met));
201 }
202 let jobs: BTreeSet<Key> = after.iter().map(|(run, _)| Key::of(run)).collect();
203 for key in jobs {
204 let succeeded_at_from = history.jobs.iter().any(|run| {
207 run.profile == profile
208 && Key::of(run) == key
209 && run.succeeded
210 && run.memory_cap == Some(from)
211 });
212 if !succeeded_at_from {
213 continue;
214 }
215 let mut row: Vec<(u64, Option<&str>)> = Vec::new();
216 for (run, ending) in after.iter().filter(|(run, _)| Key::of(run) == key) {
217 if *ending == Ending::Cancelled {
218 continue;
219 }
220 if run.succeeded {
221 row.clear();
222 } else if run.failed && !row.iter().any(|(id, _)| *id == run.run_id) {
223 row.push((run.run_id, run.branch.as_deref()));
224 }
225 let branches: BTreeSet<Option<&str>> = row.iter().map(|(_, branch)| *branch).collect();
226 if row.len() >= FAILED_RUNS && branches.len() >= 2 {
227 return Some((
228 named(run),
229 RevertCause::Failed {
230 runs: u32::try_from(row.len()).unwrap_or(u32::MAX),
231 },
232 ));
233 }
234 }
235 }
236 None
237}
238
239pub(super) fn revert(
244 config: &Config,
245 history: &History,
246 profile: &str,
247 current: Resize,
248) -> Option<(SizingRevert, Resize)> {
249 let last = history
250 .changes
251 .iter()
252 .rfind(|change| change.profile == profile && change.to.memory != change.from.memory)?;
253 if last.to.memory != current.memory || last.to.memory >= last.from.memory {
254 return None;
255 }
256 let since = last.at.saturating_mul(1000);
257 let mut events = history
258 .memory_set
259 .iter()
260 .filter(|event| event.profile == profile && event.at >= since)
261 .peekable();
262 events.peek()?;
263 if events.any(|event| event.memory != current.memory) {
264 return None;
265 }
266 let (_, revert) = too_far(history, profile).find(|(change, _)| *change == last)?;
267 let slots = match config.host.admission {
268 Admission::Caps => last.from.slots,
269 Admission::Measured => current.slots,
270 };
271 Some((
272 revert,
273 Resize {
274 memory: last.from.memory,
275 slots,
276 cpus: None,
277 },
278 ))
279}
280
281pub(super) fn revert_floor(history: &History, profile: &str) -> u64 {
284 too_far(history, profile)
285 .map(|(_, revert)| revert.to)
286 .max()
287 .unwrap_or(0)
288}