1use serde::{Deserialize, Serialize};
66use std::cell::RefCell;
67use std::time::{Duration, Instant};
68use thiserror::Error;
69
70#[derive(Clone, Debug, Serialize, Deserialize)]
109pub struct BenchSpec {
110 pub name: String,
114
115 pub iterations: u32,
119
120 pub warmup: u32,
125}
126
127impl BenchSpec {
128 pub fn new(name: impl Into<String>, iterations: u32, warmup: u32) -> Result<Self, TimingError> {
154 if iterations == 0 {
155 return Err(TimingError::NoIterations { count: iterations });
156 }
157
158 Ok(Self {
159 name: name.into(),
160 iterations,
161 warmup,
162 })
163 }
164}
165
166#[derive(Clone, Debug, Serialize, Deserialize)]
183pub struct BenchSample {
184 pub duration_ns: u64,
188}
189
190impl BenchSample {
191 fn from_duration(duration: Duration) -> Self {
193 Self {
194 duration_ns: duration.as_nanos() as u64,
195 }
196 }
197}
198
199#[derive(Clone, Debug, Serialize, Deserialize)]
228pub struct BenchReport {
229 pub spec: BenchSpec,
231
232 pub samples: Vec<BenchSample>,
236
237 pub phases: Vec<SemanticPhase>,
239
240 #[serde(default)]
242 pub resource_usage: Option<BenchResourceUsage>,
243}
244
245#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
247pub struct BenchResourceUsage {
248 pub cpu_median_ms: Option<u64>,
250 pub peak_memory_kb: Option<u64>,
252}
253
254impl BenchResourceUsage {
255 #[must_use]
256 pub fn is_empty(&self) -> bool {
257 self.cpu_median_ms.is_none() && self.peak_memory_kb.is_none()
258 }
259}
260
261impl BenchReport {
262 #[must_use]
264 pub fn mean_ns(&self) -> f64 {
265 if self.samples.is_empty() {
266 return 0.0;
267 }
268 let sum: u64 = self.samples.iter().map(|s| s.duration_ns).sum();
269 sum as f64 / self.samples.len() as f64
270 }
271
272 #[must_use]
274 pub fn median_ns(&self) -> f64 {
275 if self.samples.is_empty() {
276 return 0.0;
277 }
278 let mut sorted: Vec<u64> = self.samples.iter().map(|s| s.duration_ns).collect();
279 sorted.sort_unstable();
280 let len = sorted.len();
281 if len % 2 == 0 {
282 (sorted[len / 2 - 1] + sorted[len / 2]) as f64 / 2.0
283 } else {
284 sorted[len / 2] as f64
285 }
286 }
287
288 #[must_use]
290 pub fn std_dev_ns(&self) -> f64 {
291 if self.samples.len() < 2 {
292 return 0.0;
293 }
294 let mean = self.mean_ns();
295 let variance: f64 = self
296 .samples
297 .iter()
298 .map(|s| {
299 let diff = s.duration_ns as f64 - mean;
300 diff * diff
301 })
302 .sum::<f64>()
303 / (self.samples.len() - 1) as f64;
304 variance.sqrt()
305 }
306
307 #[must_use]
309 pub fn percentile_ns(&self, p: f64) -> f64 {
310 if self.samples.is_empty() {
311 return 0.0;
312 }
313 let mut sorted: Vec<u64> = self.samples.iter().map(|s| s.duration_ns).collect();
314 sorted.sort_unstable();
315 let p = p.clamp(0.0, 100.0) / 100.0;
316 let index = (p * (sorted.len() - 1) as f64).round() as usize;
317 sorted[index.min(sorted.len() - 1)] as f64
318 }
319
320 #[must_use]
322 pub fn min_ns(&self) -> u64 {
323 self.samples
324 .iter()
325 .map(|s| s.duration_ns)
326 .min()
327 .unwrap_or(0)
328 }
329
330 #[must_use]
332 pub fn max_ns(&self) -> u64 {
333 self.samples
334 .iter()
335 .map(|s| s.duration_ns)
336 .max()
337 .unwrap_or(0)
338 }
339
340 #[must_use]
342 pub fn summary(&self) -> BenchSummary {
343 BenchSummary {
344 name: self.spec.name.clone(),
345 iterations: self.samples.len() as u32,
346 warmup: self.spec.warmup,
347 mean_ns: self.mean_ns(),
348 median_ns: self.median_ns(),
349 std_dev_ns: self.std_dev_ns(),
350 min_ns: self.min_ns(),
351 max_ns: self.max_ns(),
352 p95_ns: self.percentile_ns(95.0),
353 p99_ns: self.percentile_ns(99.0),
354 }
355 }
356}
357
358#[derive(Clone, Debug, Serialize, Deserialize)]
360pub struct BenchSummary {
361 pub name: String,
363 pub iterations: u32,
365 pub warmup: u32,
367 pub mean_ns: f64,
369 pub median_ns: f64,
371 pub std_dev_ns: f64,
373 pub min_ns: u64,
375 pub max_ns: u64,
377 pub p95_ns: f64,
379 pub p99_ns: f64,
381}
382
383#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
385pub struct SemanticPhase {
386 pub name: String,
387 pub duration_ns: u64,
388}
389
390#[derive(Default)]
391struct SemanticPhaseCollector {
392 enabled: bool,
393 depth: usize,
394 phases: Vec<SemanticPhase>,
395}
396
397impl SemanticPhaseCollector {
398 fn reset(&mut self) {
399 self.enabled = false;
400 self.depth = 0;
401 self.phases.clear();
402 }
403
404 fn begin_measurement(&mut self) {
405 self.reset();
406 self.enabled = true;
407 }
408
409 fn finish(&mut self) -> Vec<SemanticPhase> {
410 self.enabled = false;
411 self.depth = 0;
412 std::mem::take(&mut self.phases)
413 }
414
415 fn enter_phase(&mut self) -> Option<bool> {
416 if !self.enabled {
417 return None;
418 }
419 let top_level = self.depth == 0;
420 self.depth += 1;
421 Some(top_level)
422 }
423
424 fn exit_phase(&mut self, name: &str, top_level: bool, elapsed: Duration) {
425 self.depth = self.depth.saturating_sub(1);
426 if !self.enabled || !top_level {
427 return;
428 }
429
430 let duration_ns = elapsed.as_nanos().min(u128::from(u64::MAX)) as u64;
431 if let Some(phase) = self.phases.iter_mut().find(|phase| phase.name == name) {
432 phase.duration_ns = phase.duration_ns.saturating_add(duration_ns);
433 } else {
434 self.phases.push(SemanticPhase {
435 name: name.to_string(),
436 duration_ns,
437 });
438 }
439 }
440}
441
442#[derive(Default)]
443struct ResourceUsageCollector {
444 enabled: bool,
445 current_iteration_cpu_start_ms: Option<u64>,
446 cpu_samples_ms: Vec<u64>,
447 baseline_memory_kb: Option<u64>,
448 peak_memory_kb: Option<u64>,
449}
450
451impl ResourceUsageCollector {
452 fn reset(&mut self) {
453 self.enabled = false;
454 self.current_iteration_cpu_start_ms = None;
455 self.cpu_samples_ms.clear();
456 self.baseline_memory_kb = None;
457 self.peak_memory_kb = None;
458 }
459
460 fn begin_measurement(&mut self) {
461 self.reset();
462 self.enabled = true;
463 self.refresh_baseline();
464 }
465
466 fn refresh_baseline(&mut self) {
467 if !self.enabled {
468 return;
469 }
470
471 self.current_iteration_cpu_start_ms = None;
472 self.baseline_memory_kb = current_process_memory_kb();
473 if self.baseline_memory_kb.is_some() {
474 self.peak_memory_kb.get_or_insert(0);
475 }
476 }
477
478 fn begin_iteration(&mut self) {
479 if !self.enabled {
480 return;
481 }
482
483 self.current_iteration_cpu_start_ms = current_process_cpu_ms();
484 }
485
486 fn sample(&mut self) {
487 if !self.enabled {
488 return;
489 }
490
491 let Some(baseline_memory_kb) = self.baseline_memory_kb else {
492 return;
493 };
494 let Some(current_memory_kb) = current_process_memory_kb() else {
495 return;
496 };
497 let current_peak_kb = current_memory_kb.saturating_sub(baseline_memory_kb);
498
499 match self.peak_memory_kb {
500 Some(existing_peak_kb) if existing_peak_kb >= current_peak_kb => {}
501 _ => self.peak_memory_kb = Some(current_peak_kb),
502 }
503 }
504
505 fn end_iteration(&mut self) {
506 if !self.enabled {
507 return;
508 }
509
510 let Some(start_cpu_ms) = self.current_iteration_cpu_start_ms.take() else {
511 return;
512 };
513 let Some(end_cpu_ms) = current_process_cpu_ms() else {
514 return;
515 };
516 if end_cpu_ms < start_cpu_ms {
517 return;
518 }
519
520 self.cpu_samples_ms.push(end_cpu_ms - start_cpu_ms);
521 }
522
523 fn finish(&mut self) -> Option<BenchResourceUsage> {
524 self.enabled = false;
525 let cpu_median_ms = if self.cpu_samples_ms.iter().any(|sample_ms| *sample_ms > 0) {
526 median_u64(&self.cpu_samples_ms)
527 } else {
528 None
529 };
530 let resource_usage = Some(BenchResourceUsage {
531 cpu_median_ms,
532 peak_memory_kb: self.peak_memory_kb,
533 });
534 self.current_iteration_cpu_start_ms = None;
535 self.cpu_samples_ms.clear();
536 self.baseline_memory_kb = None;
537 self.peak_memory_kb = None;
538 resource_usage.filter(|usage| !usage.is_empty())
539 }
540}
541
542thread_local! {
543 static SEMANTIC_PHASE_COLLECTOR: RefCell<SemanticPhaseCollector> =
544 RefCell::new(SemanticPhaseCollector::default());
545 static RESOURCE_USAGE_COLLECTOR: RefCell<ResourceUsageCollector> =
546 RefCell::new(ResourceUsageCollector::default());
547}
548
549struct SemanticPhaseGuard {
550 name: String,
551 started_at: Option<Instant>,
552 top_level: bool,
553}
554
555impl Drop for SemanticPhaseGuard {
556 fn drop(&mut self) {
557 let Some(started_at) = self.started_at else {
558 return;
559 };
560
561 let elapsed = started_at.elapsed();
562 SEMANTIC_PHASE_COLLECTOR.with(|collector| {
563 collector
564 .borrow_mut()
565 .exit_phase(&self.name, self.top_level, elapsed);
566 });
567 }
568}
569
570fn reset_semantic_phase_collection() {
571 SEMANTIC_PHASE_COLLECTOR.with(|collector| collector.borrow_mut().reset());
572}
573
574fn begin_semantic_phase_collection() {
575 SEMANTIC_PHASE_COLLECTOR.with(|collector| collector.borrow_mut().begin_measurement());
576}
577
578fn finish_semantic_phase_collection() -> Vec<SemanticPhase> {
579 SEMANTIC_PHASE_COLLECTOR.with(|collector| collector.borrow_mut().finish())
580}
581
582fn reset_resource_usage_collection() {
583 RESOURCE_USAGE_COLLECTOR.with(|collector| collector.borrow_mut().reset());
584}
585
586fn begin_resource_usage_collection() {
587 RESOURCE_USAGE_COLLECTOR.with(|collector| collector.borrow_mut().begin_measurement());
588}
589
590fn refresh_resource_usage_baseline() {
591 RESOURCE_USAGE_COLLECTOR.with(|collector| collector.borrow_mut().refresh_baseline());
592}
593
594fn begin_resource_usage_iteration() {
595 RESOURCE_USAGE_COLLECTOR.with(|collector| collector.borrow_mut().begin_iteration());
596}
597
598fn sample_resource_usage() {
599 RESOURCE_USAGE_COLLECTOR.with(|collector| collector.borrow_mut().sample());
600}
601
602fn end_resource_usage_iteration() {
603 RESOURCE_USAGE_COLLECTOR.with(|collector| collector.borrow_mut().end_iteration());
604}
605
606fn finish_resource_usage_collection() -> Option<BenchResourceUsage> {
607 RESOURCE_USAGE_COLLECTOR.with(|collector| collector.borrow_mut().finish())
608}
609
610fn median_u64(values: &[u64]) -> Option<u64> {
611 if values.is_empty() {
612 return None;
613 }
614
615 let mut sorted = values.to_vec();
616 sorted.sort_unstable();
617 let len = sorted.len();
618 if len % 2 == 0 {
619 Some(((u128::from(sorted[len / 2 - 1]) + u128::from(sorted[len / 2])) / 2) as u64)
620 } else {
621 Some(sorted[len / 2])
622 }
623}
624
625pub fn profile_phase<T>(name: &str, f: impl FnOnce() -> T) -> T {
630 sample_resource_usage();
631 let guard = SEMANTIC_PHASE_COLLECTOR.with(|collector| {
632 let mut collector = collector.borrow_mut();
633 match collector.enter_phase() {
634 Some(top_level) => SemanticPhaseGuard {
635 name: name.to_string(),
636 started_at: Some(Instant::now()),
637 top_level,
638 },
639 None => SemanticPhaseGuard {
640 name: String::new(),
641 started_at: None,
642 top_level: false,
643 },
644 }
645 });
646
647 let result = f();
648 drop(guard);
649 sample_resource_usage();
650 result
651}
652
653#[derive(Debug, Error)]
665pub enum TimingError {
666 #[error("iterations must be greater than zero (got {count}). Minimum recommended: 10")]
671 NoIterations {
672 count: u32,
674 },
675
676 #[error("benchmark function failed: {0}")]
680 Execution(String),
681}
682
683pub fn run_closure<F>(spec: BenchSpec, mut f: F) -> Result<BenchReport, TimingError>
745where
746 F: FnMut() -> Result<(), TimingError>,
747{
748 if spec.iterations == 0 {
749 return Err(TimingError::NoIterations {
750 count: spec.iterations,
751 });
752 }
753
754 reset_semantic_phase_collection();
755 reset_resource_usage_collection();
756
757 for _ in 0..spec.warmup {
759 f()?;
760 }
761
762 begin_semantic_phase_collection();
764 begin_resource_usage_collection();
765 let mut samples = Vec::with_capacity(spec.iterations as usize);
766 for _ in 0..spec.iterations {
767 begin_resource_usage_iteration();
768 sample_resource_usage();
769 let start = Instant::now();
770 if let Err(err) = f() {
771 let _ = finish_semantic_phase_collection();
772 let _ = finish_resource_usage_collection();
773 return Err(err);
774 }
775 sample_resource_usage();
776 end_resource_usage_iteration();
777 samples.push(BenchSample::from_duration(start.elapsed()));
778 }
779 let phases = finish_semantic_phase_collection();
780 let resource_usage = finish_resource_usage_collection();
781
782 Ok(BenchReport {
783 spec,
784 samples,
785 phases,
786 resource_usage,
787 })
788}
789
790pub fn run_closure_with_setup<S, T, F>(
818 spec: BenchSpec,
819 setup: S,
820 mut f: F,
821) -> Result<BenchReport, TimingError>
822where
823 S: FnOnce() -> T,
824 F: FnMut(&T) -> Result<(), TimingError>,
825{
826 if spec.iterations == 0 {
827 return Err(TimingError::NoIterations {
828 count: spec.iterations,
829 });
830 }
831
832 reset_semantic_phase_collection();
833 reset_resource_usage_collection();
834
835 let input = setup();
837
838 for _ in 0..spec.warmup {
840 f(&input)?;
841 }
842
843 begin_semantic_phase_collection();
845 begin_resource_usage_collection();
846 let mut samples = Vec::with_capacity(spec.iterations as usize);
847 for _ in 0..spec.iterations {
848 begin_resource_usage_iteration();
849 sample_resource_usage();
850 let start = Instant::now();
851 if let Err(err) = f(&input) {
852 let _ = finish_semantic_phase_collection();
853 let _ = finish_resource_usage_collection();
854 return Err(err);
855 }
856 sample_resource_usage();
857 end_resource_usage_iteration();
858 samples.push(BenchSample::from_duration(start.elapsed()));
859 }
860 let phases = finish_semantic_phase_collection();
861 let resource_usage = finish_resource_usage_collection();
862
863 Ok(BenchReport {
864 spec,
865 samples,
866 phases,
867 resource_usage,
868 })
869}
870
871pub fn run_closure_with_setup_per_iter<S, T, F>(
900 spec: BenchSpec,
901 mut setup: S,
902 mut f: F,
903) -> Result<BenchReport, TimingError>
904where
905 S: FnMut() -> T,
906 F: FnMut(T) -> Result<(), TimingError>,
907{
908 if spec.iterations == 0 {
909 return Err(TimingError::NoIterations {
910 count: spec.iterations,
911 });
912 }
913
914 reset_semantic_phase_collection();
915 reset_resource_usage_collection();
916
917 for _ in 0..spec.warmup {
919 let input = setup();
920 f(input)?;
921 }
922
923 begin_semantic_phase_collection();
925 begin_resource_usage_collection();
926 let mut samples = Vec::with_capacity(spec.iterations as usize);
927 for _ in 0..spec.iterations {
928 let input = setup(); refresh_resource_usage_baseline();
931 begin_resource_usage_iteration();
932 sample_resource_usage();
933 let start = Instant::now();
934 if let Err(err) = f(input) {
935 let _ = finish_semantic_phase_collection();
936 let _ = finish_resource_usage_collection();
937 return Err(err);
938 }
939 sample_resource_usage();
940 end_resource_usage_iteration();
941 samples.push(BenchSample::from_duration(start.elapsed()));
942 }
943 let phases = finish_semantic_phase_collection();
944 let resource_usage = finish_resource_usage_collection();
945
946 Ok(BenchReport {
947 spec,
948 samples,
949 phases,
950 resource_usage,
951 })
952}
953
954pub fn run_closure_with_setup_teardown<S, T, F, D>(
983 spec: BenchSpec,
984 setup: S,
985 mut f: F,
986 teardown: D,
987) -> Result<BenchReport, TimingError>
988where
989 S: FnOnce() -> T,
990 F: FnMut(&T) -> Result<(), TimingError>,
991 D: FnOnce(T),
992{
993 if spec.iterations == 0 {
994 return Err(TimingError::NoIterations {
995 count: spec.iterations,
996 });
997 }
998
999 reset_semantic_phase_collection();
1000 reset_resource_usage_collection();
1001
1002 let input = setup();
1004
1005 for _ in 0..spec.warmup {
1007 f(&input)?;
1008 }
1009
1010 begin_semantic_phase_collection();
1012 begin_resource_usage_collection();
1013 let mut samples = Vec::with_capacity(spec.iterations as usize);
1014 for _ in 0..spec.iterations {
1015 begin_resource_usage_iteration();
1016 sample_resource_usage();
1017 let start = Instant::now();
1018 if let Err(err) = f(&input) {
1019 let _ = finish_semantic_phase_collection();
1020 let _ = finish_resource_usage_collection();
1021 return Err(err);
1022 }
1023 sample_resource_usage();
1024 end_resource_usage_iteration();
1025 samples.push(BenchSample::from_duration(start.elapsed()));
1026 }
1027 let phases = finish_semantic_phase_collection();
1028 let resource_usage = finish_resource_usage_collection();
1029
1030 teardown(input);
1032
1033 Ok(BenchReport {
1034 spec,
1035 samples,
1036 phases,
1037 resource_usage,
1038 })
1039}
1040
1041#[cfg(any(target_os = "ios", target_os = "macos"))]
1042fn platform_current_process_memory_kb() -> Option<u64> {
1043 unsafe extern "C" {
1044 fn proc_pid_rusage(
1045 pid: libc::c_int,
1046 flavor: libc::c_int,
1047 buffer: *mut libc::c_void,
1048 ) -> libc::c_int;
1049 }
1050
1051 let mut info = std::mem::MaybeUninit::<libc::rusage_info_v4>::zeroed();
1052 let status = unsafe {
1053 proc_pid_rusage(
1056 libc::getpid(),
1057 libc::RUSAGE_INFO_V4,
1058 info.as_mut_ptr().cast(),
1059 )
1060 };
1061 if status != 0 {
1062 return None;
1063 }
1064
1065 let info = unsafe { info.assume_init() };
1067 Some(info.ri_phys_footprint / 1024)
1068}
1069
1070#[cfg(target_os = "android")]
1071fn platform_current_process_memory_kb() -> Option<u64> {
1072 std::fs::read_to_string("/proc/self/status")
1073 .ok()
1074 .and_then(|status| parse_proc_status_memory_kb(&status))
1075}
1076
1077#[cfg(any(test, target_os = "android"))]
1078fn parse_proc_status_memory_kb(status: &str) -> Option<u64> {
1079 status.lines().find_map(|line| {
1080 let value = line.strip_prefix("VmRSS:")?;
1081 value.split_whitespace().next()?.parse().ok()
1082 })
1083}
1084
1085#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
1086fn platform_current_process_memory_kb() -> Option<u64> {
1087 None
1088}
1089
1090#[cfg(unix)]
1091fn platform_current_process_cpu_ms() -> Option<u64> {
1092 let mut usage = std::mem::MaybeUninit::<libc::rusage>::zeroed();
1093 let status = unsafe {
1094 libc::getrusage(libc::RUSAGE_SELF, usage.as_mut_ptr())
1097 };
1098 if status != 0 {
1099 return None;
1100 }
1101
1102 let usage = unsafe { usage.assume_init() };
1104 let user_ms = timeval_to_ms(usage.ru_utime)?;
1105 let system_ms = timeval_to_ms(usage.ru_stime)?;
1106 Some(user_ms.saturating_add(system_ms))
1107}
1108
1109#[cfg(not(unix))]
1110fn platform_current_process_cpu_ms() -> Option<u64> {
1111 None
1112}
1113
1114#[cfg(unix)]
1115fn timeval_to_ms(value: libc::timeval) -> Option<u64> {
1116 if value.tv_sec < 0 || value.tv_usec < 0 {
1117 return None;
1118 }
1119
1120 Some((value.tv_sec as u64).saturating_mul(1000) + (value.tv_usec as u64) / 1000)
1121}
1122
1123#[cfg(test)]
1124thread_local! {
1125 static TEST_MEMORY_SAMPLES_KB: RefCell<Option<std::collections::VecDeque<Option<u64>>>> =
1126 const { RefCell::new(None) };
1127 static TEST_CPU_SAMPLES_MS: RefCell<Option<std::collections::VecDeque<Option<u64>>>> =
1128 const { RefCell::new(None) };
1129}
1130
1131#[cfg(test)]
1132fn take_test_memory_sample_kb() -> Option<Option<u64>> {
1133 TEST_MEMORY_SAMPLES_KB.with(|samples| {
1134 samples
1135 .borrow_mut()
1136 .as_mut()
1137 .and_then(std::collections::VecDeque::pop_front)
1138 })
1139}
1140
1141#[cfg(test)]
1142fn take_test_cpu_sample_ms() -> Option<Option<u64>> {
1143 TEST_CPU_SAMPLES_MS.with(|samples| {
1144 samples
1145 .borrow_mut()
1146 .as_mut()
1147 .and_then(std::collections::VecDeque::pop_front)
1148 })
1149}
1150
1151#[cfg(test)]
1152fn set_test_memory_samples_kb<I>(samples: I)
1153where
1154 I: IntoIterator<Item = Option<u64>>,
1155{
1156 TEST_MEMORY_SAMPLES_KB.with(|state| {
1157 *state.borrow_mut() = Some(samples.into_iter().collect());
1158 });
1159}
1160
1161#[cfg(test)]
1162fn set_test_cpu_samples_ms<I>(samples: I)
1163where
1164 I: IntoIterator<Item = Option<u64>>,
1165{
1166 TEST_CPU_SAMPLES_MS.with(|state| {
1167 *state.borrow_mut() = Some(samples.into_iter().collect());
1168 });
1169}
1170
1171#[cfg(test)]
1172fn clear_test_memory_samples_kb() {
1173 TEST_MEMORY_SAMPLES_KB.with(|state| {
1174 *state.borrow_mut() = None;
1175 });
1176}
1177
1178#[cfg(test)]
1179fn clear_test_cpu_samples_ms() {
1180 TEST_CPU_SAMPLES_MS.with(|state| {
1181 *state.borrow_mut() = None;
1182 });
1183}
1184
1185fn current_process_memory_kb() -> Option<u64> {
1186 #[cfg(test)]
1187 if let Some(sample) = take_test_memory_sample_kb() {
1188 return sample;
1189 }
1190
1191 platform_current_process_memory_kb()
1192}
1193
1194fn current_process_cpu_ms() -> Option<u64> {
1195 #[cfg(test)]
1196 if let Some(sample) = take_test_cpu_sample_ms() {
1197 return sample;
1198 }
1199
1200 platform_current_process_cpu_ms()
1201}
1202
1203#[cfg(test)]
1204mod tests {
1205 use super::*;
1206
1207 struct TestMemorySamplesGuard;
1208
1209 impl Drop for TestMemorySamplesGuard {
1210 fn drop(&mut self) {
1211 clear_test_memory_samples_kb();
1212 clear_test_cpu_samples_ms();
1213 reset_resource_usage_collection();
1214 }
1215 }
1216
1217 #[test]
1218 fn runs_benchmark_collects_requested_samples() {
1219 let spec = BenchSpec::new("noop", 3, 1).unwrap();
1220 let report = run_closure(spec, || Ok(())).unwrap();
1221
1222 assert_eq!(report.samples.len(), 3);
1223 assert_eq!(report.spec.name, "noop");
1224 assert_eq!(report.spec.iterations, 3);
1225 }
1226
1227 #[test]
1228 fn rejects_zero_iterations() {
1229 let result = BenchSpec::new("test", 0, 10);
1230 assert!(matches!(
1231 result,
1232 Err(TimingError::NoIterations { count: 0 })
1233 ));
1234 }
1235
1236 #[test]
1237 fn allows_zero_warmup() {
1238 let spec = BenchSpec::new("test", 5, 0).unwrap();
1239 assert_eq!(spec.warmup, 0);
1240
1241 let report = run_closure(spec, || Ok(())).unwrap();
1242 assert_eq!(report.samples.len(), 5);
1243 }
1244
1245 #[test]
1246 fn serializes_to_json() {
1247 let spec = BenchSpec::new("test", 10, 2).unwrap();
1248 let report = run_closure(spec, || {
1249 profile_phase("prove", || std::thread::sleep(Duration::from_millis(1)));
1250 Ok(())
1251 })
1252 .unwrap();
1253
1254 let json = serde_json::to_string(&report).unwrap();
1255 let restored: BenchReport = serde_json::from_str(&json).unwrap();
1256
1257 assert_eq!(restored.spec.name, "test");
1258 assert_eq!(restored.samples.len(), 10);
1259 assert_eq!(restored.phases.len(), 1);
1260 assert_eq!(restored.phases[0].name, "prove");
1261 assert!(restored.phases[0].duration_ns > 0);
1262 }
1263
1264 #[test]
1265 fn measured_peak_memory_uses_iteration_baseline_only() {
1266 let _guard = TestMemorySamplesGuard;
1267 set_test_memory_samples_kb([
1268 Some(100),
1269 Some(104),
1270 Some(120),
1271 Some(112),
1272 Some(130),
1273 ]);
1274
1275 let spec = BenchSpec::new("mem", 2, 1).unwrap();
1276 let report = run_closure(spec, || Ok(())).unwrap();
1277
1278 assert_eq!(
1279 report.resource_usage,
1280 Some(BenchResourceUsage {
1281 cpu_median_ms: None,
1282 peak_memory_kb: Some(30),
1283 })
1284 );
1285 }
1286
1287 #[test]
1288 fn measured_peak_memory_excludes_one_time_setup_and_teardown() {
1289 let _guard = TestMemorySamplesGuard;
1290 set_test_memory_samples_kb([Some(220), Some(225), Some(250)]);
1291
1292 let spec = BenchSpec::new("mem-setup", 1, 0).unwrap();
1293 let report = run_closure_with_setup_teardown(
1294 spec,
1295 || vec![0u8; 1024],
1296 |_buffer| Ok(()),
1297 |_buffer| {},
1298 )
1299 .unwrap();
1300
1301 assert_eq!(
1302 report.resource_usage,
1303 Some(BenchResourceUsage {
1304 cpu_median_ms: None,
1305 peak_memory_kb: Some(30),
1306 })
1307 );
1308 }
1309
1310 #[test]
1311 fn measured_peak_memory_excludes_per_iteration_setup() {
1312 let _guard = TestMemorySamplesGuard;
1313 set_test_memory_samples_kb([
1314 Some(100),
1315 Some(150),
1316 Some(150),
1317 Some(160),
1318 Some(170),
1319 Some(170),
1320 Some(190),
1321 ]);
1322
1323 let spec = BenchSpec::new("mem-setup-per-iter", 2, 0).unwrap();
1324 let report = run_closure_with_setup_per_iter(spec, || vec![0u8; 1024], |_buffer| Ok(()))
1325 .unwrap();
1326
1327 assert_eq!(
1328 report.resource_usage,
1329 Some(BenchResourceUsage {
1330 cpu_median_ms: None,
1331 peak_memory_kb: Some(20),
1332 })
1333 );
1334 }
1335
1336 #[test]
1337 fn measured_peak_memory_preserves_zero_delta() {
1338 let _guard = TestMemorySamplesGuard;
1339 set_test_memory_samples_kb([Some(100), Some(100), Some(100)]);
1340
1341 let spec = BenchSpec::new("mem-zero", 1, 0).unwrap();
1342 let report = run_closure(spec, || Ok(())).unwrap();
1343
1344 assert_eq!(
1345 report.resource_usage,
1346 Some(BenchResourceUsage {
1347 cpu_median_ms: None,
1348 peak_memory_kb: Some(0),
1349 })
1350 );
1351 }
1352
1353 #[test]
1354 fn measured_cpu_median_uses_per_iteration_deltas_only() {
1355 let _guard = TestMemorySamplesGuard;
1356 set_test_memory_samples_kb([Some(100); 7]);
1357 set_test_cpu_samples_ms([
1358 Some(100),
1359 Some(106),
1360 Some(130),
1361 Some(142),
1362 Some(200),
1363 Some(219),
1364 ]);
1365
1366 let spec = BenchSpec::new("cpu", 3, 1).unwrap();
1367 let report = run_closure(spec, || Ok(())).unwrap();
1368
1369 assert_eq!(
1370 report.resource_usage,
1371 Some(BenchResourceUsage {
1372 cpu_median_ms: Some(12),
1373 peak_memory_kb: Some(0),
1374 })
1375 );
1376 }
1377
1378 #[test]
1379 fn measured_cpu_median_excludes_per_iteration_setup() {
1380 let _guard = TestMemorySamplesGuard;
1381 set_test_memory_samples_kb([Some(100); 10]);
1382 set_test_cpu_samples_ms([
1383 Some(150),
1384 Some(158),
1385 Some(210),
1386 Some(221),
1387 Some(280),
1388 Some(286),
1389 ]);
1390
1391 let spec = BenchSpec::new("cpu-setup-per-iter", 3, 0).unwrap();
1392 let report = run_closure_with_setup_per_iter(spec, || vec![0u8; 1024], |_buffer| Ok(()))
1393 .unwrap();
1394
1395 assert_eq!(
1396 report.resource_usage,
1397 Some(BenchResourceUsage {
1398 cpu_median_ms: Some(8),
1399 peak_memory_kb: Some(0),
1400 })
1401 );
1402 }
1403
1404 #[test]
1405 fn parse_proc_status_memory_kb_reads_vm_rss() {
1406 let status = "\
1407Name:\ttest\n\
1408VmPeak:\t 90304 kB\n\
1409VmRSS:\t 21537 kB\n\
1410RssAnon:\t 10144 kB\n";
1411
1412 assert_eq!(parse_proc_status_memory_kb(status), Some(21_537));
1413 }
1414
1415 #[test]
1416 fn parse_proc_status_memory_kb_returns_none_without_vm_rss() {
1417 let status = "\
1418Name:\ttest\n\
1419VmPeak:\t 90304 kB\n\
1420VmHWM:\t 24000 kB\n";
1421
1422 assert_eq!(parse_proc_status_memory_kb(status), None);
1423 }
1424
1425 #[test]
1426 fn profile_phase_records_only_measured_iterations() {
1427 let spec = BenchSpec::new("semantic", 2, 1).unwrap();
1428 let mut call_index = 0u32;
1429 let report = run_closure(spec, || {
1430 let phase_name = if call_index == 0 {
1431 "warmup-only"
1432 } else {
1433 "prove"
1434 };
1435 call_index += 1;
1436 profile_phase(phase_name, || std::thread::sleep(Duration::from_millis(1)));
1437 Ok(())
1438 })
1439 .unwrap();
1440
1441 assert!(
1442 !report
1443 .phases
1444 .iter()
1445 .any(|phase| phase.name == "warmup-only"),
1446 "warmup phases should not be recorded"
1447 );
1448 let prove = report
1449 .phases
1450 .iter()
1451 .find(|phase| phase.name == "prove")
1452 .expect("prove phase");
1453 assert!(prove.duration_ns > 0);
1454 }
1455
1456 #[test]
1457 fn profile_phase_keeps_the_v1_model_flat() {
1458 let spec = BenchSpec::new("semantic-flat", 1, 0).unwrap();
1459 let report = run_closure(spec, || {
1460 profile_phase("prove", || {
1461 std::thread::sleep(Duration::from_millis(1));
1462 profile_phase("inner", || std::thread::sleep(Duration::from_millis(1)));
1463 });
1464 Ok(())
1465 })
1466 .unwrap();
1467
1468 assert!(report.phases.iter().any(|phase| phase.name == "prove"));
1469 assert!(
1470 !report.phases.iter().any(|phase| phase.name == "inner"),
1471 "nested phases should not create a second flat phase entry"
1472 );
1473 }
1474
1475 #[test]
1476 fn run_with_setup_calls_setup_once() {
1477 use std::sync::atomic::{AtomicU32, Ordering};
1478
1479 static SETUP_COUNT: AtomicU32 = AtomicU32::new(0);
1480 static RUN_COUNT: AtomicU32 = AtomicU32::new(0);
1481
1482 let spec = BenchSpec::new("test", 5, 2).unwrap();
1483 let report = run_closure_with_setup(
1484 spec,
1485 || {
1486 SETUP_COUNT.fetch_add(1, Ordering::SeqCst);
1487 vec![1, 2, 3]
1488 },
1489 |data| {
1490 RUN_COUNT.fetch_add(1, Ordering::SeqCst);
1491 std::hint::black_box(data.len());
1492 Ok(())
1493 },
1494 )
1495 .unwrap();
1496
1497 assert_eq!(SETUP_COUNT.load(Ordering::SeqCst), 1); assert_eq!(RUN_COUNT.load(Ordering::SeqCst), 7); assert_eq!(report.samples.len(), 5);
1500 }
1501
1502 #[test]
1503 fn run_with_setup_per_iter_calls_setup_each_time() {
1504 use std::sync::atomic::{AtomicU32, Ordering};
1505
1506 static SETUP_COUNT: AtomicU32 = AtomicU32::new(0);
1507
1508 let spec = BenchSpec::new("test", 3, 1).unwrap();
1509 let report = run_closure_with_setup_per_iter(
1510 spec,
1511 || {
1512 SETUP_COUNT.fetch_add(1, Ordering::SeqCst);
1513 vec![1, 2, 3]
1514 },
1515 |data| {
1516 std::hint::black_box(data);
1517 Ok(())
1518 },
1519 )
1520 .unwrap();
1521
1522 assert_eq!(SETUP_COUNT.load(Ordering::SeqCst), 4); assert_eq!(report.samples.len(), 3);
1524 }
1525
1526 #[test]
1527 fn run_with_setup_teardown_calls_both() {
1528 use std::sync::atomic::{AtomicU32, Ordering};
1529
1530 static SETUP_COUNT: AtomicU32 = AtomicU32::new(0);
1531 static TEARDOWN_COUNT: AtomicU32 = AtomicU32::new(0);
1532
1533 let spec = BenchSpec::new("test", 3, 1).unwrap();
1534 let report = run_closure_with_setup_teardown(
1535 spec,
1536 || {
1537 SETUP_COUNT.fetch_add(1, Ordering::SeqCst);
1538 "resource"
1539 },
1540 |_resource| Ok(()),
1541 |_resource| {
1542 TEARDOWN_COUNT.fetch_add(1, Ordering::SeqCst);
1543 },
1544 )
1545 .unwrap();
1546
1547 assert_eq!(SETUP_COUNT.load(Ordering::SeqCst), 1);
1548 assert_eq!(TEARDOWN_COUNT.load(Ordering::SeqCst), 1);
1549 assert_eq!(report.samples.len(), 3);
1550 }
1551}