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}