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