1use serde::{Deserialize, Serialize};
66use std::cell::RefCell;
67use std::sync::{
68 Arc,
69 atomic::{AtomicBool, AtomicU64, Ordering},
70 mpsc,
71};
72use std::thread::{self, JoinHandle};
73use std::time::{Duration, Instant};
74use thiserror::Error;
75
76#[derive(Clone, Debug, Serialize, Deserialize)]
115pub struct BenchSpec {
116 pub name: String,
120
121 pub iterations: u32,
125
126 pub warmup: u32,
131}
132
133impl BenchSpec {
134 pub fn new(name: impl Into<String>, iterations: u32, warmup: u32) -> Result<Self, TimingError> {
160 if iterations == 0 {
161 return Err(TimingError::NoIterations { count: iterations });
162 }
163
164 Ok(Self {
165 name: name.into(),
166 iterations,
167 warmup,
168 })
169 }
170}
171
172#[derive(Clone, Debug, Default, Serialize, Deserialize)]
192pub struct BenchSample {
193 pub duration_ns: u64,
197
198 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub cpu_time_ms: Option<u64>,
204
205 #[serde(default, skip_serializing_if = "Option::is_none")]
210 pub peak_memory_kb: Option<u64>,
211}
212
213impl BenchSample {
214 fn from_measurement(duration: Duration, resources: IterationResourceUsage) -> Self {
215 Self {
216 duration_ns: duration.as_nanos() as u64,
217 cpu_time_ms: resources.cpu_time_ms,
218 peak_memory_kb: resources.peak_memory_kb,
219 }
220 }
221}
222
223#[derive(Clone, Debug, Serialize, Deserialize)]
252pub struct BenchReport {
253 pub spec: BenchSpec,
255
256 pub samples: Vec<BenchSample>,
260
261 pub phases: Vec<SemanticPhase>,
263
264 pub timeline: Vec<HarnessTimelineSpan>,
266}
267
268#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
269pub struct HarnessTimelineSpan {
270 pub phase: String,
271 pub start_offset_ns: u64,
272 pub end_offset_ns: u64,
273 pub iteration: Option<u32>,
274}
275
276impl BenchReport {
277 #[must_use]
279 pub fn mean_ns(&self) -> f64 {
280 if self.samples.is_empty() {
281 return 0.0;
282 }
283 let sum: u64 = self.samples.iter().map(|s| s.duration_ns).sum();
284 sum as f64 / self.samples.len() as f64
285 }
286
287 #[must_use]
289 pub fn median_ns(&self) -> f64 {
290 if self.samples.is_empty() {
291 return 0.0;
292 }
293 let mut sorted: Vec<u64> = self.samples.iter().map(|s| s.duration_ns).collect();
294 sorted.sort_unstable();
295 let len = sorted.len();
296 if len % 2 == 0 {
297 (sorted[len / 2 - 1] + sorted[len / 2]) as f64 / 2.0
298 } else {
299 sorted[len / 2] as f64
300 }
301 }
302
303 #[must_use]
305 pub fn std_dev_ns(&self) -> f64 {
306 if self.samples.len() < 2 {
307 return 0.0;
308 }
309 let mean = self.mean_ns();
310 let variance: f64 = self
311 .samples
312 .iter()
313 .map(|s| {
314 let diff = s.duration_ns as f64 - mean;
315 diff * diff
316 })
317 .sum::<f64>()
318 / (self.samples.len() - 1) as f64;
319 variance.sqrt()
320 }
321
322 #[must_use]
324 pub fn percentile_ns(&self, p: f64) -> f64 {
325 if self.samples.is_empty() {
326 return 0.0;
327 }
328 let mut sorted: Vec<u64> = self.samples.iter().map(|s| s.duration_ns).collect();
329 sorted.sort_unstable();
330 let p = p.clamp(0.0, 100.0) / 100.0;
331 let index = (p * (sorted.len() - 1) as f64).round() as usize;
332 sorted[index.min(sorted.len() - 1)] as f64
333 }
334
335 #[must_use]
337 pub fn min_ns(&self) -> u64 {
338 self.samples
339 .iter()
340 .map(|s| s.duration_ns)
341 .min()
342 .unwrap_or(0)
343 }
344
345 #[must_use]
347 pub fn max_ns(&self) -> u64 {
348 self.samples
349 .iter()
350 .map(|s| s.duration_ns)
351 .max()
352 .unwrap_or(0)
353 }
354
355 #[must_use]
357 pub fn cpu_total_ms(&self) -> Option<u64> {
358 let values = self
359 .samples
360 .iter()
361 .filter_map(|sample| sample.cpu_time_ms)
362 .collect::<Vec<_>>();
363 if values.is_empty() {
364 return None;
365 }
366
367 let total = values
368 .iter()
369 .fold(0_u128, |sum, value| sum.saturating_add(u128::from(*value)));
370 Some(total.min(u128::from(u64::MAX)) as u64)
371 }
372
373 #[must_use]
375 pub fn cpu_median_ms(&self) -> Option<u64> {
376 let mut values = self
377 .samples
378 .iter()
379 .filter_map(|sample| sample.cpu_time_ms)
380 .collect::<Vec<_>>();
381 if values.is_empty() {
382 return None;
383 }
384
385 values.sort_unstable();
386 let len = values.len();
387 Some(if len % 2 == 0 {
388 let lower = u128::from(values[(len / 2) - 1]);
389 let upper = u128::from(values[len / 2]);
390 ((lower + upper) / 2) as u64
391 } else {
392 values[len / 2]
393 })
394 }
395
396 #[must_use]
398 pub fn peak_memory_kb(&self) -> Option<u64> {
399 self.samples
400 .iter()
401 .filter_map(|sample| sample.peak_memory_kb)
402 .max()
403 }
404
405 #[must_use]
407 pub fn summary(&self) -> BenchSummary {
408 BenchSummary {
409 name: self.spec.name.clone(),
410 iterations: self.samples.len() as u32,
411 warmup: self.spec.warmup,
412 mean_ns: self.mean_ns(),
413 median_ns: self.median_ns(),
414 std_dev_ns: self.std_dev_ns(),
415 min_ns: self.min_ns(),
416 max_ns: self.max_ns(),
417 p95_ns: self.percentile_ns(95.0),
418 p99_ns: self.percentile_ns(99.0),
419 }
420 }
421}
422
423#[derive(Clone, Debug, Default)]
424struct IterationResourceUsage {
425 cpu_time_ms: Option<u64>,
426 peak_memory_kb: Option<u64>,
427}
428
429fn instant_offset_ns(origin: Instant, instant: Instant) -> u64 {
430 instant
431 .duration_since(origin)
432 .as_nanos()
433 .min(u128::from(u64::MAX)) as u64
434}
435
436fn push_timeline_span(
437 timeline: &mut Vec<HarnessTimelineSpan>,
438 origin: Instant,
439 phase: &str,
440 started_at: Instant,
441 ended_at: Instant,
442 iteration: Option<u32>,
443) {
444 timeline.push(HarnessTimelineSpan {
445 phase: phase.to_string(),
446 start_offset_ns: instant_offset_ns(origin, started_at),
447 end_offset_ns: instant_offset_ns(origin, ended_at),
448 iteration,
449 });
450}
451
452#[derive(Clone, Debug, Serialize, Deserialize)]
454pub struct BenchSummary {
455 pub name: String,
457 pub iterations: u32,
459 pub warmup: u32,
461 pub mean_ns: f64,
463 pub median_ns: f64,
465 pub std_dev_ns: f64,
467 pub min_ns: u64,
469 pub max_ns: u64,
471 pub p95_ns: f64,
473 pub p99_ns: f64,
475}
476
477#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
479pub struct SemanticPhase {
480 pub name: String,
481 pub duration_ns: u64,
482}
483
484#[derive(Default)]
485struct SemanticPhaseCollector {
486 enabled: bool,
487 depth: usize,
488 phases: Vec<SemanticPhase>,
489}
490
491impl SemanticPhaseCollector {
492 fn reset(&mut self) {
493 self.enabled = false;
494 self.depth = 0;
495 self.phases.clear();
496 }
497
498 fn begin_measurement(&mut self) {
499 self.reset();
500 self.enabled = true;
501 }
502
503 fn finish(&mut self) -> Vec<SemanticPhase> {
504 self.enabled = false;
505 self.depth = 0;
506 std::mem::take(&mut self.phases)
507 }
508
509 fn enter_phase(&mut self) -> Option<bool> {
510 if !self.enabled {
511 return None;
512 }
513 let top_level = self.depth == 0;
514 self.depth += 1;
515 Some(top_level)
516 }
517
518 fn exit_phase(&mut self, name: &str, top_level: bool, elapsed: Duration) {
519 self.depth = self.depth.saturating_sub(1);
520 if !self.enabled || !top_level {
521 return;
522 }
523
524 let duration_ns = elapsed.as_nanos().min(u128::from(u64::MAX)) as u64;
525 if let Some(phase) = self.phases.iter_mut().find(|phase| phase.name == name) {
526 phase.duration_ns = phase.duration_ns.saturating_add(duration_ns);
527 } else {
528 self.phases.push(SemanticPhase {
529 name: name.to_string(),
530 duration_ns,
531 });
532 }
533 }
534}
535
536thread_local! {
537 static SEMANTIC_PHASE_COLLECTOR: RefCell<SemanticPhaseCollector> =
538 RefCell::new(SemanticPhaseCollector::default());
539}
540
541struct SemanticPhaseGuard {
542 name: String,
543 started_at: Option<Instant>,
544 top_level: bool,
545}
546
547impl Drop for SemanticPhaseGuard {
548 fn drop(&mut self) {
549 let Some(started_at) = self.started_at else {
550 return;
551 };
552
553 let elapsed = started_at.elapsed();
554 SEMANTIC_PHASE_COLLECTOR.with(|collector| {
555 collector
556 .borrow_mut()
557 .exit_phase(&self.name, self.top_level, elapsed);
558 });
559 }
560}
561
562fn reset_semantic_phase_collection() {
563 SEMANTIC_PHASE_COLLECTOR.with(|collector| collector.borrow_mut().reset());
564}
565
566fn begin_semantic_phase_collection() {
567 SEMANTIC_PHASE_COLLECTOR.with(|collector| collector.borrow_mut().begin_measurement());
568}
569
570fn finish_semantic_phase_collection() -> Vec<SemanticPhase> {
571 SEMANTIC_PHASE_COLLECTOR.with(|collector| collector.borrow_mut().finish())
572}
573
574trait ResourceMonitor {
575 type Token;
576
577 fn start(&mut self) -> Self::Token;
578
579 fn finish(&mut self, token: Self::Token) -> IterationResourceUsage;
580}
581
582#[derive(Default)]
583struct DefaultResourceMonitor;
584
585struct DefaultResourceToken {
586 cpu_time_start_ns: Option<u64>,
587 memory_sampler: Option<MemoryPeakSampler>,
588}
589
590impl ResourceMonitor for DefaultResourceMonitor {
591 type Token = DefaultResourceToken;
592
593 fn start(&mut self) -> Self::Token {
594 Self::Token {
595 cpu_time_start_ns: current_thread_cpu_time_ns(),
596 memory_sampler: MemoryPeakSampler::start(),
597 }
598 }
599
600 fn finish(&mut self, token: Self::Token) -> IterationResourceUsage {
601 let cpu_time_ms = match (token.cpu_time_start_ns, current_thread_cpu_time_ns()) {
602 (Some(start_ns), Some(end_ns)) if end_ns >= start_ns => {
603 Some(round_ns_to_ms(end_ns - start_ns))
604 }
605 _ => None,
606 };
607
608 IterationResourceUsage {
609 cpu_time_ms,
610 peak_memory_kb: token
611 .memory_sampler
612 .and_then(MemoryPeakSampler::stop)
613 .filter(|value| *value > 0),
614 }
615 }
616}
617
618fn round_ns_to_ms(ns: u64) -> u64 {
619 ((u128::from(ns) + 500_000) / 1_000_000) as u64
620}
621
622#[cfg(unix)]
623fn current_thread_cpu_time_ns() -> Option<u64> {
624 let mut ts = std::mem::MaybeUninit::<libc::timespec>::uninit();
625 let rc = unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, ts.as_mut_ptr()) };
626 if rc != 0 {
627 return None;
628 }
629
630 let ts = unsafe { ts.assume_init() };
631 let secs = u64::try_from(ts.tv_sec).ok()?;
632 let nanos = u64::try_from(ts.tv_nsec).ok()?;
633 Some(secs.saturating_mul(1_000_000_000).saturating_add(nanos))
634}
635
636#[cfg(not(unix))]
637fn current_thread_cpu_time_ns() -> Option<u64> {
638 None
639}
640
641const MEMORY_SAMPLER_INTERVAL: Duration = Duration::from_millis(1);
642type MemoryReader = Arc<dyn Fn() -> Option<u64> + Send + Sync + 'static>;
643
644struct MemoryPeakSampler {
645 baseline_kb: u64,
646 stop_flag: Arc<AtomicBool>,
647 peak_kb: Arc<AtomicU64>,
648 handle: JoinHandle<()>,
649}
650
651impl MemoryPeakSampler {
652 fn start() -> Option<Self> {
653 Self::start_with_reader(Arc::new(|| current_process_memory_kb()))
654 }
655
656 fn start_with_reader(reader: MemoryReader) -> Option<Self> {
657 let stop_flag = Arc::new(AtomicBool::new(false));
658 let peak_kb = Arc::new(AtomicU64::new(0));
659 let (ready_tx, ready_rx) = mpsc::sync_channel(1);
660 let (baseline_tx, baseline_rx) = mpsc::sync_channel(1);
661 let sampler_stop = Arc::clone(&stop_flag);
662 let sampler_peak = Arc::clone(&peak_kb);
663 let sampler_reader = Arc::clone(&reader);
664
665 let handle = thread::Builder::new()
666 .name("mobench-memory-sampler".to_string())
667 .spawn(move || {
668 let _ = sampler_reader();
672 let _ = ready_tx.send(());
673
674 let Some(baseline_kb) = baseline_rx.recv().ok().flatten() else {
675 return;
676 };
677 sampler_peak.store(baseline_kb, Ordering::Release);
678
679 while !sampler_stop.load(Ordering::Acquire) {
680 if let Some(current_kb) = sampler_reader() {
681 update_atomic_max(&sampler_peak, current_kb);
682 }
683 thread::sleep(MEMORY_SAMPLER_INTERVAL);
684 }
685
686 if let Some(current_kb) = sampler_reader() {
687 update_atomic_max(&sampler_peak, current_kb);
688 }
689 })
690 .ok()?;
691
692 if ready_rx.recv().is_err() {
693 stop_flag.store(true, Ordering::Release);
694 let _ = handle.join();
695 return None;
696 }
697
698 let baseline_kb = match reader() {
699 Some(value) => value,
700 None => {
701 let _ = baseline_tx.send(None);
702 stop_flag.store(true, Ordering::Release);
703 let _ = handle.join();
704 return None;
705 }
706 };
707 if baseline_tx.send(Some(baseline_kb)).is_err() {
708 stop_flag.store(true, Ordering::Release);
709 let _ = handle.join();
710 return None;
711 }
712
713 Some(Self {
714 baseline_kb,
715 stop_flag,
716 peak_kb,
717 handle,
718 })
719 }
720
721 fn stop(self) -> Option<u64> {
722 self.stop_flag.store(true, Ordering::Release);
723 let _ = self.handle.join();
724 let peak_kb = self.peak_kb.load(Ordering::Acquire);
725 Some(peak_kb.saturating_sub(self.baseline_kb))
726 }
727}
728
729fn update_atomic_max(target: &AtomicU64, value: u64) {
730 let mut current = target.load(Ordering::Relaxed);
731 while value > current {
732 match target.compare_exchange_weak(current, value, Ordering::Relaxed, Ordering::Relaxed) {
733 Ok(_) => break,
734 Err(observed) => current = observed,
735 }
736 }
737}
738
739#[cfg(any(target_os = "android", target_os = "linux"))]
740fn current_process_memory_kb() -> Option<u64> {
741 let statm = std::fs::read_to_string("/proc/self/statm").ok()?;
742 let resident_pages = statm
743 .split_whitespace()
744 .nth(1)
745 .and_then(|value| value.parse::<u64>().ok())?;
746 let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
747 if page_size <= 0 {
748 return None;
749 }
750 let page_size = u64::try_from(page_size).ok()?;
751 Some(resident_pages.saturating_mul(page_size) / 1024)
752}
753
754#[cfg(any(target_os = "ios", target_os = "macos"))]
755fn current_process_memory_kb() -> Option<u64> {
756 let mut info = std::mem::MaybeUninit::<libc::mach_task_basic_info_data_t>::uninit();
757 let mut count = libc::MACH_TASK_BASIC_INFO_COUNT;
758 #[allow(deprecated)]
759 let rc = unsafe {
760 libc::task_info(
761 libc::mach_task_self(),
762 libc::MACH_TASK_BASIC_INFO,
763 info.as_mut_ptr().cast::<libc::integer_t>(),
764 &mut count,
765 )
766 };
767 if rc != libc::KERN_SUCCESS {
768 return None;
769 }
770
771 let info = unsafe { info.assume_init() };
772 Some((info.resident_size / 1024) as u64)
773}
774
775#[cfg(not(any(
776 target_os = "android",
777 target_os = "linux",
778 target_os = "ios",
779 target_os = "macos"
780)))]
781fn current_process_memory_kb() -> Option<u64> {
782 None
783}
784
785fn measure_iteration<M, F>(
786 monitor: &mut M,
787 f: F,
788) -> Result<(BenchSample, Instant, Instant), TimingError>
789where
790 M: ResourceMonitor,
791 F: FnOnce() -> Result<(), TimingError>,
792{
793 let token = monitor.start();
794 let started_at = Instant::now();
795 let result = f();
796 let ended_at = Instant::now();
797 let resources = monitor.finish(token);
798 result.map(|_| {
799 (
800 BenchSample::from_measurement(ended_at.duration_since(started_at), resources),
801 started_at,
802 ended_at,
803 )
804 })
805}
806
807pub fn profile_phase<T>(name: &str, f: impl FnOnce() -> T) -> T {
812 let guard = SEMANTIC_PHASE_COLLECTOR.with(|collector| {
813 let mut collector = collector.borrow_mut();
814 match collector.enter_phase() {
815 Some(top_level) => SemanticPhaseGuard {
816 name: name.to_string(),
817 started_at: Some(Instant::now()),
818 top_level,
819 },
820 None => SemanticPhaseGuard {
821 name: String::new(),
822 started_at: None,
823 top_level: false,
824 },
825 }
826 });
827
828 let result = f();
829 drop(guard);
830 result
831}
832
833#[derive(Debug, Error)]
845pub enum TimingError {
846 #[error("iterations must be greater than zero (got {count}). Minimum recommended: 10")]
851 NoIterations {
852 count: u32,
854 },
855
856 #[error("benchmark function failed: {0}")]
860 Execution(String),
861}
862
863pub fn run_closure<F>(spec: BenchSpec, mut f: F) -> Result<BenchReport, TimingError>
925where
926 F: FnMut() -> Result<(), TimingError>,
927{
928 let mut monitor = DefaultResourceMonitor;
929 run_closure_with_monitor(spec, &mut monitor, move || f())
930}
931
932fn run_closure_with_monitor<F, M>(
933 spec: BenchSpec,
934 monitor: &mut M,
935 mut f: F,
936) -> Result<BenchReport, TimingError>
937where
938 F: FnMut() -> Result<(), TimingError>,
939 M: ResourceMonitor,
940{
941 if spec.iterations == 0 {
942 return Err(TimingError::NoIterations {
943 count: spec.iterations,
944 });
945 }
946
947 reset_semantic_phase_collection();
948 let harness_origin = Instant::now();
949 let mut timeline = Vec::new();
950
951 for iteration in 0..spec.warmup {
953 let phase_start = Instant::now();
954 f()?;
955 push_timeline_span(
956 &mut timeline,
957 harness_origin,
958 "warmup-benchmark",
959 phase_start,
960 Instant::now(),
961 Some(iteration),
962 );
963 }
964
965 begin_semantic_phase_collection();
967 let mut samples = Vec::with_capacity(spec.iterations as usize);
968 for iteration in 0..spec.iterations {
969 let (sample, start, end) = match measure_iteration(monitor, || f()) {
970 Ok(measurement) => measurement,
971 Err(err) => {
972 let _ = finish_semantic_phase_collection();
973 return Err(err);
974 }
975 };
976 samples.push(sample);
977 push_timeline_span(
978 &mut timeline,
979 harness_origin,
980 "measured-benchmark",
981 start,
982 end,
983 Some(iteration),
984 );
985 }
986 let phases = finish_semantic_phase_collection();
987
988 Ok(BenchReport {
989 spec,
990 samples,
991 phases,
992 timeline,
993 })
994}
995
996pub fn run_closure_with_setup<S, T, F>(
1024 spec: BenchSpec,
1025 setup: S,
1026 mut f: F,
1027) -> Result<BenchReport, TimingError>
1028where
1029 S: FnOnce() -> T,
1030 F: FnMut(&T) -> Result<(), TimingError>,
1031{
1032 let mut monitor = DefaultResourceMonitor;
1033 run_closure_with_setup_with_monitor(spec, &mut monitor, setup, move |input| f(input))
1034}
1035
1036fn run_closure_with_setup_with_monitor<S, T, F, M>(
1037 spec: BenchSpec,
1038 monitor: &mut M,
1039 setup: S,
1040 mut f: F,
1041) -> Result<BenchReport, TimingError>
1042where
1043 S: FnOnce() -> T,
1044 F: FnMut(&T) -> Result<(), TimingError>,
1045 M: ResourceMonitor,
1046{
1047 if spec.iterations == 0 {
1048 return Err(TimingError::NoIterations {
1049 count: spec.iterations,
1050 });
1051 }
1052
1053 reset_semantic_phase_collection();
1054 let harness_origin = Instant::now();
1055 let mut timeline = Vec::new();
1056
1057 let setup_start = Instant::now();
1059 let input = setup();
1060 push_timeline_span(
1061 &mut timeline,
1062 harness_origin,
1063 "setup",
1064 setup_start,
1065 Instant::now(),
1066 None,
1067 );
1068
1069 for iteration in 0..spec.warmup {
1071 let phase_start = Instant::now();
1072 f(&input)?;
1073 push_timeline_span(
1074 &mut timeline,
1075 harness_origin,
1076 "warmup-benchmark",
1077 phase_start,
1078 Instant::now(),
1079 Some(iteration),
1080 );
1081 }
1082
1083 begin_semantic_phase_collection();
1085 let mut samples = Vec::with_capacity(spec.iterations as usize);
1086 for iteration in 0..spec.iterations {
1087 let (sample, start, end) = match measure_iteration(monitor, || f(&input)) {
1088 Ok(measurement) => measurement,
1089 Err(err) => {
1090 let _ = finish_semantic_phase_collection();
1091 return Err(err);
1092 }
1093 };
1094 samples.push(sample);
1095 push_timeline_span(
1096 &mut timeline,
1097 harness_origin,
1098 "measured-benchmark",
1099 start,
1100 end,
1101 Some(iteration),
1102 );
1103 }
1104 let phases = finish_semantic_phase_collection();
1105
1106 Ok(BenchReport {
1107 spec,
1108 samples,
1109 phases,
1110 timeline,
1111 })
1112}
1113
1114pub fn run_closure_with_setup_per_iter<S, T, F>(
1143 spec: BenchSpec,
1144 mut setup: S,
1145 mut f: F,
1146) -> Result<BenchReport, TimingError>
1147where
1148 S: FnMut() -> T,
1149 F: FnMut(T) -> Result<(), TimingError>,
1150{
1151 let mut monitor = DefaultResourceMonitor;
1152 run_closure_with_setup_per_iter_with_monitor(
1153 spec,
1154 &mut monitor,
1155 move || setup(),
1156 move |input| f(input),
1157 )
1158}
1159
1160fn run_closure_with_setup_per_iter_with_monitor<S, T, F, M>(
1161 spec: BenchSpec,
1162 monitor: &mut M,
1163 mut setup: S,
1164 mut f: F,
1165) -> Result<BenchReport, TimingError>
1166where
1167 S: FnMut() -> T,
1168 F: FnMut(T) -> Result<(), TimingError>,
1169 M: ResourceMonitor,
1170{
1171 if spec.iterations == 0 {
1172 return Err(TimingError::NoIterations {
1173 count: spec.iterations,
1174 });
1175 }
1176
1177 reset_semantic_phase_collection();
1178 let harness_origin = Instant::now();
1179 let mut timeline = Vec::new();
1180
1181 for iteration in 0..spec.warmup {
1183 let setup_start = Instant::now();
1184 let input = setup();
1185 push_timeline_span(
1186 &mut timeline,
1187 harness_origin,
1188 "fixture-setup",
1189 setup_start,
1190 Instant::now(),
1191 Some(iteration),
1192 );
1193 let phase_start = Instant::now();
1194 f(input)?;
1195 push_timeline_span(
1196 &mut timeline,
1197 harness_origin,
1198 "warmup-benchmark",
1199 phase_start,
1200 Instant::now(),
1201 Some(iteration),
1202 );
1203 }
1204
1205 begin_semantic_phase_collection();
1207 let mut samples = Vec::with_capacity(spec.iterations as usize);
1208 for iteration in 0..spec.iterations {
1209 let setup_start = Instant::now();
1210 let input = setup(); push_timeline_span(
1212 &mut timeline,
1213 harness_origin,
1214 "fixture-setup",
1215 setup_start,
1216 Instant::now(),
1217 Some(iteration),
1218 );
1219
1220 let (sample, start, end) = match measure_iteration(monitor, || f(input)) {
1221 Ok(measurement) => measurement,
1222 Err(err) => {
1223 let _ = finish_semantic_phase_collection();
1224 return Err(err);
1225 }
1226 };
1227 samples.push(sample);
1228 push_timeline_span(
1229 &mut timeline,
1230 harness_origin,
1231 "measured-benchmark",
1232 start,
1233 end,
1234 Some(iteration),
1235 );
1236 }
1237 let phases = finish_semantic_phase_collection();
1238
1239 Ok(BenchReport {
1240 spec,
1241 samples,
1242 phases,
1243 timeline,
1244 })
1245}
1246
1247pub fn run_closure_with_setup_teardown<S, T, F, D>(
1276 spec: BenchSpec,
1277 setup: S,
1278 mut f: F,
1279 teardown: D,
1280) -> Result<BenchReport, TimingError>
1281where
1282 S: FnOnce() -> T,
1283 F: FnMut(&T) -> Result<(), TimingError>,
1284 D: FnOnce(T),
1285{
1286 let mut monitor = DefaultResourceMonitor;
1287 run_closure_with_setup_teardown_with_monitor(
1288 spec,
1289 &mut monitor,
1290 setup,
1291 move |input| f(input),
1292 teardown,
1293 )
1294}
1295
1296fn run_closure_with_setup_teardown_with_monitor<S, T, F, D, M>(
1297 spec: BenchSpec,
1298 monitor: &mut M,
1299 setup: S,
1300 mut f: F,
1301 teardown: D,
1302) -> Result<BenchReport, TimingError>
1303where
1304 S: FnOnce() -> T,
1305 F: FnMut(&T) -> Result<(), TimingError>,
1306 D: FnOnce(T),
1307 M: ResourceMonitor,
1308{
1309 if spec.iterations == 0 {
1310 return Err(TimingError::NoIterations {
1311 count: spec.iterations,
1312 });
1313 }
1314
1315 reset_semantic_phase_collection();
1316 let harness_origin = Instant::now();
1317 let mut timeline = Vec::new();
1318
1319 let setup_start = Instant::now();
1321 let input = setup();
1322 push_timeline_span(
1323 &mut timeline,
1324 harness_origin,
1325 "setup",
1326 setup_start,
1327 Instant::now(),
1328 None,
1329 );
1330
1331 for iteration in 0..spec.warmup {
1333 let phase_start = Instant::now();
1334 f(&input)?;
1335 push_timeline_span(
1336 &mut timeline,
1337 harness_origin,
1338 "warmup-benchmark",
1339 phase_start,
1340 Instant::now(),
1341 Some(iteration),
1342 );
1343 }
1344
1345 begin_semantic_phase_collection();
1347 let mut samples = Vec::with_capacity(spec.iterations as usize);
1348 for iteration in 0..spec.iterations {
1349 let (sample, start, end) = match measure_iteration(monitor, || f(&input)) {
1350 Ok(measurement) => measurement,
1351 Err(err) => {
1352 let _ = finish_semantic_phase_collection();
1353 return Err(err);
1354 }
1355 };
1356 samples.push(sample);
1357 push_timeline_span(
1358 &mut timeline,
1359 harness_origin,
1360 "measured-benchmark",
1361 start,
1362 end,
1363 Some(iteration),
1364 );
1365 }
1366 let phases = finish_semantic_phase_collection();
1367
1368 let teardown_start = Instant::now();
1370 teardown(input);
1371 push_timeline_span(
1372 &mut timeline,
1373 harness_origin,
1374 "teardown",
1375 teardown_start,
1376 Instant::now(),
1377 None,
1378 );
1379
1380 Ok(BenchReport {
1381 spec,
1382 samples,
1383 phases,
1384 timeline,
1385 })
1386}
1387
1388#[cfg(test)]
1389mod tests {
1390 use super::*;
1391
1392 #[derive(Default)]
1393 struct FakeResourceMonitor {
1394 samples: Vec<IterationResourceUsage>,
1395 started: usize,
1396 finished: usize,
1397 }
1398
1399 impl FakeResourceMonitor {
1400 fn new(samples: Vec<IterationResourceUsage>) -> Self {
1401 Self {
1402 samples,
1403 started: 0,
1404 finished: 0,
1405 }
1406 }
1407 }
1408
1409 impl ResourceMonitor for FakeResourceMonitor {
1410 type Token = usize;
1411
1412 fn start(&mut self) -> Self::Token {
1413 let token = self.started;
1414 self.started += 1;
1415 assert!(
1416 token < self.samples.len(),
1417 "resource capture should only run for measured iterations"
1418 );
1419 token
1420 }
1421
1422 fn finish(&mut self, token: Self::Token) -> IterationResourceUsage {
1423 self.finished += 1;
1424 self.samples
1425 .get(token)
1426 .cloned()
1427 .expect("resource usage for measured iteration")
1428 }
1429 }
1430
1431 #[test]
1432 fn runs_benchmark_collects_requested_samples() {
1433 let spec = BenchSpec::new("noop", 3, 1).unwrap();
1434 let report = run_closure(spec, || Ok(())).unwrap();
1435
1436 assert_eq!(report.samples.len(), 3);
1437 assert_eq!(report.spec.name, "noop");
1438 assert_eq!(report.spec.iterations, 3);
1439 }
1440
1441 #[test]
1442 fn rejects_zero_iterations() {
1443 let result = BenchSpec::new("test", 0, 10);
1444 assert!(matches!(
1445 result,
1446 Err(TimingError::NoIterations { count: 0 })
1447 ));
1448 }
1449
1450 #[test]
1451 fn allows_zero_warmup() {
1452 let spec = BenchSpec::new("test", 5, 0).unwrap();
1453 assert_eq!(spec.warmup, 0);
1454
1455 let report = run_closure(spec, || Ok(())).unwrap();
1456 assert_eq!(report.samples.len(), 5);
1457 }
1458
1459 #[test]
1460 fn serializes_to_json() {
1461 let report = BenchReport {
1462 spec: BenchSpec::new("test", 10, 2).unwrap(),
1463 samples: vec![BenchSample {
1464 duration_ns: 1_000_000,
1465 cpu_time_ms: Some(42),
1466 peak_memory_kb: Some(512),
1467 }],
1468 phases: vec![SemanticPhase {
1469 name: "prove".to_string(),
1470 duration_ns: 1_000_000,
1471 }],
1472 timeline: vec![HarnessTimelineSpan {
1473 phase: "measured-benchmark".to_string(),
1474 start_offset_ns: 0,
1475 end_offset_ns: 1_000_000,
1476 iteration: Some(0),
1477 }],
1478 };
1479
1480 let json = serde_json::to_string(&report).unwrap();
1481 let restored: BenchReport = serde_json::from_str(&json).unwrap();
1482
1483 assert_eq!(restored.spec.name, "test");
1484 assert_eq!(restored.samples.len(), 1);
1485 assert_eq!(restored.samples[0].cpu_time_ms, Some(42));
1486 assert_eq!(restored.samples[0].peak_memory_kb, Some(512));
1487 assert_eq!(restored.phases.len(), 1);
1488 assert_eq!(restored.phases[0].name, "prove");
1489 assert!(restored.phases[0].duration_ns > 0);
1490 }
1491
1492 #[test]
1493 fn profile_phase_records_only_measured_iterations() {
1494 let spec = BenchSpec::new("semantic", 2, 1).unwrap();
1495 let mut call_index = 0u32;
1496 let report = run_closure(spec, || {
1497 let phase_name = if call_index == 0 {
1498 "warmup-only"
1499 } else {
1500 "prove"
1501 };
1502 call_index += 1;
1503 profile_phase(phase_name, || std::thread::sleep(Duration::from_millis(1)));
1504 Ok(())
1505 })
1506 .unwrap();
1507
1508 assert!(
1509 !report
1510 .phases
1511 .iter()
1512 .any(|phase| phase.name == "warmup-only"),
1513 "warmup phases should not be recorded"
1514 );
1515 let prove = report
1516 .phases
1517 .iter()
1518 .find(|phase| phase.name == "prove")
1519 .expect("prove phase");
1520 assert!(prove.duration_ns > 0);
1521 }
1522
1523 #[test]
1524 fn profile_phase_keeps_the_v1_model_flat() {
1525 let spec = BenchSpec::new("semantic-flat", 1, 0).unwrap();
1526 let report = run_closure(spec, || {
1527 profile_phase("prove", || {
1528 std::thread::sleep(Duration::from_millis(1));
1529 profile_phase("inner", || std::thread::sleep(Duration::from_millis(1)));
1530 });
1531 Ok(())
1532 })
1533 .unwrap();
1534
1535 assert!(report.phases.iter().any(|phase| phase.name == "prove"));
1536 assert!(
1537 !report.phases.iter().any(|phase| phase.name == "inner"),
1538 "nested phases should not create a second flat phase entry"
1539 );
1540 }
1541
1542 #[test]
1543 fn measured_cpu_excludes_warmup_iterations() {
1544 let spec = BenchSpec::new("cpu", 2, 1).unwrap();
1545 let mut monitor = FakeResourceMonitor::new(vec![
1546 IterationResourceUsage {
1547 cpu_time_ms: Some(11),
1548 peak_memory_kb: Some(32),
1549 },
1550 IterationResourceUsage {
1551 cpu_time_ms: Some(17),
1552 peak_memory_kb: Some(64),
1553 },
1554 ]);
1555 let mut calls = 0_u32;
1556
1557 let report = run_closure_with_monitor(spec, &mut monitor, || {
1558 calls += 1;
1559 Ok(())
1560 })
1561 .unwrap();
1562
1563 assert_eq!(calls, 3);
1564 assert_eq!(monitor.started, 2);
1565 assert_eq!(monitor.finished, 2);
1566 assert_eq!(
1567 report
1568 .samples
1569 .iter()
1570 .map(|sample| sample.cpu_time_ms)
1571 .collect::<Vec<_>>(),
1572 vec![Some(11), Some(17)]
1573 );
1574 assert_eq!(report.cpu_total_ms(), Some(28));
1575 }
1576
1577 #[test]
1578 fn measured_cpu_excludes_outer_harness_and_report_overhead() {
1579 let spec = BenchSpec::new("cpu-harness", 2, 1).unwrap();
1580 let mut monitor = FakeResourceMonitor::new(vec![
1581 IterationResourceUsage {
1582 cpu_time_ms: Some(5),
1583 peak_memory_kb: Some(12),
1584 },
1585 IterationResourceUsage {
1586 cpu_time_ms: Some(7),
1587 peak_memory_kb: Some(18),
1588 },
1589 ]);
1590
1591 let mut setup_calls = 0_u32;
1592 let mut teardown_calls = 0_u32;
1593 let report = run_closure_with_setup_teardown_with_monitor(
1594 spec,
1595 &mut monitor,
1596 || {
1597 setup_calls += 1;
1598 vec![1_u8, 2, 3]
1599 },
1600 |_fixture| Ok(()),
1601 |_fixture| {
1602 teardown_calls += 1;
1603 },
1604 )
1605 .unwrap();
1606
1607 let _serialized = serde_json::to_string(&report).unwrap();
1608
1609 assert_eq!(setup_calls, 1);
1610 assert_eq!(teardown_calls, 1);
1611 assert_eq!(monitor.started, 2);
1612 assert_eq!(report.cpu_total_ms(), Some(12));
1613 assert_eq!(report.cpu_median_ms(), Some(6));
1614 }
1615
1616 #[test]
1617 fn single_iteration_cpu_median_matches_the_measured_iteration() {
1618 let spec = BenchSpec::new("single", 1, 0).unwrap();
1619 let mut monitor = FakeResourceMonitor::new(vec![IterationResourceUsage {
1620 cpu_time_ms: Some(42),
1621 peak_memory_kb: Some(24),
1622 }]);
1623
1624 let report = run_closure_with_monitor(spec, &mut monitor, || Ok(())).unwrap();
1625
1626 assert_eq!(report.samples[0].cpu_time_ms, Some(42));
1627 assert_eq!(report.cpu_total_ms(), Some(42));
1628 assert_eq!(report.cpu_median_ms(), Some(42));
1629 }
1630
1631 #[test]
1632 fn multiple_iterations_export_the_median_cpu_sample() {
1633 let spec = BenchSpec::new("median", 3, 0).unwrap();
1634 let mut monitor = FakeResourceMonitor::new(vec![
1635 IterationResourceUsage {
1636 cpu_time_ms: Some(19),
1637 peak_memory_kb: Some(10),
1638 },
1639 IterationResourceUsage {
1640 cpu_time_ms: Some(7),
1641 peak_memory_kb: Some(30),
1642 },
1643 IterationResourceUsage {
1644 cpu_time_ms: Some(11),
1645 peak_memory_kb: Some(20),
1646 },
1647 ]);
1648
1649 let report = run_closure_with_monitor(spec, &mut monitor, || Ok(())).unwrap();
1650
1651 assert_eq!(report.cpu_median_ms(), Some(11));
1652 assert_eq!(report.cpu_total_ms(), Some(37));
1653 }
1654
1655 #[test]
1656 fn peak_memory_excludes_harness_baseline_overhead() {
1657 let spec = BenchSpec::new("memory", 2, 1).unwrap();
1658 let mut monitor = FakeResourceMonitor::new(vec![
1659 IterationResourceUsage {
1660 cpu_time_ms: Some(3),
1661 peak_memory_kb: Some(48),
1662 },
1663 IterationResourceUsage {
1664 cpu_time_ms: Some(4),
1665 peak_memory_kb: Some(96),
1666 },
1667 ]);
1668
1669 let report = run_closure_with_setup_teardown_with_monitor(
1670 spec,
1671 &mut monitor,
1672 || vec![0_u8; 1024],
1673 |_fixture| Ok(()),
1674 |_fixture| {},
1675 )
1676 .unwrap();
1677
1678 assert_eq!(
1679 report
1680 .samples
1681 .iter()
1682 .map(|sample| sample.peak_memory_kb)
1683 .collect::<Vec<_>>(),
1684 vec![Some(48), Some(96)]
1685 );
1686 assert_eq!(report.peak_memory_kb(), Some(96));
1687 }
1688
1689 #[test]
1690 fn memory_peak_sampler_uses_the_first_post_startup_sample_as_its_baseline() {
1691 use std::collections::VecDeque;
1692 use std::sync::{Arc, Mutex};
1693
1694 let samples = Arc::new(Mutex::new(VecDeque::from([
1695 Some(80_u64),
1696 Some(100_u64),
1697 Some(140_u64),
1698 Some(120_u64),
1699 ])));
1700 let reader_samples = Arc::clone(&samples);
1701 let reader = Arc::new(move || {
1702 reader_samples
1703 .lock()
1704 .expect("sample queue")
1705 .pop_front()
1706 .unwrap_or(Some(120))
1707 });
1708
1709 let sampler = MemoryPeakSampler::start_with_reader(reader).expect("sampler");
1710 let peak_kb = sampler.stop().expect("peak memory");
1711
1712 assert_eq!(peak_kb, 40);
1713 }
1714
1715 #[test]
1716 fn run_with_setup_calls_setup_once() {
1717 use std::sync::atomic::{AtomicU32, Ordering};
1718
1719 static SETUP_COUNT: AtomicU32 = AtomicU32::new(0);
1720 static RUN_COUNT: AtomicU32 = AtomicU32::new(0);
1721
1722 let spec = BenchSpec::new("test", 5, 2).unwrap();
1723 let report = run_closure_with_setup(
1724 spec,
1725 || {
1726 SETUP_COUNT.fetch_add(1, Ordering::SeqCst);
1727 vec![1, 2, 3]
1728 },
1729 |data| {
1730 RUN_COUNT.fetch_add(1, Ordering::SeqCst);
1731 std::hint::black_box(data.len());
1732 Ok(())
1733 },
1734 )
1735 .unwrap();
1736
1737 assert_eq!(SETUP_COUNT.load(Ordering::SeqCst), 1); assert_eq!(RUN_COUNT.load(Ordering::SeqCst), 7); assert_eq!(report.samples.len(), 5);
1740 }
1741
1742 #[test]
1743 fn run_with_setup_per_iter_calls_setup_each_time() {
1744 use std::sync::atomic::{AtomicU32, Ordering};
1745
1746 static SETUP_COUNT: AtomicU32 = AtomicU32::new(0);
1747
1748 let spec = BenchSpec::new("test", 3, 1).unwrap();
1749 let report = run_closure_with_setup_per_iter(
1750 spec,
1751 || {
1752 SETUP_COUNT.fetch_add(1, Ordering::SeqCst);
1753 vec![1, 2, 3]
1754 },
1755 |data| {
1756 std::hint::black_box(data);
1757 Ok(())
1758 },
1759 )
1760 .unwrap();
1761
1762 assert_eq!(SETUP_COUNT.load(Ordering::SeqCst), 4); assert_eq!(report.samples.len(), 3);
1764 }
1765
1766 #[test]
1767 fn run_with_setup_teardown_calls_both() {
1768 use std::sync::atomic::{AtomicU32, Ordering};
1769
1770 static SETUP_COUNT: AtomicU32 = AtomicU32::new(0);
1771 static TEARDOWN_COUNT: AtomicU32 = AtomicU32::new(0);
1772
1773 let spec = BenchSpec::new("test", 3, 1).unwrap();
1774 let report = run_closure_with_setup_teardown(
1775 spec,
1776 || {
1777 SETUP_COUNT.fetch_add(1, Ordering::SeqCst);
1778 "resource"
1779 },
1780 |_resource| Ok(()),
1781 |_resource| {
1782 TEARDOWN_COUNT.fetch_add(1, Ordering::SeqCst);
1783 },
1784 )
1785 .unwrap();
1786
1787 assert_eq!(SETUP_COUNT.load(Ordering::SeqCst), 1);
1788 assert_eq!(TEARDOWN_COUNT.load(Ordering::SeqCst), 1);
1789 assert_eq!(report.samples.len(), 3);
1790 }
1791
1792 #[test]
1793 fn bench_report_serializes_exact_harness_timeline() {
1794 let spec = BenchSpec::new("timeline", 2, 1).unwrap();
1795 let report = run_closure_with_setup_teardown(
1796 spec,
1797 || {
1798 std::thread::sleep(Duration::from_millis(1));
1799 "resource"
1800 },
1801 |_resource| {
1802 std::thread::sleep(Duration::from_millis(1));
1803 Ok(())
1804 },
1805 |_resource| {
1806 std::thread::sleep(Duration::from_millis(1));
1807 },
1808 )
1809 .unwrap();
1810
1811 let json = serde_json::to_value(&report).unwrap();
1812 assert_eq!(json["timeline"][0]["phase"], "setup");
1813 assert_eq!(json["timeline"][1]["phase"], "warmup-benchmark");
1814 assert_eq!(json["timeline"][2]["phase"], "measured-benchmark");
1815 assert_eq!(json["timeline"][3]["phase"], "measured-benchmark");
1816 assert_eq!(json["timeline"][4]["phase"], "teardown");
1817 }
1818}