runmat_runtime/builtins/timing/
tic.rs1use once_cell::sync::Lazy;
4use runmat_builtins::{
5 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
6 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
7};
8use runmat_macros::runtime_builtin;
9use runmat_time::Instant;
10use runmat_value::{IntValue, Value};
11use std::sync::Mutex;
12use std::time::Duration;
13
14use crate::builtins::common::spec::{
15 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
16 ReductionNaN, ResidencyPolicy, ShapeRequirements,
17};
18use crate::builtins::timing::type_resolvers::tic_type;
19
20#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::timing::tic")]
21pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
22 name: "tic",
23 op_kind: GpuOpKind::Custom("timer"),
24 supported_precisions: &[],
25 broadcast: BroadcastSemantics::None,
26 provider_hooks: &[],
27 constant_strategy: ConstantStrategy::InlineLiteral,
28 residency: ResidencyPolicy::GatherImmediately,
29 nan_mode: ReductionNaN::Include,
30 two_pass_threshold: None,
31 workgroup_size: None,
32 accepts_nan_mode: false,
33 notes: "Stopwatch state lives on the host. Providers are never consulted for tic/toc.",
34};
35
36#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::timing::tic")]
37pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
38 name: "tic",
39 shape: ShapeRequirements::Any,
40 constant_strategy: ConstantStrategy::InlineLiteral,
41 elementwise: None,
42 reduction: None,
43 emits_nan: false,
44 notes: "Timing builtins are executed eagerly on the host and do not participate in fusion.",
45};
46
47static MONOTONIC_ORIGIN: Lazy<Instant> = Lazy::new(Instant::now);
48static STOPWATCH: Lazy<Mutex<StopwatchState>> = Lazy::new(|| Mutex::new(StopwatchState::default()));
49
50#[cfg(test)]
51pub(crate) static TEST_GUARD: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
52
53#[cfg(test)]
54pub(crate) fn clear_stopwatch_for_test() {
55 STOPWATCH.lock().unwrap().stack.clear();
56}
57
58#[derive(Default)]
59struct StopwatchState {
60 stack: Vec<Instant>,
61}
62
63impl StopwatchState {
64 fn push(&mut self, instant: Instant) {
65 self.stack.push(instant);
66 }
67
68 fn latest(&self) -> Option<Instant> {
69 self.stack.last().copied()
70 }
71}
72
73const BUILTIN_NAME: &str = "tic";
74
75const TIC_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
76 name: "timerVal",
77 ty: BuiltinParamType::NumericScalar,
78 arity: BuiltinParamArity::Required,
79 default: None,
80 description: "Timer handle used by toc.",
81}];
82
83const TIC_INPUTS: [BuiltinParamDescriptor; 0] = [];
84
85const TIC_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
86 label: "timerVal = tic()",
87 inputs: &TIC_INPUTS,
88 outputs: &TIC_OUTPUT,
89}];
90
91const TIC_ERROR_STATE_LOCK: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
92 code: "RM.TIC.STATE_LOCK",
93 identifier: Some("RunMat:tic:StateLockFailed"),
94 when: "Internal stopwatch state cannot be acquired.",
95 message: "tic: failed to acquire stopwatch state",
96};
97
98const TIC_ERRORS: [BuiltinErrorDescriptor; 1] = [TIC_ERROR_STATE_LOCK];
99
100pub const TIC_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
101 signatures: &TIC_SIGNATURES,
102 output_mode: BuiltinOutputMode::Fixed,
103 completion_policy: BuiltinCompletionPolicy::Public,
104 errors: &TIC_ERRORS,
105};
106
107fn stopwatch_error_with_message(
108 builtin: &str,
109 message: impl Into<String>,
110 error: &BuiltinErrorDescriptor,
111) -> crate::RuntimeError {
112 let mut builder = crate::build_runtime_error(message).with_builtin(builtin);
113 if let Some(identifier) = error.identifier {
114 builder = builder.with_identifier(identifier);
115 }
116 builder.build()
117}
118
119#[runtime_builtin(
121 name = "tic",
122 category = "timing",
123 summary = "Start a high-resolution stopwatch and optionally return a toc handle.",
124 keywords = "tic,timing,profiling,benchmark",
125 sink = true,
126 type_resolver(tic_type),
127 descriptor(crate::builtins::timing::tic::TIC_DESCRIPTOR),
128 builtin_path = "crate::builtins::timing::tic"
129)]
130pub async fn tic_builtin() -> crate::BuiltinResult<Value> {
131 record_tic(BUILTIN_NAME).map(|handle| Value::Int(IntValue::U64(handle)))
132}
133
134pub(crate) fn record_tic(builtin: &str) -> Result<u64, crate::RuntimeError> {
136 let _origin = *MONOTONIC_ORIGIN;
137 let now = Instant::now();
138 {
139 let mut guard = STOPWATCH.lock().map_err(|_| {
140 stopwatch_error_with_message(
141 builtin,
142 TIC_ERROR_STATE_LOCK.message,
143 &TIC_ERROR_STATE_LOCK,
144 )
145 })?;
146 guard.push(now);
147 }
148 Ok(encode_instant(now))
149}
150
151pub(crate) fn latest_start(builtin: &str) -> Result<Option<Instant>, crate::RuntimeError> {
153 let guard = STOPWATCH.lock().map_err(|_| {
154 stopwatch_error_with_message(builtin, TIC_ERROR_STATE_LOCK.message, &TIC_ERROR_STATE_LOCK)
155 })?;
156 Ok(guard.latest())
157}
158
159pub(crate) fn encode_instant(instant: Instant) -> u64 {
161 instant
162 .checked_duration_since(*MONOTONIC_ORIGIN)
163 .unwrap_or(Duration::ZERO)
164 .as_secs_f64()
165 .to_bits()
166}
167
168pub(crate) fn decode_handle(
170 handle: u64,
171 builtin: &str,
172 error: &BuiltinErrorDescriptor,
173) -> Result<Instant, crate::RuntimeError> {
174 let seconds = f64::from_bits(handle);
175 if !seconds.is_finite() || seconds.is_sign_negative() {
176 return Err(stopwatch_error_with_message(builtin, error.message, error));
177 }
178 let duration = Duration::try_from_secs_f64(seconds)
179 .map_err(|_| stopwatch_error_with_message(builtin, error.message, error))?;
180 (*MONOTONIC_ORIGIN)
181 .checked_add(duration)
182 .ok_or_else(|| stopwatch_error_with_message(builtin, error.message, error))
183}
184
185pub(crate) fn elapsed_since(start: Instant) -> Duration {
188 Instant::now()
189 .checked_duration_since(start)
190 .unwrap_or(Duration::ZERO)
191}
192
193#[cfg(test)]
194pub(crate) mod tests {
195 use super::*;
196 use futures::executor::block_on;
197 use std::thread;
198 use std::time::Duration;
199
200 const TEST_INVALID_HANDLE_ERROR: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
201 code: "RM.TOC.INVALID_HANDLE",
202 identifier: Some("RunMat:toc:InvalidTimerHandle"),
203 when: "The timer handle is non-finite or negative.",
204 message: "toc: invalid timer handle",
205 };
206
207 fn reset_stopwatch() {
208 let mut guard = STOPWATCH.lock().unwrap();
209 guard.stack.clear();
210 }
211
212 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
213 #[test]
214 fn tic_returns_monotonic_handle() {
215 let _guard = TEST_GUARD.lock().unwrap();
216 reset_stopwatch();
217 let handle = block_on(tic_builtin()).expect("tic");
218 assert!(matches!(handle, Value::Int(IntValue::U64(_))));
219 assert!(latest_start(BUILTIN_NAME).expect("latest").is_some());
220 }
221
222 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
223 #[test]
224 fn tic_handles_increase_over_time() {
225 let _guard = TEST_GUARD.lock().unwrap();
226 reset_stopwatch();
227 let first = block_on(tic_builtin()).expect("tic");
228 thread::sleep(Duration::from_millis(5));
229 let second = block_on(tic_builtin()).expect("tic");
230 let Value::Int(IntValue::U64(first)) = first else {
231 panic!("uint64 timer")
232 };
233 let Value::Int(IntValue::U64(second)) = second else {
234 panic!("uint64 timer")
235 };
236 assert!(f64::from_bits(second) > f64::from_bits(first));
237 }
238
239 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
240 #[test]
241 fn decode_roundtrip_matches_handle() {
242 let _guard = TEST_GUARD.lock().unwrap();
243 reset_stopwatch();
244 let handle = block_on(tic_builtin()).expect("tic");
245 let Value::Int(IntValue::U64(handle)) = handle else {
246 panic!("uint64 timer")
247 };
248 let decoded = decode_handle(handle, "toc", &TEST_INVALID_HANDLE_ERROR).expect("decode");
249 let round_trip = encode_instant(decoded);
250 let delta = (f64::from_bits(round_trip) - f64::from_bits(handle)).abs();
251 assert!(delta < 1e-9, "delta {delta}");
252 }
253
254 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
255 #[test]
256 fn latest_start_does_not_consume_timer() {
257 let _guard = TEST_GUARD.lock().unwrap();
258 reset_stopwatch();
259 block_on(tic_builtin()).expect("tic");
260 assert!(latest_start(BUILTIN_NAME).expect("latest").is_some());
261 assert!(latest_start(BUILTIN_NAME).expect("second latest").is_some());
262 }
263
264 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
265 #[test]
266 fn decode_handle_rejects_invalid_values() {
267 let _guard = TEST_GUARD.lock().unwrap();
268 assert!(decode_handle(f64::NAN.to_bits(), "toc", &TEST_INVALID_HANDLE_ERROR).is_err());
269 assert!(decode_handle((-1.0_f64).to_bits(), "toc", &TEST_INVALID_HANDLE_ERROR).is_err());
270 assert!(decode_handle(f64::MAX.to_bits(), "toc", &TEST_INVALID_HANDLE_ERROR).is_err());
271 }
272}