Skip to main content

runmat_runtime/builtins/timing/
toc.rs

1//! MATLAB-compatible `toc` builtin that reports elapsed stopwatch time.
2
3use 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/// Read elapsed time from the stopwatch stack or a specific handle.
159#[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        // Stack still contains the entry, and no-argument toc reads it again.
291        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}