Skip to main content

msime_engine/
time.rs

1//! 引擎读取的单调时钟。原生平台就是 `std::time::Instant`;wasm32-unknown-unknown 的 std 没有时钟,`std::time::Instant::now` 会 panic,所以那里换成宿主经 `set_host_clock` 注入的毫秒读数(浏览器里是 `performance.now()` 和 `Date.now()`)。注入之前 `Instant::now()` 返回零点,经过的时间读作 0,不会 panic。
2
3#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
4pub use std::time::Instant;
5
6#[cfg(all(target_family = "wasm", target_os = "unknown"))]
7pub use host::Instant;
8
9/// 宿主提供的两个时钟读数,都是毫秒。
10#[derive(Clone, Copy)]
11pub struct HostClock {
12    /// 单调毫秒(performance.now)
13    pub steady_ms: fn() -> f64,
14    /// Unix 毫秒(Date.now)
15    pub wall_ms: fn() -> f64,
16}
17
18/// 只在 wasm 上有效;原生平台是空操作
19pub fn set_host_clock(clock: HostClock) {
20    #[cfg(all(target_family = "wasm", target_os = "unknown"))]
21    host::store(clock);
22    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
23    let _ = clock;
24}
25
26/// wasm 上没有注入时返回 None
27pub fn host_wall_ms() -> Option<f64> {
28    #[cfg(all(target_family = "wasm", target_os = "unknown"))]
29    return host::injected().map(|clock| (clock.wall_ms)());
30    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
31    None
32}
33
34/// wasm 上的 `Instant`。原生测试也编译这一份,让算术和注入前的行为能在原生 `cargo test` 里验证。
35#[cfg(any(test, all(target_family = "wasm", target_os = "unknown")))]
36mod host {
37    use std::ops::{Add, AddAssign, Sub, SubAssign};
38    use std::sync::{PoisonError, RwLock};
39    use std::time::Duration;
40
41    use super::HostClock;
42
43    static CLOCK: RwLock<Option<HostClock>> = RwLock::new(None);
44
45    #[cfg_attr(
46        all(test, not(all(target_family = "wasm", target_os = "unknown"))),
47        expect(
48            dead_code,
49            reason = "原生平台的 set_host_clock 是空操作,测试也不注入全局时钟,避免并行测试互相干扰"
50        )
51    )]
52    pub(super) fn store(clock: HostClock) {
53        *CLOCK.write().unwrap_or_else(PoisonError::into_inner) = Some(clock);
54    }
55
56    pub(super) fn injected() -> Option<HostClock> {
57        *CLOCK.read().unwrap_or_else(PoisonError::into_inner)
58    }
59
60    /// 自宿主时钟原点起的时间。
61    #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
62    pub struct Instant(Duration);
63
64    impl Instant {
65        pub fn now() -> Self {
66            Self::read(injected())
67        }
68
69        /// 没有注入时钟时是零点。
70        pub(super) fn read(clock: Option<HostClock>) -> Self {
71            Self::from_ms(clock.map_or(0.0, |clock| (clock.steady_ms)()))
72        }
73
74        /// 负数、NaN 和无穷按零点处理,超出 `Duration` 范围的读数取最大值,所以任何宿主读数都不会 panic。
75        pub(super) fn from_ms(millis: f64) -> Self {
76            if !millis.is_finite() || millis <= 0.0 {
77                return Instant(Duration::ZERO);
78            }
79            Instant(Duration::try_from_secs_f64(millis / 1000.0).unwrap_or(Duration::MAX))
80        }
81
82        pub fn saturating_duration_since(&self, earlier: Instant) -> Duration {
83            self.0.saturating_sub(earlier.0)
84        }
85
86        /// 与 std 的当前行为一致:`earlier` 更晚时返回零而不是 panic。
87        pub fn duration_since(&self, earlier: Instant) -> Duration {
88            self.saturating_duration_since(earlier)
89        }
90
91        pub fn elapsed(&self) -> Duration {
92            Instant::now().saturating_duration_since(*self)
93        }
94
95        pub fn checked_add(&self, duration: Duration) -> Option<Instant> {
96            self.0.checked_add(duration).map(Instant)
97        }
98
99        pub fn checked_sub(&self, duration: Duration) -> Option<Instant> {
100            self.0.checked_sub(duration).map(Instant)
101        }
102    }
103
104    impl Add<Duration> for Instant {
105        type Output = Instant;
106        /// 与 std 不同,溢出时饱和而不 panic。
107        fn add(self, other: Duration) -> Instant {
108            Instant(self.0.saturating_add(other))
109        }
110    }
111
112    impl AddAssign<Duration> for Instant {
113        fn add_assign(&mut self, other: Duration) {
114            *self = *self + other;
115        }
116    }
117
118    impl Sub<Duration> for Instant {
119        type Output = Instant;
120        /// 与 std 不同,减到时钟原点之前时停在原点而不 panic:注入前或页面刚打开时读数接近零,`now() - 窗口` 不能因此中断输入。
121        fn sub(self, other: Duration) -> Instant {
122            Instant(self.0.saturating_sub(other))
123        }
124    }
125
126    impl SubAssign<Duration> for Instant {
127        fn sub_assign(&mut self, other: Duration) {
128            *self = *self - other;
129        }
130    }
131
132    impl Sub<Instant> for Instant {
133        type Output = Duration;
134        fn sub(self, other: Instant) -> Duration {
135            self.saturating_duration_since(other)
136        }
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use std::time::Duration;
143
144    use super::host::Instant as HostInstant;
145    use super::HostClock;
146
147    #[test]
148    fn native_instant_is_std() {
149        let now: std::time::Instant = super::Instant::now();
150        assert!(now.elapsed() < Duration::from_secs(60));
151    }
152
153    #[test]
154    fn native_has_no_host_wall_clock() {
155        super::set_host_clock(HostClock {
156            steady_ms: || 5.0,
157            wall_ms: || 7.0,
158        });
159        assert_eq!(super::host_wall_ms(), None);
160    }
161
162    #[test]
163    fn uninjected_clock_reads_zero() {
164        let zero = HostInstant::read(None);
165        assert_eq!(zero, HostInstant::from_ms(0.0));
166        assert_eq!(
167            zero.saturating_duration_since(HostInstant::from_ms(0.0)),
168            Duration::ZERO
169        );
170    }
171
172    #[test]
173    fn host_instant_before_injection_is_the_origin() {
174        // 原生测试从不调用 host::store,所以全局时钟始终没有注入
175        let now = HostInstant::now();
176        assert_eq!(now, HostInstant::from_ms(0.0));
177        assert_eq!(now.elapsed(), Duration::ZERO);
178    }
179
180    #[test]
181    fn injected_clock_is_read() {
182        let clock = HostClock {
183            steady_ms: || 1500.0,
184            wall_ms: || 0.0,
185        };
186        assert_eq!(
187            HostInstant::read(Some(clock)).duration_since(HostInstant::from_ms(0.0)),
188            Duration::from_millis(1500)
189        );
190    }
191
192    #[test]
193    fn invalid_readings_clamp_to_zero() {
194        let zero = HostInstant::from_ms(0.0);
195        assert_eq!(HostInstant::from_ms(-3.0), zero);
196        assert_eq!(HostInstant::from_ms(f64::NAN), zero);
197        assert_eq!(HostInstant::from_ms(f64::INFINITY), zero);
198    }
199
200    #[test]
201    fn arithmetic_matches_std_semantics() {
202        let early = HostInstant::from_ms(1000.0);
203        let late = HostInstant::from_ms(3500.0);
204        assert_eq!(late - early, Duration::from_millis(2500));
205        assert_eq!(early - late, Duration::ZERO);
206        assert_eq!(late.duration_since(early), Duration::from_millis(2500));
207        assert_eq!(early.duration_since(late), Duration::ZERO);
208        assert_eq!(early + Duration::from_millis(2500), late);
209        assert_eq!(late - Duration::from_millis(2500), early);
210        assert_eq!(
211            early.checked_sub(Duration::from_secs(2)),
212            None,
213            "before the clock origin"
214        );
215        assert_eq!(early.checked_add(Duration::from_millis(2500)), Some(late));
216        assert_eq!(
217            HostInstant::from_ms(f64::MAX).checked_add(Duration::MAX),
218            None
219        );
220        assert_eq!(
221            HostInstant::from_ms(f64::MAX) + Duration::MAX,
222            HostInstant::from_ms(f64::MAX)
223        );
224        let mut moving = early;
225        moving += Duration::from_millis(2500);
226        assert_eq!(moving, late);
227        moving -= Duration::from_millis(2500);
228        assert_eq!(moving, early);
229        assert!(early < late);
230    }
231
232    #[test]
233    fn subtracting_past_origin_stops_at_origin() {
234        assert_eq!(
235            HostInstant::from_ms(1.0) - Duration::from_secs(1),
236            HostInstant::from_ms(0.0)
237        );
238    }
239}