use std::time::Duration;
pub fn collect_latency_samples<F>(warmup: usize, iterations: usize, mut sample: F) -> Vec<Duration>
where
F: FnMut() -> Duration,
{
for _ in 0..warmup {
let _ = sample();
}
(0..iterations).map(|_| sample()).collect()
}
pub fn summarize_latency_samples(samples: &[Duration]) -> Option<HandoffLatencySummary> {
summarize_handoff_latencies(samples, EmptySampleSet::Handoff).ok()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct HandoffLatencySummary {
pub sample_count: usize,
pub p50: Duration,
pub p99: Duration,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct HandoffLatencyComparison {
pub handoff: HandoffLatencySummary,
pub fallback: HandoffLatencySummary,
}
impl HandoffLatencyComparison {
pub fn p50_savings(&self) -> Duration {
self.fallback.p50.saturating_sub(self.handoff.p50)
}
pub fn p99_savings(&self) -> Duration {
self.fallback.p99.saturating_sub(self.handoff.p99)
}
pub fn proves_handoff_faster(&self) -> bool {
self.handoff.p50 < self.fallback.p50 && self.handoff.p99 < self.fallback.p99
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum HandoffLatencyError {
#[error("handoff latency comparison received no handoff samples")]
EmptyHandoffSamples,
#[error("handoff latency comparison received no fallback samples")]
EmptyFallbackSamples,
#[error(
"handoff P50 was not faster than fallback: handoff {handoff:?}, fallback {fallback:?}"
)]
P50NotFaster {
handoff: Duration,
fallback: Duration,
},
#[error(
"handoff P99 was not faster than fallback: handoff {handoff:?}, fallback {fallback:?}"
)]
P99NotFaster {
handoff: Duration,
fallback: Duration,
},
}
pub fn compare_handoff_latency(
handoff_samples: &[Duration],
fallback_samples: &[Duration],
) -> Result<HandoffLatencyComparison, HandoffLatencyError> {
let handoff = summarize_handoff_latencies(handoff_samples, EmptySampleSet::Handoff)?;
let fallback = summarize_handoff_latencies(fallback_samples, EmptySampleSet::Fallback)?;
let comparison = HandoffLatencyComparison { handoff, fallback };
if comparison.handoff.p50 >= comparison.fallback.p50 {
return Err(HandoffLatencyError::P50NotFaster {
handoff: comparison.handoff.p50,
fallback: comparison.fallback.p50,
});
}
if comparison.handoff.p99 >= comparison.fallback.p99 {
return Err(HandoffLatencyError::P99NotFaster {
handoff: comparison.handoff.p99,
fallback: comparison.fallback.p99,
});
}
Ok(comparison)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum EmptySampleSet {
Handoff,
Fallback,
}
fn summarize_handoff_latencies(
samples: &[Duration],
empty: EmptySampleSet,
) -> Result<HandoffLatencySummary, HandoffLatencyError> {
if samples.is_empty() {
return Err(match empty {
EmptySampleSet::Handoff => HandoffLatencyError::EmptyHandoffSamples,
EmptySampleSet::Fallback => HandoffLatencyError::EmptyFallbackSamples,
});
}
let mut sorted = samples.to_vec();
sorted.sort_unstable();
Ok(HandoffLatencySummary {
sample_count: sorted.len(),
p50: percentile_nearest_rank(&sorted, 50),
p99: percentile_nearest_rank(&sorted, 99),
})
}
fn percentile_nearest_rank(sorted: &[Duration], percentile: usize) -> Duration {
debug_assert!(!sorted.is_empty());
debug_assert!((1..=100).contains(&percentile));
let rank = sorted.len() * percentile;
let index = rank.div_ceil(100).saturating_sub(1);
sorted[index.min(sorted.len() - 1)]
}