use std::sync::OnceLock;
pub const MAX_BENCHMARK_COUNT: u32 = 1_000_000;
#[must_use]
pub fn saturating_u128_to_u64(value: u128) -> u64 {
value.min(u128::from(u64::MAX)) as u64
}
#[must_use]
pub fn saturating_usize_to_u32(value: usize) -> u32 {
u32::try_from(value).unwrap_or(u32::MAX)
}
#[must_use]
pub fn saturating_sum_u64(values: impl IntoIterator<Item = u64>) -> u64 {
saturating_u128_to_u64(values.into_iter().fold(0_u128, |total, value| {
total.saturating_add(u128::from(value))
}))
}
#[must_use]
pub fn rounded_percent_u64(part: u64, total: u64) -> Option<u64> {
if total == 0 {
return None;
}
let rounded = (u128::from(part) * 100 + (u128::from(total) / 2)) / u128::from(total);
Some(saturating_u128_to_u64(rounded))
}
#[must_use]
pub fn sdk_v1_mean_u64(values: impl IntoIterator<Item = u64>) -> f64 {
let (sum, count) = values
.into_iter()
.fold((0_u128, 0_usize), |(sum, count), value| {
(sum.saturating_add(u128::from(value)), count + 1)
});
if count == 0 {
0.0
} else {
sum as f64 / count as f64
}
}
#[must_use]
pub fn sdk_v1_std_dev_u64<I>(values: I) -> f64
where
I: Iterator<Item = u64> + Clone,
{
let (sum, count) = values
.clone()
.fold((0_u128, 0_usize), |(sum, count), value| {
(sum.saturating_add(u128::from(value)), count + 1)
});
if count < 2 {
return 0.0;
}
let mean = sum as f64 / count as f64;
let variance = values
.map(|value| {
let difference = value as f64 - mean;
difference * difference
})
.sum::<f64>()
/ (count - 1) as f64;
variance.sqrt()
}
#[derive(Debug)]
pub struct Distribution<'a> {
samples: DistributionSamples<'a>,
sorted: OnceLock<Vec<u64>>,
sum: u128,
}
#[derive(Debug)]
enum DistributionSamples<'a> {
Borrowed(&'a [u64]),
Sorted(Vec<u64>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CliV1Summary {
pub mean_ns: u64,
pub median_ns: u64,
pub p95_ns: u64,
pub min_ns: u64,
pub max_ns: u64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SdkV1Summary {
pub mean_ns: f64,
pub median_ns: f64,
pub std_dev_ns: f64,
pub min_ns: u64,
pub max_ns: u64,
pub p95_ns: f64,
pub p99_ns: f64,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ResourceSample {
pub cpu_time_ms: Option<u64>,
pub peak_memory_growth_kb: Option<u64>,
pub process_peak_memory_kb: Option<u64>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ResourceAggregate {
pub cpu_total_ms: Option<u64>,
pub cpu_median_ms: Option<u64>,
pub peak_memory_growth_kb: Option<u64>,
pub process_peak_memory_kb: Option<u64>,
}
#[derive(Debug, Default)]
pub struct ResourceAccumulator {
cpu_samples: Vec<u64>,
cpu_total_ms: u128,
peak_memory_growth_kb: Option<u64>,
process_peak_memory_kb: Option<u64>,
}
impl ResourceAccumulator {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn record(&mut self, sample: ResourceSample) {
if let Some(cpu_time_ms) = sample.cpu_time_ms {
self.cpu_samples.push(cpu_time_ms);
self.cpu_total_ms = self.cpu_total_ms.saturating_add(u128::from(cpu_time_ms));
}
if let Some(peak_memory_growth_kb) = sample.peak_memory_growth_kb {
self.peak_memory_growth_kb = Some(
self.peak_memory_growth_kb
.map_or(peak_memory_growth_kb, |current| {
current.max(peak_memory_growth_kb)
}),
);
}
if let Some(process_peak_memory_kb) = sample.process_peak_memory_kb {
self.process_peak_memory_kb = Some(
self.process_peak_memory_kb
.map_or(process_peak_memory_kb, |current| {
current.max(process_peak_memory_kb)
}),
);
}
}
#[must_use]
pub fn finish(mut self) -> ResourceAggregate {
self.cpu_samples.sort_unstable();
let cpu_median_ms = match self.cpu_samples.len() {
0 => None,
len if len % 2 == 1 => Some(self.cpu_samples[len / 2]),
len => {
let lower = u128::from(self.cpu_samples[(len / 2) - 1]);
let upper = u128::from(self.cpu_samples[len / 2]);
Some(saturating_u128_to_u64((lower + upper) / 2))
}
};
ResourceAggregate {
cpu_total_ms: (!self.cpu_samples.is_empty())
.then_some(saturating_u128_to_u64(self.cpu_total_ms)),
cpu_median_ms,
peak_memory_growth_kb: self.peak_memory_growth_kb,
process_peak_memory_kb: self.process_peak_memory_kb,
}
}
}
impl<'a> Distribution<'a> {
#[must_use]
pub fn from_slice(samples: &'a [u64]) -> Self {
let sum = samples
.iter()
.fold(0_u128, |total, sample| total + u128::from(*sample));
Self {
samples: DistributionSamples::Borrowed(samples),
sorted: OnceLock::new(),
sum,
}
}
#[must_use]
pub fn from_vec(mut samples: Vec<u64>) -> Self {
let sum = samples
.iter()
.fold(0_u128, |total, sample| total + u128::from(*sample));
samples.sort_unstable();
Self {
samples: DistributionSamples::Sorted(samples),
sorted: OnceLock::new(),
sum,
}
}
#[must_use]
pub fn cli_v1_summary(&self) -> Option<CliV1Summary> {
let len = self.values().len();
if len == 0 {
return None;
}
let sorted = self.sorted();
let median_ns = if len % 2 == 1 {
sorted[len / 2]
} else {
let lower = u128::from(sorted[(len / 2) - 1]);
let upper = u128::from(sorted[len / 2]);
saturating_u128_to_u64((lower + upper) / 2)
};
let p95_rank = (95_u128 * len as u128).div_ceil(100) as usize;
Some(CliV1Summary {
mean_ns: saturating_u128_to_u64(self.sum / len as u128),
median_ns,
p95_ns: sorted[p95_rank.saturating_sub(1)],
min_ns: sorted[0],
max_ns: sorted[len - 1],
})
}
#[must_use]
pub fn sdk_v1_summary(&self) -> SdkV1Summary {
if self.values().is_empty() {
return SdkV1Summary {
mean_ns: 0.0,
median_ns: 0.0,
std_dev_ns: 0.0,
min_ns: 0,
max_ns: 0,
p95_ns: 0.0,
p99_ns: 0.0,
};
}
SdkV1Summary {
mean_ns: self.sdk_v1_mean(),
median_ns: self.sdk_v1_median(),
std_dev_ns: self.sdk_v1_std_dev(),
min_ns: self.min().unwrap_or(0),
max_ns: self.max().unwrap_or(0),
p95_ns: self.sdk_v1_percentile(95.0),
p99_ns: self.sdk_v1_percentile(99.0),
}
}
#[must_use]
pub fn sdk_v1_mean(&self) -> f64 {
if self.values().is_empty() {
0.0
} else {
sdk_v1_mean_u64(self.values().iter().copied())
}
}
#[must_use]
pub fn sdk_v1_median(&self) -> f64 {
let len = self.values().len();
if len == 0 {
return 0.0;
}
let sorted = self.sorted();
if len % 2 == 1 {
sorted[len / 2] as f64
} else {
(sorted[(len / 2) - 1] as f64 + sorted[len / 2] as f64) / 2.0
}
}
#[must_use]
pub fn sdk_v1_std_dev(&self) -> f64 {
sdk_v1_std_dev_u64(self.values().iter().copied())
}
#[must_use]
pub fn min(&self) -> Option<u64> {
self.values().iter().copied().min()
}
#[must_use]
pub fn max(&self) -> Option<u64> {
self.values().iter().copied().max()
}
#[must_use]
pub fn sdk_v1_percentile(&self, percentile: f64) -> f64 {
if self.values().is_empty() {
return 0.0;
}
let sorted = self.sorted();
let percentile = percentile.clamp(0.0, 100.0) / 100.0;
let index = (percentile * (sorted.len() - 1) as f64).round() as usize;
sorted[index.min(sorted.len() - 1)] as f64
}
fn sorted(&self) -> &[u64] {
match &self.samples {
DistributionSamples::Sorted(samples) => samples,
DistributionSamples::Borrowed(samples) => self.sorted.get_or_init(|| {
let mut sorted = samples.to_vec();
sorted.sort_unstable();
sorted
}),
}
}
fn values(&self) -> &[u64] {
match &self.samples {
DistributionSamples::Borrowed(samples) => samples,
DistributionSamples::Sorted(samples) => samples,
}
}
}