runmat_runtime/builtins/timing/
tic.rs1use 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#[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
86pub(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
98pub(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
106pub(crate) fn encode_instant(instant: Instant) -> f64 {
108 instant.duration_since(*MONOTONIC_ORIGIN).as_secs_f64()
109}
110
111pub(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}