Skip to main content

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 runmat_time::Instant;
6use std::sync::Mutex;
7use std::time::Duration;
8
9use crate::builtins::common::spec::{
10    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
11    ReductionNaN, ResidencyPolicy, ShapeRequirements,
12};
13use crate::builtins::timing::type_resolvers::tic_type;
14
15#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::timing::tic")]
16pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
17    name: "tic",
18    op_kind: GpuOpKind::Custom("timer"),
19    supported_precisions: &[],
20    broadcast: BroadcastSemantics::None,
21    provider_hooks: &[],
22    constant_strategy: ConstantStrategy::InlineLiteral,
23    residency: ResidencyPolicy::GatherImmediately,
24    nan_mode: ReductionNaN::Include,
25    two_pass_threshold: None,
26    workgroup_size: None,
27    accepts_nan_mode: false,
28    notes: "Stopwatch state lives on the host. Providers are never consulted for tic/toc.",
29};
30
31#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::timing::tic")]
32pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
33    name: "tic",
34    shape: ShapeRequirements::Any,
35    constant_strategy: ConstantStrategy::InlineLiteral,
36    elementwise: None,
37    reduction: None,
38    emits_nan: false,
39    notes: "Timing builtins are executed eagerly on the host and do not participate in fusion.",
40};
41
42static MONOTONIC_ORIGIN: Lazy<Instant> = Lazy::new(Instant::now);
43static STOPWATCH: Lazy<Mutex<StopwatchState>> = Lazy::new(|| Mutex::new(StopwatchState::default()));
44
45#[cfg(test)]
46pub(crate) static TEST_GUARD: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
47
48#[derive(Default)]
49struct StopwatchState {
50    stack: Vec<Instant>,
51}
52
53impl StopwatchState {
54    fn push(&mut self, instant: Instant) {
55        self.stack.push(instant);
56    }
57
58    fn pop(&mut self) -> Option<Instant> {
59        self.stack.pop()
60    }
61}
62
63const BUILTIN_NAME: &str = "tic";
64const LOCK_ERR: &str = "tic: failed to acquire stopwatch state";
65
66fn stopwatch_error(builtin: &str, message: impl Into<String>) -> crate::RuntimeError {
67    crate::build_runtime_error(message)
68        .with_builtin(builtin)
69        .build()
70}
71
72/// Start a stopwatch timer and return a handle suitable for `toc`.
73#[runtime_builtin(
74    name = "tic",
75    category = "timing",
76    summary = "Start a stopwatch timer and optionally return a handle for toc.",
77    keywords = "tic,timing,profiling,benchmark",
78    sink = true,
79    type_resolver(tic_type),
80    builtin_path = "crate::builtins::timing::tic"
81)]
82pub async fn tic_builtin() -> crate::BuiltinResult<f64> {
83    record_tic(BUILTIN_NAME)
84}
85
86/// Record a `tic` start time and return the encoded handle.
87pub(crate) fn record_tic(builtin: &str) -> Result<f64, crate::RuntimeError> {
88    let now = Instant::now();
89    {
90        let mut guard = STOPWATCH
91            .lock()
92            .map_err(|_| stopwatch_error(builtin, LOCK_ERR))?;
93        guard.push(now);
94    }
95    Ok(encode_instant(now))
96}
97
98/// Remove and return the most recently recorded `tic`, if any.
99pub(crate) fn take_latest_start(builtin: &str) -> Result<Option<Instant>, crate::RuntimeError> {
100    let mut guard = STOPWATCH
101        .lock()
102        .map_err(|_| stopwatch_error(builtin, LOCK_ERR))?;
103    Ok(guard.pop())
104}
105
106/// Encode an `Instant` into the scalar handle returned by `tic`.
107pub(crate) fn encode_instant(instant: Instant) -> f64 {
108    instant.duration_since(*MONOTONIC_ORIGIN).as_secs_f64()
109}
110
111/// Decode a scalar handle into an `Instant`.
112pub(crate) fn decode_handle(handle: f64, builtin: &str) -> Result<Instant, crate::RuntimeError> {
113    if !handle.is_finite() || handle.is_sign_negative() {
114        return Err(crate::build_runtime_error("toc: invalid timer handle")
115            .with_builtin(builtin)
116            .with_identifier("RunMat:toc:InvalidTimerHandle")
117            .build());
118    }
119    let duration = Duration::from_secs_f64(handle);
120    Ok((*MONOTONIC_ORIGIN) + duration)
121}
122
123#[cfg(test)]
124pub(crate) mod tests {
125    use super::*;
126    use futures::executor::block_on;
127    use std::thread;
128    use std::time::Duration;
129
130    fn reset_stopwatch() {
131        let mut guard = STOPWATCH.lock().unwrap();
132        guard.stack.clear();
133    }
134
135    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
136    #[test]
137    fn tic_returns_monotonic_handle() {
138        let _guard = TEST_GUARD.lock().unwrap();
139        reset_stopwatch();
140        let handle = block_on(tic_builtin()).expect("tic");
141        assert!(handle >= 0.0);
142        assert!(take_latest_start(BUILTIN_NAME).expect("take").is_some());
143    }
144
145    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
146    #[test]
147    fn tic_handles_increase_over_time() {
148        let _guard = TEST_GUARD.lock().unwrap();
149        reset_stopwatch();
150        let first = block_on(tic_builtin()).expect("tic");
151        thread::sleep(Duration::from_millis(5));
152        let second = block_on(tic_builtin()).expect("tic");
153        assert!(second > first);
154    }
155
156    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
157    #[test]
158    fn decode_roundtrip_matches_handle() {
159        let _guard = TEST_GUARD.lock().unwrap();
160        reset_stopwatch();
161        let handle = block_on(tic_builtin()).expect("tic");
162        let decoded = decode_handle(handle, "toc").expect("decode");
163        let round_trip = encode_instant(decoded);
164        let delta = (round_trip - handle).abs();
165        assert!(delta < 1e-9, "delta {delta}");
166    }
167
168    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
169    #[test]
170    fn take_latest_start_pops_stack() {
171        let _guard = TEST_GUARD.lock().unwrap();
172        reset_stopwatch();
173        block_on(tic_builtin()).expect("tic");
174        assert!(take_latest_start(BUILTIN_NAME).expect("take").is_some());
175        assert!(take_latest_start(BUILTIN_NAME)
176            .expect("second take")
177            .is_none());
178    }
179
180    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
181    #[test]
182    fn decode_handle_rejects_invalid_values() {
183        let _guard = TEST_GUARD.lock().unwrap();
184        assert!(decode_handle(f64::NAN, "toc").is_err());
185        assert!(decode_handle(-1.0, "toc").is_err());
186    }
187}