wiwi/clock_timer.rs
1pub extern crate chrono;
2extern crate tokio;
3
4use crate::prelude::*;
5use chrono::{ DateTime, Local, TimeDelta, TimeZone };
6use std::future::Future;
7use tokio::time::sleep;
8
9/// An interval tracking clock. Takes a start time, an end time or a run duration,
10/// and an interval. Calls to [`tick`][ClockTimer::tick] will return only if
11/// the current time is at or past the time of the next interval, waiting so
12/// that it is before returning. It yields timing information when returning.
13/// If this falls behind time for some reason, the ticks will be yielded with
14/// the time information at when it was supposed to yield, until catching up.
15pub struct ClockTimer {
16 /// The time the next tick will trigger
17 ///
18 /// In a newly created clock timer, this is the starting time
19 next_tick: DateTime<Local>,
20
21 /// How often this clock timer will yield ticks
22 interval: TimeDelta,
23
24 /// How much time has elapsed since the first tick
25 ///
26 /// More precisely, this tracks how much time is between the first
27 /// tick, and the next tick if there is one. Otherwise, the value in this
28 /// field is meaningless.
29 elapsed: TimeDelta,
30
31 /// How much time is remaining
32 ///
33 /// More precisely, this tracks how much time is remaining after the time in
34 /// [`next_tick`](ClockTimer::next_tick)
35 remaining: TimeDelta
36}
37
38/// Timing information for one tick
39pub struct Tick {
40 /// The time of this tick (or, if this tick was delayed, what time this tick
41 /// was scheduled to be yielded at)
42 this_tick: DateTime<Local>,
43
44 /// The duration from the first tick to this tick (scheduled time),
45 /// ie. the time the clock timer has been running
46 elapsed: TimeDelta,
47
48 /// The duration from this tick to the last tick (scheduled time),
49 /// ie. the remaining time the clock timer will run before stopping
50 remaining: TimeDelta,
51
52 /// Whether or not this tick has been delayed
53 ///
54 /// Note: We have not properly tested this (except in the
55 /// [april fools prank](https://www.fimfiction.net/story/553695/) that this
56 /// was built for of course heh), and we suspect this value is always `true`
57 /// no matter if it was _actually_ delayed by the definition of what you'd
58 /// expect. You might expect this to be `true` if previous task took too long
59 /// or something, ie. this was called delayed because of the application
60 /// logic itself, rather than little OS scheduling runtime things, ie. OS
61 /// thread scheduling, tokio task scheduling, syncronisation stuff, etc etc.
62 /// We expect this to always be `true`, because tokio will not wake up and
63 /// poll again a task until the time has passed, and never before, and if
64 /// there's any tiny bit of delay introduced anywhere detectable by the time
65 /// APIs, be it from OS thread syncronisation, or tokio syncronisation, or
66 /// the arithmetic and time putting the task away to sleep by the async
67 /// runtime, or something, which, based on how these things work, this will
68 /// likely always happen and make ths `true`.
69 ///
70 /// ...whew ramble
71 delayed: bool
72}
73
74impl ClockTimer {
75 /// Gets a [`ClockTimer`] builder
76 #[inline]
77 pub fn builder() -> builder2::Builder {
78 builder2::Builder::new()
79 }
80
81 /// Runs the next tick and returns timing information for it, if this
82 /// interval is not finished already.
83 #[inline]
84 pub async fn tick(&mut self) -> Option<Tick> {
85 if self.remaining < TimeDelta::zero() { return None }
86
87 let mut tick = Tick {
88 this_tick: self.next_tick,
89 elapsed: self.elapsed,
90 remaining: self.remaining,
91 delayed: false
92 };
93
94 self.next_tick += self.interval;
95 self.elapsed += self.interval;
96 self.remaining -= self.interval;
97
98 let delta = tick.this_tick - Local::now();
99
100 // TODO: rethink delayed detection?
101 // because it is highly likely that due to various factors out of our
102 // control (eg. OS scheduling, tokio runtime scheduling, work stealing,
103 // syncronisation stuff, etc etc), we won't get polled until technically
104 // after our scheduled time, leading this to always be true? tests needed,
105 // and this delay is in the order of milliseconds, or maybe even micros/nanos
106 if delta <= TimeDelta::zero() {
107 // highly unlikely, but if delta somehow manages to hit exactly 0,
108 // we consider it on time. Maybe we should say like, if now is
109 // within 1ms after the set tick time? dunno (see above todo comment)
110 if delta < TimeDelta::zero() { tick.delayed = true }
111 return Some(tick)
112 }
113
114 // we checked above and returned if `delta` is lte zero,
115 // so this won't panic
116 sleep(delta.to_std().unwrap()).await;
117 Some(tick)
118 }
119
120 /// Convenience function, equivalent to running a `while let Some(tick)`
121 /// loop. When awaited on, the closure provided will be called every tick.
122 /// This consumes self and runs it to completion.
123 #[inline]
124 pub async fn run_to_end<F, Fu>(mut self, mut f: F)
125 where
126 F: FnMut(Tick) -> Fu,
127 Fu: Future<Output = ()>
128 {
129 while let Some(tick) = self.tick().await {
130 f(tick).await;
131 }
132 }
133}
134
135impl Tick {
136 /// Get time of this tick
137 #[inline]
138 pub fn time(&self) -> DateTime<Local> {
139 self.this_tick
140 }
141
142 /// Get elapsed time since the start of this timer
143 #[inline]
144 pub fn elapsed(&self) -> TimeDelta {
145 self.elapsed
146 }
147
148 /// Get remaining runtime of this timer
149 #[inline]
150 pub fn remaining(&self) -> TimeDelta {
151 self.remaining
152 }
153
154 /// Get start time of this timer
155 #[inline]
156 pub fn start_time(&self) -> DateTime<Local> {
157 self.this_tick - self.elapsed
158 }
159
160 /// Get end time of this timer
161 #[inline]
162 pub fn end_time(&self) -> DateTime<Local> {
163 self.this_tick + self.remaining
164 }
165
166 /// Get total runtime of this timer, including elapsed
167 /// time and remaining time
168 #[inline]
169 pub fn total_runtime(&self) -> TimeDelta {
170 self.elapsed + self.remaining
171 }
172
173 /// Returns if this tick was delayed. This tick is considered delayed if
174 /// the tick function was called after the time of this tick had already past.
175 ///
176 /// Note: does the same thing as [`past_due`][Self::past_due]
177 #[inline]
178 pub fn delayed(&self) -> bool {
179 self.delayed
180 }
181
182 /// Returns if this tick is past due. This tick is considered past due if
183 /// the tick function was called after the time of this tick had already past.
184 ///
185 /// Note: does the same thing as [`delayed`][Self::delayed]
186 #[inline]
187 pub fn past_due(&self) -> bool {
188 self.delayed
189 }
190}
191
192/// [`ClockTimer`] builder structs
193pub mod builder {
194 use super::*;
195
196 /// Builder for [`ClockTimer`].
197 pub struct Builder {
198 /// Forcing users to use [`new`] because I dunno style or something, that
199 /// [`new`] call and this struct is just literally gonna get optimised
200 /// away to nothing
201 ///
202 /// [`new`]: Builder::new
203 __private: ()
204 }
205
206 impl Builder {
207 /// New builder. You can also obtain a builder through [`ClockTimer::builder`]
208 // there is no default that makes sense here
209 #[expect(clippy::new_without_default, reason = "api design")]
210 #[inline]
211 pub fn new() -> Self {
212 // its gonna optimise away to be noop lol
213 // I think it provides a good API though,
214 Self { __private: () }
215 }
216
217 /// Sets the start date/time of the ClockTimer, or in other words, the
218 /// time of the first tick.
219 #[inline]
220 pub fn with_start_datetime<TZ: TimeZone>(self, datetime: DateTime<TZ>) -> BuilderWithStart {
221 let start = datetime.with_timezone(&Local);
222 BuilderWithStart { start }
223 }
224 }
225
226 /// Intermediate builder state struct, returned after calling a method on
227 /// [`Builder`]
228 ///
229 /// Most likely you won't need to ever interact with this type directly.
230 /// You're probably looking for [`Builder`].
231 pub struct BuilderWithStart {
232 /// The provided start datetime
233 start: DateTime<Local>
234 }
235
236 impl BuilderWithStart {
237 /// Sets the end date/time of the ClockTimer. ClockTimer will run until
238 /// this time is _passed_. A tick _will_ be emitted if the last tick is equal
239 /// to the end time.
240 #[inline]
241 pub fn with_end_datetime<TZ: TimeZone>(self, datetime: DateTime<TZ>) -> BuilderWithEnd {
242 let Self { start } = self;
243 let end = datetime.with_timezone(&Local);
244 BuilderWithEnd { start, end }
245 }
246
247 /// Sets a duration to run this ClockTimer for. Internally, the end time
248 /// is calculated and stored based on start time and the provided duration.
249 #[inline]
250 pub fn with_duration(self, duration: TimeDelta) -> BuilderWithEnd {
251 let Self { start } = self;
252 let end = start + duration;
253 BuilderWithEnd { start, end }
254 }
255 }
256
257 /// Intermediate builder state struct, returned after calling a method on
258 /// [`BuilderWithStart`]
259 ///
260 /// Most likely you won't need to ever interact with this type directly.
261 /// You're probably looking for [`Builder`].
262 pub struct BuilderWithEnd {
263 /// The provided start datetime (from prev stage of builder)
264 start: DateTime<Local>,
265
266 /// The end datetime, either provided or calculated
267 /// from a runtime duration
268 end: DateTime<Local>
269 }
270
271 impl BuilderWithEnd {
272 /// Sets interval to run at, or the time between ticks.
273 #[inline]
274 pub fn with_interval(self, interval: TimeDelta) -> BuilderWithInterval {
275 let Self { start, end } = self;
276 BuilderWithInterval { start, end, interval }
277 }
278 }
279
280 /// Intermediate builder state struct, returned after calling a method on
281 /// [`BuilderWithEnd`]
282 ///
283 /// Most likely you won't need to ever interact with this type directly.
284 /// You're probably looking for [`Builder`].
285 pub struct BuilderWithInterval {
286 /// The provided start datetime (from prev stage of builder)
287 start: DateTime<Local>,
288
289 /// The end datetime, either provided or calculated from a runtime
290 /// duration (from prev stage of builder)
291 end: DateTime<Local>,
292
293 /// The provided trigger interval
294 interval: TimeDelta
295 }
296
297 impl BuilderWithInterval {
298 /// Builds and returns a [`ClockTimer`]
299 #[inline]
300 pub fn build(self) -> ClockTimer {
301 let Self { start: next_tick, end, interval } = self;
302 let elapsed = TimeDelta::zero();
303 let remaining = end - next_tick;
304
305 ClockTimer { next_tick, interval, elapsed, remaining }
306 }
307 }
308}
309
310pub mod builder2 {
311 use super::*;
312 use crate::builder::{ Init, Uninit };
313
314 #[repr(transparent)]
315 pub struct Builder<
316 Start = Uninit,
317 End = Uninit,
318 Interval = Uninit
319 > {
320 inner: BuilderInner,
321 __marker: PhantomData<(Start, End, Interval)>
322 }
323
324 struct BuilderInner {
325 start: MaybeUninit<DateTime<Local>>,
326 end: MaybeUninit<DateTime<Local>>,
327 interval: MaybeUninit<TimeDelta>
328 }
329
330 impl Builder {
331 #[expect(clippy::new_without_default, reason = "api design")]
332 #[inline]
333 pub fn new() -> Builder {
334 Builder {
335 inner: BuilderInner {
336 start: MaybeUninit::uninit(),
337 end: MaybeUninit::uninit(),
338 interval: MaybeUninit::uninit()
339 },
340 __marker: PhantomData
341 }
342 }
343 }
344
345 impl<End, Interval> Builder<Uninit, End, Interval> {
346 #[inline]
347 pub fn with_start_datetime<TZ: TimeZone>(mut self, datetime: DateTime<TZ>) -> Builder<Init, End, Interval> {
348 self.inner.start.write(datetime.with_timezone(&Local));
349 Builder { inner: self.inner, __marker: PhantomData }
350 }
351 }
352
353 impl<Start, Interval> Builder<Start, Uninit, Interval> {
354 #[inline]
355 pub fn with_end_datetime<TZ: TimeZone>(mut self, datetime: DateTime<TZ>) -> Builder<Start, Init, Interval> {
356 self.inner.end.write(datetime.with_timezone(&Local));
357 Builder { inner: self.inner, __marker: PhantomData }
358 }
359 }
360
361 impl<Interval> Builder<Init, Uninit, Interval> {
362 #[inline]
363 pub fn with_duration(mut self, duration: TimeDelta) -> Builder<Init, Init, Interval> {
364 // SAFETY: enforced by type system (typestate pattern)
365 let start = unsafe { self.inner.start.assume_init() };
366
367 self.inner.end.write(start + duration);
368 Builder { inner: self.inner, __marker: PhantomData }
369 }
370 }
371
372 impl<Start, End> Builder<Start, End, Uninit> {
373 #[inline]
374 pub fn with_interval(mut self, interval: TimeDelta) -> Builder<Start, End, Init> {
375 self.inner.interval.write(interval);
376 Builder { inner: self.inner, __marker: PhantomData }
377 }
378 }
379
380 impl Builder<Init, Init, Init> {
381 #[inline]
382 pub fn build(self) -> ClockTimer {
383 // SAFETY: enforced by type system (typestate pattern)
384 let start = unsafe { self.inner.start.assume_init() };
385 // SAFETY: enforced by type system (typestate pattern)
386 let end = unsafe { self.inner.end.assume_init() };
387 // SAFETY: enforced by type system (typestate pattern)
388 let interval = unsafe { self.inner.interval.assume_init() };
389
390 ClockTimer {
391 next_tick: start,
392 interval,
393 elapsed: TimeDelta::zero(),
394 remaining: end - start
395 }
396 }
397 }
398}