seq_runtime/time_ops.rs
1//! Time operations for Seq
2//!
3//! Provides timing primitives for performance measurement and delays.
4//!
5//! # Usage from Seq
6//!
7//! ```seq
8//! time.now # ( -- Int ) microseconds since epoch
9//! time.nanos # ( -- Int ) nanoseconds (monotonic, for timing)
10//! 100 time.sleep-ms # ( Int -- ) sleep for N milliseconds
11//! ```
12//!
13//! # Example: Measuring execution time
14//!
15//! ```seq
16//! : benchmark ( -- )
17//! time.nanos # start time
18//! do-work
19//! time.nanos # end time
20//! swap - # elapsed nanos
21//! 1000000 / # convert to ms
22//! "Elapsed: " write
23//! int->string write
24//! "ms" write-line
25//! ;
26//! ```
27
28use crate::stack::{Stack, pop, push};
29use crate::value::Value;
30use std::time::{Duration, SystemTime, UNIX_EPOCH};
31
32/// Get current time in microseconds since Unix epoch
33///
34/// Stack effect: ( -- Int )
35///
36/// Returns wall-clock time. Good for timestamps.
37/// For measuring durations, prefer `time.nanos` which uses a monotonic clock.
38///
39/// # Safety
40/// - `stack` must be a valid stack pointer (may be null for empty stack)
41#[unsafe(no_mangle)]
42pub unsafe extern "C" fn patch_seq_time_now(stack: Stack) -> Stack {
43 let micros = SystemTime::now()
44 .duration_since(UNIX_EPOCH)
45 .map(|d| d.as_micros() as i64)
46 .unwrap_or(0);
47
48 unsafe { push(stack, Value::Int(micros)) }
49}
50
51/// Get monotonic nanoseconds for precise timing
52///
53/// Stack effect: ( -- Int )
54///
55/// Returns nanoseconds elapsed since the first call to this function.
56/// Uses CLOCK_MONOTONIC for thread-independent consistent values.
57/// Values start near zero for easier arithmetic.
58///
59/// # Safety
60/// - `stack` must be a valid stack pointer (may be null for empty stack)
61#[unsafe(no_mangle)]
62pub unsafe extern "C" fn patch_seq_time_nanos(stack: Stack) -> Stack {
63 let nanos = elapsed_nanos();
64 unsafe { push(stack, Value::Int(nanos)) }
65}
66
67/// Get elapsed nanoseconds since program start.
68///
69/// Thread-safe, consistent across all threads. Uses a lazily-initialized
70/// base time to ensure values start near zero.
71#[inline]
72fn elapsed_nanos() -> i64 {
73 use std::sync::atomic::{AtomicI64, Ordering};
74
75 // Base time is initialized on first call (value 0 means uninitialized)
76 static BASE_NANOS: AtomicI64 = AtomicI64::new(0);
77
78 let current = raw_monotonic_nanos();
79
80 // Try to read existing base time
81 let base = BASE_NANOS.load(Ordering::Relaxed);
82 if base != 0 {
83 return current.saturating_sub(base);
84 }
85
86 // First call: try to set the base time
87 match BASE_NANOS.compare_exchange(0, current, Ordering::Relaxed, Ordering::Relaxed) {
88 Ok(_) => 0, // We set the base, elapsed is 0
89 Err(actual_base) => current.saturating_sub(actual_base), // Another thread set it
90 }
91}
92
93/// Get raw monotonic nanoseconds from the system clock.
94///
95/// On Unix: Uses `clock_gettime(CLOCK_MONOTONIC)` directly to get absolute
96/// nanoseconds since boot. This is thread-independent - the same value is
97/// returned regardless of which OS thread calls it.
98///
99/// On Windows: Falls back to `Instant::now()` with a process-wide base time.
100#[inline]
101#[cfg(unix)]
102fn raw_monotonic_nanos() -> i64 {
103 let mut ts = libc::timespec {
104 tv_sec: 0,
105 tv_nsec: 0,
106 };
107 // SAFETY: ts is a valid pointer to a timespec struct
108 unsafe {
109 libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts);
110 }
111 // Convert to nanoseconds, saturating at i64::MAX
112 // Explicit i64 casts for portability (tv_sec/tv_nsec types vary by platform)
113 #[allow(clippy::unnecessary_cast)] // Required for 32-bit platforms
114 let secs = (ts.tv_sec as i64).saturating_mul(1_000_000_000);
115 #[allow(clippy::unnecessary_cast)]
116 secs.saturating_add(ts.tv_nsec as i64)
117}
118
119/// Windows fallback using Instant with a process-wide base time.
120/// Uses OnceLock for thread-safe one-time initialization.
121#[inline]
122#[cfg(not(unix))]
123fn raw_monotonic_nanos() -> i64 {
124 use std::sync::OnceLock;
125 use std::time::Instant;
126
127 static BASE: OnceLock<Instant> = OnceLock::new();
128 let base = BASE.get_or_init(Instant::now);
129 base.elapsed().as_nanos().try_into().unwrap_or(i64::MAX)
130}
131
132/// Sleep for a specified number of milliseconds
133///
134/// Stack effect: ( Int -- )
135///
136/// Yields the current strand to the scheduler while sleeping.
137/// Uses `may::coroutine::sleep` for cooperative scheduling.
138///
139/// # Safety
140/// - `stack` must be a valid, non-null stack pointer with an Int on top
141#[unsafe(no_mangle)]
142pub unsafe extern "C" fn patch_seq_time_sleep_ms(stack: Stack) -> Stack {
143 assert!(!stack.is_null(), "time.sleep-ms: stack is empty");
144
145 let (rest, value) = unsafe { pop(stack) };
146
147 match value {
148 Value::Int(ms) => {
149 if ms < 0 {
150 panic!("time.sleep-ms: duration must be non-negative, got {}", ms);
151 }
152
153 // Use may's coroutine-aware sleep for cooperative scheduling
154 may::coroutine::sleep(Duration::from_millis(ms as u64));
155
156 rest
157 }
158 _ => panic!(
159 "time.sleep-ms: expected Int duration on stack, got {:?}",
160 value
161 ),
162 }
163}
164
165// Public re-exports
166pub use patch_seq_time_nanos as time_nanos;
167pub use patch_seq_time_now as time_now;
168pub use patch_seq_time_sleep_ms as time_sleep_ms;
169
170#[cfg(test)]
171mod tests;