Skip to main content

runnerd_core/
admission.rs

1//! Admission by measured peaks: a job's memory peak and average CPU predicted from the job history (`Predictor`),
2//! and the history replayed through admission by caps and by predictions, with or without a CPU budget
3//! (`simulate`), to show what each would have done with the same jobs; and what the host's memory leaves runners
4//! (`host`). Pure: no I/O, no clock.
5
6pub mod host;
7
8use std::collections::{BTreeMap, BTreeSet};
9
10use crate::api::Spread;
11use crate::api::{Basis, JobPeak};
12use crate::config::{self, Config};
13use crate::sizing::{GRANULE, Ran, spread};
14
15/// For this long after a run at its cap or killed for memory, in milliseconds, its job is admitted at its cap: 7
16/// days. After a run past what it reserved, its job reserves its largest peak for as long.
17pub const UNPREDICTABLE_FOR: u64 = 7 * 24 * 3600 * 1000;
18
19/// From this many successful runs of its own, fewer than `[sizing] min_jobs` runs, a job reserves its own largest
20/// peak while no other copy of it is queued or running; otherwise the larger of that and its profile's p95.
21pub const OWN_RUNS: u64 = 2;
22
23/// While the slice uses this percent of its `MemoryMax` or more, measured admission starts nothing.
24pub const HIGH_WATER: u64 = 90;
25
26/// CPU predictions round up to this many millicores: a tenth of a CPU.
27pub const CPU_GRANULE: u64 = 100;
28
29/// A job as admission tells jobs apart: its repository, workflow and name.
30#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
31pub struct Key {
32    /// `owner/name`.
33    pub repository: String,
34    /// The workflow's name, when known.
35    pub workflow: Option<String>,
36    /// The job's name in the workflow.
37    pub name: String,
38}
39
40impl Key {
41    /// A finished run's key.
42    pub fn of(run: &Ran) -> Key {
43        Key {
44            repository: run.repository.clone(),
45            workflow: run.workflow.clone(),
46            name: run.name.clone(),
47        }
48    }
49}
50
51/// Whether other copies of a job may run beside the one predicted: queued with it, or running. A few light runs
52/// say little about a burst of copies, after a cache was invalidated, so only one copy at a time goes by them.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum Copies {
55    /// No other copy is queued or running.
56    Alone,
57    /// Another copy is queued or running.
58    Several,
59}
60
61/// The memory admission reserves for a job, at most its cap, and what it goes by.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct Prediction {
64    /// Bytes.
65    pub memory: u64,
66    /// Why that much.
67    pub basis: Basis,
68}
69
70/// The CPU a runner reserves for a job, in millicores, at most its profile's `cpus`, and what it goes by.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct CpuPrediction {
73    /// Millicores.
74    pub cpu: u64,
75    /// Why that much: `Basis::Job`, `Profile`, `ProfileMax` or `Cap`.
76    pub basis: Basis,
77}
78
79/// The CPU averages of a set of runs, sorted, and how many of those runs succeeded.
80#[derive(Debug, Clone, Default)]
81struct Averages {
82    sorted: Vec<u64>,
83    succeeded: u64,
84}
85
86impl Averages {
87    fn add(&mut self, average: u64, succeeded: bool) {
88        let at = self.sorted.partition_point(|known| *known <= average);
89        self.sorted.insert(at, average);
90        self.succeeded += u64::from(succeeded);
91    }
92
93    fn runs(&self) -> u64 {
94        self.sorted.len() as u64
95    }
96
97    fn p95(&self) -> u64 {
98        spread(self.sorted.clone()).p95
99    }
100
101    fn max(&self) -> u64 {
102        self.sorted.last().copied().unwrap_or(0)
103    }
104}
105
106/// The peaks of a set of runs, sorted, until when a run at its cap makes them unpredictable, and until when a run
107/// past what it reserved makes the largest of them, or that run's peak if more, the prediction.
108#[derive(Debug, Clone, Default)]
109struct Peaks {
110    sorted: Vec<u64>,
111    /// How many of those runs succeeded.
112    succeeded: u64,
113    unpredictable_until: u64,
114    overran_until: u64,
115    /// The largest peak of a run past what it reserved, cancelled runs' included.
116    overran_peak: u64,
117}
118
119impl Peaks {
120    fn add(&mut self, peak: u64) {
121        let at = self.sorted.partition_point(|known| *known <= peak);
122        self.sorted.insert(at, peak);
123    }
124
125    fn runs(&self) -> u64 {
126        self.sorted.len() as u64
127    }
128
129    fn p95(&self) -> u64 {
130        spread(self.sorted.clone()).p95
131    }
132
133    fn max(&self) -> u64 {
134        self.sorted.last().copied().unwrap_or(0)
135    }
136}
137
138/// A job in one profile, as the predictor keeps its runs.
139#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
140struct InProfile {
141    key: Key,
142    profile: String,
143}
144
145impl InProfile {
146    fn new(key: &Key, profile: &str) -> InProfile {
147        InProfile {
148            key: key.clone(),
149            profile: profile.to_owned(),
150        }
151    }
152}
153
154/// Predicts a job's peak from the runs it learned.
155#[derive(Debug, Clone)]
156pub struct Predictor {
157    margin: u32,
158    min_jobs: u32,
159    /// By job and the profile it ran in: a job of one name may run in several pools of its repository, whose runs
160    /// say nothing of each other's (ADR 0058).
161    jobs: BTreeMap<InProfile, Peaks>,
162    profiles: BTreeMap<String, Peaks>,
163    /// The CPU averages of the runs that have one, apart from the peaks: a run whose peak says nothing of its
164    /// memory still says what CPU it used, and must not make its job known to memory predictions.
165    cpu_jobs: BTreeMap<InProfile, Averages>,
166    cpu_profiles: BTreeMap<String, Averages>,
167}
168
169impl Predictor {
170    /// A predictor that knows no run yet, with `[sizing]`'s margin and minimum runs.
171    pub fn new(rules: &config::Sizing) -> Predictor {
172        Predictor {
173            margin: rules.margin,
174            min_jobs: rules.min_jobs,
175            jobs: BTreeMap::new(),
176            profiles: BTreeMap::new(),
177            cpu_jobs: BTreeMap::new(),
178            cpu_profiles: BTreeMap::new(),
179        }
180    }
181
182    /// Learns a run of `profile` that finished at `at` (ms), having run under `cap`: its peak, and how it ended.
183    pub fn learn(&mut self, run: &Ran, profile: &str, cap: u64, at: u64) {
184        self.mark(run, profile, cap, at);
185        let peak = run.memory_peak;
186        if let Some(job) = self.jobs.get_mut(&InProfile::new(&Key::of(run), profile)) {
187            job.add(peak);
188            job.succeeded += u64::from(run.succeeded);
189        }
190        self.profiles
191            .entry(profile.to_owned())
192            .or_default()
193            .add(peak);
194        self.learn_cpu(run, profile);
195    }
196
197    /// Learns the CPU a finished run of `profile` used, without its peak: as `learn` does, and alone for a run whose
198    /// peak says nothing of its memory, as one sampled with page cache counted.
199    pub fn learn_cpu(&mut self, run: &Ran, profile: &str) {
200        let Some(average) = run.cpu_average else {
201            return;
202        };
203        self.cpu_profiles
204            .entry(profile.to_owned())
205            .or_default()
206            .add(average, run.succeeded);
207        self.cpu_jobs
208            .entry(InProfile::new(&Key::of(run), profile))
209            .or_default()
210            .add(average, run.succeeded);
211    }
212
213    /// The CPU to reserve for a run of `key` in `profile`, whose `cpus` are `cap` millicores: the job's average
214    /// CPU (its CPU time over its run time), by the same fallbacks as memory without the margin. CPU is shared, not
215    /// a kill limit, so neither a run at its cap nor copies of the job beside it change it.
216    pub fn predict_cpu(&self, key: &Key, profile: &str, cap: u64) -> CpuPrediction {
217        let job = self.cpu_jobs.get(&InProfile::new(key, profile));
218        let min_jobs = u64::from(self.min_jobs);
219        let peers = self
220            .cpu_profiles
221            .get(profile)
222            .filter(|peers| peers.runs() >= min_jobs);
223        let (cpu, basis) = match (job.filter(|job| job.runs() > 0), peers) {
224            (Some(job), _) if job.runs() >= min_jobs => (
225                job.p95(),
226                Basis::Job {
227                    runs: job.runs(),
228                    peak: JobPeak::P95,
229                },
230            ),
231            (Some(job), Some(_)) if job.succeeded >= OWN_RUNS => (
232                job.max(),
233                Basis::Job {
234                    runs: job.runs(),
235                    peak: JobPeak::Largest,
236                },
237            ),
238            (Some(job), Some(peers)) => (
239                job.max().max(peers.p95()),
240                Basis::Profile { runs: peers.runs() },
241            ),
242            (None, Some(peers)) => (peers.max(), Basis::ProfileMax { runs: peers.runs() }),
243            _ => (cap, Basis::Cap),
244        };
245        CpuPrediction {
246            cpu: cpu
247                .div_ceil(CPU_GRANULE)
248                .saturating_mul(CPU_GRANULE)
249                .clamp(CPU_GRANULE, cap.max(CPU_GRANULE)),
250            basis,
251        }
252    }
253
254    /// Learns only how a run of `profile` ended, not its peak, as for a cancelled one: a run that met its cap
255    /// (`Ran::met_cap`) makes its job in that profile unpredictable for `UNPREDICTABLE_FOR`, a peak past what it
256    /// reserved makes it reserve its largest peak for as long.
257    pub fn mark(&mut self, run: &Ran, profile: &str, cap: u64, at: u64) {
258        let peak = run.memory_peak;
259        let job = self
260            .jobs
261            .entry(InProfile::new(&Key::of(run), profile))
262            .or_default();
263        if run.met_cap(cap) {
264            job.unpredictable_until = job.unpredictable_until.max(at + UNPREDICTABLE_FOR);
265        }
266        if run.reserved.is_some_and(|reserved| peak > reserved) {
267            job.overran_until = job.overran_until.max(at + UNPREDICTABLE_FOR);
268            job.overran_peak = job.overran_peak.max(peak);
269        }
270    }
271
272    /// The memory to reserve for a run of `key` in `profile`, whose cap is `cap`, at `now` (ms), with `copies`
273    /// of it beside.
274    pub fn predict(
275        &self,
276        key: &Key,
277        profile: &str,
278        cap: u64,
279        now: u64,
280        copies: Copies,
281    ) -> Prediction {
282        let job = self.jobs.get(&InProfile::new(key, profile));
283        if let Some(until) = job
284            .map(|job| job.unpredictable_until)
285            .filter(|until| *until > now)
286        {
287            return Prediction {
288                memory: cap,
289                basis: Basis::Unpredictable {
290                    until: until / 1000,
291                },
292            };
293        }
294        let min_jobs = u64::from(self.min_jobs);
295        let (memory, basis) = match (job, self.profiles.get(profile)) {
296            // Seen only cancelled, so its peaks say nothing.
297            (Some(job), _) if job.runs() == 0 => (cap, Basis::Cap),
298            (Some(job), _) if job.overran_until > now => (
299                self.with_margin(job.max().max(job.overran_peak)),
300                Basis::Overran {
301                    until: job.overran_until / 1000,
302                },
303            ),
304            (Some(job), _) if job.runs() >= min_jobs => (
305                self.with_margin(job.p95()),
306                Basis::Job {
307                    runs: job.runs(),
308                    peak: JobPeak::P95,
309                },
310            ),
311            (Some(job), Some(peers))
312                if peers.runs() >= min_jobs
313                    && job.succeeded >= OWN_RUNS
314                    && copies == Copies::Alone =>
315            {
316                (
317                    self.with_margin(job.max()),
318                    Basis::Job {
319                        runs: job.runs(),
320                        peak: JobPeak::Largest,
321                    },
322                )
323            }
324            (Some(job), Some(peers)) if peers.runs() >= min_jobs => (
325                self.with_margin(job.max().max(peers.p95())),
326                Basis::Profile { runs: peers.runs() },
327            ),
328            // A job never seen may be the profile's heaviest.
329            (None, Some(peers)) if peers.runs() >= min_jobs => (
330                self.with_margin(peers.max()),
331                Basis::ProfileMax { runs: peers.runs() },
332            ),
333            _ => (cap, Basis::Cap),
334        };
335        Prediction {
336            memory: memory.min(cap),
337            basis,
338        }
339    }
340
341    /// `peak` plus the margin, rounded up to 256M.
342    fn with_margin(&self, peak: u64) -> u64 {
343        let memory = peak.saturating_add(peak.saturating_mul(u64::from(self.margin)) / 100);
344        memory
345            .div_ceil(GRANULE)
346            .saturating_mul(GRANULE)
347            .max(GRANULE)
348    }
349}
350
351/// How a replay admits a queued job.
352#[derive(Debug, Clone, Copy, PartialEq, Eq)]
353pub enum Policy {
354    /// When a slot is free and the caps of the running jobs and its own fit the budget: what runnerd does now.
355    Caps,
356    /// As the daemon admits: when a slot is free, the running jobs (each at the larger of its prediction and its
357    /// peak) and the largest prediction among the repository's waiting jobs fit the budget, and the running jobs'
358    /// peaks are under `HIGH_WATER` percent of it. A started job then reserves its own prediction.
359    Measured,
360}
361
362/// What a replay did with the history's jobs.
363#[derive(Debug, Clone, PartialEq, Eq)]
364pub struct Replay {
365    /// The jobs replayed: finished ones of a configured profile.
366    pub jobs: u64,
367    /// Of those, the ones the policy never started: more than the budget holds.
368    pub never_started: u64,
369    /// Seconds from queued to started.
370    pub wait: Spread,
371    /// The mean of those, in seconds.
372    pub mean_wait: u64,
373    /// The most jobs that ran at once.
374    pub most_running: u64,
375    /// The most memory, in bytes, the running jobs' peaks and the mirror add up to at one moment: as if every
376    /// running job peaked at once, which is more than the slice ever holds. Not a bound on the daemon, which can
377    /// overlap more: it sees what jobs use, less than their peaks while they warm up, and the replay leaves out
378    /// start latency, seeds, shells and cancelled jobs.
379    pub worst_overlap: u64,
380    /// Seconds from the first job queued to the last one finished.
381    pub makespan: u64,
382    /// Jobs whose peak was above what admission reserved for them when they ended.
383    pub under_predicted: u64,
384    /// How many times the high-water mark held a job back that the predictions had room for.
385    pub held_back: u64,
386    /// How many times the CPU budget held a job back that memory and its slots had room for.
387    pub cpu_held: u64,
388    /// The most CPU, in millicores, the running jobs' averages add up to at one moment.
389    pub worst_cpu: u64,
390}
391
392/// A job the replay started.
393struct Running<'run> {
394    run: &'run Ran,
395    profile: &'run str,
396    ends: u64,
397    reserved: u64,
398    peak: u64,
399    /// The CPU it reserves, in millicores.
400    cpu: u64,
401}
402
403/// Replays `jobs` through `policy`: each arrives when it was queued and runs as long as it ran, in a profile with
404/// `slots` (by name; the config's where absent) under the slice's `memory_max`. Jobs start as soon as the policy
405/// admits them, oldest first, a job that does not fit letting a later one past, as `scheduler::plan` does.
406/// Measured admission predicts from the runs finished before it decides, as the daemon would, and holds each
407/// running job at its peak for its whole run, which the daemon does not see. A job with copies waiting or running
408/// beside it is predicted as one with `Copies::Several`. Each moment, the running jobs are predicted again, the
409/// first running copy of a job going by its own runs, as the daemon does each tick. Unlike the daemon, the replay
410/// knows a job from its start: it never sees a runner whose job is not known yet, and the order jobs started
411/// stands for the daemon's slot order.
412///
413/// With a `cpu_budget` in millicores, a job also starts only when the CPU the running jobs reserve and its own fit
414/// it: by caps, each its profile's `cpus`; measured, each its predicted average (`Predictor::predict_cpu`), at most
415/// the budget. The replay holds each job's run time whatever the CPU it shares: it shows what admission holds back,
416/// not how much a busy CPU slows the jobs.
417pub fn simulate(
418    config: &Config,
419    memory_max: Option<u64>,
420    cpu_budget: Option<u64>,
421    jobs: &[Ran],
422    policy: Policy,
423    slots: &BTreeMap<String, u32>,
424) -> Replay {
425    let mirror = config.mirror_memory();
426    let budget = memory_max.map(|max| max.saturating_sub(mirror));
427    let mut queue: Vec<&Ran> = jobs
428        .iter()
429        .filter(|run| config.profiles.contains_key(&run.profile))
430        .filter(|run| matches!((run.started_at, run.finished_at), (Some(start), Some(end)) if end >= start))
431        .collect();
432    queue.sort_by_key(|run| run.queued_at);
433    let mut replayer = Replayer {
434        config,
435        budget,
436        cpu_budget,
437        policy,
438        slots,
439        predictor: Predictor::new(&config.sizing),
440        running: Vec::new(),
441        waits: Vec::new(),
442        most_running: 0,
443        worst_overlap: 0,
444        last_end: 0,
445        under_predicted: 0,
446        held_back: 0,
447        cpu_held: 0,
448        worst_cpu: 0,
449    };
450    let mut waiting: Vec<&Ran> = Vec::new();
451    let mut next = 0;
452    loop {
453        let arrival = queue.get(next).map(|run| run.queued_at);
454        let end = replayer.running.iter().map(|running| running.ends).min();
455        let Some(now) = [arrival, end].into_iter().flatten().min() else {
456            break;
457        };
458        replayer.finish(now);
459        replayer.rereserve(now);
460        while let Some(run) = queue.get(next).filter(|run| run.queued_at <= now) {
461            waiting.push(run);
462            next += 1;
463        }
464        let mut index = 0;
465        while let Some(run) = waiting.get(index).copied() {
466            if replayer.admit(run, &waiting, now) {
467                waiting.remove(index);
468            } else {
469                index += 1;
470            }
471        }
472        replayer.most_running = replayer.most_running.max(replayer.running.len() as u64);
473        let peaks: u64 = replayer.running.iter().map(|running| running.peak).sum();
474        replayer.worst_overlap = replayer.worst_overlap.max(peaks + mirror);
475        let cpu: u64 = replayer
476            .running
477            .iter()
478            .map(|running| running.run.cpu_average.unwrap_or(0))
479            .sum();
480        replayer.worst_cpu = replayer.worst_cpu.max(cpu);
481    }
482    let waits = replayer.waits;
483    let first = queue.first().map_or(0, |run| run.queued_at);
484    Replay {
485        jobs: queue.len() as u64,
486        never_started: waiting.len() as u64,
487        mean_wait: waits.iter().sum::<u64>() / (waits.len() as u64).max(1),
488        wait: spread(waits),
489        most_running: replayer.most_running,
490        worst_overlap: replayer.worst_overlap,
491        makespan: replayer.last_end.saturating_sub(first) / 1000,
492        under_predicted: replayer.under_predicted,
493        held_back: replayer.held_back,
494        cpu_held: replayer.cpu_held,
495        worst_cpu: replayer.worst_cpu,
496    }
497}
498
499/// A replay's state between two moments.
500struct Replayer<'run> {
501    config: &'run Config,
502    budget: Option<u64>,
503    cpu_budget: Option<u64>,
504    policy: Policy,
505    slots: &'run BTreeMap<String, u32>,
506    predictor: Predictor,
507    running: Vec<Running<'run>>,
508    waits: Vec<u64>,
509    most_running: u64,
510    worst_overlap: u64,
511    last_end: u64,
512    under_predicted: u64,
513    held_back: u64,
514    cpu_held: u64,
515    worst_cpu: u64,
516}
517
518impl<'run> Replayer<'run> {
519    /// Ends the jobs that end by `now`; measured admission learns their peaks.
520    fn finish(&mut self, now: u64) {
521        let (ended, running): (Vec<_>, Vec<_>) = std::mem::take(&mut self.running)
522            .into_iter()
523            .partition(|running| running.ends <= now);
524        self.running = running;
525        for ended in ended {
526            let cap = self.config.profiles[ended.profile].memory.0;
527            self.under_predicted += u64::from(ended.peak > ended.reserved);
528            let reserved = (self.policy == Policy::Measured).then_some(ended.reserved);
529            let run = Ran {
530                reserved,
531                ..ended.run.clone()
532            };
533            self.predictor.learn(&run, ended.profile, cap, ended.ends);
534            self.last_end = self.last_end.max(ended.ends);
535        }
536    }
537
538    /// Under measured admission, predicts each running job again, as the daemon's runners follow their job's
539    /// prediction every tick: the first running copy of a job, in the order they started, goes by its own runs, and
540    /// the others have it beside them.
541    fn rereserve(&mut self, now: u64) {
542        if self.policy != Policy::Measured {
543            return;
544        }
545        let cpus: Vec<u64> = self
546            .running
547            .iter()
548            .map(|running| self.cpu(&Key::of(running.run), running.profile))
549            .collect();
550        let mut seen = BTreeSet::new();
551        for (running, cpu) in self.running.iter_mut().zip(cpus) {
552            let key = Key::of(running.run);
553            let copies = if seen.insert(key.clone()) {
554                Copies::Alone
555            } else {
556                Copies::Several
557            };
558            let limits = &self.config.profiles[running.profile];
559            running.reserved = self
560                .predictor
561                .predict(&key, running.profile, limits.memory.0, now, copies)
562                .memory;
563            running.cpu = cpu;
564        }
565    }
566
567    /// The CPU a run of `key` in `profile` reserves, in millicores: its profile's `cpus` by caps, its predicted
568    /// average measured, at most the CPU budget.
569    fn cpu(&self, key: &Key, profile: &str) -> u64 {
570        let cap = u64::from(self.config.profiles[profile].cpus).saturating_mul(1000);
571        let cpu = match self.policy {
572            Policy::Caps => cap,
573            Policy::Measured => self.predictor.predict_cpu(key, profile, cap).cpu,
574        };
575        self.cpu_budget.map_or(cpu, |budget| cpu.min(budget))
576    }
577
578    /// What a running job takes of the budget: under measured admission, as the daemon counts it, the larger of
579    /// what it reserved and what it uses, here its peak for the whole run.
580    fn taken(&self) -> u64 {
581        self.running
582            .iter()
583            .map(|running| match self.policy {
584                Policy::Caps => running.reserved,
585                Policy::Measured => running.reserved.max(running.peak),
586            })
587            .sum()
588    }
589
590    /// Starts `run` at `now` if the policy admits it, `waiting` being every job not started yet; whether it did.
591    /// As in the daemon, the runner starts reserving the largest prediction among the waiting jobs of its
592    /// repository's pool (those that ran in its profile), then its job's own.
593    fn admit(&mut self, run: &'run Ran, waiting: &[&Ran], now: u64) -> bool {
594        let Some((profile, limits)) = self.config.profiles.get_key_value(&run.profile) else {
595            return false;
596        };
597        let slots = self.slots.get(profile).copied().unwrap_or(limits.slots);
598        let in_profile = self
599            .running
600            .iter()
601            .filter(|running| running.profile == profile.as_str())
602            .count();
603        if in_profile >= slots as usize {
604            return false;
605        }
606        let cap = limits.memory.0;
607        let running = |key: &Key| {
608            self.running
609                .iter()
610                .any(|running| Key::of(running.run) == *key)
611        };
612        // A waiting job with a copy waiting or running beside it, as the daemon sees its queue.
613        let queued = |job: &Ran| {
614            let key = Key::of(job);
615            let waiting = waiting.iter().filter(|other| Key::of(other) == key).count();
616            let copies = if waiting > 1 || running(&key) {
617                Copies::Several
618            } else {
619                Copies::Alone
620            };
621            self.predictor
622                .predict(&key, profile, cap, now, copies)
623                .memory
624        };
625        // The started job, once known: the first of its copies to run goes by its own runs.
626        let started = || {
627            let key = Key::of(run);
628            let copies = if running(&key) {
629                Copies::Several
630            } else {
631                Copies::Alone
632            };
633            self.predictor
634                .predict(&key, profile, cap, now, copies)
635                .memory
636        };
637        let (starting, reserved) = match self.policy {
638            Policy::Caps => (cap, cap),
639            Policy::Measured => (
640                waiting
641                    .iter()
642                    .filter(|job| job.repository == run.repository && job.profile == run.profile)
643                    .map(|job| queued(job))
644                    .max()
645                    .unwrap_or(0)
646                    .max(started()),
647                started(),
648            ),
649        };
650        if let Some(budget) = self.budget {
651            if self.taken() + starting > budget {
652                return false;
653            }
654            let peaks: u64 = self.running.iter().map(|running| running.peak).sum();
655            if self.policy == Policy::Measured
656                && peaks.saturating_mul(100) >= budget.saturating_mul(HIGH_WATER)
657            {
658                self.held_back += 1;
659                return false;
660            }
661        }
662        let cpu = self.cpu(&Key::of(run), profile);
663        if let Some(cpu_budget) = self.cpu_budget {
664            // As the daemon starts a runner: at the most CPU any waiting job of its repository's pool reserves.
665            let starting = waiting
666                .iter()
667                .filter(|job| job.repository == run.repository && job.profile == run.profile)
668                .map(|job| self.cpu(&Key::of(job), profile))
669                .max()
670                .unwrap_or(0)
671                .max(cpu);
672            let taken: u64 = self.running.iter().map(|running| running.cpu).sum();
673            if taken + starting > cpu_budget {
674                self.cpu_held += 1;
675                return false;
676            }
677        }
678        // A job over its cap is killed at it.
679        let peak = run.memory_peak.min(cap);
680        let ran_for = run
681            .finished_at
682            .unwrap_or_default()
683            .saturating_sub(run.started_at.unwrap_or_default());
684        self.waits.push(now.saturating_sub(run.queued_at) / 1000);
685        self.running.push(Running {
686            run,
687            profile,
688            ends: now + ran_for,
689            reserved,
690            peak,
691            cpu,
692        });
693        true
694    }
695}
696
697#[cfg(test)]
698mod tests;