132 lines
4.0 KiB
Rust
132 lines
4.0 KiB
Rust
use std::cmp::Ordering;
|
|||
|
|
use std::collections::BTreeSet;
|
||
|
|
|
||
|
|
pub const WINDOW: f64 = 60.0;
|
||
|
|
|
||
|
|
#[derive(Copy, Clone, PartialEq)]
|
||
|
|
struct RequestTime {
|
||
|
|
time: f64,
|
||
|
|
id: usize,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Eq for RequestTime {}
|
||
|
|
|
||
|
|
impl Ord for RequestTime {
|
||
|
|
fn cmp(&self, other: &Self) -> Ordering {
|
||
|
|
self.time
|
||
|
|
.total_cmp(&other.time)
|
||
|
|
.then(self.id.cmp(&other.id))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl PartialOrd for RequestTime {
|
||
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||
|
|
Some(self.cmp(other))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Nearest-rank p99 of response times for requests arriving in [t, t + WINDOW).
|
||
|
|
/// Emit one point per distinct arrival time t <= time_limit.
|
||
|
|
pub fn windowed_p99(mut requests: Vec<(f64, f64)>, time_limit: f64) -> Vec<(f64, f64)> {
|
||
|
|
requests.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||
|
|
let mut result = Vec::new();
|
||
|
|
let mut items = BTreeSet::new();
|
||
|
|
let mut end = 0;
|
||
|
|
let mut start = 0;
|
||
|
|
while start < requests.len() && requests[start].0 <= time_limit {
|
||
|
|
let time = requests[start].0;
|
||
|
|
while end < requests.len() && requests[end].0 < time + WINDOW {
|
||
|
|
items.insert(RequestTime {
|
||
|
|
time: requests[end].1,
|
||
|
|
id: end,
|
||
|
|
});
|
||
|
|
end += 1;
|
||
|
|
}
|
||
|
|
// ceil(0.99 * n), computed exactly, without floating-point rounding.
|
||
|
|
let rank = items.len() - items.len() / 100;
|
||
|
|
let p99 = items.iter().rev().nth(items.len() - rank).unwrap().time;
|
||
|
|
result.push((time, p99));
|
||
|
|
// Equal response times remain distinct observations thanks to id.
|
||
|
|
// Remove the entire arrival-time group before advancing the window.
|
||
|
|
while start < requests.len() && requests[start].0 == time {
|
||
|
|
items.remove(&RequestTime {
|
||
|
|
time: requests[start].1,
|
||
|
|
id: start,
|
||
|
|
});
|
||
|
|
start += 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
result
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn expires_old_requests_and_excludes_right_boundary() {
|
||
|
|
let requests = vec![
|
||
|
|
(0.0, 100.0),
|
||
|
|
(1.0, 10.0),
|
||
|
|
(61.0, 1.0),
|
||
|
|
(62.0, 1.0),
|
||
|
|
(123.0, 3.0),
|
||
|
|
];
|
||
|
|
assert_eq!(
|
||
|
|
windowed_p99(requests, 123.0),
|
||
|
|
vec![
|
||
|
|
(0.0, 100.0),
|
||
|
|
(1.0, 10.0),
|
||
|
|
(61.0, 1.0),
|
||
|
|
(62.0, 1.0),
|
||
|
|
(123.0, 3.0)
|
||
|
|
]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn uses_nearest_rank_and_counts_equal_response_times() {
|
||
|
|
let mut requests = vec![(0.0, 1.0); 99];
|
||
|
|
requests.push((0.0, 100.0));
|
||
|
|
assert_eq!(windowed_p99(requests, 0.0), vec![(0.0, 1.0)]);
|
||
|
|
let requests = (1..=100).map(|n| (0.0, n as f64)).collect();
|
||
|
|
assert_eq!(windowed_p99(requests, 0.0), vec![(0.0, 99.0)]);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn sorts_arrivals_and_limits_reported_times() {
|
||
|
|
assert_eq!(
|
||
|
|
windowed_p99(vec![(120.0, 8.0), (60.0, 2.0), (0.0, 4.0)], 60.0),
|
||
|
|
vec![(0.0, 4.0), (60.0, 2.0)]
|
||
|
|
);
|
||
|
|
assert!(windowed_p99(Vec::new(), 60.0).is_empty());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn matches_independent_sort_per_window_reference() {
|
||
|
|
let requests: Vec<_> = (0..400)
|
||
|
|
.map(|i| (((i * 37) % 241) as f64, ((i * 17) % 31 + 1) as f64))
|
||
|
|
.collect();
|
||
|
|
let mut times: Vec<_> = requests
|
||
|
|
.iter()
|
||
|
|
.map(|r| r.0)
|
||
|
|
.filter(|t| *t <= 180.0)
|
||
|
|
.collect();
|
||
|
|
times.sort_by(f64::total_cmp);
|
||
|
|
times.dedup();
|
||
|
|
let expected: Vec<_> = times
|
||
|
|
.into_iter()
|
||
|
|
.map(|t| {
|
||
|
|
let mut values: Vec<_> = requests
|
||
|
|
.iter()
|
||
|
|
.filter(|r| r.0 >= t && r.0 < t + WINDOW)
|
||
|
|
.map(|r| r.1)
|
||
|
|
.collect();
|
||
|
|
values.sort_by(f64::total_cmp);
|
||
|
|
let rank = (0.99 * values.len() as f64).ceil() as usize;
|
||
|
|
(t, values[rank - 1])
|
||
|
|
})
|
||
|
|
.collect();
|
||
|
|
assert_eq!(windowed_p99(requests, 180.0), expected);
|
||
|
|
}
|
||
|
|
}
|