runmat_runtime/builtins/timing/
toc.rs1use 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#[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 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}