use std::time::Duration;
use teksilo_canvas::live_image::internal::PassTimings;
pub const SAMPLES: usize = 1024;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct Percentiles {
pub p50: u32,
pub p90: u32,
pub p99: u32,
pub max: u32,
pub samples: u32,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct LiveImageTimings {
pub prepare: Percentiles,
pub lock_hold: Percentiles,
pub commit_to_upload: Percentiles,
}
#[derive(Debug)]
struct Ring {
samples: Vec<u32>,
next: usize,
}
impl Ring {
fn new() -> Self {
Self {
samples: Vec::with_capacity(SAMPLES),
next: 0,
}
}
fn push(&mut self, duration: Duration) {
let micros = u32::try_from(duration.as_micros()).unwrap_or(u32::MAX);
if self.samples.len() < SAMPLES {
self.samples.push(micros);
} else {
self.samples[self.next] = micros;
}
self.next = (self.next + 1) % SAMPLES;
}
fn percentiles(&self) -> Percentiles {
let mut sorted = self.samples.clone();
sorted.sort_unstable();
let Some(&max) = sorted.last() else {
return Percentiles::default();
};
let rank = |p: usize| sorted[(p * sorted.len()).div_ceil(100) - 1];
Percentiles {
p50: rank(50),
p90: rank(90),
p99: rank(99),
max,
samples: sorted.len() as u32,
}
}
}
#[derive(Debug)]
pub(crate) struct LiveTimingRings {
prepare: Ring,
lock_hold: Ring,
commit_to_upload: Ring,
}
impl LiveTimingRings {
pub(crate) fn new() -> Self {
Self {
prepare: Ring::new(),
lock_hold: Ring::new(),
commit_to_upload: Ring::new(),
}
}
pub(crate) fn record(&mut self, prepare: Option<Duration>, pass: &PassTimings) {
if let Some(prepare) = prepare {
self.prepare.push(prepare);
}
for &hold in &pass.lock_holds {
self.lock_hold.push(hold);
}
for &delay in &pass.commit_to_upload {
self.commit_to_upload.push(delay);
}
}
pub(crate) fn timings(&self) -> LiveImageTimings {
LiveImageTimings {
prepare: self.prepare.percentiles(),
lock_hold: self.lock_hold.percentiles(),
commit_to_upload: self.commit_to_upload.percentiles(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ring_of(micros: impl IntoIterator<Item = u64>) -> Ring {
let mut ring = Ring::new();
for us in micros {
ring.push(Duration::from_micros(us));
}
ring
}
#[test]
fn percentiles_are_by_nearest_rank() {
let p = ring_of(1..=100).percentiles();
assert_eq!(
(p.p50, p.p90, p.p99, p.max, p.samples),
(50, 90, 99, 100, 100)
);
let p = ring_of([7, 1, 3, 9, 5, 2, 8, 4, 10, 6]).percentiles();
assert_eq!((p.p50, p.p90, p.p99, p.max), (5, 9, 10, 10));
let p = ring_of([42]).percentiles();
assert_eq!((p.p50, p.p90, p.p99, p.max, p.samples), (42, 42, 42, 42, 1));
assert_eq!(Ring::new().percentiles(), Percentiles::default());
}
#[test]
fn a_histogram_covers_its_latest_samples_only() {
let ring =
ring_of(std::iter::repeat_n(1000, SAMPLES).chain(std::iter::repeat_n(1, SAMPLES)));
let p = ring.percentiles();
assert_eq!((p.max, p.samples), (1, SAMPLES as u32));
let ring =
ring_of(std::iter::repeat_n(1000, SAMPLES).chain(std::iter::repeat_n(1, SAMPLES / 2)));
let p = ring.percentiles();
assert_eq!((p.p50, p.max, p.samples), (1, 1000, SAMPLES as u32));
}
#[test]
fn a_duration_past_u32_microseconds_saturates() {
let mut ring = Ring::new();
ring.push(Duration::from_secs(10_000));
ring.push(Duration::from_nanos(999));
let p = ring.percentiles();
assert_eq!((p.p50, p.max), (0, u32::MAX));
}
#[test]
fn a_render_records_its_prepare_and_what_its_pass_timed() {
let mut rings = LiveTimingRings::new();
let pass = PassTimings {
lock_holds: vec![Duration::from_micros(30), Duration::from_micros(10)],
commit_to_upload: vec![Duration::from_micros(500)],
};
rings.record(Some(Duration::from_micros(80)), &pass);
rings.record(None, &PassTimings::default());
let t = rings.timings();
assert_eq!((t.prepare.samples, t.prepare.max), (1, 80));
assert_eq!(
(t.lock_hold.samples, t.lock_hold.p50, t.lock_hold.max),
(2, 10, 30)
);
assert_eq!(
(t.commit_to_upload.samples, t.commit_to_upload.max),
(1, 500)
);
}
}