runmat_runtime/builtins/timing/
toc.rs

1//! MATLAB-compatible `toc` builtin that reports elapsed stopwatch time.
2
3use runmat_builtins::Value;
4use runmat_macros::runtime_builtin;
5use std::convert::TryFrom;
6use std::time::Instant;
7
8use crate::builtins::common::spec::{
9    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
10    ReductionNaN, ResidencyPolicy, ShapeRequirements,
11};
12use crate::builtins::timing::tic::{decode_handle, take_latest_start};
13#[cfg(feature = "doc_export")]
14use crate::register_builtin_doc_text;
15use crate::{register_builtin_fusion_spec, register_builtin_gpu_spec};
16
17#[cfg(feature = "doc_export")]
18pub const DOC_MD: &str = r#"---
19title: "toc"
20category: "timing"
21keywords: ["toc", "timer", "elapsed time", "profiling", "benchmark"]
22summary: "Read the elapsed time since the most recent tic or an explicit handle."
23references: []
24gpu_support:
25  elementwise: false
26  reduction: false
27  precisions: []
28  broadcasting: "none"
29  notes: "toc always runs on the host CPU. GPU providers are not consulted."
30fusion:
31  elementwise: false
32  reduction: false
33  max_inputs: 1
34  constants: "inline"
35requires_feature: null
36tested:
37  unit: "builtins::timing::toc::tests"
38  integration: "builtins::timing::toc::tests"
39---
40
41# What does the `toc` function do in MATLAB / RunMat?
42`toc` returns the elapsed wall-clock time in seconds since the last matching `tic`, or since the `tic`
43handle you pass as an argument. It mirrors the stopwatch utilities that MATLAB users rely on for ad-hoc
44profiling and benchmarking.
45
46## How does the `toc` function behave in MATLAB / RunMat?
47- `toc` without inputs pops the most recent `tic` from the stopwatch stack and returns the elapsed seconds.
48- `toc(t)` accepts a handle previously produced by `tic` and measures the time since that handle without
49  altering the stack.
50- Calling `toc` before `tic` raises the MATLAB-compatible error identifier `MATLAB:toc:NoMatchingTic`.
51- Passing anything other than a finite, non-negative scalar handle raises `MATLAB:toc:InvalidTimerHandle`.
52- The stopwatch uses a monotonic host clock, so measurements are immune to wall-clock adjustments.
53
54## `toc` Function GPU Execution Behaviour
55The stopwatch lives entirely on the host. `toc` never transfers tensors or consults acceleration providers,
56so there are no GPU hooks to implement. Expressions that combine `toc` with GPU-resident data gather any
57numeric operands back to the CPU before evaluating the timer logic, and the builtin is excluded from fusion
58plans entirely.
59
60## Examples of using the `toc` function in MATLAB / RunMat
61
62### Measuring elapsed time since the last tic
63
64```matlab
65tic;
66pause(0.25);
67elapsed = toc;
68```
69
70`elapsed` contains the seconds since the `tic`. The matching stopwatch entry is removed automatically.
71
72### Using toc with an explicit tic handle
73
74```matlab
75token = tic;
76heavyComputation();
77elapsed = toc(token);
78```
79
80Passing the handle makes `toc` leave the global stopwatch stack untouched, so earlier timers keep running.
81
82### Timing nested stages with toc
83
84```matlab
85tic;          % Outer stopwatch
86stage1();
87inner = tic;  % Nested stopwatch
88stage2();
89stage2Time = toc(inner);
90totalTime = toc;
91```
92
93`stage2Time` measures only the inner section, while `totalTime` spans the entire outer region.
94
95### Printing elapsed time without capturing output
96
97```matlab
98tic;
99longRunningTask();
100toc;   % Displays the elapsed seconds because the result is not assigned
101```
102
103When you omit an output argument, RunMat displays the elapsed seconds in the console. Add a semicolon or
104capture the result to suppress the text, mirroring MATLAB's default command-window behaviour.
105
106### Measuring immediately with toc(tic)
107
108```matlab
109elapsed = toc(tic);  % Starts a timer and reads it right away
110```
111
112This idiom is equivalent to separate `tic`/`toc` calls, and the stopwatch entry created by the inner `tic`
113remains on the stack for later use.
114
115## GPU residency in RunMat (Do I need `gpuArray`?)
116No. Timing utilities never touch GPU memory. You can freely combine `toc` with code that produces or consumes
117`gpuArray` values—the stopwatch itself still executes on the CPU.
118
119## FAQ
120
121### What happens if I call `toc` before `tic`?
122The builtin raises `MATLAB:toc:NoMatchingTic`, matching MATLAB's behaviour when no stopwatch start exists.
123
124### Does `toc` remove the matching `tic`?
125Yes when called without arguments. The most-recent stopwatch entry is popped so nested timers unwind in order.
126When you pass a handle (`toc(t)`), the stack remains unchanged and you may reuse the handle multiple times.
127
128### Can I reuse a `tic` handle after calling `toc(t)`?
129Yes. Handles are deterministic timestamps, so you can call `toc(handle)` multiple times or store the handle in
130structures for later inspection.
131
132### Does `toc` print output?
133When you do not capture the result, the interpreter shows the elapsed seconds. Assigning the return value (or
134ending the statement with a semicolon) suppresses the display, just like in MATLAB.
135
136### Is `toc` affected by GPU execution or fusion?
137No. The stopwatch uses the host's monotonic clock. GPU acceleration, fusion, and pipeline residency do not
138change the measured interval.
139
140### How accurate is the reported time?
141`toc` relies on `std::time::Instant`, typically offering microsecond precision on modern platforms. The actual
142resolution depends on your operating system.
143
144## See Also
145[tic](./tic)
146
147## Source & Feedback
148- Implementation: [`crates/runmat-runtime/src/builtins/timing/toc.rs`](https://github.com/runmat-org/runmat/blob/main/crates/runmat-runtime/src/builtins/timing/toc.rs)
149- Found a behavioural difference? [Open an issue](https://github.com/runmat-org/runmat/issues/new/choose) with a minimal repro.
150"#;
151
152pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
153    name: "toc",
154    op_kind: GpuOpKind::Custom("timer"),
155    supported_precisions: &[],
156    broadcast: BroadcastSemantics::None,
157    provider_hooks: &[],
158    constant_strategy: ConstantStrategy::InlineLiteral,
159    residency: ResidencyPolicy::GatherImmediately,
160    nan_mode: ReductionNaN::Include,
161    two_pass_threshold: None,
162    workgroup_size: None,
163    accepts_nan_mode: false,
164    notes: "Stopwatch state lives on the host. Providers are never consulted for toc.",
165};
166
167register_builtin_gpu_spec!(GPU_SPEC);
168
169pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
170    name: "toc",
171    shape: ShapeRequirements::Any,
172    constant_strategy: ConstantStrategy::InlineLiteral,
173    elementwise: None,
174    reduction: None,
175    emits_nan: false,
176    notes: "Timing builtins execute eagerly on the host and do not participate in fusion.",
177};
178
179register_builtin_fusion_spec!(FUSION_SPEC);
180
181#[cfg(feature = "doc_export")]
182register_builtin_doc_text!("toc", DOC_MD);
183
184const ERR_NO_MATCHING_TIC: &str = "MATLAB:toc:NoMatchingTic";
185const ERR_INVALID_HANDLE: &str = "MATLAB:toc:InvalidTimerHandle";
186const ERR_TOO_MANY_INPUTS: &str = "MATLAB:toc:TooManyInputs";
187
188/// Read elapsed time from the stopwatch stack or a specific handle.
189#[runtime_builtin(
190    name = "toc",
191    category = "timing",
192    summary = "Read the elapsed time since the most recent tic or an explicit handle.",
193    keywords = "toc,timing,profiling,benchmark"
194)]
195pub fn toc_builtin(args: Vec<Value>) -> Result<f64, String> {
196    match args.len() {
197        0 => latest_elapsed(),
198        1 => elapsed_from_value(&args[0]),
199        _ => Err(ERR_TOO_MANY_INPUTS.to_string()),
200    }
201}
202
203fn latest_elapsed() -> Result<f64, String> {
204    let start = take_latest_start()?.ok_or_else(|| ERR_NO_MATCHING_TIC.to_string())?;
205    Ok(start.elapsed().as_secs_f64())
206}
207
208fn elapsed_from_value(value: &Value) -> Result<f64, String> {
209    let handle = f64::try_from(value).map_err(|_| ERR_INVALID_HANDLE.to_string())?;
210    let instant = decode_handle(handle)?;
211    let now = Instant::now();
212    let elapsed = now
213        .checked_duration_since(instant)
214        .ok_or_else(|| ERR_INVALID_HANDLE.to_string())?;
215    Ok(elapsed.as_secs_f64())
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use crate::builtins::timing::tic::{encode_instant, record_tic, take_latest_start, TEST_GUARD};
222    use std::time::Duration;
223
224    #[cfg(feature = "doc_export")]
225    use crate::builtins::common::test_support;
226
227    fn clear_tic_stack() {
228        while let Ok(Some(_)) = take_latest_start() {}
229    }
230
231    #[test]
232    fn toc_requires_matching_tic() {
233        let _guard = TEST_GUARD.lock().unwrap();
234        clear_tic_stack();
235        let err = toc_builtin(Vec::new()).unwrap_err();
236        assert_eq!(err, ERR_NO_MATCHING_TIC);
237    }
238
239    #[test]
240    fn toc_reports_elapsed_for_latest_start() {
241        let _guard = TEST_GUARD.lock().unwrap();
242        clear_tic_stack();
243        record_tic().expect("tic");
244        std::thread::sleep(Duration::from_millis(5));
245        let elapsed = toc_builtin(Vec::new()).expect("toc");
246        assert!(elapsed >= 0.0);
247        assert!(take_latest_start().unwrap().is_none());
248    }
249
250    #[test]
251    fn toc_with_handle_measures_without_popping_stack() {
252        let _guard = TEST_GUARD.lock().unwrap();
253        clear_tic_stack();
254        let handle = record_tic().expect("tic");
255        std::thread::sleep(Duration::from_millis(5));
256        let elapsed = toc_builtin(vec![Value::Num(handle)]).expect("toc(handle)");
257        assert!(elapsed >= 0.0);
258        // Stack still contains the entry so a subsequent toc pops it.
259        let later = toc_builtin(Vec::new()).expect("second toc");
260        assert!(later >= elapsed);
261    }
262
263    #[test]
264    fn toc_rejects_invalid_handle() {
265        let _guard = TEST_GUARD.lock().unwrap();
266        clear_tic_stack();
267        let err = toc_builtin(vec![Value::Num(f64::NAN)]).unwrap_err();
268        assert_eq!(err, ERR_INVALID_HANDLE);
269    }
270
271    #[test]
272    fn toc_rejects_future_handle() {
273        let _guard = TEST_GUARD.lock().unwrap();
274        clear_tic_stack();
275        let future_handle = encode_instant(Instant::now()) + 10_000.0;
276        let err = toc_builtin(vec![Value::Num(future_handle)]).unwrap_err();
277        assert_eq!(err, ERR_INVALID_HANDLE);
278    }
279
280    #[test]
281    fn toc_rejects_string_handle() {
282        let _guard = TEST_GUARD.lock().unwrap();
283        clear_tic_stack();
284        let err = toc_builtin(vec![Value::from("not a timer")]).unwrap_err();
285        assert_eq!(err, ERR_INVALID_HANDLE);
286    }
287
288    #[test]
289    fn toc_rejects_extra_arguments() {
290        let _guard = TEST_GUARD.lock().unwrap();
291        clear_tic_stack();
292        let err = toc_builtin(vec![Value::Num(0.0), Value::Num(0.0)]).unwrap_err();
293        assert_eq!(err, ERR_TOO_MANY_INPUTS);
294    }
295
296    #[test]
297    fn toc_nested_timers() {
298        let _guard = TEST_GUARD.lock().unwrap();
299        clear_tic_stack();
300        record_tic().expect("outer");
301        std::thread::sleep(Duration::from_millis(2));
302        record_tic().expect("inner");
303        std::thread::sleep(Duration::from_millis(4));
304        let inner = toc_builtin(Vec::new()).expect("inner toc");
305        assert!(inner >= 0.0);
306        std::thread::sleep(Duration::from_millis(2));
307        let outer = toc_builtin(Vec::new()).expect("outer toc");
308        assert!(outer >= inner);
309    }
310
311    #[test]
312    #[cfg(feature = "doc_export")]
313    fn doc_examples_present() {
314        let _guard = TEST_GUARD.lock().unwrap();
315        let blocks = test_support::doc_examples(DOC_MD);
316        assert!(!blocks.is_empty());
317    }
318
319    #[test]
320    #[cfg(feature = "wgpu")]
321    fn toc_ignores_wgpu_provider() {
322        let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
323            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
324        );
325        let _guard = TEST_GUARD.lock().unwrap();
326        clear_tic_stack();
327        record_tic().expect("tic");
328        std::thread::sleep(Duration::from_millis(1));
329        let elapsed = toc_builtin(Vec::new()).expect("toc");
330        assert!(elapsed >= 0.0);
331    }
332}