Skip to main content

u_nesting_core/
timing.rs

1//! WASM-compatible timing abstraction.
2//!
3//! Wall-clock timing backed by [`web_time::Instant`], which transparently maps
4//! to [`std::time::Instant`] on native targets and to `performance.now()` on
5//! `wasm32`. This keeps time-based termination (`time_limit_ms`) working on both
6//! native and WASM — iteration limits remain the safety net that bounds every
7//! algorithm regardless of clock resolution.
8//!
9//! A previous WASM build used a no-op timer that always reported zero elapsed
10//! time; that silently disabled `time_limit_ms` on WASM, so strategies ran to
11//! their full iteration cap (multi-second freezes in the browser).
12
13mod inner {
14    use web_time::{Duration, Instant};
15
16    /// A wall-clock timer backed by [`web_time::Instant`].
17    ///
18    /// On native targets this is exactly [`std::time::Instant`]; on `wasm32` it
19    /// uses the JS `performance.now()` clock via `web-time`.
20    #[derive(Debug, Clone, Copy)]
21    pub struct Timer(Instant);
22
23    impl Timer {
24        /// Starts the timer.
25        pub fn now() -> Self {
26            Timer(Instant::now())
27        }
28
29        /// Returns the elapsed time since the timer was started.
30        pub fn elapsed(&self) -> Duration {
31            self.0.elapsed()
32        }
33
34        /// Returns elapsed time in milliseconds.
35        pub fn elapsed_ms(&self) -> u64 {
36            self.0.elapsed().as_millis() as u64
37        }
38    }
39}
40
41pub use inner::Timer;
42
43/// Whether `limit` (if any) has elapsed since `start`.
44pub(crate) fn expired(start: &Timer, limit: Option<web_time::Duration>) -> bool {
45    limit.is_some_and(|limit| start.elapsed() > limit)
46}
47
48/// Evaluates `items` a batch at a time, stopping once `limit` has elapsed since
49/// `start`, and drops the items it did not reach.
50///
51/// A runner that evaluates a whole population before looking at the clock
52/// overruns its time limit by that whole population — many seconds when one
53/// evaluation is a full placement. Checking between batches bounds the overrun
54/// to one batch. The first batch is always evaluated, so at least one item
55/// survives. A batch is as many items as there are worker threads, so parallel
56/// evaluation keeps its throughput.
57pub(crate) fn evaluate_within<T>(
58    items: &mut Vec<T>,
59    start: &Timer,
60    limit: Option<web_time::Duration>,
61    mut evaluate: impl FnMut(&mut [T]),
62) {
63    #[cfg(feature = "parallel")]
64    let batch = rayon::current_num_threads().max(1);
65    #[cfg(not(feature = "parallel"))]
66    let batch = 1;
67
68    let mut done = 0;
69    while done < items.len() {
70        let end = (done + batch).min(items.len());
71        evaluate(&mut items[done..end]);
72        done = end;
73        if expired(start, limit) {
74            break;
75        }
76    }
77    items.truncate(done);
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use web_time::Duration;
84
85    #[test]
86    fn evaluation_stops_at_the_limit_and_keeps_what_it_evaluated() {
87        let start = Timer::now();
88        let mut items: Vec<u32> = (0..1000).collect();
89        evaluate_within(
90            &mut items,
91            &start,
92            Some(Duration::from_millis(20)),
93            |batch| {
94                std::thread::sleep(Duration::from_millis(5));
95                batch.iter_mut().for_each(|x| *x += 1_000_000);
96            },
97        );
98        assert!(
99            !items.is_empty() && items.len() < 1000,
100            "evaluated {}",
101            items.len()
102        );
103        assert!(
104            items.iter().all(|&x| x >= 1_000_000),
105            "an unevaluated item survived"
106        );
107    }
108
109    #[test]
110    fn without_a_limit_everything_is_evaluated() {
111        let start = Timer::now();
112        let mut items = vec![0u8; 50];
113        evaluate_within(&mut items, &start, None, |batch| {
114            batch.iter_mut().for_each(|x| *x = 1)
115        });
116        assert_eq!(items, vec![1u8; 50]);
117    }
118}