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 now = Instant::now();
131 {
132 let mut guard = STOPWATCH.lock().map_err(|_| {
133 stopwatch_error_with_message(
134 builtin,
135 TIC_ERROR_STATE_LOCK.message,
136 &TIC_ERROR_STATE_LOCK,
137 )
138 })?;
139 guard.push(now);
140 }
141 Ok(encode_instant(now))
142}
143
144pub(crate) fn take_latest_start(builtin: &str) -> Result<Option<Instant>, crate::RuntimeError> {
146 let mut guard = STOPWATCH.lock().map_err(|_| {
147 stopwatch_error_with_message(builtin, TIC_ERROR_STATE_LOCK.message, &TIC_ERROR_STATE_LOCK)
148 })?;
149 Ok(guard.pop())
150}
151
152pub(crate) fn encode_instant(instant: Instant) -> f64 {
154 instant.duration_since(*MONOTONIC_ORIGIN).as_secs_f64()
155}
156
157pub(crate) fn decode_handle(
159 handle: f64,
160 builtin: &str,
161 error: &BuiltinErrorDescriptor,
162) -> Result<Instant, crate::RuntimeError> {
163 if !handle.is_finite() || handle.is_sign_negative() {
164 return Err(stopwatch_error_with_message(builtin, error.message, error));
165 }
166 let duration = Duration::from_secs_f64(handle);
167 Ok((*MONOTONIC_ORIGIN) + duration)
168}
169
170#[cfg(test)]
171pub(crate) mod tests {
172 use super::*;
173 use futures::executor::block_on;
174 use std::thread;
175 use std::time::Duration;
176
177 const TEST_INVALID_HANDLE_ERROR: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
178 code: "RM.TOC.INVALID_HANDLE",
179 identifier: Some("RunMat:toc:InvalidTimerHandle"),
180 when: "The timer handle is non-finite or negative.",
181 message: "toc: invalid timer handle",
182 };
183
184 fn reset_stopwatch() {
185 let mut guard = STOPWATCH.lock().unwrap();
186 guard.stack.clear();
187 }
188
189 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
190 #[test]
191 fn tic_returns_monotonic_handle() {
192 let _guard = TEST_GUARD.lock().unwrap();
193 reset_stopwatch();
194 let handle = block_on(tic_builtin()).expect("tic");
195 assert!(handle >= 0.0);
196 assert!(take_latest_start(BUILTIN_NAME).expect("take").is_some());
197 }
198
199 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
200 #[test]
201 fn tic_handles_increase_over_time() {
202 let _guard = TEST_GUARD.lock().unwrap();
203 reset_stopwatch();
204 let first = block_on(tic_builtin()).expect("tic");
205 thread::sleep(Duration::from_millis(5));
206 let second = block_on(tic_builtin()).expect("tic");
207 assert!(second > first);
208 }
209
210 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
211 #[test]
212 fn decode_roundtrip_matches_handle() {
213 let _guard = TEST_GUARD.lock().unwrap();
214 reset_stopwatch();
215 let handle = block_on(tic_builtin()).expect("tic");
216 let decoded = decode_handle(handle, "toc", &TEST_INVALID_HANDLE_ERROR).expect("decode");
217 let round_trip = encode_instant(decoded);
218 let delta = (round_trip - handle).abs();
219 assert!(delta < 1e-9, "delta {delta}");
220 }
221
222 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
223 #[test]
224 fn take_latest_start_pops_stack() {
225 let _guard = TEST_GUARD.lock().unwrap();
226 reset_stopwatch();
227 block_on(tic_builtin()).expect("tic");
228 assert!(take_latest_start(BUILTIN_NAME).expect("take").is_some());
229 assert!(take_latest_start(BUILTIN_NAME)
230 .expect("second take")
231 .is_none());
232 }
233
234 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
235 #[test]
236 fn decode_handle_rejects_invalid_values() {
237 let _guard = TEST_GUARD.lock().unwrap();
238 assert!(decode_handle(f64::NAN, "toc", &TEST_INVALID_HANDLE_ERROR).is_err());
239 assert!(decode_handle(-1.0, "toc", &TEST_INVALID_HANDLE_ERROR).is_err());
240 }
241}