1use std::time::Duration;
20
21use teksilo_canvas::live_image::internal::PassTimings;
22
23pub const SAMPLES: usize = 1024;
25
26#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
29#[non_exhaustive]
30pub struct Percentiles {
31 pub p50: u32,
32 pub p90: u32,
33 pub p99: u32,
34 pub max: u32,
35 pub samples: u32,
37}
38
39#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
42#[non_exhaustive]
43pub struct LiveImageTimings {
44 pub prepare: Percentiles,
47 pub lock_hold: Percentiles,
49 pub commit_to_upload: Percentiles,
52}
53
54#[derive(Debug)]
56struct Ring {
57 samples: Vec<u32>,
58 next: usize,
59}
60
61impl Ring {
62 fn new() -> Self {
63 Self {
64 samples: Vec::with_capacity(SAMPLES),
65 next: 0,
66 }
67 }
68
69 fn push(&mut self, duration: Duration) {
70 let micros = u32::try_from(duration.as_micros()).unwrap_or(u32::MAX);
71 if self.samples.len() < SAMPLES {
72 self.samples.push(micros);
73 } else {
74 self.samples[self.next] = micros;
75 }
76 self.next = (self.next + 1) % SAMPLES;
77 }
78
79 fn percentiles(&self) -> Percentiles {
80 let mut sorted = self.samples.clone();
81 sorted.sort_unstable();
82 let Some(&max) = sorted.last() else {
83 return Percentiles::default();
84 };
85 let rank = |p: usize| sorted[(p * sorted.len()).div_ceil(100) - 1];
88 Percentiles {
89 p50: rank(50),
90 p90: rank(90),
91 p99: rank(99),
92 max,
93 samples: sorted.len() as u32,
94 }
95 }
96}
97
98#[derive(Debug)]
100pub(crate) struct LiveTimingRings {
101 prepare: Ring,
102 lock_hold: Ring,
103 commit_to_upload: Ring,
104}
105
106impl LiveTimingRings {
107 pub(crate) fn new() -> Self {
108 Self {
109 prepare: Ring::new(),
110 lock_hold: Ring::new(),
111 commit_to_upload: Ring::new(),
112 }
113 }
114
115 pub(crate) fn record(&mut self, prepare: Option<Duration>, pass: &PassTimings) {
118 if let Some(prepare) = prepare {
119 self.prepare.push(prepare);
120 }
121 for &hold in &pass.lock_holds {
122 self.lock_hold.push(hold);
123 }
124 for &delay in &pass.commit_to_upload {
125 self.commit_to_upload.push(delay);
126 }
127 }
128
129 pub(crate) fn timings(&self) -> LiveImageTimings {
130 LiveImageTimings {
131 prepare: self.prepare.percentiles(),
132 lock_hold: self.lock_hold.percentiles(),
133 commit_to_upload: self.commit_to_upload.percentiles(),
134 }
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 fn ring_of(micros: impl IntoIterator<Item = u64>) -> Ring {
143 let mut ring = Ring::new();
144 for us in micros {
145 ring.push(Duration::from_micros(us));
146 }
147 ring
148 }
149
150 #[test]
151 fn percentiles_are_by_nearest_rank() {
152 let p = ring_of(1..=100).percentiles();
153 assert_eq!(
154 (p.p50, p.p90, p.p99, p.max, p.samples),
155 (50, 90, 99, 100, 100)
156 );
157 let p = ring_of([7, 1, 3, 9, 5, 2, 8, 4, 10, 6]).percentiles();
159 assert_eq!((p.p50, p.p90, p.p99, p.max), (5, 9, 10, 10));
160 let p = ring_of([42]).percentiles();
161 assert_eq!((p.p50, p.p90, p.p99, p.max, p.samples), (42, 42, 42, 42, 1));
162 assert_eq!(Ring::new().percentiles(), Percentiles::default());
163 }
164
165 #[test]
166 fn a_histogram_covers_its_latest_samples_only() {
167 let ring =
169 ring_of(std::iter::repeat_n(1000, SAMPLES).chain(std::iter::repeat_n(1, SAMPLES)));
170 let p = ring.percentiles();
171 assert_eq!((p.max, p.samples), (1, SAMPLES as u32));
172 let ring =
174 ring_of(std::iter::repeat_n(1000, SAMPLES).chain(std::iter::repeat_n(1, SAMPLES / 2)));
175 let p = ring.percentiles();
176 assert_eq!((p.p50, p.max, p.samples), (1, 1000, SAMPLES as u32));
177 }
178
179 #[test]
180 fn a_duration_past_u32_microseconds_saturates() {
181 let mut ring = Ring::new();
182 ring.push(Duration::from_secs(10_000));
183 ring.push(Duration::from_nanos(999));
184 let p = ring.percentiles();
185 assert_eq!((p.p50, p.max), (0, u32::MAX));
186 }
187
188 #[test]
189 fn a_render_records_its_prepare_and_what_its_pass_timed() {
190 let mut rings = LiveTimingRings::new();
191 let pass = PassTimings {
192 lock_holds: vec![Duration::from_micros(30), Duration::from_micros(10)],
193 commit_to_upload: vec![Duration::from_micros(500)],
194 };
195 rings.record(Some(Duration::from_micros(80)), &pass);
196 rings.record(None, &PassTimings::default());
197 let t = rings.timings();
198 assert_eq!((t.prepare.samples, t.prepare.max), (1, 80));
199 assert_eq!(
200 (t.lock_hold.samples, t.lock_hold.p50, t.lock_hold.max),
201 (2, 10, 30)
202 );
203 assert_eq!(
204 (t.commit_to_upload.samples, t.commit_to_upload.max),
205 (1, 500)
206 );
207 }
208}