all_is_cubes_base/
time.rs

1use core::cmp::Ordering;
2use core::fmt;
3use core::ops;
4
5use crate::util::ConciseDebug;
6use manyfmt::Refmt as _;
7
8// -------------------------------------------------------------------------------------------------
9
10#[doc(no_inline)]
11pub use bevy_platform::time::Instant;
12#[doc(no_inline)]
13pub use core::time::Duration;
14
15// -------------------------------------------------------------------------------------------------
16
17/// A request regarding how much real time should be spent on a computation.
18#[derive(Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
19#[non_exhaustive]
20pub enum Deadline {
21    /// Stop immediately after the minimum necessary activities.
22    ///
23    /// Arithmetically, this is “negative infinity”; it is less than all finite deadlines.
24    Asap,
25    /// Stop as close to the given time (before or after) as is feasible.
26    At(Instant),
27    /// Don't stop until all the work is done.
28    ///
29    /// This choice is appropriate when deterministic results are desired.
30    ///
31    /// Arithmetically, this is “positive infinity”; it is greater than all finite deadlines.
32    Whenever,
33}
34
35impl Deadline {
36    /// Returns the time between `start` and the deadline, or [`None`] if there is no
37    /// deadline and the remaining time is unbounded.
38    ///
39    /// If the deadline is already past, returns `Some(Duration::ZERO)`
40    ///
41    /// (This does not return [`Duration::MAX`] since that would be likely to cause
42    /// unintended arithmetic overflows.)
43    #[inline]
44    pub fn remaining_since(&self, start: Instant) -> Option<Duration> {
45        match self {
46            Deadline::Asap => Some(Duration::ZERO),
47            Deadline::At(deadline) => Some(deadline.saturating_duration_since(start)),
48            Deadline::Whenever => None,
49        }
50    }
51}
52
53impl ops::Add<Duration> for Deadline {
54    type Output = Self;
55    #[inline]
56    fn add(self, rhs: Duration) -> Self::Output {
57        match self {
58            Deadline::Asap => Deadline::Asap,
59            Deadline::At(i) => Deadline::At(i + rhs),
60            Deadline::Whenever => Deadline::Whenever,
61        }
62    }
63}
64impl ops::Sub<Duration> for Deadline {
65    type Output = Self;
66    #[inline]
67    fn sub(self, rhs: Duration) -> Self::Output {
68        match self {
69            Deadline::Asap => Deadline::Asap,
70            #[allow(clippy::unchecked_time_subtraction, reason = "TODO: can we do better?")]
71            Deadline::At(i) => Deadline::At(i - rhs),
72            Deadline::Whenever => Deadline::Whenever,
73        }
74    }
75}
76
77// Allow comparing `Deadline` and `Instant` without wrapping.
78impl PartialEq<Instant> for Deadline {
79    #[mutants::skip] // trivial
80    #[inline]
81    fn eq(&self, other: &Instant) -> bool {
82        self.partial_cmp(other) == Some(Ordering::Equal)
83    }
84}
85impl PartialEq<Deadline> for Instant {
86    #[mutants::skip] // trivial
87    #[inline]
88    fn eq(&self, other: &Deadline) -> bool {
89        other.eq(self)
90    }
91}
92impl PartialOrd<Instant> for Deadline {
93    #[inline]
94    fn partial_cmp(&self, other: &Instant) -> Option<Ordering> {
95        Some(match self {
96            Deadline::Asap => Ordering::Less,
97            Deadline::At(i) => i.cmp(other),
98            Deadline::Whenever => Ordering::Greater,
99        })
100    }
101}
102impl PartialOrd<Deadline> for Instant {
103    #[inline]
104    fn partial_cmp(&self, other: &Deadline) -> Option<Ordering> {
105        other.partial_cmp(self).map(Ordering::reverse)
106    }
107}
108
109impl From<Instant> for Deadline {
110    #[inline]
111    fn from(value: Instant) -> Self {
112        Self::At(value)
113    }
114}
115
116// -------------------------------------------------------------------------------------------------
117
118/// Summary of the time taken by a set of events.
119///
120/// This type is produced by performance measurements within several subsystems of All is Cubes.
121///
122/// It may be created by [`TimeStats::default()`] (empty), or [`TimeStats::one()`] (single event),
123/// and multiple events may be aggregated using the `+=` operator.
124/// It may be formatted for reading using the [`fmt::Display`] implementation.
125#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
126#[non_exhaustive]
127#[expect(clippy::module_name_repetitions, reason = "TODO: find a better name")]
128pub struct TimeStats {
129    /// The number of events aggregated into this [`TimeStats`].
130    pub count: usize,
131    /// The sum of the durations of all events.
132    pub sum: Duration,
133    /// The minimum duration of all events, or [`None`] if there were no events.
134    pub min: Option<Duration>,
135    /// The maximum duration of all events, or [`Duration::ZERO`] if there were no events.
136    pub max: Duration,
137}
138
139impl TimeStats {
140    /// Constructs a [`TimeStats`] for a single event.
141    ///
142    /// Multiple of these may then be aggregated using the `+=` operator.
143    #[inline]
144    pub const fn one(duration: Duration) -> Self {
145        Self {
146            count: 1,
147            sum: duration,
148            min: Some(duration),
149            max: duration,
150        }
151    }
152
153    /// Record an event based on the given previous time and current time, then update
154    /// the previous time value.
155    ///
156    /// Returns the duration that was recorded.
157    #[doc(hidden)] // for now, not making writing conveniences public
158    #[inline]
159    pub fn record_consecutive_interval(
160        &mut self,
161        last_marked_instant: &mut Instant,
162        now: Instant,
163    ) -> Duration {
164        let previous = *last_marked_instant;
165        *last_marked_instant = now;
166
167        let duration = now.saturating_duration_since(previous);
168        *self += Self::one(duration);
169        duration
170    }
171}
172
173impl ops::AddAssign for TimeStats {
174    #[inline]
175    fn add_assign(&mut self, rhs: Self) {
176        *self = TimeStats {
177            count: self.count + rhs.count,
178            sum: self.sum + rhs.sum,
179            min: self.min.map_or(rhs.min, |value| Some(value.min(rhs.min?))),
180            max: self.max.max(rhs.max),
181        };
182    }
183}
184
185impl fmt::Display for TimeStats {
186    #[allow(clippy::missing_inline_in_public_items)]
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        let max = self.max.refmt(&ConciseDebug);
189        let count = self.count;
190        let sum = self.sum.refmt(&ConciseDebug);
191        match self.min {
192            None => write!(f, "(-------- .. {max}) for {count:3}, total {sum}"),
193            Some(min) => {
194                let min = min.refmt(&ConciseDebug);
195                write!(f, "({min} .. {max}) for {count:3}, total {sum}")
196            }
197        }
198    }
199}
200
201// -------------------------------------------------------------------------------------------------
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn deadline_ordering() {
209        let i = Instant::now();
210        let mut deadlines = [
211            Deadline::At(i + Duration::from_secs(1)),
212            Deadline::Asap,
213            Deadline::Whenever,
214            Deadline::At(i),
215        ];
216        deadlines.sort();
217        assert_eq!(
218            deadlines,
219            [
220                Deadline::Asap,
221                Deadline::At(i),
222                Deadline::At(i + Duration::from_secs(1)),
223                Deadline::Whenever,
224            ]
225        );
226    }
227}