1use runmat_builtins::{
4 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
5 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
6};
7use runmat_builtins::{
8 BuiltinExtensionDescriptor, BuiltinExtensionMode, BuiltinIntegerBackendRule,
9 BuiltinIntegerCapabilityDescriptor, BuiltinIntegerClass, BuiltinIntegerComputationDomain,
10 BuiltinIntegerInputAvailability, BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule,
11 BuiltinIntegerOverflowRule, BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule,
12};
13use runmat_macros::runtime_builtin;
14use runmat_time::Instant;
15use runmat_value::IntValue;
16use runmat_value::Value;
17
18use crate::builtins::common::spec::{
19 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
20 ReductionNaN, ResidencyPolicy, ShapeRequirements,
21};
22use crate::builtins::timing::tic::{decode_handle, elapsed_since, latest_start};
23use crate::builtins::timing::type_resolvers::toc_type;
24
25#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::timing::toc")]
26pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
27 name: "toc",
28 op_kind: GpuOpKind::Custom("timer"),
29 supported_precisions: &[],
30 broadcast: BroadcastSemantics::None,
31 provider_hooks: &[],
32 constant_strategy: ConstantStrategy::InlineLiteral,
33 residency: ResidencyPolicy::GatherImmediately,
34 nan_mode: ReductionNaN::Include,
35 two_pass_threshold: None,
36 workgroup_size: None,
37 accepts_nan_mode: false,
38 notes: "Stopwatch state lives on the host. Providers are never consulted for toc.",
39};
40
41#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::timing::toc")]
42pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
43 name: "toc",
44 shape: ShapeRequirements::Any,
45 constant_strategy: ConstantStrategy::InlineLiteral,
46 elementwise: None,
47 reduction: None,
48 emits_nan: false,
49 notes: "Timing builtins execute eagerly on the host and do not participate in fusion.",
50};
51
52const BUILTIN_NAME: &str = "toc";
53
54const LEGACY_DOUBLE_HANDLE_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
55 id: "toc-legacy-double-timer-handle",
56 mode: BuiltinExtensionMode::RunMatOnly,
57 description: "toc accepts an older RunMat double-valued timer handle",
58 error_identifier: Some("RunMat:compatibility:TocLegacyDoubleTimerHandleExtension"),
59};
60pub const TOC_EXTENSIONS: [BuiltinExtensionDescriptor; 1] = [LEGACY_DOUBLE_HANDLE_EXTENSION];
61
62const UINT64_TIMER_CLASS: [BuiltinIntegerClass; 1] = [BuiltinIntegerClass::Uint64];
63const TOC_TIMER_INPUT: [BuiltinIntegerInputCapability; 1] = [BuiltinIntegerInputCapability {
64 name: "timerVal",
65 classes: &UINT64_TIMER_CLASS,
66 availability: BuiltinIntegerInputAvailability::Documented,
67 scalar_double: BuiltinIntegerScalarDoubleRule::Rejected,
68 notes:
69 "tic returns an opaque uint64 token; toc reads its exact bits without numeric conversion.",
70}];
71pub const TOC_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
72 [BuiltinIntegerCapabilityDescriptor {
73 form: "elapsed = toc(uint64_timerVal)",
74 inputs: &TOC_TIMER_INPUT,
75 computation_domain: BuiltinIntegerComputationDomain::Structural,
76 output_class: BuiltinIntegerOutputClassRule::Double,
77 overflow: BuiltinIntegerOverflowRule::Error,
78 backend: BuiltinIntegerBackendRule::HostOnly,
79 overload: BuiltinIntegerOverloadKind::StructuralParameter,
80 notes: "The uint64 value is an opaque stopwatch token rather than an elapsed-time count.",
81 }];
82
83const TOC_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
84 name: "elapsed",
85 ty: BuiltinParamType::NumericScalar,
86 arity: BuiltinParamArity::Required,
87 default: None,
88 description: "Elapsed time in seconds.",
89}];
90
91const TOC_INPUTS_NONE: [BuiltinParamDescriptor; 0] = [];
92const TOC_INPUTS_HANDLE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
93 name: "timerVal",
94 ty: BuiltinParamType::NumericScalar,
95 arity: BuiltinParamArity::Optional,
96 default: None,
97 description: "Handle returned by tic.",
98}];
99
100const TOC_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
101 BuiltinSignatureDescriptor {
102 label: "elapsed = toc()",
103 inputs: &TOC_INPUTS_NONE,
104 outputs: &TOC_OUTPUT,
105 },
106 BuiltinSignatureDescriptor {
107 label: "elapsed = toc(timerVal)",
108 inputs: &TOC_INPUTS_HANDLE,
109 outputs: &TOC_OUTPUT,
110 },
111];
112
113const TOC_ERROR_NO_MATCHING_TIC: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
114 code: "RM.TOC.NO_MATCHING_TIC",
115 identifier: Some("RunMat:toc:NoMatchingTic"),
116 when: "toc() is called without a matching prior tic().",
117 message: "toc: no matching tic",
118};
119
120const TOC_ERROR_INVALID_HANDLE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
121 code: "RM.TOC.INVALID_HANDLE",
122 identifier: Some("RunMat:toc:InvalidTimerHandle"),
123 when: "The timer handle is missing, malformed, non-finite, negative, or points to a future instant.",
124 message: "toc: invalid timer handle",
125};
126
127const TOC_ERROR_TOO_MANY_INPUTS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
128 code: "RM.TOC.TOO_MANY_INPUTS",
129 identifier: Some("RunMat:toc:TooManyInputs"),
130 when: "More than one input argument is supplied.",
131 message: "toc: too many input arguments",
132};
133
134const TOC_ERRORS: [BuiltinErrorDescriptor; 3] = [
135 TOC_ERROR_NO_MATCHING_TIC,
136 TOC_ERROR_INVALID_HANDLE,
137 TOC_ERROR_TOO_MANY_INPUTS,
138];
139
140pub const TOC_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
141 signatures: &TOC_SIGNATURES,
142 output_mode: BuiltinOutputMode::Fixed,
143 completion_policy: BuiltinCompletionPolicy::Public,
144 errors: &TOC_ERRORS,
145};
146
147fn toc_error_with_message(
148 message: impl Into<String>,
149 error: &'static BuiltinErrorDescriptor,
150) -> crate::RuntimeError {
151 let mut builder = crate::build_runtime_error(message).with_builtin(BUILTIN_NAME);
152 if let Some(identifier) = error.identifier {
153 builder = builder.with_identifier(identifier);
154 }
155 builder.build()
156}
157
158#[runtime_builtin(
160 name = "toc",
161 category = "timing",
162 summary = "Return elapsed time since the latest tic or a specific tic handle.",
163 keywords = "toc,timing,profiling,benchmark",
164 type_resolver(toc_type),
165 descriptor(crate::builtins::timing::toc::TOC_DESCRIPTOR),
166 extensions(crate::builtins::timing::toc::TOC_EXTENSIONS),
167 integer_capabilities(crate::builtins::timing::toc::TOC_INTEGER_CAPABILITIES),
168 builtin_path = "crate::builtins::timing::toc"
169)]
170pub async fn toc_builtin(args: Vec<Value>) -> crate::BuiltinResult<f64> {
171 match args.len() {
172 0 => latest_elapsed(),
173 1 => elapsed_from_value(&args[0]),
174 _ => Err(toc_error_with_message(
175 TOC_ERROR_TOO_MANY_INPUTS.message,
176 &TOC_ERROR_TOO_MANY_INPUTS,
177 )),
178 }
179}
180
181fn latest_elapsed() -> Result<f64, crate::RuntimeError> {
182 let start = latest_start(BUILTIN_NAME)?.ok_or_else(|| {
183 toc_error_with_message(
184 TOC_ERROR_NO_MATCHING_TIC.message,
185 &TOC_ERROR_NO_MATCHING_TIC,
186 )
187 })?;
188 Ok(elapsed_since(start).as_secs_f64())
189}
190
191fn elapsed_from_value(value: &Value) -> Result<f64, crate::RuntimeError> {
192 let handle = match value {
193 Value::Int(IntValue::U64(handle)) => *handle,
194 Value::Tensor(tensor) if tensor.len() == 1 => match tensor
195 .integer_storage()
196 .and_then(|storage| storage.value_at(0))
197 {
198 Some(IntValue::U64(handle)) => handle,
199 _ => legacy_or_invalid_handle(value)?,
200 },
201 _ => legacy_or_invalid_handle(value)?,
202 };
203 let instant = decode_handle(handle, BUILTIN_NAME, &TOC_ERROR_INVALID_HANDLE)?;
204 let now = Instant::now();
205 let elapsed = now.checked_duration_since(instant).ok_or_else(|| {
206 toc_error_with_message(TOC_ERROR_INVALID_HANDLE.message, &TOC_ERROR_INVALID_HANDLE)
207 })?;
208 Ok(elapsed.as_secs_f64())
209}
210
211fn legacy_or_invalid_handle(value: &Value) -> Result<u64, crate::RuntimeError> {
212 let seconds = match value {
213 Value::Num(seconds) => *seconds,
214 Value::Tensor(tensor) if tensor.len() == 1 && tensor.integer_storage().is_none() => {
215 crate::builtins::common::tensor::tensor_value_f64(tensor, 0)
216 }
217 _ => {
218 return Err(toc_error_with_message(
219 TOC_ERROR_INVALID_HANDLE.message,
220 &TOC_ERROR_INVALID_HANDLE,
221 ))
222 }
223 };
224 if !seconds.is_finite() || seconds.is_sign_negative() {
225 return Err(toc_error_with_message(
226 TOC_ERROR_INVALID_HANDLE.message,
227 &TOC_ERROR_INVALID_HANDLE,
228 ));
229 }
230 crate::compatibility::ensure_builtin_extension_enabled(
231 &LEGACY_DOUBLE_HANDLE_EXTENSION,
232 BUILTIN_NAME,
233 )?;
234 Ok(seconds.to_bits())
235}
236
237#[cfg(test)]
238pub(crate) mod tests {
239 use super::*;
240 use crate::builtins::timing::tic::{
241 clear_stopwatch_for_test, encode_instant, record_tic, TEST_GUARD,
242 };
243 use futures::executor::block_on;
244 use std::time::Duration;
245
246 fn clear_tic_stack() {
247 clear_stopwatch_for_test();
248 }
249
250 fn assert_toc_error_identifier(err: crate::RuntimeError, identifier: &str) {
251 assert_eq!(
252 err.identifier(),
253 Some(identifier),
254 "message: {}",
255 err.message()
256 );
257 }
258
259 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
260 #[test]
261 fn toc_requires_matching_tic() {
262 let _guard = TEST_GUARD.lock().unwrap();
263 clear_tic_stack();
264 let err = block_on(toc_builtin(Vec::new())).unwrap_err();
265 assert_toc_error_identifier(err, TOC_ERROR_NO_MATCHING_TIC.identifier.unwrap());
266 }
267
268 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
269 #[test]
270 fn toc_reports_elapsed_for_latest_start() {
271 let _guard = TEST_GUARD.lock().unwrap();
272 clear_tic_stack();
273 record_tic("tic").expect("tic");
274 std::thread::sleep(Duration::from_millis(5));
275 let elapsed = block_on(toc_builtin(Vec::new())).expect("toc");
276 assert!(elapsed >= 0.0);
277 assert!(latest_start(BUILTIN_NAME).unwrap().is_some());
278 }
279
280 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
281 #[test]
282 fn toc_with_handle_measures_without_popping_stack() {
283 let _guard = TEST_GUARD.lock().unwrap();
284 clear_tic_stack();
285 let handle = record_tic("tic").expect("tic");
286 std::thread::sleep(Duration::from_millis(5));
287 let elapsed =
288 block_on(toc_builtin(vec![Value::Int(IntValue::U64(handle))])).expect("toc(handle)");
289 assert!(elapsed >= 0.0);
290 let later = block_on(toc_builtin(Vec::new())).expect("second toc");
292 assert!(later >= elapsed);
293 }
294
295 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
296 #[test]
297 fn toc_rejects_invalid_handle() {
298 let _guard = TEST_GUARD.lock().unwrap();
299 clear_tic_stack();
300 let err = block_on(toc_builtin(vec![Value::Num(f64::NAN)])).unwrap_err();
301 assert_toc_error_identifier(err, TOC_ERROR_INVALID_HANDLE.identifier.unwrap());
302 }
303
304 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
305 #[test]
306 fn toc_rejects_overflowing_handle() {
307 let _guard = TEST_GUARD.lock().unwrap();
308 clear_tic_stack();
309 let _extensions = crate::compatibility::push_runmat_extensions_enabled(true);
310 let err = block_on(toc_builtin(vec![Value::Num(f64::MAX)])).unwrap_err();
311 assert_toc_error_identifier(err, TOC_ERROR_INVALID_HANDLE.identifier.unwrap());
312 }
313
314 #[test]
315 fn toc_rejects_legacy_double_token_in_matlab_mode() {
316 let _guard = TEST_GUARD.lock().unwrap();
317 clear_tic_stack();
318 let handle = record_tic("tic").expect("tic");
319 let _strict = crate::compatibility::push_runmat_extensions_enabled(false);
320 let err = block_on(toc_builtin(vec![Value::Num(f64::from_bits(handle))])).unwrap_err();
321 assert_toc_error_identifier(
322 err,
323 LEGACY_DOUBLE_HANDLE_EXTENSION.error_identifier.unwrap(),
324 );
325 }
326
327 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
328 #[test]
329 fn toc_rejects_future_handle() {
330 let _guard = TEST_GUARD.lock().unwrap();
331 clear_tic_stack();
332 let future_seconds = f64::from_bits(encode_instant(Instant::now())) + 10_000.0;
333 let err = block_on(toc_builtin(vec![Value::Int(IntValue::U64(
334 future_seconds.to_bits(),
335 ))]))
336 .unwrap_err();
337 assert_toc_error_identifier(err, TOC_ERROR_INVALID_HANDLE.identifier.unwrap());
338 }
339
340 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
341 #[test]
342 fn toc_rejects_string_handle() {
343 let _guard = TEST_GUARD.lock().unwrap();
344 clear_tic_stack();
345 let err = block_on(toc_builtin(vec![Value::from("not a timer")])).unwrap_err();
346 assert_toc_error_identifier(err, TOC_ERROR_INVALID_HANDLE.identifier.unwrap());
347 }
348
349 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
350 #[test]
351 fn toc_rejects_extra_arguments() {
352 let _guard = TEST_GUARD.lock().unwrap();
353 clear_tic_stack();
354 let err = block_on(toc_builtin(vec![Value::Num(0.0), Value::Num(0.0)])).unwrap_err();
355 assert_toc_error_identifier(err, TOC_ERROR_TOO_MANY_INPUTS.identifier.unwrap());
356 }
357
358 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
359 #[test]
360 fn toc_nested_timers() {
361 let _guard = TEST_GUARD.lock().unwrap();
362 clear_tic_stack();
363 record_tic("tic").expect("outer");
364 std::thread::sleep(Duration::from_millis(2));
365 record_tic("tic").expect("inner");
366 std::thread::sleep(Duration::from_millis(4));
367 let inner = block_on(toc_builtin(Vec::new())).expect("inner toc");
368 assert!(inner >= 0.0);
369 std::thread::sleep(Duration::from_millis(2));
370 let inner_again = block_on(toc_builtin(Vec::new())).expect("second inner toc");
371 assert!(inner_again >= inner);
372 }
373
374 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
375 #[test]
376 #[cfg(feature = "wgpu")]
377 fn toc_ignores_wgpu_provider() {
378 let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
379 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
380 );
381 let _guard = TEST_GUARD.lock().unwrap();
382 clear_tic_stack();
383 record_tic("tic").expect("tic");
384 std::thread::sleep(Duration::from_millis(1));
385 let elapsed = block_on(toc_builtin(Vec::new())).expect("toc");
386 assert!(elapsed >= 0.0);
387 }
388}