1use runmat_time::Instant;
7use std::cmp::Ordering;
8
9use runmat_builtins::{
10 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
11 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor, Value,
12};
13use runmat_macros::runtime_builtin;
14
15use crate::builtins::common::spec::{
16 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
17 ReductionNaN, ResidencyPolicy, ShapeRequirements,
18};
19use crate::builtins::timing::type_resolvers::timeit_type;
20
21const TARGET_BATCH_SECONDS: f64 = 0.005;
22const MAX_BATCH_SECONDS: f64 = 0.25;
23const LOOP_COUNT_LIMIT: usize = 1 << 20;
24const MIN_SAMPLE_COUNT: usize = 7;
25const MAX_SAMPLE_COUNT: usize = 21;
26const BUILTIN_NAME: &str = "timeit";
27
28const TIMEIT_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
29 name: "t",
30 ty: BuiltinParamType::NumericScalar,
31 arity: BuiltinParamArity::Required,
32 default: None,
33 description: "Median execution time per invocation in seconds.",
34}];
35
36const TIMEIT_INPUTS_ONE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
37 name: "f",
38 ty: BuiltinParamType::Any,
39 arity: BuiltinParamArity::Required,
40 default: None,
41 description: "Zero-input function handle to benchmark.",
42}];
43
44const TIMEIT_INPUTS_TWO: [BuiltinParamDescriptor; 2] = [
45 BuiltinParamDescriptor {
46 name: "f",
47 ty: BuiltinParamType::Any,
48 arity: BuiltinParamArity::Required,
49 default: None,
50 description: "Zero-input function handle to benchmark.",
51 },
52 BuiltinParamDescriptor {
53 name: "numOutputs",
54 ty: BuiltinParamType::IntegerScalar,
55 arity: BuiltinParamArity::Optional,
56 default: Some("1"),
57 description: "Requested output count for invoking the benchmarked handle.",
58 },
59];
60
61const TIMEIT_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
62 BuiltinSignatureDescriptor {
63 label: "t = timeit(f)",
64 inputs: &TIMEIT_INPUTS_ONE,
65 outputs: &TIMEIT_OUTPUT,
66 },
67 BuiltinSignatureDescriptor {
68 label: "t = timeit(f, numOutputs)",
69 inputs: &TIMEIT_INPUTS_TWO,
70 outputs: &TIMEIT_OUTPUT,
71 },
72];
73
74const TIMEIT_ERROR_TOO_MANY_INPUTS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
75 code: "RM.TIMEIT.TOO_MANY_INPUTS",
76 identifier: Some("RunMat:timeit:TooManyInputs"),
77 when: "More than two input arguments are supplied.",
78 message: "timeit: too many input arguments",
79};
80
81const TIMEIT_ERROR_NUM_OUTPUTS_SCALAR: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
82 code: "RM.TIMEIT.NUM_OUTPUTS_SCALAR",
83 identifier: Some("RunMat:timeit:NumOutputsScalar"),
84 when: "numOutputs is not a scalar numeric/integer value.",
85 message: "timeit: numOutputs must be a scalar numeric value",
86};
87
88const TIMEIT_ERROR_NUM_OUTPUTS_FINITE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
89 code: "RM.TIMEIT.NUM_OUTPUTS_FINITE",
90 identifier: Some("RunMat:timeit:NumOutputsFinite"),
91 when: "numOutputs is NaN or infinite.",
92 message: "timeit: numOutputs must be finite",
93};
94
95const TIMEIT_ERROR_NUM_OUTPUTS_NONNEG: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
96 code: "RM.TIMEIT.NUM_OUTPUTS_NONNEGATIVE",
97 identifier: Some("RunMat:timeit:NumOutputsNonnegative"),
98 when: "numOutputs is negative.",
99 message: "timeit: numOutputs must be a nonnegative integer",
100};
101
102const TIMEIT_ERROR_NUM_OUTPUTS_INTEGER: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
103 code: "RM.TIMEIT.NUM_OUTPUTS_INTEGER",
104 identifier: Some("RunMat:timeit:NumOutputsInteger"),
105 when: "numOutputs has a non-integer numeric value.",
106 message: "timeit: numOutputs must be an integer value",
107};
108
109const TIMEIT_ERROR_EMPTY_HANDLE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
110 code: "RM.TIMEIT.EMPTY_HANDLE",
111 identifier: Some("RunMat:timeit:EmptyFunctionHandle"),
112 when: "A function-handle string or payload is empty after trimming.",
113 message: "timeit: empty function handle string",
114};
115
116const TIMEIT_ERROR_EXPECTS_AT_HANDLE_STRING: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
117 code: "RM.TIMEIT.EXPECTS_AT_HANDLE_STRING",
118 identifier: Some("RunMat:timeit:ExpectedAtHandleString"),
119 when: "A string/char function handle does not begin with '@'.",
120 message: "timeit: expected a function handle string beginning with '@'",
121};
122
123const TIMEIT_ERROR_HANDLE_KIND: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
124 code: "RM.TIMEIT.HANDLE_KIND",
125 identifier: Some("RunMat:timeit:HandleKind"),
126 when: "Function handle argument is not a scalar string/char or callable handle value.",
127 message: "timeit: function handle must be a string scalar or function handle",
128};
129
130const TIMEIT_ERROR_FIRST_ARG_KIND: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
131 code: "RM.TIMEIT.FIRST_ARG_KIND",
132 identifier: Some("RunMat:timeit:FirstArgKind"),
133 when: "First argument is not a function handle value.",
134 message: "timeit: first argument must be a function handle",
135};
136
137const TIMEIT_ERRORS: [BuiltinErrorDescriptor; 9] = [
138 TIMEIT_ERROR_TOO_MANY_INPUTS,
139 TIMEIT_ERROR_NUM_OUTPUTS_SCALAR,
140 TIMEIT_ERROR_NUM_OUTPUTS_FINITE,
141 TIMEIT_ERROR_NUM_OUTPUTS_NONNEG,
142 TIMEIT_ERROR_NUM_OUTPUTS_INTEGER,
143 TIMEIT_ERROR_EMPTY_HANDLE,
144 TIMEIT_ERROR_EXPECTS_AT_HANDLE_STRING,
145 TIMEIT_ERROR_HANDLE_KIND,
146 TIMEIT_ERROR_FIRST_ARG_KIND,
147];
148
149pub const TIMEIT_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
150 signatures: &TIMEIT_SIGNATURES,
151 output_mode: BuiltinOutputMode::Fixed,
152 completion_policy: BuiltinCompletionPolicy::Public,
153 errors: &TIMEIT_ERRORS,
154};
155
156fn timeit_error_with_message(
157 message: impl Into<String>,
158 error: &'static BuiltinErrorDescriptor,
159) -> crate::RuntimeError {
160 let mut builder = crate::build_runtime_error(message).with_builtin(BUILTIN_NAME);
161 if let Some(identifier) = error.identifier {
162 builder = builder.with_identifier(identifier);
163 }
164 builder.build()
165}
166
167#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::timing::timeit")]
168pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
169 name: "timeit",
170 op_kind: GpuOpKind::Custom("timer"),
171 supported_precisions: &[],
172 broadcast: BroadcastSemantics::None,
173 provider_hooks: &[],
174 constant_strategy: ConstantStrategy::InlineLiteral,
175 residency: ResidencyPolicy::GatherImmediately,
176 nan_mode: ReductionNaN::Include,
177 two_pass_threshold: None,
178 workgroup_size: None,
179 accepts_nan_mode: false,
180 notes: "Host-side helper; GPU kernels execute only if invoked by the timed function.",
181};
182
183#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::timing::timeit")]
184pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
185 name: "timeit",
186 shape: ShapeRequirements::Any,
187 constant_strategy: ConstantStrategy::InlineLiteral,
188 elementwise: None,
189 reduction: None,
190 emits_nan: false,
191 notes: "Timing helper; excluded from fusion planning.",
192};
193
194#[runtime_builtin(
195 name = "timeit",
196 category = "timing",
197 summary = "Measure runtime of zero-argument function handles using repeated execution.",
198 keywords = "timeit,benchmark,timing,performance,gpu",
199 accel = "helper",
200 type_resolver(timeit_type),
201 descriptor(crate::builtins::timing::timeit::TIMEIT_DESCRIPTOR),
202 builtin_path = "crate::builtins::timing::timeit"
203)]
204async fn timeit_builtin(func: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
205 let requested_outputs = parse_num_outputs(&rest)?;
206 let callable = prepare_callable(func, requested_outputs)?;
207
208 callable.invoke().await?;
210
211 let loop_count = determine_loop_count(&callable).await?;
212 let samples = collect_samples(&callable, loop_count).await?;
213 if samples.is_empty() {
214 return Ok(Value::Num(0.0));
215 }
216
217 Ok(Value::Num(compute_median(samples)))
218}
219
220fn parse_num_outputs(rest: &[Value]) -> Result<Option<usize>, crate::RuntimeError> {
221 match rest.len() {
222 0 => Ok(None),
223 1 => parse_non_negative_integer(&rest[0]).map(Some),
224 _ => Err(timeit_error_with_message(
225 TIMEIT_ERROR_TOO_MANY_INPUTS.message,
226 &TIMEIT_ERROR_TOO_MANY_INPUTS,
227 )),
228 }
229}
230
231fn parse_non_negative_integer(value: &Value) -> Result<usize, crate::RuntimeError> {
232 match value {
233 Value::Int(iv) => {
234 let raw = iv.to_i64();
235 if raw < 0 {
236 Err(timeit_error_with_message(
237 TIMEIT_ERROR_NUM_OUTPUTS_NONNEG.message,
238 &TIMEIT_ERROR_NUM_OUTPUTS_NONNEG,
239 ))
240 } else {
241 Ok(raw as usize)
242 }
243 }
244 Value::Num(n) => {
245 if !n.is_finite() {
246 return Err(timeit_error_with_message(
247 TIMEIT_ERROR_NUM_OUTPUTS_FINITE.message,
248 &TIMEIT_ERROR_NUM_OUTPUTS_FINITE,
249 ));
250 }
251 if *n < 0.0 {
252 return Err(timeit_error_with_message(
253 TIMEIT_ERROR_NUM_OUTPUTS_NONNEG.message,
254 &TIMEIT_ERROR_NUM_OUTPUTS_NONNEG,
255 ));
256 }
257 let rounded = n.round();
258 if (rounded - n).abs() > f64::EPSILON {
259 return Err(timeit_error_with_message(
260 TIMEIT_ERROR_NUM_OUTPUTS_INTEGER.message,
261 &TIMEIT_ERROR_NUM_OUTPUTS_INTEGER,
262 ));
263 }
264 Ok(rounded as usize)
265 }
266 _ => Err(timeit_error_with_message(
267 TIMEIT_ERROR_NUM_OUTPUTS_SCALAR.message,
268 &TIMEIT_ERROR_NUM_OUTPUTS_SCALAR,
269 )),
270 }
271}
272
273async fn determine_loop_count(callable: &TimeitCallable) -> Result<usize, crate::RuntimeError> {
274 let mut loops = 1usize;
275 loop {
276 let elapsed = run_batch(callable, loops).await?;
277 if elapsed >= TARGET_BATCH_SECONDS
278 || elapsed >= MAX_BATCH_SECONDS
279 || loops >= LOOP_COUNT_LIMIT
280 {
281 return Ok(loops);
282 }
283 loops = loops.saturating_mul(2);
284 if loops == 0 {
285 return Ok(LOOP_COUNT_LIMIT);
286 }
287 }
288}
289
290async fn collect_samples(
291 callable: &TimeitCallable,
292 loop_count: usize,
293) -> Result<Vec<f64>, crate::RuntimeError> {
294 let mut samples = Vec::with_capacity(MIN_SAMPLE_COUNT);
295 while samples.len() < MIN_SAMPLE_COUNT {
296 let elapsed = run_batch(callable, loop_count).await?;
297 let per_iter = elapsed / loop_count as f64;
298 samples.push(per_iter);
299 if samples.len() >= MAX_SAMPLE_COUNT || elapsed >= MAX_BATCH_SECONDS {
300 break;
301 }
302 }
303 Ok(samples)
304}
305
306async fn run_batch(
307 callable: &TimeitCallable,
308 loop_count: usize,
309) -> Result<f64, crate::RuntimeError> {
310 let start = Instant::now();
311 for _ in 0..loop_count {
312 let value = callable.invoke().await?;
313 drop(value);
314 }
315 Ok(start.elapsed().as_secs_f64())
316}
317
318fn compute_median(mut samples: Vec<f64>) -> f64 {
319 if samples.is_empty() {
320 return 0.0;
321 }
322 samples.sort_by(|a, b| match (a.is_nan(), b.is_nan()) {
323 (true, true) => Ordering::Equal,
324 (true, false) => Ordering::Greater,
325 (false, true) => Ordering::Less,
326 (false, false) => a.partial_cmp(b).unwrap_or_else(|| {
327 if a < b {
328 Ordering::Less
329 } else {
330 Ordering::Greater
331 }
332 }),
333 });
334 let mid = samples.len() / 2;
335 if samples.len() % 2 == 1 {
336 samples[mid]
337 } else {
338 (samples[mid - 1] + samples[mid]) * 0.5
339 }
340}
341
342#[derive(Clone, Debug)]
343struct TimeitCallable {
344 handle: Value,
345 num_outputs: Option<usize>,
346}
347
348impl TimeitCallable {
349 async fn invoke(&self) -> Result<Value, crate::RuntimeError> {
350 let requested_outputs = self.num_outputs.unwrap_or(1);
351 let value =
352 crate::call_feval_async_with_outputs(self.handle.clone(), &[], requested_outputs)
353 .await?;
354 drop(value);
355 Ok(Value::Num(0.0))
356 }
357}
358
359fn prepare_callable(
360 func: Value,
361 num_outputs: Option<usize>,
362) -> Result<TimeitCallable, crate::RuntimeError> {
363 fn normalize_name(name: &str) -> Result<String, crate::RuntimeError> {
364 let trimmed = name.trim();
365 if trimmed.is_empty() {
366 Err(timeit_error_with_message(
367 TIMEIT_ERROR_EMPTY_HANDLE.message,
368 &TIMEIT_ERROR_EMPTY_HANDLE,
369 ))
370 } else {
371 Ok(trimmed.to_string())
372 }
373 }
374
375 fn canonicalize_text_handle(handle: String) -> Value {
376 let name = handle.strip_prefix('@').unwrap_or(handle.as_str());
377 handle_for_name(name).unwrap_or(Value::String(handle))
378 }
379
380 match func {
381 Value::String(text) => parse_handle_string(&text).map(|handle| TimeitCallable {
382 handle: canonicalize_text_handle(handle),
383 num_outputs,
384 }),
385 Value::CharArray(arr) => {
386 if arr.rows != 1 {
387 Err(timeit_error_with_message(
388 TIMEIT_ERROR_HANDLE_KIND.message,
389 &TIMEIT_ERROR_HANDLE_KIND,
390 ))
391 } else {
392 let text: String = arr.data.iter().collect();
393 parse_handle_string(&text).map(|handle| TimeitCallable {
394 handle: canonicalize_text_handle(handle),
395 num_outputs,
396 })
397 }
398 }
399 Value::StringArray(sa) => {
400 if sa.data.len() == 1 {
401 parse_handle_string(&sa.data[0]).map(|handle| TimeitCallable {
402 handle: canonicalize_text_handle(handle),
403 num_outputs,
404 })
405 } else {
406 Err(timeit_error_with_message(
407 TIMEIT_ERROR_HANDLE_KIND.message,
408 &TIMEIT_ERROR_HANDLE_KIND,
409 ))
410 }
411 }
412 Value::FunctionHandle(name) => {
413 let normalized = normalize_name(&name)?;
414 Ok(TimeitCallable {
415 handle: handle_for_name(&normalized)
416 .unwrap_or_else(|| Value::String(format!("@{normalized}"))),
417 num_outputs,
418 })
419 }
420 Value::ExternalFunctionHandle(name) => {
421 let normalized = normalize_name(&name)?;
422 Ok(TimeitCallable {
423 handle: if crate::is_well_formed_qualified_name(&normalized) {
424 handle_for_name(&normalized)
425 .unwrap_or_else(|| Value::ExternalFunctionHandle(normalized))
426 } else {
427 Value::ExternalFunctionHandle(normalized)
428 },
429 num_outputs,
430 })
431 }
432 Value::BoundFunctionHandle { name, function } => {
433 let normalized = normalize_name(&name)?;
434 Ok(TimeitCallable {
435 handle: Value::BoundFunctionHandle {
436 name: normalized,
437 function,
438 },
439 num_outputs,
440 })
441 }
442 Value::Closure(mut closure) => Ok(TimeitCallable {
443 handle: {
444 if closure.bound_function.is_none() {
445 if let Some(function) = crate::user_functions::resolve_semantic_function_by_name(
446 &closure.function_name,
447 ) {
448 closure.bound_function = Some(function);
449 }
450 }
451 Value::Closure(closure)
452 },
453 num_outputs,
454 }),
455 other => Err(timeit_error_with_message(
456 format!("timeit: first argument must be a function handle, got {other:?}"),
457 &TIMEIT_ERROR_FIRST_ARG_KIND,
458 )),
459 }
460}
461
462fn handle_for_name(name: &str) -> Option<Value> {
463 let function = crate::user_functions::resolve_semantic_function_by_name(name)?;
464 Some(Value::BoundFunctionHandle {
465 name: name.to_string(),
466 function,
467 })
468}
469
470fn parse_handle_string(text: &str) -> Result<String, crate::RuntimeError> {
471 let trimmed = text.trim();
472 if let Some(rest) = trimmed.strip_prefix('@') {
473 if rest.trim().is_empty() {
474 Err(timeit_error_with_message(
475 TIMEIT_ERROR_EMPTY_HANDLE.message,
476 &TIMEIT_ERROR_EMPTY_HANDLE,
477 ))
478 } else {
479 Ok(format!("@{}", rest.trim()))
480 }
481 } else {
482 Err(timeit_error_with_message(
483 TIMEIT_ERROR_EXPECTS_AT_HANDLE_STRING.message,
484 &TIMEIT_ERROR_EXPECTS_AT_HANDLE_STRING,
485 ))
486 }
487}
488
489#[cfg(test)]
490pub(crate) mod tests {
491 use super::*;
492 use futures::executor::block_on;
493 use runmat_builtins::{Closure, IntValue};
494 use std::sync::atomic::{AtomicUsize, Ordering};
495 use std::sync::Arc;
496
497 const TIMEIT_HELPER_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
498 name: "y",
499 ty: BuiltinParamType::NumericScalar,
500 arity: BuiltinParamArity::Required,
501 default: None,
502 description: "Helper scalar return value.",
503 }];
504
505 const TIMEIT_HELPER_SIGNATURES: [BuiltinSignatureDescriptor; 1] =
506 [BuiltinSignatureDescriptor {
507 label: "y = __timeit_helper()",
508 inputs: &[],
509 outputs: &TIMEIT_HELPER_OUTPUT,
510 }];
511
512 const TIMEIT_HELPER_ERRORS: [BuiltinErrorDescriptor; 0] = [];
513
514 pub const TIMEIT_TEST_HELPER_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
515 signatures: &TIMEIT_HELPER_SIGNATURES,
516 output_mode: BuiltinOutputMode::Fixed,
517 completion_policy: BuiltinCompletionPolicy::HiddenInternal,
518 errors: &TIMEIT_HELPER_ERRORS,
519 };
520
521 static COUNTER_DEFAULT: AtomicUsize = AtomicUsize::new(0);
522 static COUNTER_NUM_OUTPUTS: AtomicUsize = AtomicUsize::new(0);
523 static COUNTER_INVALID: AtomicUsize = AtomicUsize::new(0);
524 static COUNTER_ZERO_OUTPUTS: AtomicUsize = AtomicUsize::new(0);
525
526 #[runtime_builtin(
527 name = "__timeit_helper_counter_default",
528 type_resolver(crate::builtins::timing::type_resolvers::timeit_type),
529 descriptor(crate::builtins::timing::timeit::tests::TIMEIT_TEST_HELPER_DESCRIPTOR),
530 builtin_path = "crate::builtins::timing::timeit::tests"
531 )]
532 async fn helper_counter_default() -> crate::BuiltinResult<Value> {
533 COUNTER_DEFAULT.fetch_add(1, Ordering::SeqCst);
534 Ok(Value::Num(1.0))
535 }
536
537 #[runtime_builtin(
538 name = "__timeit_helper_counter_outputs",
539 type_resolver(crate::builtins::timing::type_resolvers::timeit_type),
540 descriptor(crate::builtins::timing::timeit::tests::TIMEIT_TEST_HELPER_DESCRIPTOR),
541 builtin_path = "crate::builtins::timing::timeit::tests"
542 )]
543 async fn helper_counter_outputs() -> crate::BuiltinResult<Value> {
544 COUNTER_NUM_OUTPUTS.fetch_add(1, Ordering::SeqCst);
545 Ok(Value::Num(1.0))
546 }
547
548 #[runtime_builtin(
549 name = "__timeit_helper_counter_invalid",
550 type_resolver(crate::builtins::timing::type_resolvers::timeit_type),
551 descriptor(crate::builtins::timing::timeit::tests::TIMEIT_TEST_HELPER_DESCRIPTOR),
552 builtin_path = "crate::builtins::timing::timeit::tests"
553 )]
554 async fn helper_counter_invalid() -> crate::BuiltinResult<Value> {
555 COUNTER_INVALID.fetch_add(1, Ordering::SeqCst);
556 Ok(Value::Num(1.0))
557 }
558
559 #[runtime_builtin(
560 name = "__timeit_helper_zero_outputs",
561 type_resolver(crate::builtins::timing::type_resolvers::timeit_type),
562 descriptor(crate::builtins::timing::timeit::tests::TIMEIT_TEST_HELPER_DESCRIPTOR),
563 builtin_path = "crate::builtins::timing::timeit::tests"
564 )]
565 async fn helper_counter_zero_outputs() -> crate::BuiltinResult<Value> {
566 COUNTER_ZERO_OUTPUTS.fetch_add(1, Ordering::SeqCst);
567 Ok(Value::Num(0.0))
568 }
569
570 fn default_handle() -> Value {
571 Value::String("@__timeit_helper_counter_default".to_string())
572 }
573
574 fn assert_timeit_error_contains(err: &crate::RuntimeError, needle: &str) {
575 let message = err.message().to_ascii_lowercase();
576 assert!(
577 message.contains(&needle.to_ascii_lowercase()),
578 "unexpected error text: {}",
579 err.message()
580 );
581 }
582
583 fn assert_timeit_error_identifier(err: &crate::RuntimeError, identifier: &'static str) {
584 assert_eq!(err.identifier(), Some(identifier), "{}", err.message());
585 }
586
587 fn outputs_handle() -> Value {
588 Value::String("@__timeit_helper_counter_outputs".to_string())
589 }
590
591 fn invalid_handle() -> Value {
592 Value::String("@__timeit_helper_counter_invalid".to_string())
593 }
594
595 fn zero_outputs_handle() -> Value {
596 Value::String("@__timeit_helper_zero_outputs".to_string())
597 }
598
599 #[test]
600 fn timeit_test_helper_descriptor_is_attached_shape() {
601 assert_eq!(
602 TIMEIT_TEST_HELPER_DESCRIPTOR.signatures[0].label,
603 "y = __timeit_helper()"
604 );
605 }
606
607 #[test]
608 fn timeit_accepts_external_function_handle() {
609 let callable = prepare_callable(
610 Value::ExternalFunctionHandle("pkg.callback".to_string()),
611 Some(2),
612 )
613 .expect("timeit should accept external function handle");
614 assert_eq!(
615 callable.handle,
616 Value::ExternalFunctionHandle("pkg.callback".to_string())
617 );
618 assert_eq!(callable.num_outputs, Some(2));
619 }
620
621 #[test]
622 fn timeit_rejects_empty_function_handle_name_value() {
623 let err = prepare_callable(Value::FunctionHandle(" ".to_string()), None)
624 .expect_err("timeit should reject empty function-handle payload name");
625 assert_timeit_error_contains(&err, "empty function handle");
626 assert_timeit_error_identifier(&err, TIMEIT_ERROR_EMPTY_HANDLE.identifier.unwrap());
627 }
628
629 #[test]
630 fn timeit_rejects_empty_external_function_handle_name_value() {
631 let err = prepare_callable(Value::ExternalFunctionHandle(" ".to_string()), None)
632 .expect_err("timeit should reject empty external function-handle payload name");
633 assert_timeit_error_contains(&err, "empty function handle");
634 assert_timeit_error_identifier(&err, TIMEIT_ERROR_EMPTY_HANDLE.identifier.unwrap());
635 }
636
637 #[test]
638 fn timeit_trims_function_handle_name_for_semantic_resolution() {
639 let _resolver_guard =
640 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
641 (name == "__timeit_helper_counter_default").then_some(188)
642 })));
643 let callable = prepare_callable(
644 Value::FunctionHandle(" __timeit_helper_counter_default ".to_string()),
645 None,
646 )
647 .expect("timeit should normalize function-handle payload name");
648 assert_eq!(
649 callable.handle,
650 Value::BoundFunctionHandle {
651 name: "__timeit_helper_counter_default".to_string(),
652 function: 188,
653 }
654 );
655 }
656
657 #[test]
658 fn timeit_callable_invoke_honors_multi_requested_outputs() {
659 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
660 Arc::new(|function, args, requested_outputs| {
661 assert_eq!(function, 612);
662 assert!(args.is_empty());
663 assert_eq!(requested_outputs, 3);
664 Box::pin(async {
665 Ok(Value::OutputList(vec![
666 Value::Num(1.0),
667 Value::Num(2.0),
668 Value::Num(3.0),
669 ]))
670 })
671 }),
672 ));
673
674 let callable = prepare_callable(
675 Value::BoundFunctionHandle {
676 name: "function_target".to_string(),
677 function: 612,
678 },
679 Some(3),
680 )
681 .expect("timeit should accept semantic callback handles");
682
683 let invoked = block_on(callable.invoke()).expect("timeit callable invoke should succeed");
684 assert_eq!(invoked, Value::Num(0.0));
685 }
686
687 #[test]
688 fn timeit_string_handle_prefers_semantic_resolver_identity() {
689 let _resolver_guard =
690 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
691 (name == "__timeit_helper_counter_default").then_some(87)
692 })));
693 let callable = prepare_callable(
694 Value::String("@__timeit_helper_counter_default".to_string()),
695 None,
696 )
697 .expect("timeit should accept string function handle");
698 assert_eq!(
699 callable.handle,
700 Value::BoundFunctionHandle {
701 name: "__timeit_helper_counter_default".to_string(),
702 function: 87,
703 }
704 );
705 }
706
707 #[test]
708 fn timeit_char_handle_prefers_semantic_resolver_identity() {
709 let _resolver_guard =
710 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
711 (name == "__timeit_helper_counter_default").then_some(88)
712 })));
713 let callable = prepare_callable(
714 Value::CharArray(runmat_builtins::CharArray::new_row(
715 "@__timeit_helper_counter_default",
716 )),
717 None,
718 )
719 .expect("timeit should accept char function handle");
720 assert_eq!(
721 callable.handle,
722 Value::BoundFunctionHandle {
723 name: "__timeit_helper_counter_default".to_string(),
724 function: 88,
725 }
726 );
727 }
728
729 #[test]
730 fn timeit_external_function_handle_prefers_semantic_resolver_identity() {
731 let _resolver_guard =
732 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
733 (name == "pkg.callback").then_some(86)
734 })));
735 let callable = prepare_callable(
736 Value::ExternalFunctionHandle("pkg.callback".to_string()),
737 Some(2),
738 )
739 .expect("timeit should accept external function handle");
740 assert_eq!(
741 callable.handle,
742 Value::BoundFunctionHandle {
743 name: "pkg.callback".to_string(),
744 function: 86,
745 }
746 );
747 assert_eq!(callable.num_outputs, Some(2));
748 }
749
750 #[test]
751 fn timeit_accepts_semantic_function_handle() {
752 let callable = prepare_callable(
753 Value::BoundFunctionHandle {
754 name: "function_target".to_string(),
755 function: 41,
756 },
757 Some(1),
758 )
759 .expect("timeit should accept semantic function handle");
760 assert_eq!(
761 callable.handle,
762 Value::BoundFunctionHandle {
763 name: "function_target".to_string(),
764 function: 41,
765 }
766 );
767 assert_eq!(callable.num_outputs, Some(1));
768 }
769
770 #[test]
771 fn timeit_name_only_closure_prefers_semantic_resolver_identity() {
772 let _resolver_guard =
773 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
774 (name == "__timeit_helper_counter_default").then_some(89)
775 })));
776 let callable = prepare_callable(
777 Value::Closure(Closure {
778 function_name: "__timeit_helper_counter_default".to_string(),
779 bound_function: None,
780 captures: vec![Value::Num(9.0)],
781 }),
782 None,
783 )
784 .expect("timeit should accept closure callback");
785 assert_eq!(
786 callable.handle,
787 Value::Closure(Closure {
788 function_name: "__timeit_helper_counter_default".to_string(),
789 bound_function: Some(89),
790 captures: vec![Value::Num(9.0)],
791 })
792 );
793 }
794
795 #[test]
796 fn timeit_name_only_closure_without_resolver_keeps_name_shaped_identity() {
797 let callable = prepare_callable(
798 Value::Closure(Closure {
799 function_name: "__timeit_helper_counter_default".to_string(),
800 bound_function: None,
801 captures: vec![Value::Num(9.0)],
802 }),
803 None,
804 )
805 .expect("timeit should accept closure callback");
806 assert_eq!(
807 callable.handle,
808 Value::Closure(Closure {
809 function_name: "__timeit_helper_counter_default".to_string(),
810 bound_function: None,
811 captures: vec![Value::Num(9.0)],
812 })
813 );
814 }
815
816 #[test]
817 fn timeit_external_function_handle_surfaces_undefined_function() {
818 let err = block_on(timeit_builtin(
819 Value::ExternalFunctionHandle("pkg.missing_callback".to_string()),
820 Vec::new(),
821 ))
822 .expect_err("unresolved external callback should fail");
823 assert_eq!(err.identifier(), Some("RunMat:UndefinedFunction"));
824 }
825
826 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
827 #[test]
828 fn timeit_measures_time() {
829 COUNTER_DEFAULT.store(0, Ordering::SeqCst);
830 let result = block_on(timeit_builtin(default_handle(), Vec::new())).expect("timeit");
831 match result {
832 Value::Num(v) => assert!(v >= 0.0),
833 other => panic!("expected numeric result, got {other:?}"),
834 }
835 assert!(
836 COUNTER_DEFAULT.load(Ordering::SeqCst) >= MIN_SAMPLE_COUNT,
837 "expected at least {} invocations",
838 MIN_SAMPLE_COUNT
839 );
840 }
841
842 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
843 #[test]
844 fn timeit_accepts_num_outputs_argument() {
845 COUNTER_NUM_OUTPUTS.store(0, Ordering::SeqCst);
846 let args = vec![Value::Int(IntValue::I32(3))];
847 let _ = block_on(timeit_builtin(outputs_handle(), args)).expect("timeit numOutputs");
848 assert!(
849 COUNTER_NUM_OUTPUTS.load(Ordering::SeqCst) >= MIN_SAMPLE_COUNT,
850 "expected at least {} invocations",
851 MIN_SAMPLE_COUNT
852 );
853 }
854
855 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
856 #[test]
857 fn timeit_supports_zero_outputs() {
858 COUNTER_ZERO_OUTPUTS.store(0, Ordering::SeqCst);
859 let args = vec![Value::Int(IntValue::I32(0))];
860 let _ = block_on(timeit_builtin(zero_outputs_handle(), args)).expect("timeit zero outputs");
861 assert!(
862 COUNTER_ZERO_OUTPUTS.load(Ordering::SeqCst) >= MIN_SAMPLE_COUNT,
863 "expected at least {} invocations",
864 MIN_SAMPLE_COUNT
865 );
866 }
867
868 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
869 #[test]
870 #[cfg(feature = "wgpu")]
871 fn timeit_runs_with_wgpu_provider_registered() {
872 let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
873 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
874 );
875 let result =
876 block_on(timeit_builtin(default_handle(), Vec::new())).expect("timeit with wgpu");
877 match result {
878 Value::Num(v) => assert!(v >= 0.0),
879 other => panic!("expected numeric result, got {other:?}"),
880 }
881 }
882
883 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
884 #[test]
885 fn timeit_rejects_non_function_input() {
886 let err = block_on(timeit_builtin(Value::Num(1.0), Vec::new())).unwrap_err();
887 assert_timeit_error_contains(&err, "function");
888 assert_timeit_error_identifier(&err, TIMEIT_ERROR_FIRST_ARG_KIND.identifier.unwrap());
889 }
890
891 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
892 #[test]
893 fn timeit_rejects_invalid_num_outputs() {
894 COUNTER_INVALID.store(0, Ordering::SeqCst);
895 let err = block_on(timeit_builtin(invalid_handle(), vec![Value::Num(-1.0)])).unwrap_err();
896 assert_timeit_error_contains(&err, "nonnegative");
897 assert_timeit_error_identifier(&err, TIMEIT_ERROR_NUM_OUTPUTS_NONNEG.identifier.unwrap());
898 assert_eq!(COUNTER_INVALID.load(Ordering::SeqCst), 0);
899 }
900
901 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
902 #[test]
903 fn timeit_rejects_extra_arguments() {
904 let err = block_on(timeit_builtin(
905 default_handle(),
906 vec![Value::from(1.0), Value::from(2.0)],
907 ))
908 .unwrap_err();
909 assert_timeit_error_contains(&err, "too many");
910 assert_timeit_error_identifier(&err, TIMEIT_ERROR_TOO_MANY_INPUTS.identifier.unwrap());
911 }
912}