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