runmat_runtime/builtins/timing/
tic.rs

1//! MATLAB-compatible `tic` builtin with precise stopwatch semantics for RunMat.
2
3use once_cell::sync::Lazy;
4use runmat_macros::runtime_builtin;
5use std::sync::Mutex;
6use std::time::{Duration, Instant};
7
8use crate::builtins::common::spec::{
9    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
10    ReductionNaN, ResidencyPolicy, ShapeRequirements,
11};
12#[cfg(feature = "doc_export")]
13use crate::register_builtin_doc_text;
14use crate::{register_builtin_fusion_spec, register_builtin_gpu_spec};
15
16#[cfg(feature = "doc_export")]
17pub const DOC_MD: &str = r#"---
18title: "tic"
19category: "timing"
20keywords: ["tic", "timer", "profile", "benchmark", "performance"]
21summary: "Start a high-resolution stopwatch and optionally return a handle for toc."
22references: []
23gpu_support:
24  elementwise: false
25  reduction: false
26  precisions: []
27  broadcasting: "none"
28  notes: "Stopwatch helpers always run on the host CPU; GPU providers are not consulted."
29fusion:
30  elementwise: false
31  reduction: false
32  max_inputs: 0
33  constants: "inline"
34requires_feature: null
35tested:
36  unit: "builtins::timing::tic::tests"
37  integration: "runmat_runtime::io::tests::test_tic_toc"
38---
39
40# What does the `tic` function do in MATLAB / RunMat?
41`tic` starts a high-resolution stopwatch. Calls to `toc` report the elapsed time in seconds. When you assign
42the return value (for example, `t = tic;`), the resulting handle can be passed to `toc(t)` to measure a
43different code region while keeping the global stopwatch untouched.
44
45## How does the `tic` function behave in MATLAB / RunMat?
46- Uses the host's monotonic clock for nanosecond-resolution timing.
47- Supports nested timers: each call pushes a new start time on an internal stack. `toc` without inputs always
48  reads the most recent `tic` and removes it, leaving earlier timers intact so outer scopes continue measuring.
49- Returns an opaque scalar handle (a `double`) that encodes the monotonic timestamp. The handle can be stored
50  or passed explicitly to `toc`.
51- Executes entirely on the CPU. There are no GPU variants because `tic` interacts with wall-clock state.
52- Calling `toc` before `tic` raises the MATLAB-compatible error `MATLAB:toc:NoMatchingTic`.
53
54## How does `tic` behave with RunMat Accelerate?
55`tic` never leaves the CPU. When called while tensors reside on the GPU, the stopwatch state stays on the
56host. There are no acceleration-provider hooks for timers, so the runtime neither uploads nor gathers data.
57Fusion plans skip the builtin entirely because it has no numeric inputs.
58
59## Examples of using the `tic` function in MATLAB / RunMat
60
61### Measuring a simple loop
62
63```matlab
64tic;
65for k = 1:1e5
66    sqrt(k);
67end
68elapsed = toc;
69```
70
71`elapsed` reports the seconds since the matching `tic`.
72
73### Capturing and reusing the tic handle
74
75```matlab
76t = tic;
77heavyComputation();
78elapsed = toc(t);
79```
80
81Using the handle lets you insert additional timing regions without resetting the default stopwatch.
82
83### Nesting timers for staged profiling
84
85```matlab
86tic;              % Outer stopwatch
87stage1();         % Work you want to measure once
88inner = tic;      % Nested stopwatch
89stage2();
90innerT = toc(inner);  % Elapsed time for stage2 only
91outerT = toc;         % Elapsed time for everything since the first tic
92```
93
94`toc` without inputs reads the most recent `tic`, so nested regions work naturally.
95
96### Measuring asynchronous work
97
98```matlab
99token = tic;
100future = backgroundTask();
101wait(future);
102elapsed = toc(token);
103```
104
105Handles can be stored in structures or passed to callbacks while asynchronous work completes.
106
107### Resetting the stopwatch after a measurement
108
109```matlab
110elapsed1 = toc(tic);  % Equivalent to separate tic/toc calls
111pause(0.1);
112elapsed2 = toc(tic);  % Starts a new timer immediately
113```
114
115Calling `toc(tic)` starts a new stopwatch and immediately measures it, mirroring MATLAB idioms.
116
117## FAQ
118
119### Does `tic` print anything when called without a semicolon?
120No. `tic` is marked as a sink builtin, so scripts do not display the returned handle unless you assign it or
121explicitly request output.
122
123### Is the returned handle portable across sessions?
124No. The handle encodes a monotonic timestamp that is only meaningful within the current RunMat process. Passing
125it to another session or saving it to disk is undefined behaviour, matching MATLAB.
126
127### Can I run `tic` on a worker thread?
128Yes. Each thread shares the same stopwatch stack. Nested `tic`/`toc` pairs remain well-defined, but you should
129serialise access at the script level to avoid interleaving unrelated timings.
130
131### How accurate is the measurement?
132`tic` relies on `std::time::Instant`, typically providing microsecond or better precision. The actual resolution
133depends on your operating system. There is no artificial jitter or throttling introduced by RunMat.
134
135### Does `tic` participate in GPU fusion?
136No. Timer builtins are tagged as CPU-only. Expressions containing `tic` are always executed on the host, and
137any GPU-resident tensors are gathered automatically by surrounding code when necessary.
138
139## See Also
140[toc](./toc), [timeit](./timeit), [profile](../diagnostics/profile)
141
142## Source & Feedback
143- Implementation: [`crates/runmat-runtime/src/builtins/timing/tic.rs`](https://github.com/runmat-org/runmat/blob/main/crates/runmat-runtime/src/builtins/timing/tic.rs)
144- Found a behavioural difference? [Open an issue](https://github.com/runmat-org/runmat/issues/new/choose) with a minimal repro.
145"#;
146
147pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
148    name: "tic",
149    op_kind: GpuOpKind::Custom("timer"),
150    supported_precisions: &[],
151    broadcast: BroadcastSemantics::None,
152    provider_hooks: &[],
153    constant_strategy: ConstantStrategy::InlineLiteral,
154    residency: ResidencyPolicy::GatherImmediately,
155    nan_mode: ReductionNaN::Include,
156    two_pass_threshold: None,
157    workgroup_size: None,
158    accepts_nan_mode: false,
159    notes: "Stopwatch state lives on the host. Providers are never consulted for tic/toc.",
160};
161
162register_builtin_gpu_spec!(GPU_SPEC);
163
164pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
165    name: "tic",
166    shape: ShapeRequirements::Any,
167    constant_strategy: ConstantStrategy::InlineLiteral,
168    elementwise: None,
169    reduction: None,
170    emits_nan: false,
171    notes: "Timing builtins are executed eagerly on the host and do not participate in fusion.",
172};
173
174register_builtin_fusion_spec!(FUSION_SPEC);
175
176#[cfg(feature = "doc_export")]
177register_builtin_doc_text!("tic", DOC_MD);
178
179static MONOTONIC_ORIGIN: Lazy<Instant> = Lazy::new(Instant::now);
180static STOPWATCH: Lazy<Mutex<StopwatchState>> = Lazy::new(|| Mutex::new(StopwatchState::default()));
181
182#[cfg(test)]
183pub(crate) static TEST_GUARD: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
184
185#[derive(Default)]
186struct StopwatchState {
187    stack: Vec<Instant>,
188}
189
190impl StopwatchState {
191    fn push(&mut self, instant: Instant) {
192        self.stack.push(instant);
193    }
194
195    fn pop(&mut self) -> Option<Instant> {
196        self.stack.pop()
197    }
198}
199
200const LOCK_ERR: &str = "tic: failed to acquire stopwatch state";
201
202/// Start a stopwatch timer and return a handle suitable for `toc`.
203#[runtime_builtin(
204    name = "tic",
205    category = "timing",
206    summary = "Start a stopwatch timer and optionally return a handle for toc.",
207    keywords = "tic,timing,profiling,benchmark",
208    sink = true
209)]
210pub fn tic_builtin() -> Result<f64, String> {
211    record_tic()
212}
213
214/// Record a `tic` start time and return the encoded handle.
215pub(crate) fn record_tic() -> Result<f64, String> {
216    let now = Instant::now();
217    {
218        let mut guard = STOPWATCH.lock().map_err(|_| LOCK_ERR.to_string())?;
219        guard.push(now);
220    }
221    Ok(encode_instant(now))
222}
223
224/// Remove and return the most recently recorded `tic`, if any.
225pub(crate) fn take_latest_start() -> Result<Option<Instant>, String> {
226    let mut guard = STOPWATCH.lock().map_err(|_| LOCK_ERR.to_string())?;
227    Ok(guard.pop())
228}
229
230/// Encode an `Instant` into the scalar handle returned by `tic`.
231pub(crate) fn encode_instant(instant: Instant) -> f64 {
232    instant.duration_since(*MONOTONIC_ORIGIN).as_secs_f64()
233}
234
235/// Decode a scalar handle into an `Instant`.
236pub(crate) fn decode_handle(handle: f64) -> Result<Instant, String> {
237    if !handle.is_finite() || handle.is_sign_negative() {
238        return Err("MATLAB:toc:InvalidTimerHandle".to_string());
239    }
240    let duration = Duration::from_secs_f64(handle);
241    Ok((*MONOTONIC_ORIGIN) + duration)
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247    use std::thread;
248    use std::time::Duration;
249
250    #[cfg(feature = "doc_export")]
251    use crate::builtins::common::test_support;
252
253    fn reset_stopwatch() {
254        let mut guard = STOPWATCH.lock().unwrap();
255        guard.stack.clear();
256    }
257
258    #[test]
259    fn tic_returns_monotonic_handle() {
260        let _guard = TEST_GUARD.lock().unwrap();
261        reset_stopwatch();
262        let handle = tic_builtin().expect("tic");
263        assert!(handle >= 0.0);
264        assert!(take_latest_start().expect("take").is_some());
265    }
266
267    #[test]
268    fn tic_handles_increase_over_time() {
269        let _guard = TEST_GUARD.lock().unwrap();
270        reset_stopwatch();
271        let first = tic_builtin().expect("tic");
272        thread::sleep(Duration::from_millis(5));
273        let second = tic_builtin().expect("tic");
274        assert!(second > first);
275    }
276
277    #[test]
278    fn decode_roundtrip_matches_handle() {
279        let _guard = TEST_GUARD.lock().unwrap();
280        reset_stopwatch();
281        let handle = tic_builtin().expect("tic");
282        let decoded = decode_handle(handle).expect("decode");
283        let round_trip = encode_instant(decoded);
284        let delta = (round_trip - handle).abs();
285        assert!(delta < 1e-9, "delta {delta}");
286    }
287
288    #[test]
289    fn take_latest_start_pops_stack() {
290        let _guard = TEST_GUARD.lock().unwrap();
291        reset_stopwatch();
292        tic_builtin().expect("tic");
293        assert!(take_latest_start().expect("take").is_some());
294        assert!(take_latest_start().expect("second take").is_none());
295    }
296
297    #[test]
298    fn decode_handle_rejects_invalid_values() {
299        let _guard = TEST_GUARD.lock().unwrap();
300        assert!(decode_handle(f64::NAN).is_err());
301        assert!(decode_handle(-1.0).is_err());
302    }
303
304    #[test]
305    #[cfg(feature = "doc_export")]
306    fn doc_examples_present() {
307        let _guard = TEST_GUARD.lock().unwrap();
308        let blocks = test_support::doc_examples(DOC_MD);
309        assert!(!blocks.is_empty());
310    }
311}