1use std::fmt::Write as _;
6
7use serde::{Deserialize, Serialize};
8
9use crate::config::ByteSize;
10
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "kebab-case")]
14pub enum AdmissionPolicy {
15 #[default]
17 Caps,
18 Measured,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "kebab-case", tag = "kind")]
25pub enum Basis {
26 Job {
30 runs: u64,
32 #[serde(default, skip_serializing_if = "JobPeak::is_p95")]
35 peak: JobPeak,
36 },
37 Profile {
40 runs: u64,
42 },
43 ProfileMax {
46 runs: u64,
48 },
49 Overran {
51 until: u64,
53 },
54 Cap,
56 Unpredictable {
58 until: u64,
60 },
61 Unmeasured,
63}
64
65#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(rename_all = "kebab-case")]
68pub enum JobPeak {
69 #[default]
71 P95,
72 Largest,
74}
75
76impl JobPeak {
77 #[expect(
78 clippy::trivially_copy_pass_by_ref,
79 reason = "serde's skip_serializing_if passes a reference"
80 )]
81 fn is_p95(&self) -> bool {
82 *self == JobPeak::P95
83 }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
88pub struct Admission {
89 pub policy: AdmissionPolicy,
91 pub reserved: u64,
94 pub basis: Basis,
96 pub free: Option<u64>,
99 pub slice: Option<u64>,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub cpu: Option<CpuAdmission>,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
109pub struct CpuAdmission {
110 pub reserved: u64,
112 pub basis: Basis,
114 pub free: u64,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub waited: Option<u64>,
120}
121
122impl Admission {
123 pub fn line(&self) -> String {
127 let size = |bytes: u64| ByteSize(bytes).to_string();
128 let reserved = size(self.reserved);
129 let why = match self.basis {
130 Basis::Job {
131 runs,
132 peak: JobPeak::P95,
133 } => {
134 format!("reserved {reserved}, the p95 peak of {runs} runs of its job + the margin")
135 }
136 Basis::Job {
137 runs,
138 peak: JobPeak::Largest,
139 } => {
140 format!(
141 "reserved {reserved}, the largest peak of {runs} runs of its job + the margin"
142 )
143 }
144 Basis::Profile { runs } => format!(
145 "reserved {reserved}, from {runs} runs of its profile: its job had too few of its own"
146 ),
147 Basis::ProfileMax { runs } => format!(
148 "reserved {reserved}, the largest peak of {runs} runs of its profile + the margin: its job had no \
149 runs of its own"
150 ),
151 Basis::Overran { .. } => format!(
152 "reserved {reserved}, its job's largest peak + the margin: a run of it went past what it reserved \
153 lately"
154 ),
155 Basis::Cap if self.policy == AdmissionPolicy::Caps => {
156 format!("reserved its {reserved} cap")
157 }
158 Basis::Cap => format!("reserved its {reserved} cap: too few runs to predict from"),
159 Basis::Unpredictable { .. } => format!(
160 "reserved its {reserved} cap: a run of its job ran out of memory at its cap or was killed for memory lately"
161 ),
162 Basis::Unmeasured => {
163 format!("reserved its {reserved} cap: no recent sample of the slice's memory")
164 }
165 };
166 let policy = match self.policy {
167 AdmissionPolicy::Caps => "caps",
168 AdmissionPolicy::Measured => "measured peaks",
169 };
170 let mut line = format!("admitted by {policy}: {why}");
171 if let Some(free) = self.free {
172 let _ = write!(line, "; {} of the budget was free", size(free));
173 }
174 if let Some(slice) = self.slice {
175 let _ = write!(line, ", the slice at {}", size(slice));
176 }
177 if let Some(cpu) = self.cpu {
178 let _ = write!(line, ". {}", cpu.line());
179 }
180 line
181 }
182}
183
184impl CpuAdmission {
185 pub fn line(&self) -> String {
188 let reserved = cpus(self.reserved);
189 let why = match self.basis {
190 Basis::Job {
191 runs,
192 peak: JobPeak::P95,
193 } => format!("the p95 average of {runs} runs of its job"),
194 Basis::Job {
195 runs,
196 peak: JobPeak::Largest,
197 } => format!("the largest average of {runs} runs of its job"),
198 Basis::Profile { runs } => {
199 format!("from {runs} runs of its profile: its job had too few of its own")
200 }
201 Basis::ProfileMax { runs } => format!(
202 "the largest average of {runs} runs of its profile: its job had no runs of its own"
203 ),
204 Basis::Cap
205 | Basis::Overran { .. }
206 | Basis::Unpredictable { .. }
207 | Basis::Unmeasured => "its profile's cpus".to_owned(),
208 };
209 let mut line = format!(
210 "Reserved {reserved}, {why}; {} of the budget were free",
211 cpus(self.free)
212 );
213 if let Some(waited) = self.waited {
214 let _ = write!(line, "; it waited {} for CPU", super::duration(waited));
215 }
216 line
217 }
218}
219
220pub fn cores(millicores: u64) -> String {
222 tenths(millicores.div_ceil(100))
223}
224
225pub(super) fn free_cores(millicores: u64) -> String {
227 tenths(millicores / 100)
228}
229
230fn tenths(tenths: u64) -> String {
231 if tenths.is_multiple_of(10) {
232 (tenths / 10).to_string()
233 } else {
234 format!("{}.{}", tenths / 10, tenths % 10)
235 }
236}
237
238pub fn cpus(millicores: u64) -> String {
240 match cores(millicores).as_str() {
241 "1" => "1 CPU".to_owned(),
242 number => format!("{number} CPUs"),
243 }
244}
245
246#[cfg(all(test, any(feature = "host", feature = "client")))]
248mod tests;