krabka_bench_driver/soak/client/
windows.rs1use {
16 crate::{
17 hist,
18 soak::{
19 contract::Contract,
20 evidence::{LatencySummary, PathMeasurement},
21 schedule::ScheduledFault,
22 },
23 },
24 hdrhistogram::{CreationError, Histogram},
25 krabka_units::prelude::*,
26 std::sync::Mutex,
27};
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct Span {
32 pub start: i64,
33 pub end: i64,
34}
35
36impl Span {
37 fn contains(self, at: i64) -> bool { self.start <= at && at < self.end }
38}
39
40#[must_use]
43pub fn planned_span(contract: &Contract, fault: &ScheduledFault, load_start_ms: i64) -> Span {
44 Span {
45 start: load_start_ms + fault.start.millis_i64(),
46 end: load_start_ms
47 + (fault.end() + fault.recovery_budget(&contract.deadlines)).millis_i64(),
48 }
49}
50
51#[must_use]
53pub fn steady_millis(load: Span, spans: &[Span]) -> i64 {
54 let mut clipped: Vec<Span> = spans
55 .iter()
56 .map(|span| Span {
57 start: span.start.max(load.start),
58 end: span.end.min(load.end),
59 })
60 .filter(|span| span.start < span.end)
61 .collect();
62 clipped.sort_by_key(|span| span.start);
63 let mut covered = 0;
64 let mut reached = load.start;
65 for span in clipped {
66 let start = span.start.max(reached);
67 if span.end > start {
68 covered += span.end - start;
69 reached = span.end;
70 }
71 }
72 (load.end - load.start).max(0) - covered
73}
74
75pub struct Windows {
78 planned: Vec<Span>,
79 actual: Mutex<Vec<Span>>,
80 active_since: Mutex<Option<i64>>,
82}
83
84impl Windows {
85 #[must_use]
86 pub fn new(planned: Vec<Span>) -> Self {
87 Self {
88 planned,
89 actual: Mutex::new(Vec::new()),
90 active_since: Mutex::new(None),
91 }
92 }
93
94 pub fn disturbed(&self, at: i64) -> bool {
96 let active = self
97 .active_since
98 .lock()
99 .map_or(true, |since| since.is_some_and(|since| at >= since));
100 let actual = self
101 .actual
102 .lock()
103 .map_or(true, |spans| spans.iter().any(|span| span.contains(at)));
104 active || actual || self.planned.iter().any(|span| span.contains(at))
105 }
106
107 pub fn injected(&self, at: i64) {
109 if let Ok(mut since) = self.active_since.lock() {
110 *since = Some(at);
111 }
112 }
113
114 pub fn cleared(&self, span: Span) {
116 if let Ok(mut spans) = self.actual.lock() {
117 spans.push(span);
118 }
119 if let Ok(mut since) = self.active_since.lock() {
120 *since = None;
121 }
122 }
123
124 pub fn spans(&self) -> Vec<Span> {
126 let mut spans = self.planned.clone();
127 if let Ok(actual) = self.actual.lock() {
128 spans.extend(actual.iter().copied());
129 }
130 if let Ok(since) = self.active_since.lock()
131 && let Some(since) = *since
132 {
133 spans.push(Span {
134 start: since,
135 end: i64::MAX,
136 });
137 }
138 spans
139 }
140}
141
142pub struct Latencies {
145 steady: Histogram<u64>,
146 disturbed: Histogram<u64>,
147 pub steady_count: u64,
149}
150
151impl Latencies {
152 pub fn new() -> Result<Self, CreationError> {
158 Ok(Self {
159 steady: hist::new()?,
160 disturbed: hist::new()?,
161 steady_count: 0,
162 })
163 }
164
165 pub fn record(&mut self, disturbed: bool, latency: Time) {
166 if disturbed {
167 hist::record(&mut self.disturbed, latency);
168 } else {
169 hist::record(&mut self.steady, latency);
170 self.steady_count += 1;
171 }
172 }
173
174 pub fn merge(&mut self, other: &Self) {
175 let _ = self.steady.add(&other.steady);
178 let _ = self.disturbed.add(&other.disturbed);
179 self.steady_count += other.steady_count;
180 }
181
182 fn steady(&self) -> LatencySummary { summary(&self.steady) }
183
184 fn disturbed(&self) -> LatencySummary { summary(&self.disturbed) }
185}
186
187fn summary(histogram: &Histogram<u64>) -> LatencySummary {
188 let percentiles = hist::percentiles(histogram);
189 LatencySummary {
190 p50: percentiles.p50,
191 p99: percentiles.p99,
192 p999: percentiles.p999,
193 count: percentiles.count,
194 }
195}
196
197#[must_use]
202pub fn measurement(
203 path: &str,
204 produce: &Latencies,
205 consume: &Latencies,
206 steady: Time,
207) -> PathMeasurement {
208 let acks = f64::from(u32::try_from(produce.steady_count).unwrap_or(u32::MAX));
209 let seconds = steady.secs_f64();
210 PathMeasurement {
211 path: path.to_owned(),
212 produce_steady: produce.steady(),
213 produce_disturbed: produce.disturbed(),
214 consume_steady: consume.steady(),
215 consume_disturbed: consume.disturbed(),
216 steady_rate: Frequency::from_per_sec(if seconds > 0.0 { acks / seconds } else { 0.0 }),
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use {
223 super::{Latencies, Span, Windows, measurement, planned_span, steady_millis},
224 crate::soak::{
225 contract::{Contract, FaultKind, Target, fixture::SELF_CHECK},
226 schedule::ScheduledFault,
227 },
228 assert2::check,
229 krabka_units::prelude::*,
230 };
231
232 const fn span(start: i64, end: i64) -> Span { Span { start, end } }
233
234 #[yare::parameterized(
235 nothing_disturbed = { &[], 100 },
236 one_inside = { &[span(10, 30)], 80 },
237 overlapping = { &[span(10, 30), span(20, 40)], 70 },
238 nested = { &[span(10, 50), span(20, 30)], 60 },
239 unsorted_disjoint = { &[span(60, 70), span(10, 20)], 80 },
240 clipped_at_both_ends = { &[span(-50, 10), span(90, 500)], 80 },
241 outside = { &[span(200, 300)], 100 },
242 everything = { &[span(0, 100)], 0 },
243 )]
244 fn steady_time_is_the_load_minus_every_span(spans: &[Span], steady: i64) {
245 check!(steady_millis(span(0, 100), spans) == steady);
246 }
247
248 #[test]
249 fn a_planned_span_runs_to_the_longest_deadline_after_cleanup() {
250 let mut contract = Contract::from_yaml(SELF_CHECK).unwrap();
251 contract.deadlines.leader_failover = secs(20);
252 contract.deadlines.isr_rejoin = secs(30);
253 let kind = FaultKind::Restart { graceful: false };
254 let restart = ScheduledFault {
255 id: "restart#0".into(),
256 plan: "restart".into(),
257 checks: kind.checks(Target::Leader),
258 kind,
259 target: Target::Leader,
260 start: secs(140),
261 duration: secs(5),
262 };
263 check!(planned_span(&contract, &restart, 1_000) == span(141_000, 176_000));
266 }
267
268 #[yare::parameterized(
269 before = { 5, false },
270 planned = { 15, true },
271 between = { 25, false },
272 actual = { 45, true },
273 active = { 70, true },
274 )]
275 fn a_sample_is_disturbed_inside_any_span_or_a_live_fault(at: i64, disturbed: bool) {
276 let windows = Windows::new(vec![span(10, 20)]);
277 windows.injected(40);
278 windows.cleared(span(40, 50));
279 windows.injected(60);
280 check!(windows.disturbed(at) == disturbed);
281 }
282
283 #[test]
284 fn a_cleared_fault_stops_disturbing_after_its_span() {
285 let windows = Windows::new(Vec::new());
286 windows.injected(40);
287 check!(windows.disturbed(1_000));
288 windows.cleared(span(40, 50));
289 check!(!windows.disturbed(1_000));
290 check!(windows.spans() == [span(40, 50)]);
291 }
292
293 #[test]
294 fn the_steady_rate_counts_only_steady_acknowledgements() {
295 let mut produce = Latencies::new().unwrap();
296 for _ in 0..20 {
297 produce.record(false, millis(4));
298 }
299 for _ in 0..7 {
300 produce.record(true, secs(3));
301 }
302 let mut consume = Latencies::new().unwrap();
303 let mut other = Latencies::new().unwrap();
304 other.record(false, millis(9));
305 consume.merge(&other);
306 let measured = measurement("p", &produce, &consume, secs(10));
307 check!(measured.produce_steady.count == 20);
308 check!(measured.produce_disturbed.count == 7);
309 check!(measured.produce_steady.p999 < millis(5));
310 check!(measured.produce_disturbed.p50 > secs(2));
311 check!(measured.consume_steady.count == 1);
312 check!(measured.consume_disturbed.count == 0);
313 check!((measured.steady_rate.per_sec_f64() - 2.0).abs() < 1.0e-9);
314 }
315}