Skip to main content

teksilo_render/
live_timings.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! How long the live pass takes, as histograms: what the frame-time and
5//! latency measurements of live pictures read. Built in debug builds and
6//! with the `live-image-timings` feature; a release build without it keeps
7//! no samples.
8//!
9//! Three quantities, each over its last [`SAMPLES`] samples, in whole
10//! microseconds:
11//!
12//! - `prepare`: one render's live pass, mip chains included, for a frame
13//!   that draws a live picture;
14//! - `lock_hold`: each hold of a source's lock by the renderer, which is how
15//!   long a producer can be kept waiting;
16//! - `commit_to_upload`: for each completed upload, from the commit of the
17//!   generation it uploaded to the end of its writes.
18
19use std::time::Duration;
20
21use teksilo_canvas::live_image::internal::PassTimings;
22
23/// How many of the latest samples a histogram covers.
24pub const SAMPLES: usize = 1024;
25
26/// The 50th, 90th and 99th percentiles and the largest of a histogram's
27/// samples, in whole microseconds, by nearest rank; all 0 with no sample.
28#[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    /// How many samples they are over: at most [`SAMPLES`].
36    pub samples: u32,
37}
38
39/// What [`Renderer::live_texture_timings`](crate::Renderer::live_texture_timings)
40/// reports.
41#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
42#[non_exhaustive]
43pub struct LiveImageTimings {
44    /// One render's live pass, mip chains included, for a frame that draws
45    /// a live picture.
46    pub prepare: Percentiles,
47    /// Each hold of a source's lock by the renderer.
48    pub lock_hold: Percentiles,
49    /// Each completed upload, from its generation's commit to the end of
50    /// its writes.
51    pub commit_to_upload: Percentiles,
52}
53
54/// The latest [`SAMPLES`] durations, in microseconds.
55#[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        // Nearest rank: the smallest sample with at least `p` of them at or
86        // below it.
87        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/// The three histograms a renderer keeps.
99#[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    /// One render's samples: its live pass, when it drew a live picture, and
116    /// what the pass timed under the sources' locks.
117    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        // Ten samples: the 99th percentile is the largest.
158        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        // 1024 samples of 1000 µs, then 1024 of 1 µs: the first are gone.
168        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        // Half overwritten: the old ones still count.
173        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}