Skip to main content

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 runmat_time::Instant;
6use std::convert::TryFrom;
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};
13use crate::builtins::timing::type_resolvers::toc_type;
14
15#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::timing::toc")]
16pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
17    name: "toc",
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 toc.",
29};
30
31#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::timing::toc")]
32pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
33    name: "toc",
34    shape: ShapeRequirements::Any,
35    constant_strategy: ConstantStrategy::InlineLiteral,
36    elementwise: None,
37    reduction: None,
38    emits_nan: false,
39    notes: "Timing builtins execute eagerly on the host and do not participate in fusion.",
40};
41
42const BUILTIN_NAME: &str = "toc";
43const ERR_NO_MATCHING_TIC: &str = "RunMat:toc:NoMatchingTic";
44const ERR_INVALID_HANDLE: &str = "RunMat:toc:InvalidTimerHandle";
45const ERR_TOO_MANY_INPUTS: &str = "RunMat:toc:TooManyInputs";
46
47fn toc_error_with_identifier(message: impl Into<String>, identifier: &str) -> crate::RuntimeError {
48    crate::build_runtime_error(message)
49        .with_builtin(BUILTIN_NAME)
50        .with_identifier(identifier)
51        .build()
52        .into()
53}
54
55/// Read elapsed time from the stopwatch stack or a specific handle.
56#[runtime_builtin(
57    name = "toc",
58    category = "timing",
59    summary = "Read the elapsed time since the most recent tic or an explicit handle.",
60    keywords = "toc,timing,profiling,benchmark",
61    type_resolver(toc_type),
62    builtin_path = "crate::builtins::timing::toc"
63)]
64pub async fn toc_builtin(args: Vec<Value>) -> crate::BuiltinResult<f64> {
65    match args.len() {
66        0 => latest_elapsed(),
67        1 => elapsed_from_value(&args[0]),
68        _ => Err(toc_error_with_identifier(
69            "toc: too many input arguments",
70            ERR_TOO_MANY_INPUTS,
71        )),
72    }
73}
74
75fn latest_elapsed() -> Result<f64, crate::RuntimeError> {
76    let start = take_latest_start(BUILTIN_NAME)?
77        .ok_or_else(|| toc_error_with_identifier("toc: no matching tic", ERR_NO_MATCHING_TIC))?;
78    Ok(start.elapsed().as_secs_f64())
79}
80
81fn elapsed_from_value(value: &Value) -> Result<f64, crate::RuntimeError> {
82    let handle = f64::try_from(value)
83        .map_err(|_| toc_error_with_identifier("toc: invalid timer handle", ERR_INVALID_HANDLE))?;
84    let instant = decode_handle(handle, BUILTIN_NAME)?;
85    let now = Instant::now();
86    let elapsed = now.checked_duration_since(instant).ok_or_else(|| {
87        toc_error_with_identifier("toc: invalid timer handle", ERR_INVALID_HANDLE)
88    })?;
89    Ok(elapsed.as_secs_f64())
90}
91
92#[cfg(test)]
93pub(crate) mod tests {
94    use super::*;
95    use crate::builtins::timing::tic::{encode_instant, record_tic, take_latest_start, TEST_GUARD};
96    use futures::executor::block_on;
97    use std::time::Duration;
98
99    fn clear_tic_stack() {
100        while let Ok(Some(_)) = take_latest_start(BUILTIN_NAME) {}
101    }
102
103    fn assert_toc_error_identifier(err: crate::RuntimeError, identifier: &str) {
104        assert_eq!(
105            err.identifier(),
106            Some(identifier),
107            "message: {}",
108            err.message()
109        );
110    }
111
112    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
113    #[test]
114    fn toc_requires_matching_tic() {
115        let _guard = TEST_GUARD.lock().unwrap();
116        clear_tic_stack();
117        let err = block_on(toc_builtin(Vec::new())).unwrap_err();
118        assert_toc_error_identifier(err, ERR_NO_MATCHING_TIC);
119    }
120
121    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
122    #[test]
123    fn toc_reports_elapsed_for_latest_start() {
124        let _guard = TEST_GUARD.lock().unwrap();
125        clear_tic_stack();
126        record_tic("tic").expect("tic");
127        std::thread::sleep(Duration::from_millis(5));
128        let elapsed = block_on(toc_builtin(Vec::new())).expect("toc");
129        assert!(elapsed >= 0.0);
130        assert!(take_latest_start(BUILTIN_NAME).unwrap().is_none());
131    }
132
133    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
134    #[test]
135    fn toc_with_handle_measures_without_popping_stack() {
136        let _guard = TEST_GUARD.lock().unwrap();
137        clear_tic_stack();
138        let handle = record_tic("tic").expect("tic");
139        std::thread::sleep(Duration::from_millis(5));
140        let elapsed = block_on(toc_builtin(vec![Value::Num(handle)])).expect("toc(handle)");
141        assert!(elapsed >= 0.0);
142        // Stack still contains the entry so a subsequent toc pops it.
143        let later = block_on(toc_builtin(Vec::new())).expect("second toc");
144        assert!(later >= elapsed);
145    }
146
147    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
148    #[test]
149    fn toc_rejects_invalid_handle() {
150        let _guard = TEST_GUARD.lock().unwrap();
151        clear_tic_stack();
152        let err = block_on(toc_builtin(vec![Value::Num(f64::NAN)])).unwrap_err();
153        assert_toc_error_identifier(err, ERR_INVALID_HANDLE);
154    }
155
156    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
157    #[test]
158    fn toc_rejects_future_handle() {
159        let _guard = TEST_GUARD.lock().unwrap();
160        clear_tic_stack();
161        let future_handle = encode_instant(Instant::now()) + 10_000.0;
162        let err = block_on(toc_builtin(vec![Value::Num(future_handle)])).unwrap_err();
163        assert_toc_error_identifier(err, ERR_INVALID_HANDLE);
164    }
165
166    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
167    #[test]
168    fn toc_rejects_string_handle() {
169        let _guard = TEST_GUARD.lock().unwrap();
170        clear_tic_stack();
171        let err = block_on(toc_builtin(vec![Value::from("not a timer")])).unwrap_err();
172        assert_toc_error_identifier(err, ERR_INVALID_HANDLE);
173    }
174
175    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
176    #[test]
177    fn toc_rejects_extra_arguments() {
178        let _guard = TEST_GUARD.lock().unwrap();
179        clear_tic_stack();
180        let err = block_on(toc_builtin(vec![Value::Num(0.0), Value::Num(0.0)])).unwrap_err();
181        assert_toc_error_identifier(err, ERR_TOO_MANY_INPUTS);
182    }
183
184    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
185    #[test]
186    fn toc_nested_timers() {
187        let _guard = TEST_GUARD.lock().unwrap();
188        clear_tic_stack();
189        record_tic("tic").expect("outer");
190        std::thread::sleep(Duration::from_millis(2));
191        record_tic("tic").expect("inner");
192        std::thread::sleep(Duration::from_millis(4));
193        let inner = block_on(toc_builtin(Vec::new())).expect("inner toc");
194        assert!(inner >= 0.0);
195        std::thread::sleep(Duration::from_millis(2));
196        let outer = block_on(toc_builtin(Vec::new())).expect("outer toc");
197        assert!(outer >= inner);
198    }
199
200    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
201    #[test]
202    #[cfg(feature = "wgpu")]
203    fn toc_ignores_wgpu_provider() {
204        let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
205            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
206        );
207        let _guard = TEST_GUARD.lock().unwrap();
208        clear_tic_stack();
209        record_tic("tic").expect("tic");
210        std::thread::sleep(Duration::from_millis(1));
211        let elapsed = block_on(toc_builtin(Vec::new())).expect("toc");
212        assert!(elapsed >= 0.0);
213    }
214}