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, Value,
6};
7use runmat_macros::runtime_builtin;
8use runmat_time::Instant;
9use std::convert::TryFrom;
10
11use crate::builtins::common::spec::{
12    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
13    ReductionNaN, ResidencyPolicy, ShapeRequirements,
14};
15use crate::builtins::timing::tic::{decode_handle, take_latest_start};
16use crate::builtins::timing::type_resolvers::toc_type;
17
18#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::timing::toc")]
19pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
20    name: "toc",
21    op_kind: GpuOpKind::Custom("timer"),
22    supported_precisions: &[],
23    broadcast: BroadcastSemantics::None,
24    provider_hooks: &[],
25    constant_strategy: ConstantStrategy::InlineLiteral,
26    residency: ResidencyPolicy::GatherImmediately,
27    nan_mode: ReductionNaN::Include,
28    two_pass_threshold: None,
29    workgroup_size: None,
30    accepts_nan_mode: false,
31    notes: "Stopwatch state lives on the host. Providers are never consulted for toc.",
32};
33
34#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::timing::toc")]
35pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
36    name: "toc",
37    shape: ShapeRequirements::Any,
38    constant_strategy: ConstantStrategy::InlineLiteral,
39    elementwise: None,
40    reduction: None,
41    emits_nan: false,
42    notes: "Timing builtins execute eagerly on the host and do not participate in fusion.",
43};
44
45const BUILTIN_NAME: &str = "toc";
46
47const TOC_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
48    name: "elapsed",
49    ty: BuiltinParamType::NumericScalar,
50    arity: BuiltinParamArity::Required,
51    default: None,
52    description: "Elapsed time in seconds.",
53}];
54
55const TOC_INPUTS_NONE: [BuiltinParamDescriptor; 0] = [];
56const TOC_INPUTS_HANDLE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
57    name: "timerVal",
58    ty: BuiltinParamType::NumericScalar,
59    arity: BuiltinParamArity::Optional,
60    default: None,
61    description: "Handle returned by tic.",
62}];
63
64const TOC_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
65    BuiltinSignatureDescriptor {
66        label: "elapsed = toc()",
67        inputs: &TOC_INPUTS_NONE,
68        outputs: &TOC_OUTPUT,
69    },
70    BuiltinSignatureDescriptor {
71        label: "elapsed = toc(timerVal)",
72        inputs: &TOC_INPUTS_HANDLE,
73        outputs: &TOC_OUTPUT,
74    },
75];
76
77const TOC_ERROR_NO_MATCHING_TIC: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
78    code: "RM.TOC.NO_MATCHING_TIC",
79    identifier: Some("RunMat:toc:NoMatchingTic"),
80    when: "toc() is called without a matching prior tic().",
81    message: "toc: no matching tic",
82};
83
84const TOC_ERROR_INVALID_HANDLE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
85    code: "RM.TOC.INVALID_HANDLE",
86    identifier: Some("RunMat:toc:InvalidTimerHandle"),
87    when: "The timer handle is missing, malformed, non-finite, negative, or points to a future instant.",
88    message: "toc: invalid timer handle",
89};
90
91const TOC_ERROR_TOO_MANY_INPUTS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
92    code: "RM.TOC.TOO_MANY_INPUTS",
93    identifier: Some("RunMat:toc:TooManyInputs"),
94    when: "More than one input argument is supplied.",
95    message: "toc: too many input arguments",
96};
97
98const TOC_ERRORS: [BuiltinErrorDescriptor; 3] = [
99    TOC_ERROR_NO_MATCHING_TIC,
100    TOC_ERROR_INVALID_HANDLE,
101    TOC_ERROR_TOO_MANY_INPUTS,
102];
103
104pub const TOC_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
105    signatures: &TOC_SIGNATURES,
106    output_mode: BuiltinOutputMode::Fixed,
107    completion_policy: BuiltinCompletionPolicy::Public,
108    errors: &TOC_ERRORS,
109};
110
111fn toc_error_with_message(
112    message: impl Into<String>,
113    error: &'static BuiltinErrorDescriptor,
114) -> crate::RuntimeError {
115    let mut builder = crate::build_runtime_error(message).with_builtin(BUILTIN_NAME);
116    if let Some(identifier) = error.identifier {
117        builder = builder.with_identifier(identifier);
118    }
119    builder.build()
120}
121
122/// Read elapsed time from the stopwatch stack or a specific handle.
123#[runtime_builtin(
124    name = "toc",
125    category = "timing",
126    summary = "Return elapsed time since the latest tic or a specific tic handle.",
127    keywords = "toc,timing,profiling,benchmark",
128    type_resolver(toc_type),
129    descriptor(crate::builtins::timing::toc::TOC_DESCRIPTOR),
130    builtin_path = "crate::builtins::timing::toc"
131)]
132pub async fn toc_builtin(args: Vec<Value>) -> crate::BuiltinResult<f64> {
133    match args.len() {
134        0 => latest_elapsed(),
135        1 => elapsed_from_value(&args[0]),
136        _ => Err(toc_error_with_message(
137            TOC_ERROR_TOO_MANY_INPUTS.message,
138            &TOC_ERROR_TOO_MANY_INPUTS,
139        )),
140    }
141}
142
143fn latest_elapsed() -> Result<f64, crate::RuntimeError> {
144    let start = take_latest_start(BUILTIN_NAME)?.ok_or_else(|| {
145        toc_error_with_message(
146            TOC_ERROR_NO_MATCHING_TIC.message,
147            &TOC_ERROR_NO_MATCHING_TIC,
148        )
149    })?;
150    Ok(start.elapsed().as_secs_f64())
151}
152
153fn elapsed_from_value(value: &Value) -> Result<f64, crate::RuntimeError> {
154    let handle = f64::try_from(value).map_err(|_| {
155        toc_error_with_message(TOC_ERROR_INVALID_HANDLE.message, &TOC_ERROR_INVALID_HANDLE)
156    })?;
157    let instant = decode_handle(handle, BUILTIN_NAME, &TOC_ERROR_INVALID_HANDLE)?;
158    let now = Instant::now();
159    let elapsed = now.checked_duration_since(instant).ok_or_else(|| {
160        toc_error_with_message(TOC_ERROR_INVALID_HANDLE.message, &TOC_ERROR_INVALID_HANDLE)
161    })?;
162    Ok(elapsed.as_secs_f64())
163}
164
165#[cfg(test)]
166pub(crate) mod tests {
167    use super::*;
168    use crate::builtins::timing::tic::{encode_instant, record_tic, take_latest_start, TEST_GUARD};
169    use futures::executor::block_on;
170    use std::time::Duration;
171
172    fn clear_tic_stack() {
173        while let Ok(Some(_)) = take_latest_start(BUILTIN_NAME) {}
174    }
175
176    fn assert_toc_error_identifier(err: crate::RuntimeError, identifier: &str) {
177        assert_eq!(
178            err.identifier(),
179            Some(identifier),
180            "message: {}",
181            err.message()
182        );
183    }
184
185    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
186    #[test]
187    fn toc_requires_matching_tic() {
188        let _guard = TEST_GUARD.lock().unwrap();
189        clear_tic_stack();
190        let err = block_on(toc_builtin(Vec::new())).unwrap_err();
191        assert_toc_error_identifier(err, TOC_ERROR_NO_MATCHING_TIC.identifier.unwrap());
192    }
193
194    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
195    #[test]
196    fn toc_reports_elapsed_for_latest_start() {
197        let _guard = TEST_GUARD.lock().unwrap();
198        clear_tic_stack();
199        record_tic("tic").expect("tic");
200        std::thread::sleep(Duration::from_millis(5));
201        let elapsed = block_on(toc_builtin(Vec::new())).expect("toc");
202        assert!(elapsed >= 0.0);
203        assert!(take_latest_start(BUILTIN_NAME).unwrap().is_none());
204    }
205
206    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
207    #[test]
208    fn toc_with_handle_measures_without_popping_stack() {
209        let _guard = TEST_GUARD.lock().unwrap();
210        clear_tic_stack();
211        let handle = record_tic("tic").expect("tic");
212        std::thread::sleep(Duration::from_millis(5));
213        let elapsed = block_on(toc_builtin(vec![Value::Num(handle)])).expect("toc(handle)");
214        assert!(elapsed >= 0.0);
215        // Stack still contains the entry so a subsequent toc pops it.
216        let later = block_on(toc_builtin(Vec::new())).expect("second toc");
217        assert!(later >= elapsed);
218    }
219
220    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
221    #[test]
222    fn toc_rejects_invalid_handle() {
223        let _guard = TEST_GUARD.lock().unwrap();
224        clear_tic_stack();
225        let err = block_on(toc_builtin(vec![Value::Num(f64::NAN)])).unwrap_err();
226        assert_toc_error_identifier(err, TOC_ERROR_INVALID_HANDLE.identifier.unwrap());
227    }
228
229    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
230    #[test]
231    fn toc_rejects_future_handle() {
232        let _guard = TEST_GUARD.lock().unwrap();
233        clear_tic_stack();
234        let future_handle = encode_instant(Instant::now()) + 10_000.0;
235        let err = block_on(toc_builtin(vec![Value::Num(future_handle)])).unwrap_err();
236        assert_toc_error_identifier(err, TOC_ERROR_INVALID_HANDLE.identifier.unwrap());
237    }
238
239    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
240    #[test]
241    fn toc_rejects_string_handle() {
242        let _guard = TEST_GUARD.lock().unwrap();
243        clear_tic_stack();
244        let err = block_on(toc_builtin(vec![Value::from("not a timer")])).unwrap_err();
245        assert_toc_error_identifier(err, TOC_ERROR_INVALID_HANDLE.identifier.unwrap());
246    }
247
248    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
249    #[test]
250    fn toc_rejects_extra_arguments() {
251        let _guard = TEST_GUARD.lock().unwrap();
252        clear_tic_stack();
253        let err = block_on(toc_builtin(vec![Value::Num(0.0), Value::Num(0.0)])).unwrap_err();
254        assert_toc_error_identifier(err, TOC_ERROR_TOO_MANY_INPUTS.identifier.unwrap());
255    }
256
257    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
258    #[test]
259    fn toc_nested_timers() {
260        let _guard = TEST_GUARD.lock().unwrap();
261        clear_tic_stack();
262        record_tic("tic").expect("outer");
263        std::thread::sleep(Duration::from_millis(2));
264        record_tic("tic").expect("inner");
265        std::thread::sleep(Duration::from_millis(4));
266        let inner = block_on(toc_builtin(Vec::new())).expect("inner toc");
267        assert!(inner >= 0.0);
268        std::thread::sleep(Duration::from_millis(2));
269        let outer = block_on(toc_builtin(Vec::new())).expect("outer toc");
270        assert!(outer >= inner);
271    }
272
273    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
274    #[test]
275    #[cfg(feature = "wgpu")]
276    fn toc_ignores_wgpu_provider() {
277        let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
278            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
279        );
280        let _guard = TEST_GUARD.lock().unwrap();
281        clear_tic_stack();
282        record_tic("tic").expect("tic");
283        std::thread::sleep(Duration::from_millis(1));
284        let elapsed = block_on(toc_builtin(Vec::new())).expect("toc");
285        assert!(elapsed >= 0.0);
286    }
287}