runmat_runtime/builtins/timing/
tic.rs1use 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#[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
214pub(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
224pub(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
230pub(crate) fn encode_instant(instant: Instant) -> f64 {
232 instant.duration_since(*MONOTONIC_ORIGIN).as_secs_f64()
233}
234
235pub(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}