Skip to main content

runmat_runtime/builtins/timing/
cputime.rs

1//! MATLAB-compatible legacy `cputime` builtin.
2
3#[cfg(target_arch = "wasm32")]
4use once_cell::sync::Lazy;
5use runmat_builtins::{
6    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
7    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
8};
9use runmat_macros::runtime_builtin;
10#[cfg(target_arch = "wasm32")]
11use runmat_time::Instant;
12use runmat_value::Value;
13#[cfg(windows)]
14use std::ffi::c_void;
15
16use crate::builtins::common::spec::{
17    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
18    ReductionNaN, ResidencyPolicy, ShapeRequirements,
19};
20use crate::builtins::timing::type_resolvers::cputime_type;
21use crate::{build_runtime_error, BuiltinResult, RuntimeError};
22
23const BUILTIN_NAME: &str = "cputime";
24
25#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::timing::cputime")]
26pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
27    name: "cputime",
28    op_kind: GpuOpKind::Custom("timer"),
29    supported_precisions: &[],
30    broadcast: BroadcastSemantics::None,
31    provider_hooks: &[],
32    constant_strategy: ConstantStrategy::InlineLiteral,
33    residency: ResidencyPolicy::GatherImmediately,
34    nan_mode: ReductionNaN::Include,
35    two_pass_threshold: None,
36    workgroup_size: None,
37    accepts_nan_mode: false,
38    notes: "Host-side process timing helper. GPU providers are never consulted.",
39};
40
41#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::timing::cputime")]
42pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
43    name: "cputime",
44    shape: ShapeRequirements::Any,
45    constant_strategy: ConstantStrategy::InlineLiteral,
46    elementwise: None,
47    reduction: None,
48    emits_nan: false,
49    notes: "Timing builtins execute eagerly on the host and do not participate in fusion.",
50};
51
52const CPUTIME_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
53    name: "t",
54    ty: BuiltinParamType::NumericScalar,
55    arity: BuiltinParamArity::Required,
56    default: None,
57    description: "Total CPU time used by the current RunMat process in seconds.",
58}];
59
60const CPUTIME_INPUTS: [BuiltinParamDescriptor; 0] = [];
61
62const CPUTIME_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
63    label: "t = cputime()",
64    inputs: &CPUTIME_INPUTS,
65    outputs: &CPUTIME_OUTPUT,
66}];
67
68const CPUTIME_ERROR_TOO_MANY_INPUTS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
69    code: "RM.CPUTIME.TOO_MANY_INPUTS",
70    identifier: Some("RunMat:cputime:TooManyInputs"),
71    when: "Any input arguments are supplied.",
72    message: "cputime: too many input arguments",
73};
74
75const CPUTIME_ERROR_UNAVAILABLE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
76    code: "RM.CPUTIME.UNAVAILABLE",
77    identifier: Some("RunMat:cputime:Unavailable"),
78    when: "The host platform reports that process CPU time is unavailable.",
79    message: "cputime: process CPU time is unavailable",
80};
81
82const CPUTIME_ERRORS: [BuiltinErrorDescriptor; 2] =
83    [CPUTIME_ERROR_TOO_MANY_INPUTS, CPUTIME_ERROR_UNAVAILABLE];
84
85pub const CPUTIME_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
86    signatures: &CPUTIME_SIGNATURES,
87    output_mode: BuiltinOutputMode::Fixed,
88    completion_policy: BuiltinCompletionPolicy::Public,
89    errors: &CPUTIME_ERRORS,
90};
91
92#[cfg(target_arch = "wasm32")]
93static FALLBACK_ORIGIN: Lazy<Instant> = Lazy::new(Instant::now);
94
95#[cfg(windows)]
96#[repr(C)]
97#[derive(Clone, Copy)]
98struct FileTime {
99    low_date_time: u32,
100    high_date_time: u32,
101}
102
103#[cfg(windows)]
104#[link(name = "kernel32")]
105extern "system" {
106    fn GetCurrentProcess() -> *mut c_void;
107    fn GetProcessTimes(
108        process: *mut c_void,
109        creation_time: *mut FileTime,
110        exit_time: *mut FileTime,
111        kernel_time: *mut FileTime,
112        user_time: *mut FileTime,
113    ) -> i32;
114}
115
116#[runtime_builtin(
117    name = "cputime",
118    category = "timing",
119    summary = "Return total CPU time used by the current process.",
120    keywords = "cputime,cpu time,timing,profiling,legacy",
121    accel = "metadata",
122    type_resolver(cputime_type),
123    descriptor(crate::builtins::timing::cputime::CPUTIME_DESCRIPTOR),
124    builtin_path = "crate::builtins::timing::cputime"
125)]
126fn cputime_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
127    if !args.is_empty() {
128        return Err(cputime_error(
129            CPUTIME_ERROR_TOO_MANY_INPUTS.message,
130            &CPUTIME_ERROR_TOO_MANY_INPUTS,
131        ));
132    }
133    Ok(Value::Num(process_cpu_seconds()?))
134}
135
136#[cfg(all(unix, not(target_arch = "wasm32")))]
137fn process_cpu_seconds() -> BuiltinResult<f64> {
138    let mut usage = std::mem::MaybeUninit::<libc::rusage>::uninit();
139    // SAFETY: `usage` points to valid writable memory for libc to initialize.
140    let rc = unsafe { libc::getrusage(libc::RUSAGE_SELF, usage.as_mut_ptr()) };
141    if rc != 0 {
142        return Err(cputime_error(
143            CPUTIME_ERROR_UNAVAILABLE.message,
144            &CPUTIME_ERROR_UNAVAILABLE,
145        ));
146    }
147    // SAFETY: getrusage returned success, so the structure has been initialized.
148    let usage = unsafe { usage.assume_init() };
149    Ok(timeval_seconds(usage.ru_utime) + timeval_seconds(usage.ru_stime))
150}
151
152#[cfg(windows)]
153fn process_cpu_seconds() -> BuiltinResult<f64> {
154    let mut creation_time = std::mem::MaybeUninit::<FileTime>::uninit();
155    let mut exit_time = std::mem::MaybeUninit::<FileTime>::uninit();
156    let mut kernel_time = std::mem::MaybeUninit::<FileTime>::uninit();
157    let mut user_time = std::mem::MaybeUninit::<FileTime>::uninit();
158    // SAFETY: GetCurrentProcess returns a valid pseudo-handle for the current
159    // process, and all FILETIME pointers reference writable uninitialized
160    // storage for GetProcessTimes to fill.
161    let ok = unsafe {
162        GetProcessTimes(
163            GetCurrentProcess(),
164            creation_time.as_mut_ptr(),
165            exit_time.as_mut_ptr(),
166            kernel_time.as_mut_ptr(),
167            user_time.as_mut_ptr(),
168        )
169    };
170    if ok == 0 {
171        return Err(cputime_error(
172            CPUTIME_ERROR_UNAVAILABLE.message,
173            &CPUTIME_ERROR_UNAVAILABLE,
174        ));
175    }
176    // SAFETY: GetProcessTimes returned success, so kernel/user times are initialized.
177    let kernel_time = unsafe { kernel_time.assume_init() };
178    let user_time = unsafe { user_time.assume_init() };
179    Ok((filetime_ticks(kernel_time) + filetime_ticks(user_time)) as f64 / 10_000_000.0)
180}
181
182#[cfg(target_arch = "wasm32")]
183fn process_cpu_seconds() -> BuiltinResult<f64> {
184    // Browsers do not expose process CPU accounting. Preserve a monotonic
185    // scalar clock so compatibility code can still take differences, and
186    // document the wall-clock fallback.
187    Ok(Instant::now()
188        .checked_duration_since(*FALLBACK_ORIGIN)
189        .unwrap_or_default()
190        .as_secs_f64())
191}
192
193#[cfg(windows)]
194fn filetime_ticks(value: FileTime) -> u64 {
195    ((value.high_date_time as u64) << 32) | value.low_date_time as u64
196}
197
198#[cfg(all(unix, not(target_arch = "wasm32")))]
199fn timeval_seconds(value: libc::timeval) -> f64 {
200    value.tv_sec as f64 + value.tv_usec as f64 / 1_000_000.0
201}
202
203fn cputime_error(
204    message: impl Into<String>,
205    error: &'static BuiltinErrorDescriptor,
206) -> RuntimeError {
207    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
208    if let Some(identifier) = error.identifier {
209        builder = builder.with_identifier(identifier);
210    }
211    builder.build()
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
219    #[test]
220    fn cputime_returns_finite_nonnegative_scalar() {
221        let value = cputime_builtin(Vec::new()).expect("cputime");
222        let Value::Num(time) = value else {
223            panic!("expected numeric scalar")
224        };
225        assert!(time.is_finite());
226        assert!(time >= 0.0);
227    }
228
229    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
230    #[test]
231    fn cputime_is_monotonic_for_current_process() {
232        let Value::Num(first) = cputime_builtin(Vec::new()).expect("first") else {
233            panic!("expected numeric scalar")
234        };
235        for value in 0_u64..10_000 {
236            std::hint::black_box(value.wrapping_mul(value));
237        }
238        let Value::Num(second) = cputime_builtin(Vec::new()).expect("second") else {
239            panic!("expected numeric scalar")
240        };
241        assert!(second >= first);
242    }
243
244    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
245    #[test]
246    fn cputime_rejects_inputs() {
247        let err = cputime_builtin(vec![Value::Num(1.0)]).unwrap_err();
248        assert_eq!(err.identifier(), CPUTIME_ERROR_TOO_MANY_INPUTS.identifier);
249    }
250}