retrofire_core/render/
stats.rs

1//! Rendering statistics.
2
3use alloc::{format, string::String};
4use core::fmt::{self, Display, Formatter};
5use core::ops::AddAssign;
6use core::time::Duration;
7#[cfg(feature = "std")]
8use std::time::Instant;
9
10//
11// Types
12//
13
14/// Collects and accumulates rendering statistics and performance data.
15#[derive(Clone, Debug, Default)]
16pub struct Stats {
17    /// Time spent rendering.
18    pub time: Duration,
19    /// Number of render calls issued.
20    pub calls: f32,
21    /// Number of frames rendered.
22    pub frames: f32,
23
24    /// Objects, primitives, vertices, and fragments input/output.
25    pub objs: Throughput,
26    pub prims: Throughput,
27    pub verts: Throughput,
28    pub frags: Throughput,
29
30    #[cfg(feature = "std")]
31    start: Option<Instant>,
32}
33
34#[derive(Copy, Clone, Debug, Default)]
35pub struct Throughput {
36    // Count of items submitted for rendering.
37    pub i: usize,
38    // Count of items output to the render target.
39    pub o: usize,
40}
41
42//
43// Impls
44//
45
46impl Stats {
47    /// Creates a new zeroed `Stats` instance.
48    pub fn new() -> Self {
49        Self::default()
50    }
51    /// Creates a `Stats` instance that records the time of its creation.
52    ///
53    /// Call [`finish`][Self::finish] to write the elapsed time to `self.time`.
54    /// Useful for timing frames, rendering calls, etc.
55    ///
56    /// Equivalent to [`Stats::new`] if the `std` feature is not enabled.
57    pub fn start() -> Self {
58        Self {
59            #[cfg(feature = "std")]
60            start: Some(Instant::now()),
61            ..Self::default()
62        }
63    }
64
65    /// Stops the timer and records the elapsed time to `self.time`.
66    ///
67    /// No-op if the timer was not running. This method is also no-op unless
68    /// the `std` feature is enabled.
69    pub fn finish(self) -> Self {
70        Self {
71            #[cfg(feature = "std")]
72            time: self
73                .start
74                .map(|st| st.elapsed())
75                .unwrap_or(self.time),
76            ..self
77        }
78    }
79
80    /// Returns the average throughput in items per second.
81    pub fn per_sec(&self) -> Self {
82        let secs = if self.time.is_zero() {
83            1.0
84        } else {
85            self.time.as_secs_f32()
86        };
87        let [objs, prims, verts, frags] =
88            self.throughput().map(|stat| stat.per_sec(secs));
89        Self {
90            frames: self.frames / secs,
91            calls: self.calls / secs,
92            time: Duration::from_secs(1),
93            objs,
94            prims,
95            verts,
96            frags,
97            #[cfg(feature = "std")]
98            start: None,
99        }
100    }
101    /// Returns the average throughput in items per frame.
102    pub fn per_frame(&self) -> Self {
103        let frames = self.frames.max(1.0);
104        let [objs, prims, verts, frags] = self
105            .throughput()
106            .map(|stat| stat.per_frame(frames));
107        Self {
108            frames: 1.0,
109            calls: self.calls / frames,
110            time: self.time.div_f32(frames),
111            objs,
112            prims,
113            verts,
114            frags,
115            #[cfg(feature = "std")]
116            start: None,
117        }
118    }
119
120    fn throughput(&self) -> [Throughput; 4] {
121        [self.objs, self.prims, self.verts, self.frags]
122    }
123
124    fn throughput_mut(&mut self) -> [&mut Throughput; 4] {
125        let Self { objs, prims, verts, frags, .. } = self;
126        [objs, prims, verts, frags]
127    }
128}
129
130impl Throughput {
131    fn per_sec(&self, secs: f32) -> Self {
132        Self {
133            i: (self.i as f32 / secs) as usize,
134            o: (self.o as f32 / secs) as usize,
135        }
136    }
137    fn per_frame(&self, frames: f32) -> Self {
138        Self {
139            i: self.i / frames as usize,
140            o: self.o / frames as usize,
141        }
142    }
143}
144
145impl Display for Stats {
146    #[rustfmt::skip]
147    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
148        let w = f.width().unwrap_or(16);
149        let per_s = self.per_sec();
150        let per_f = self.per_frame();
151        write!(f,
152            " STATS  {:>w$} │ {:>w$} │ {:>w$}\n\
153             ────────{empty:─>w$}─┼─{empty:─>w$}─┼─{empty:─>w$}─\n \
154              time   {:>w$} │ {empty:w$} │ {:>w$}\n \
155              calls  {:>w$} │ {:>w$.1} │ {:>w$.1}\n \
156              frames {:>w$} │ {:>w$.1} │\n\
157             ────────{empty:─>w$}─┼─{empty:─>w$}─┼─{empty:─>w$}─\n",
158            "TOTAL", "PER SEC", "PER FRAME",
159            human_time(self.time), human_time(per_f.time),
160            self.calls, per_s.calls, per_f.calls,
161            self.frames, per_s.frames,
162            empty = ""
163        )?;
164
165        let labels = ["objs", "prims", "verts", "frags"];
166        for (i, lbl) in (0..4).zip(labels) {
167            let [tot, per_s, per_f] = [self, &per_s, &per_f].map(|s| s.throughput()[i]);
168
169            if f.alternate() {
170                writeln!(f, " {lbl:6} {tot:#w$} │ {per_s:#w$} │ {per_f:#w$}")?;
171            } else {
172                writeln!(f, " {lbl:6} {tot:w$} │ {per_s:w$} │ {per_f:w$}")?;
173            }
174        }
175        Ok(())
176    }
177}
178
179impl Display for Throughput {
180    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
181        let &Self { i, o } = self;
182        let w = f.width().unwrap_or(10);
183        if f.alternate() {
184            if i == 0 {
185                write!(f, "{:>w$}", "--")
186            } else {
187                let pct = 100.0 * o as f32 / i as f32;
188                write!(f, "{pct:>w$.1}%", w = w - 1)
189            }
190        } else {
191            let io = format!("{} / {}", human_num(i), human_num(o));
192            write!(f, "{io:>w$}")
193        }
194    }
195}
196
197impl AddAssign for Stats {
198    /// Appends the stats of `other` to `self`.
199    fn add_assign(&mut self, other: Self) {
200        self.time += other.time;
201        self.calls += other.calls;
202        self.frames += other.frames;
203        for i in 0..4 {
204            *self.throughput_mut()[i] += other.throughput()[i];
205        }
206    }
207}
208
209impl AddAssign for Throughput {
210    fn add_assign(&mut self, rhs: Self) {
211        self.i += rhs.i;
212        self.o += rhs.o;
213    }
214}
215
216fn human_num(n: usize) -> String {
217    if n < 1_000 {
218        format!("{n:5}")
219    } else if n < 100_000 {
220        format!("{:4.1}k", n as f32 / 1_000.)
221    } else if n < 1_000_000 {
222        format!("{:4}k", n / 1_000)
223    } else if n < 100_000_000 {
224        format!("{:4.1}M", n as f32 / 1_000_000.)
225    } else if n < 1_000_000_000 {
226        format!("{:4}M", n / 1_000_000)
227    } else if (n as u64) < 100_000_000_000 {
228        format!("{:4.1}G", n as f32 / 1_000_000_000.)
229    } else {
230        format!("{n:5.1e}")
231    }
232}
233
234fn human_time(d: Duration) -> String {
235    let secs = d.as_secs_f32();
236    if secs < 1e-3 {
237        format!("{:4.1}μs", secs * 1_000_000.)
238    } else if secs < 1.0 {
239        format!("{:4.1}ms", secs * 1_000.)
240    } else if secs < 60.0 {
241        format!("{:.1}s", secs)
242    } else {
243        format!("{:.0}min {:02.0}s", secs / 60.0, secs % 60.0)
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use core::array::from_fn;
250    use core::time::Duration;
251
252    use super::*;
253
254    #[test]
255    fn stats_display() {
256        let [objs, prims, verts, frags] = from_fn(|i| Throughput {
257            i: 12345 * (i + 1),
258            o: 4321 * (i + 1),
259        });
260        let stats = Stats {
261            frames: 1234.0,
262            calls: 5678.0,
263            time: Duration::from_millis(4321),
264            objs,
265            prims,
266            verts,
267            frags,
268            #[cfg(feature = "std")]
269            start: None,
270        };
271
272        assert_eq!(
273            format!("{stats}"),
274            " \
275 STATS             TOTAL │          PER SEC │        PER FRAME
276─────────────────────────┼──────────────────┼──────────────────
277 time               4.3s │                  │            3.5ms
278 calls              5678 │           1314.0 │              4.6
279 frames             1234 │            285.6 │
280─────────────────────────┼──────────────────┼──────────────────
281 objs      12.3k /  4.3k │     2.9k /  1.0k │       10 /     3
282 prims     24.7k /  8.6k │     5.7k /  2.0k │       20 /     7
283 verts     37.0k / 13.0k │     8.6k /  3.0k │       30 /    10
284 frags     49.4k / 17.3k │    11.4k /  4.0k │       40 /    14
285"
286        );
287
288        assert_eq!(
289            format!("{stats:#}"),
290            " \
291 STATS             TOTAL │          PER SEC │        PER FRAME
292─────────────────────────┼──────────────────┼──────────────────
293 time               4.3s │                  │            3.5ms
294 calls              5678 │           1314.0 │              4.6
295 frames             1234 │            285.6 │
296─────────────────────────┼──────────────────┼──────────────────
297 objs              35.0% │            35.0% │            30.0%
298 prims             35.0% │            35.0% │            35.0%
299 verts             35.0% │            35.0% │            33.3%
300 frags             35.0% │            35.0% │            35.0%
301"
302        );
303    }
304
305    #[test]
306    fn human_nums() {
307        assert_eq!(human_num(10), "   10");
308        assert_eq!(human_num(123), "  123");
309        assert_eq!(human_num(1_234), " 1.2k");
310        assert_eq!(human_num(12_3456), " 123k");
311        assert_eq!(human_num(1_234_567), " 1.2M");
312        assert_eq!(human_num(123_456_789), " 123M");
313        assert_eq!(human_num(1_234_567_890), " 1.2G");
314        assert_eq!(human_num(123_456_789_000), "1.2e11");
315    }
316
317    #[test]
318    fn human_times() {
319        assert_eq!(human_time(Duration::from_micros(123)), "123.0μs");
320        assert_eq!(human_time(Duration::from_millis(123)), "123.0ms");
321        assert_eq!(human_time(Duration::from_millis(1234)), "1.2s");
322        assert_eq!(human_time(Duration::from_secs(1234)), "21min 34s");
323    }
324}