1use std::sync::OnceLock;
4
5pub const MAX_BENCHMARK_COUNT: u32 = 1_000_000;
7
8#[must_use]
10pub fn saturating_u128_to_u64(value: u128) -> u64 {
11 value.min(u128::from(u64::MAX)) as u64
12}
13
14#[must_use]
16pub fn saturating_usize_to_u32(value: usize) -> u32 {
17 u32::try_from(value).unwrap_or(u32::MAX)
18}
19
20#[must_use]
22pub fn saturating_sum_u64(values: impl IntoIterator<Item = u64>) -> u64 {
23 saturating_u128_to_u64(values.into_iter().fold(0_u128, |total, value| {
24 total.saturating_add(u128::from(value))
25 }))
26}
27
28#[must_use]
30pub fn rounded_percent_u64(part: u64, total: u64) -> Option<u64> {
31 if total == 0 {
32 return None;
33 }
34 let rounded = (u128::from(part) * 100 + (u128::from(total) / 2)) / u128::from(total);
35 Some(saturating_u128_to_u64(rounded))
36}
37
38#[must_use]
40pub fn sdk_v1_mean_u64(values: impl IntoIterator<Item = u64>) -> f64 {
41 let (sum, count) = values
42 .into_iter()
43 .fold((0_u128, 0_usize), |(sum, count), value| {
44 (sum.saturating_add(u128::from(value)), count + 1)
45 });
46 if count == 0 {
47 0.0
48 } else {
49 sum as f64 / count as f64
50 }
51}
52
53#[must_use]
55pub fn sdk_v1_std_dev_u64<I>(values: I) -> f64
56where
57 I: Iterator<Item = u64> + Clone,
58{
59 let (sum, count) = values
60 .clone()
61 .fold((0_u128, 0_usize), |(sum, count), value| {
62 (sum.saturating_add(u128::from(value)), count + 1)
63 });
64 if count < 2 {
65 return 0.0;
66 }
67 let mean = sum as f64 / count as f64;
68 let variance = values
69 .map(|value| {
70 let difference = value as f64 - mean;
71 difference * difference
72 })
73 .sum::<f64>()
74 / (count - 1) as f64;
75 variance.sqrt()
76}
77
78#[derive(Debug)]
82pub struct Distribution<'a> {
83 samples: DistributionSamples<'a>,
84 sorted: OnceLock<Vec<u64>>,
85 sum: u128,
86}
87
88#[derive(Debug)]
89enum DistributionSamples<'a> {
90 Borrowed(&'a [u64]),
91 Sorted(Vec<u64>),
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub struct CliV1Summary {
97 pub mean_ns: u64,
98 pub median_ns: u64,
99 pub p95_ns: u64,
100 pub min_ns: u64,
101 pub max_ns: u64,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq)]
106pub struct SdkV1Summary {
107 pub mean_ns: f64,
108 pub median_ns: f64,
109 pub std_dev_ns: f64,
110 pub min_ns: u64,
111 pub max_ns: u64,
112 pub p95_ns: f64,
113 pub p99_ns: f64,
114}
115
116#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
118pub struct ResourceSample {
119 pub cpu_time_ms: Option<u64>,
120 pub peak_memory_growth_kb: Option<u64>,
121 pub process_peak_memory_kb: Option<u64>,
122}
123
124#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
126pub struct ResourceAggregate {
127 pub cpu_total_ms: Option<u64>,
128 pub cpu_median_ms: Option<u64>,
129 pub peak_memory_growth_kb: Option<u64>,
130 pub process_peak_memory_kb: Option<u64>,
131}
132
133#[derive(Debug, Default)]
135pub struct ResourceAccumulator {
136 cpu_samples: Vec<u64>,
137 cpu_total_ms: u128,
138 peak_memory_growth_kb: Option<u64>,
139 process_peak_memory_kb: Option<u64>,
140}
141
142impl ResourceAccumulator {
143 #[must_use]
145 pub fn new() -> Self {
146 Self::default()
147 }
148
149 pub fn record(&mut self, sample: ResourceSample) {
151 if let Some(cpu_time_ms) = sample.cpu_time_ms {
152 self.cpu_samples.push(cpu_time_ms);
153 self.cpu_total_ms = self.cpu_total_ms.saturating_add(u128::from(cpu_time_ms));
154 }
155 if let Some(peak_memory_growth_kb) = sample.peak_memory_growth_kb {
156 self.peak_memory_growth_kb = Some(
157 self.peak_memory_growth_kb
158 .map_or(peak_memory_growth_kb, |current| {
159 current.max(peak_memory_growth_kb)
160 }),
161 );
162 }
163 if let Some(process_peak_memory_kb) = sample.process_peak_memory_kb {
164 self.process_peak_memory_kb = Some(
165 self.process_peak_memory_kb
166 .map_or(process_peak_memory_kb, |current| {
167 current.max(process_peak_memory_kb)
168 }),
169 );
170 }
171 }
172
173 #[must_use]
175 pub fn finish(mut self) -> ResourceAggregate {
176 self.cpu_samples.sort_unstable();
177 let cpu_median_ms = match self.cpu_samples.len() {
178 0 => None,
179 len if len % 2 == 1 => Some(self.cpu_samples[len / 2]),
180 len => {
181 let lower = u128::from(self.cpu_samples[(len / 2) - 1]);
182 let upper = u128::from(self.cpu_samples[len / 2]);
183 Some(saturating_u128_to_u64((lower + upper) / 2))
184 }
185 };
186
187 ResourceAggregate {
188 cpu_total_ms: (!self.cpu_samples.is_empty())
189 .then_some(saturating_u128_to_u64(self.cpu_total_ms)),
190 cpu_median_ms,
191 peak_memory_growth_kb: self.peak_memory_growth_kb,
192 process_peak_memory_kb: self.process_peak_memory_kb,
193 }
194 }
195}
196
197impl<'a> Distribution<'a> {
198 #[must_use]
200 pub fn from_slice(samples: &'a [u64]) -> Self {
201 let sum = samples
202 .iter()
203 .fold(0_u128, |total, sample| total + u128::from(*sample));
204 Self {
205 samples: DistributionSamples::Borrowed(samples),
206 sorted: OnceLock::new(),
207 sum,
208 }
209 }
210
211 #[must_use]
213 pub fn from_vec(mut samples: Vec<u64>) -> Self {
214 let sum = samples
215 .iter()
216 .fold(0_u128, |total, sample| total + u128::from(*sample));
217 samples.sort_unstable();
218 Self {
219 samples: DistributionSamples::Sorted(samples),
220 sorted: OnceLock::new(),
221 sum,
222 }
223 }
224
225 #[must_use]
227 pub fn cli_v1_summary(&self) -> Option<CliV1Summary> {
228 let len = self.values().len();
229 if len == 0 {
230 return None;
231 }
232 let sorted = self.sorted();
233
234 let median_ns = if len % 2 == 1 {
235 sorted[len / 2]
236 } else {
237 let lower = u128::from(sorted[(len / 2) - 1]);
238 let upper = u128::from(sorted[len / 2]);
239 saturating_u128_to_u64((lower + upper) / 2)
240 };
241 let p95_rank = (95_u128 * len as u128).div_ceil(100) as usize;
242
243 Some(CliV1Summary {
244 mean_ns: saturating_u128_to_u64(self.sum / len as u128),
245 median_ns,
246 p95_ns: sorted[p95_rank.saturating_sub(1)],
247 min_ns: sorted[0],
248 max_ns: sorted[len - 1],
249 })
250 }
251
252 #[must_use]
254 pub fn sdk_v1_summary(&self) -> SdkV1Summary {
255 if self.values().is_empty() {
256 return SdkV1Summary {
257 mean_ns: 0.0,
258 median_ns: 0.0,
259 std_dev_ns: 0.0,
260 min_ns: 0,
261 max_ns: 0,
262 p95_ns: 0.0,
263 p99_ns: 0.0,
264 };
265 }
266
267 SdkV1Summary {
268 mean_ns: self.sdk_v1_mean(),
269 median_ns: self.sdk_v1_median(),
270 std_dev_ns: self.sdk_v1_std_dev(),
271 min_ns: self.min().unwrap_or(0),
272 max_ns: self.max().unwrap_or(0),
273 p95_ns: self.sdk_v1_percentile(95.0),
274 p99_ns: self.sdk_v1_percentile(99.0),
275 }
276 }
277
278 #[must_use]
280 pub fn sdk_v1_mean(&self) -> f64 {
281 if self.values().is_empty() {
282 0.0
283 } else {
284 sdk_v1_mean_u64(self.values().iter().copied())
285 }
286 }
287
288 #[must_use]
290 pub fn sdk_v1_median(&self) -> f64 {
291 let len = self.values().len();
292 if len == 0 {
293 return 0.0;
294 }
295 let sorted = self.sorted();
296 if len % 2 == 1 {
297 sorted[len / 2] as f64
298 } else {
299 (sorted[(len / 2) - 1] as f64 + sorted[len / 2] as f64) / 2.0
300 }
301 }
302
303 #[must_use]
305 pub fn sdk_v1_std_dev(&self) -> f64 {
306 sdk_v1_std_dev_u64(self.values().iter().copied())
307 }
308
309 #[must_use]
311 pub fn min(&self) -> Option<u64> {
312 self.values().iter().copied().min()
313 }
314
315 #[must_use]
317 pub fn max(&self) -> Option<u64> {
318 self.values().iter().copied().max()
319 }
320
321 #[must_use]
323 pub fn sdk_v1_percentile(&self, percentile: f64) -> f64 {
324 if self.values().is_empty() {
325 return 0.0;
326 }
327 let sorted = self.sorted();
328 let percentile = percentile.clamp(0.0, 100.0) / 100.0;
329 let index = (percentile * (sorted.len() - 1) as f64).round() as usize;
330 sorted[index.min(sorted.len() - 1)] as f64
331 }
332
333 fn sorted(&self) -> &[u64] {
334 match &self.samples {
335 DistributionSamples::Sorted(samples) => samples,
336 DistributionSamples::Borrowed(samples) => self.sorted.get_or_init(|| {
337 let mut sorted = samples.to_vec();
338 sorted.sort_unstable();
339 sorted
340 }),
341 }
342 }
343
344 fn values(&self) -> &[u64] {
345 match &self.samples {
346 DistributionSamples::Borrowed(samples) => samples,
347 DistributionSamples::Sorted(samples) => samples,
348 }
349 }
350}