running_process/broker/server/handoff/
latency.rs1use std::time::Duration;
9
10pub fn collect_latency_samples<F>(warmup: usize, iterations: usize, mut sample: F) -> Vec<Duration>
19where
20 F: FnMut() -> Duration,
21{
22 for _ in 0..warmup {
23 let _ = sample();
24 }
25 (0..iterations).map(|_| sample()).collect()
26}
27
28pub fn summarize_latency_samples(samples: &[Duration]) -> Option<HandoffLatencySummary> {
33 summarize_handoff_latencies(samples, EmptySampleSet::Handoff).ok()
34}
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub struct HandoffLatencySummary {
39 pub sample_count: usize,
41 pub p50: Duration,
43 pub p99: Duration,
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub struct HandoffLatencyComparison {
50 pub handoff: HandoffLatencySummary,
52 pub fallback: HandoffLatencySummary,
54}
55
56impl HandoffLatencyComparison {
57 pub fn p50_savings(&self) -> Duration {
59 self.fallback.p50.saturating_sub(self.handoff.p50)
60 }
61
62 pub fn p99_savings(&self) -> Duration {
64 self.fallback.p99.saturating_sub(self.handoff.p99)
65 }
66
67 pub fn proves_handoff_faster(&self) -> bool {
69 self.handoff.p50 < self.fallback.p50 && self.handoff.p99 < self.fallback.p99
70 }
71}
72
73#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
75pub enum HandoffLatencyError {
76 #[error("handoff latency comparison received no handoff samples")]
78 EmptyHandoffSamples,
79 #[error("handoff latency comparison received no fallback samples")]
81 EmptyFallbackSamples,
82 #[error(
84 "handoff P50 was not faster than fallback: handoff {handoff:?}, fallback {fallback:?}"
85 )]
86 P50NotFaster {
87 handoff: Duration,
89 fallback: Duration,
91 },
92 #[error(
94 "handoff P99 was not faster than fallback: handoff {handoff:?}, fallback {fallback:?}"
95 )]
96 P99NotFaster {
97 handoff: Duration,
99 fallback: Duration,
101 },
102}
103
104pub fn compare_handoff_latency(
110 handoff_samples: &[Duration],
111 fallback_samples: &[Duration],
112) -> Result<HandoffLatencyComparison, HandoffLatencyError> {
113 let handoff = summarize_handoff_latencies(handoff_samples, EmptySampleSet::Handoff)?;
114 let fallback = summarize_handoff_latencies(fallback_samples, EmptySampleSet::Fallback)?;
115 let comparison = HandoffLatencyComparison { handoff, fallback };
116
117 if comparison.handoff.p50 >= comparison.fallback.p50 {
118 return Err(HandoffLatencyError::P50NotFaster {
119 handoff: comparison.handoff.p50,
120 fallback: comparison.fallback.p50,
121 });
122 }
123 if comparison.handoff.p99 >= comparison.fallback.p99 {
124 return Err(HandoffLatencyError::P99NotFaster {
125 handoff: comparison.handoff.p99,
126 fallback: comparison.fallback.p99,
127 });
128 }
129
130 Ok(comparison)
131}
132
133#[derive(Clone, Copy, Debug, PartialEq, Eq)]
134enum EmptySampleSet {
135 Handoff,
136 Fallback,
137}
138
139fn summarize_handoff_latencies(
140 samples: &[Duration],
141 empty: EmptySampleSet,
142) -> Result<HandoffLatencySummary, HandoffLatencyError> {
143 if samples.is_empty() {
144 return Err(match empty {
145 EmptySampleSet::Handoff => HandoffLatencyError::EmptyHandoffSamples,
146 EmptySampleSet::Fallback => HandoffLatencyError::EmptyFallbackSamples,
147 });
148 }
149
150 let mut sorted = samples.to_vec();
151 sorted.sort_unstable();
152 Ok(HandoffLatencySummary {
153 sample_count: sorted.len(),
154 p50: percentile_nearest_rank(&sorted, 50),
155 p99: percentile_nearest_rank(&sorted, 99),
156 })
157}
158
159fn percentile_nearest_rank(sorted: &[Duration], percentile: usize) -> Duration {
160 debug_assert!(!sorted.is_empty());
161 debug_assert!((1..=100).contains(&percentile));
162
163 let rank = sorted.len() * percentile;
164 let index = rank.div_ceil(100).saturating_sub(1);
165 sorted[index.min(sorted.len() - 1)]
166}