Skip to main content

moonlit_pdk/
clock.rs

1//! Monotonic clock access. `ctx.clock().start()` yields a `Timer` whose
2//! `elapsed_ms()` re-reads the live clock — an honest stopwatch across an
3//! arbitrarily long operation, with the plugin never touching raw nanos.
4
5use crate::context::Host;
6
7/// Clock handle, created via `ctx.clock()`.
8pub struct Clock<'a> {
9  host: &'a dyn Host,
10}
11
12impl<'a> Clock<'a> {
13  #[must_use]
14  pub(crate) fn new(host: &'a dyn Host) -> Self {
15    Self { host }
16  }
17  /// Current monotonic reading in nanoseconds.
18  #[must_use]
19  pub fn now(&self) -> u64 {
20    self.host.monotonic_nanos()
21  }
22  /// Start a stopwatch at the current instant.
23  pub fn start(&self) -> Timer<'a> {
24    Timer {
25      host: self.host,
26      start: self.host.monotonic_nanos(),
27    }
28  }
29  /// Sleep for `ms` milliseconds. Backed by the host's monotonic sleep.
30  pub fn sleep_ms(&self, ms: u64) {
31    self.host.sleep_nanos(ms.saturating_mul(1_000_000));
32  }
33}
34
35/// A running stopwatch: holds the host and start instant; `elapsed_ms`
36/// re-reads the live clock, so it stays correct however long the gap.
37#[must_use = "a timer measures nothing until `.elapsed_ms()` is read"]
38pub struct Timer<'a> {
39  host: &'a dyn Host,
40  start: u64,
41}
42
43impl Timer<'_> {
44  /// Elapsed milliseconds since `start`, saturating.
45  #[must_use]
46  pub fn elapsed_ms(&self) -> u64 {
47    self.host.monotonic_nanos().saturating_sub(self.start) / 1_000_000
48  }
49}
50
51#[cfg(test)]
52mod tests {
53  use crate::Context;
54  use crate::testing::MockHost;
55
56  #[test]
57  fn timer_elapsed_ms_from_scripted_clock() {
58    // start() reads 0ns; elapsed_ms() reads 5_000_000ns -> 5ms.
59    let host = MockHost::new().with_clock(&[0, 5_000_000]);
60    let ctx = Context::new(&host, "/w".into(), "s".into());
61    let t = ctx.clock().start();
62    assert_eq!(t.elapsed_ms(), 5);
63  }
64
65  #[test]
66  fn now_passes_through_and_saturates_non_increasing() {
67    let host = MockHost::new().with_clock(&[10, 3]); // second reading < first
68    let ctx = Context::new(&host, "/w".into(), "s".into());
69    let t = ctx.clock().start(); // reads 10
70    assert_eq!(t.elapsed_ms(), 0); // (3 - 10) saturates to 0, never underflows
71  }
72
73  #[test]
74  fn empty_clock_script_reads_zero() {
75    let host = MockHost::new();
76    let ctx = Context::new(&host, "/w".into(), "s".into());
77    assert_eq!(ctx.clock().now(), 0);
78  }
79
80  #[test]
81  fn sleep_ms_converts_to_nanos_and_records() {
82    let host = MockHost::new();
83    let ctx = Context::new(&host, "/w".into(), "s".into());
84    ctx.clock().sleep_ms(5);
85    ctx.clock().sleep_ms(0);
86    assert_eq!(host.recorded_sleeps(), vec![5_000_000, 0]);
87  }
88}