1use mobench_runtime::{
66 Distribution, MAX_BENCHMARK_COUNT, saturating_sum_u64, saturating_u128_to_u64,
67 saturating_usize_to_u32, sdk_v1_mean_u64, sdk_v1_std_dev_u64,
68};
69use serde::{Deserialize, Serialize};
70use std::cell::RefCell;
71#[cfg(not(target_arch = "wasm32"))]
72use std::sync::{Arc, mpsc};
73#[cfg(not(target_arch = "wasm32"))]
74use std::thread::{self, JoinHandle};
75use std::time::Duration;
76#[cfg(not(target_arch = "wasm32"))]
77use std::time::Instant;
78use thiserror::Error;
79
80#[cfg(target_arch = "wasm32")]
81mod browser_time {
82 use std::time::Duration;
83 use wasm_bindgen::prelude::*;
84
85 #[wasm_bindgen]
86 extern "C" {
87 #[wasm_bindgen(js_namespace = performance, js_name = now)]
88 fn performance_now_ms() -> f64;
89 }
90
91 #[derive(Clone, Copy)]
96 pub(super) struct Instant(f64);
97
98 impl Instant {
99 pub(super) fn now() -> Self {
100 Self(performance_now_ms())
101 }
102
103 pub(super) fn duration_since(self, earlier: Self) -> Duration {
104 Duration::from_secs_f64(((self.0 - earlier.0).max(0.0)) / 1_000.0)
105 }
106
107 pub(super) fn elapsed(self) -> Duration {
108 Self::now().duration_since(self)
109 }
110 }
111}
112
113#[cfg(target_arch = "wasm32")]
114use browser_time::Instant;
115
116#[derive(Clone, Debug, Serialize, Deserialize)]
155#[serde(try_from = "UncheckedBenchSpec")]
156pub struct BenchSpec {
157 pub name: String,
161
162 pub iterations: u32,
166
167 pub warmup: u32,
172}
173
174#[derive(Deserialize)]
175struct UncheckedBenchSpec {
176 name: String,
177 iterations: u32,
178 warmup: u32,
179}
180
181impl TryFrom<UncheckedBenchSpec> for BenchSpec {
182 type Error = TimingError;
183
184 fn try_from(spec: UncheckedBenchSpec) -> Result<Self, Self::Error> {
185 Self::new(spec.name, spec.iterations, spec.warmup)
186 }
187}
188
189impl BenchSpec {
190 pub fn new(name: impl Into<String>, iterations: u32, warmup: u32) -> Result<Self, TimingError> {
216 validate_benchmark_counts(iterations, warmup)?;
217
218 Ok(Self {
219 name: name.into(),
220 iterations,
221 warmup,
222 })
223 }
224
225 pub fn validate(&self) -> Result<(), TimingError> {
227 validate_benchmark_counts(self.iterations, self.warmup)
228 }
229}
230
231fn validate_benchmark_counts(iterations: u32, warmup: u32) -> Result<(), TimingError> {
232 if iterations == 0 {
233 return Err(TimingError::NoIterations { count: iterations });
234 }
235 if iterations > MAX_BENCHMARK_COUNT {
236 return Err(TimingError::Execution(format!(
237 "iterations must not exceed {MAX_BENCHMARK_COUNT} (got {iterations})"
238 )));
239 }
240 if warmup > MAX_BENCHMARK_COUNT {
241 return Err(TimingError::Execution(format!(
242 "warmup must not exceed {MAX_BENCHMARK_COUNT} (got {warmup})"
243 )));
244 }
245 Ok(())
246}
247
248#[derive(Clone, Debug, Default, Serialize, Deserialize)]
270pub struct BenchSample {
271 pub duration_ns: u64,
275
276 #[serde(default, skip_serializing_if = "Option::is_none")]
281 pub cpu_time_ms: Option<u64>,
282
283 #[serde(default, skip_serializing_if = "Option::is_none")]
289 pub peak_memory_kb: Option<u64>,
290
291 #[serde(default, skip_serializing_if = "Option::is_none")]
296 pub process_peak_memory_kb: Option<u64>,
297}
298
299impl BenchSample {
300 fn from_measurement(duration: Duration, resources: IterationResourceUsage) -> Self {
301 Self {
302 duration_ns: saturating_u128_to_u64(duration.as_nanos()),
303 cpu_time_ms: resources.cpu_time_ms,
304 peak_memory_kb: resources.peak_memory_kb,
305 process_peak_memory_kb: resources.process_peak_memory_kb,
306 }
307 }
308}
309
310#[derive(Clone, Debug, Serialize, Deserialize)]
339pub struct BenchReport {
340 pub spec: BenchSpec,
342
343 pub samples: Vec<BenchSample>,
347
348 #[serde(default, skip_serializing_if = "Vec::is_empty")]
353 pub phases: Vec<SemanticPhase>,
354
355 #[serde(default, skip_serializing_if = "Vec::is_empty")]
360 pub timeline: Vec<HarnessTimelineSpan>,
361}
362
363#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
364pub struct HarnessTimelineSpan {
365 pub phase: String,
366 pub start_offset_ns: u64,
367 pub end_offset_ns: u64,
368 pub iteration: Option<u32>,
369}
370
371impl BenchReport {
372 #[must_use]
374 pub fn mean_ns(&self) -> f64 {
375 sdk_v1_mean_u64(self.samples.iter().map(|sample| sample.duration_ns))
376 }
377
378 #[must_use]
380 pub fn median_ns(&self) -> f64 {
381 let durations = self.duration_values();
382 Distribution::from_vec(durations).sdk_v1_median()
383 }
384
385 #[must_use]
387 pub fn std_dev_ns(&self) -> f64 {
388 sdk_v1_std_dev_u64(self.samples.iter().map(|sample| sample.duration_ns))
389 }
390
391 #[must_use]
393 pub fn percentile_ns(&self, p: f64) -> f64 {
394 let durations = self.duration_values();
395 Distribution::from_vec(durations).sdk_v1_percentile(p)
396 }
397
398 #[must_use]
400 pub fn min_ns(&self) -> u64 {
401 self.samples
402 .iter()
403 .map(|sample| sample.duration_ns)
404 .min()
405 .unwrap_or(0)
406 }
407
408 #[must_use]
410 pub fn max_ns(&self) -> u64 {
411 self.samples
412 .iter()
413 .map(|sample| sample.duration_ns)
414 .max()
415 .unwrap_or(0)
416 }
417
418 #[must_use]
420 pub fn cpu_total_ms(&self) -> Option<u64> {
421 let mut values = self
422 .samples
423 .iter()
424 .filter_map(|sample| sample.cpu_time_ms)
425 .peekable();
426 values.peek()?;
427 Some(saturating_sum_u64(values))
428 }
429
430 #[must_use]
432 pub fn cpu_median_ms(&self) -> Option<u64> {
433 let values = self
434 .samples
435 .iter()
436 .filter_map(|sample| sample.cpu_time_ms)
437 .collect::<Vec<_>>();
438 Distribution::from_vec(values)
439 .cli_v1_summary()
440 .map(|summary| summary.median_ns)
441 }
442
443 #[must_use]
448 pub fn peak_memory_kb(&self) -> Option<u64> {
449 self.samples
450 .iter()
451 .filter_map(|sample| sample.peak_memory_kb)
452 .max()
453 }
454
455 #[must_use]
460 #[doc(alias = "peak_memory_kb")]
461 pub fn peak_memory_growth_kb(&self) -> Option<u64> {
462 self.peak_memory_kb()
463 }
464
465 #[must_use]
470 pub fn process_peak_memory_kb(&self) -> Option<u64> {
471 self.samples
472 .iter()
473 .filter_map(|sample| sample.process_peak_memory_kb)
474 .max()
475 }
476
477 #[must_use]
479 pub fn summary(&self) -> BenchSummary {
480 let durations = self.duration_values();
481 let statistics = Distribution::from_vec(durations).sdk_v1_summary();
482 BenchSummary {
483 name: self.spec.name.clone(),
484 iterations: saturating_usize_to_u32(self.samples.len()),
485 warmup: self.spec.warmup,
486 mean_ns: statistics.mean_ns,
487 median_ns: statistics.median_ns,
488 std_dev_ns: statistics.std_dev_ns,
489 min_ns: statistics.min_ns,
490 max_ns: statistics.max_ns,
491 p95_ns: statistics.p95_ns,
492 p99_ns: statistics.p99_ns,
493 }
494 }
495
496 fn duration_values(&self) -> Vec<u64> {
497 self.samples
498 .iter()
499 .map(|sample| sample.duration_ns)
500 .collect()
501 }
502}
503
504#[derive(Clone, Debug, Default)]
505struct IterationResourceUsage {
506 cpu_time_ms: Option<u64>,
507 peak_memory_kb: Option<u64>,
508 process_peak_memory_kb: Option<u64>,
509}
510
511fn instant_offset_ns(origin: Instant, instant: Instant) -> u64 {
512 instant
513 .duration_since(origin)
514 .as_nanos()
515 .min(u128::from(u64::MAX)) as u64
516}
517
518fn push_timeline_span(
519 timeline: &mut Vec<HarnessTimelineSpan>,
520 origin: Instant,
521 phase: &str,
522 started_at: Instant,
523 ended_at: Instant,
524 iteration: Option<u32>,
525) {
526 timeline.push(HarnessTimelineSpan {
527 phase: phase.to_string(),
528 start_offset_ns: instant_offset_ns(origin, started_at),
529 end_offset_ns: instant_offset_ns(origin, ended_at),
530 iteration,
531 });
532}
533
534#[derive(Clone, Debug, Serialize, Deserialize)]
536pub struct BenchSummary {
537 pub name: String,
539 pub iterations: u32,
541 pub warmup: u32,
543 pub mean_ns: f64,
545 pub median_ns: f64,
547 pub std_dev_ns: f64,
549 pub min_ns: u64,
551 pub max_ns: u64,
553 pub p95_ns: f64,
555 pub p99_ns: f64,
557}
558
559#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
561pub struct SemanticPhase {
562 pub name: String,
563 pub duration_ns: u64,
564}
565
566#[derive(Default)]
567struct SemanticPhaseCollector {
568 enabled: bool,
569 depth: usize,
570 phases: Vec<SemanticPhase>,
571}
572
573impl SemanticPhaseCollector {
574 fn reset(&mut self) {
575 self.enabled = false;
576 self.depth = 0;
577 self.phases.clear();
578 }
579
580 fn begin_measurement(&mut self) {
581 self.reset();
582 self.enabled = true;
583 }
584
585 fn finish(&mut self) -> Vec<SemanticPhase> {
586 self.enabled = false;
587 self.depth = 0;
588 std::mem::take(&mut self.phases)
589 }
590
591 fn enter_phase(&mut self) -> Option<bool> {
592 if !self.enabled {
593 return None;
594 }
595 let top_level = self.depth == 0;
596 self.depth += 1;
597 Some(top_level)
598 }
599
600 fn exit_phase(&mut self, name: &str, top_level: bool, elapsed: Duration) {
601 self.depth = self.depth.saturating_sub(1);
602 if !self.enabled || !top_level {
603 return;
604 }
605
606 let duration_ns = elapsed.as_nanos().min(u128::from(u64::MAX)) as u64;
607 if let Some(phase) = self.phases.iter_mut().find(|phase| phase.name == name) {
608 phase.duration_ns = phase.duration_ns.saturating_add(duration_ns);
609 } else {
610 self.phases.push(SemanticPhase {
611 name: name.to_string(),
612 duration_ns,
613 });
614 }
615 }
616}
617
618thread_local! {
619 static SEMANTIC_PHASE_COLLECTOR: RefCell<SemanticPhaseCollector> =
620 RefCell::new(SemanticPhaseCollector::default());
621}
622
623struct SemanticPhaseGuard {
624 name: String,
625 started_at: Option<Instant>,
626 top_level: bool,
627}
628
629impl Drop for SemanticPhaseGuard {
630 fn drop(&mut self) {
631 let Some(started_at) = self.started_at else {
632 return;
633 };
634
635 let elapsed = started_at.elapsed();
636 SEMANTIC_PHASE_COLLECTOR.with(|collector| {
637 collector
638 .borrow_mut()
639 .exit_phase(&self.name, self.top_level, elapsed);
640 });
641 }
642}
643
644fn reset_semantic_phase_collection() {
645 SEMANTIC_PHASE_COLLECTOR.with(|collector| collector.borrow_mut().reset());
646}
647
648fn begin_semantic_phase_collection() {
649 SEMANTIC_PHASE_COLLECTOR.with(|collector| collector.borrow_mut().begin_measurement());
650}
651
652fn finish_semantic_phase_collection() -> Vec<SemanticPhase> {
653 SEMANTIC_PHASE_COLLECTOR.with(|collector| collector.borrow_mut().finish())
654}
655
656trait ResourceMonitor {
657 type Token;
658
659 fn start(&mut self) -> Self::Token;
660
661 fn finish(&mut self, token: Self::Token) -> IterationResourceUsage;
662}
663
664#[derive(Default)]
665struct DefaultResourceMonitor {
666 #[cfg(not(target_arch = "wasm32"))]
674 memory_sampler: Option<PersistentMemorySampler>,
675 #[cfg(not(target_arch = "wasm32"))]
678 sampler_init_attempted: bool,
679}
680
681#[derive(Clone, Copy, Debug, PartialEq, Eq)]
682struct ProcessCpuTimeSnapshot {
683 user_ns: u64,
684 system_ns: u64,
685}
686
687impl ProcessCpuTimeSnapshot {
688 #[cfg(unix)]
689 fn from_rusage_timevals(user: libc::timeval, system: libc::timeval) -> Option<Self> {
690 Some(Self {
691 user_ns: timeval_to_ns(user)?,
692 system_ns: timeval_to_ns(system)?,
693 })
694 }
695
696 #[cfg(unix)]
697 fn total_ns(self) -> u64 {
698 self.user_ns.saturating_add(self.system_ns)
699 }
700}
701
702struct DefaultResourceToken {
703 cpu_time_start: Option<ProcessCpuTimeSnapshot>,
704 #[cfg(not(target_arch = "wasm32"))]
707 has_memory_window: bool,
708}
709
710impl ResourceMonitor for DefaultResourceMonitor {
711 type Token = DefaultResourceToken;
712
713 fn start(&mut self) -> Self::Token {
714 #[cfg(not(target_arch = "wasm32"))]
715 {
716 if !self.sampler_init_attempted {
717 self.memory_sampler = PersistentMemorySampler::start();
718 self.sampler_init_attempted = true;
719 }
720 let has_memory_window = self
721 .memory_sampler
722 .as_ref()
723 .is_some_and(PersistentMemorySampler::begin_window);
724 Self::Token {
725 cpu_time_start: current_process_cpu_time(),
726 has_memory_window,
727 }
728 }
729
730 #[cfg(target_arch = "wasm32")]
731 Self::Token {
732 cpu_time_start: None,
733 }
734 }
735
736 fn finish(&mut self, token: Self::Token) -> IterationResourceUsage {
737 let cpu_time_ms = token
738 .cpu_time_start
739 .zip(current_process_cpu_time())
740 .and_then(|(start, end)| process_cpu_delta_ms(start, end));
741
742 #[cfg(not(target_arch = "wasm32"))]
743 {
744 let memory_peak = if token.has_memory_window {
745 self.memory_sampler
746 .as_ref()
747 .and_then(PersistentMemorySampler::end_window)
748 } else {
749 None
750 };
751
752 IterationResourceUsage {
753 cpu_time_ms,
754 peak_memory_kb: memory_peak
755 .and_then(|peak| (peak.growth_kb > 0).then_some(peak.growth_kb)),
756 process_peak_memory_kb: memory_peak
757 .and_then(|peak| (peak.process_peak_kb > 0).then_some(peak.process_peak_kb)),
758 }
759 }
760
761 #[cfg(target_arch = "wasm32")]
762 {
763 IterationResourceUsage {
764 cpu_time_ms,
765 ..IterationResourceUsage::default()
766 }
767 }
768 }
769}
770
771#[cfg(unix)]
772fn round_ns_to_ms(ns: u64) -> u64 {
773 ((u128::from(ns) + 500_000) / 1_000_000) as u64
774}
775
776#[cfg(unix)]
777fn process_cpu_delta_ms(start: ProcessCpuTimeSnapshot, end: ProcessCpuTimeSnapshot) -> Option<u64> {
778 Some(round_ns_to_ms(
779 end.total_ns().checked_sub(start.total_ns())?,
780 ))
781}
782
783#[cfg(not(unix))]
784fn process_cpu_delta_ms(
785 _start: ProcessCpuTimeSnapshot,
786 _end: ProcessCpuTimeSnapshot,
787) -> Option<u64> {
788 None
789}
790
791#[cfg(unix)]
792fn timeval_to_ns(value: libc::timeval) -> Option<u64> {
793 let secs = u64::try_from(value.tv_sec).ok()?;
794 let micros = u64::try_from(value.tv_usec).ok()?;
795 Some(
796 secs.saturating_mul(1_000_000_000)
797 .saturating_add(micros.saturating_mul(1_000)),
798 )
799}
800
801#[cfg(unix)]
802fn current_process_cpu_time() -> Option<ProcessCpuTimeSnapshot> {
803 let mut usage = std::mem::MaybeUninit::<libc::rusage>::uninit();
804 let rc = unsafe { libc::getrusage(libc::RUSAGE_SELF, usage.as_mut_ptr()) };
808 if rc != 0 {
809 return None;
810 }
811
812 let usage = unsafe { usage.assume_init() };
814 ProcessCpuTimeSnapshot::from_rusage_timevals(usage.ru_utime, usage.ru_stime)
815}
816
817#[cfg(not(unix))]
818fn current_process_cpu_time() -> Option<ProcessCpuTimeSnapshot> {
819 None
820}
821
822#[cfg(not(target_arch = "wasm32"))]
823const MEMORY_SAMPLER_INTERVAL: Duration = Duration::from_millis(1);
824#[cfg(not(target_arch = "wasm32"))]
825type MemoryReader = Arc<dyn Fn() -> Option<u64> + Send + Sync + 'static>;
826
827#[cfg(not(target_arch = "wasm32"))]
828#[derive(Clone, Copy, Debug, PartialEq, Eq)]
829struct ProcessMemoryPeak {
830 growth_kb: u64,
831 process_peak_kb: u64,
832}
833
834#[cfg(not(target_arch = "wasm32"))]
842struct PersistentMemorySampler {
843 cmd_tx: mpsc::SyncSender<SamplerCmd>,
844 result_rx: mpsc::Receiver<Option<ProcessMemoryPeak>>,
845 handle: Option<JoinHandle<()>>,
846}
847
848#[cfg(not(target_arch = "wasm32"))]
849enum SamplerCmd {
850 Begin(mpsc::SyncSender<bool>),
851 End,
852 Shutdown,
853}
854
855#[cfg(not(target_arch = "wasm32"))]
856impl PersistentMemorySampler {
857 fn start() -> Option<Self> {
858 Self::start_with_reader(Arc::new(current_process_memory_kb))
859 }
860
861 fn start_with_reader(reader: MemoryReader) -> Option<Self> {
862 let (cmd_tx, cmd_rx) = mpsc::sync_channel::<SamplerCmd>(1);
863 let (result_tx, result_rx) = mpsc::sync_channel::<Option<ProcessMemoryPeak>>(1);
864 let (ready_tx, ready_rx) = mpsc::sync_channel::<()>(1);
865
866 let handle = thread::Builder::new()
867 .name("mobench-memory-sampler".to_string())
868 .spawn(move || {
869 let _ = reader();
873 if ready_tx.send(()).is_err() {
874 return;
875 }
876 drop(ready_tx);
877
878 Self::run(reader, &cmd_rx, &result_tx);
879 })
880 .ok()?;
881
882 if ready_rx.recv().is_err() {
883 let _ = cmd_tx.send(SamplerCmd::Shutdown);
886 let _ = handle.join();
887 return None;
888 }
889
890 Some(Self {
891 cmd_tx,
892 result_rx,
893 handle: Some(handle),
894 })
895 }
896
897 fn run(
898 reader: MemoryReader,
899 cmd_rx: &mpsc::Receiver<SamplerCmd>,
900 result_tx: &mpsc::SyncSender<Option<ProcessMemoryPeak>>,
901 ) {
902 while let Ok(cmd) = cmd_rx.recv() {
903 match cmd {
904 SamplerCmd::Begin(ack_tx) => {
905 let baseline = match reader() {
906 Some(v) => v,
907 None => {
908 let _ = ack_tx.send(false);
909 continue;
910 }
911 };
912 if ack_tx.send(true).is_err() {
913 continue;
914 }
915 let mut peak = baseline;
916 let shutting_down = loop {
917 match cmd_rx.recv_timeout(MEMORY_SAMPLER_INTERVAL) {
918 Ok(SamplerCmd::End) => break false,
919 Ok(SamplerCmd::Shutdown) => break true,
920 Ok(SamplerCmd::Begin(ack_tx)) => {
924 let _ = ack_tx.send(false);
925 }
926 Err(mpsc::RecvTimeoutError::Timeout) => {
927 if let Some(current) = reader()
928 && current > peak
929 {
930 peak = current;
931 }
932 }
933 Err(mpsc::RecvTimeoutError::Disconnected) => break true,
934 }
935 };
936 if let Some(current) = reader()
940 && current > peak
941 {
942 peak = current;
943 }
944 let _ = result_tx.send(Some(ProcessMemoryPeak {
945 growth_kb: peak.saturating_sub(baseline),
946 process_peak_kb: peak,
947 }));
948 if shutting_down {
949 return;
950 }
951 }
952 SamplerCmd::Shutdown => return,
953 SamplerCmd::End => {}
955 }
956 }
957 }
958
959 fn begin_window(&self) -> bool {
960 let (ack_tx, ack_rx) = mpsc::sync_channel(1);
961 self.cmd_tx
962 .send(SamplerCmd::Begin(ack_tx))
963 .ok()
964 .and_then(|()| ack_rx.recv().ok())
965 .unwrap_or(false)
966 }
967
968 fn end_window(&self) -> Option<ProcessMemoryPeak> {
969 self.cmd_tx.send(SamplerCmd::End).ok()?;
970 self.result_rx.recv().ok().flatten()
971 }
972}
973
974#[cfg(not(target_arch = "wasm32"))]
975impl Drop for PersistentMemorySampler {
976 fn drop(&mut self) {
977 let _ = self.cmd_tx.send(SamplerCmd::Shutdown);
978 if let Some(handle) = self.handle.take() {
979 let _ = handle.join();
980 }
981 }
982}
983
984#[cfg(any(target_os = "android", target_os = "linux"))]
985fn current_process_memory_kb() -> Option<u64> {
986 let statm = std::fs::read_to_string("/proc/self/statm").ok()?;
987 let resident_pages = statm
988 .split_whitespace()
989 .nth(1)
990 .and_then(|value| value.parse::<u64>().ok())?;
991 let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
994 if page_size <= 0 {
995 return None;
996 }
997 let page_size = u64::try_from(page_size).ok()?;
998 Some(resident_pages.saturating_mul(page_size) / 1024)
999}
1000
1001#[cfg(any(target_os = "ios", target_os = "macos"))]
1002fn current_process_memory_kb() -> Option<u64> {
1003 let mut info = std::mem::MaybeUninit::<libc::mach_task_basic_info_data_t>::uninit();
1004 let mut count = libc::MACH_TASK_BASIC_INFO_COUNT;
1005 #[allow(deprecated)]
1011 let rc = unsafe {
1017 libc::task_info(
1018 libc::mach_task_self(),
1019 libc::MACH_TASK_BASIC_INFO,
1020 info.as_mut_ptr().cast::<libc::integer_t>(),
1021 &mut count,
1022 )
1023 };
1024 if rc != libc::KERN_SUCCESS {
1025 return None;
1026 }
1027
1028 let info = unsafe { info.assume_init() };
1031 Some(info.resident_size / 1024)
1032}
1033
1034#[cfg(not(any(
1035 target_os = "android",
1036 target_os = "linux",
1037 target_os = "ios",
1038 target_os = "macos",
1039 target_arch = "wasm32"
1040)))]
1041fn current_process_memory_kb() -> Option<u64> {
1042 None
1043}
1044
1045fn measure_iteration<M, F>(
1046 monitor: &mut M,
1047 f: F,
1048) -> Result<(BenchSample, Instant, Instant), TimingError>
1049where
1050 M: ResourceMonitor,
1051 F: FnOnce() -> Result<(), TimingError>,
1052{
1053 let token = monitor.start();
1054 let started_at = Instant::now();
1055 let result = f();
1056 let ended_at = Instant::now();
1057 let resources = monitor.finish(token);
1058 result.map(|_| {
1059 (
1060 BenchSample::from_measurement(ended_at.duration_since(started_at), resources),
1061 started_at,
1062 ended_at,
1063 )
1064 })
1065}
1066
1067pub fn profile_phase<T>(name: &str, f: impl FnOnce() -> T) -> T {
1072 let guard = SEMANTIC_PHASE_COLLECTOR.with(|collector| {
1073 let mut collector = collector.borrow_mut();
1074 match collector.enter_phase() {
1075 Some(top_level) => SemanticPhaseGuard {
1076 name: name.to_string(),
1077 started_at: Some(Instant::now()),
1078 top_level,
1079 },
1080 None => SemanticPhaseGuard {
1081 name: String::new(),
1082 started_at: None,
1083 top_level: false,
1084 },
1085 }
1086 });
1087
1088 let result = f();
1089 drop(guard);
1090 result
1091}
1092
1093#[derive(Debug, Error)]
1105pub enum TimingError {
1106 #[error("iterations must be greater than zero (got {count}). Minimum recommended: 10")]
1111 NoIterations {
1112 count: u32,
1114 },
1115
1116 #[error("benchmark function failed: {0}")]
1120 Execution(String),
1121}
1122
1123pub fn run_closure<F>(spec: BenchSpec, f: F) -> Result<BenchReport, TimingError>
1185where
1186 F: FnMut() -> Result<(), TimingError>,
1187{
1188 let mut monitor = DefaultResourceMonitor::default();
1189 run_closure_with_monitor(spec, &mut monitor, f)
1190}
1191
1192fn run_closure_with_monitor<F, M>(
1193 spec: BenchSpec,
1194 monitor: &mut M,
1195 mut f: F,
1196) -> Result<BenchReport, TimingError>
1197where
1198 F: FnMut() -> Result<(), TimingError>,
1199 M: ResourceMonitor,
1200{
1201 spec.validate()?;
1202
1203 reset_semantic_phase_collection();
1204 let harness_origin = Instant::now();
1205 let mut timeline = Vec::new();
1206
1207 for iteration in 0..spec.warmup {
1209 let phase_start = Instant::now();
1210 f()?;
1211 push_timeline_span(
1212 &mut timeline,
1213 harness_origin,
1214 "warmup-benchmark",
1215 phase_start,
1216 Instant::now(),
1217 Some(iteration),
1218 );
1219 }
1220
1221 begin_semantic_phase_collection();
1223 let mut samples = Vec::with_capacity(spec.iterations as usize);
1224 for iteration in 0..spec.iterations {
1225 let (sample, start, end) = match measure_iteration(monitor, &mut f) {
1226 Ok(measurement) => measurement,
1227 Err(err) => {
1228 let _ = finish_semantic_phase_collection();
1229 return Err(err);
1230 }
1231 };
1232 samples.push(sample);
1233 push_timeline_span(
1234 &mut timeline,
1235 harness_origin,
1236 "measured-benchmark",
1237 start,
1238 end,
1239 Some(iteration),
1240 );
1241 }
1242 let phases = finish_semantic_phase_collection();
1243
1244 Ok(BenchReport {
1245 spec,
1246 samples,
1247 phases,
1248 timeline,
1249 })
1250}
1251
1252pub fn run_closure_with_setup<S, T, F>(
1280 spec: BenchSpec,
1281 setup: S,
1282 mut f: F,
1283) -> Result<BenchReport, TimingError>
1284where
1285 S: FnOnce() -> T,
1286 F: FnMut(&T) -> Result<(), TimingError>,
1287{
1288 let mut monitor = DefaultResourceMonitor::default();
1289 run_closure_with_setup_with_monitor(spec, &mut monitor, setup, move |input| f(input))
1290}
1291
1292fn run_closure_with_setup_with_monitor<S, T, F, M>(
1293 spec: BenchSpec,
1294 monitor: &mut M,
1295 setup: S,
1296 mut f: F,
1297) -> Result<BenchReport, TimingError>
1298where
1299 S: FnOnce() -> T,
1300 F: FnMut(&T) -> Result<(), TimingError>,
1301 M: ResourceMonitor,
1302{
1303 spec.validate()?;
1304
1305 reset_semantic_phase_collection();
1306 let harness_origin = Instant::now();
1307 let mut timeline = Vec::new();
1308
1309 let setup_start = Instant::now();
1311 let input = setup();
1312 push_timeline_span(
1313 &mut timeline,
1314 harness_origin,
1315 "setup",
1316 setup_start,
1317 Instant::now(),
1318 None,
1319 );
1320
1321 for iteration in 0..spec.warmup {
1323 let phase_start = Instant::now();
1324 f(&input)?;
1325 push_timeline_span(
1326 &mut timeline,
1327 harness_origin,
1328 "warmup-benchmark",
1329 phase_start,
1330 Instant::now(),
1331 Some(iteration),
1332 );
1333 }
1334
1335 begin_semantic_phase_collection();
1337 let mut samples = Vec::with_capacity(spec.iterations as usize);
1338 for iteration in 0..spec.iterations {
1339 let (sample, start, end) = match measure_iteration(monitor, || f(&input)) {
1340 Ok(measurement) => measurement,
1341 Err(err) => {
1342 let _ = finish_semantic_phase_collection();
1343 return Err(err);
1344 }
1345 };
1346 samples.push(sample);
1347 push_timeline_span(
1348 &mut timeline,
1349 harness_origin,
1350 "measured-benchmark",
1351 start,
1352 end,
1353 Some(iteration),
1354 );
1355 }
1356 let phases = finish_semantic_phase_collection();
1357
1358 Ok(BenchReport {
1359 spec,
1360 samples,
1361 phases,
1362 timeline,
1363 })
1364}
1365
1366pub fn run_closure_with_setup_per_iter<S, T, F>(
1395 spec: BenchSpec,
1396 setup: S,
1397 f: F,
1398) -> Result<BenchReport, TimingError>
1399where
1400 S: FnMut() -> T,
1401 F: FnMut(T) -> Result<(), TimingError>,
1402{
1403 let mut monitor = DefaultResourceMonitor::default();
1404 run_closure_with_setup_per_iter_with_monitor(spec, &mut monitor, setup, f)
1405}
1406
1407fn run_closure_with_setup_per_iter_with_monitor<S, T, F, M>(
1408 spec: BenchSpec,
1409 monitor: &mut M,
1410 mut setup: S,
1411 mut f: F,
1412) -> Result<BenchReport, TimingError>
1413where
1414 S: FnMut() -> T,
1415 F: FnMut(T) -> Result<(), TimingError>,
1416 M: ResourceMonitor,
1417{
1418 spec.validate()?;
1419
1420 reset_semantic_phase_collection();
1421 let harness_origin = Instant::now();
1422 let mut timeline = Vec::new();
1423
1424 for iteration in 0..spec.warmup {
1426 let setup_start = Instant::now();
1427 let input = setup();
1428 push_timeline_span(
1429 &mut timeline,
1430 harness_origin,
1431 "fixture-setup",
1432 setup_start,
1433 Instant::now(),
1434 Some(iteration),
1435 );
1436 let phase_start = Instant::now();
1437 f(input)?;
1438 push_timeline_span(
1439 &mut timeline,
1440 harness_origin,
1441 "warmup-benchmark",
1442 phase_start,
1443 Instant::now(),
1444 Some(iteration),
1445 );
1446 }
1447
1448 begin_semantic_phase_collection();
1450 let mut samples = Vec::with_capacity(spec.iterations as usize);
1451 for iteration in 0..spec.iterations {
1452 let setup_start = Instant::now();
1453 let input = setup(); push_timeline_span(
1455 &mut timeline,
1456 harness_origin,
1457 "fixture-setup",
1458 setup_start,
1459 Instant::now(),
1460 Some(iteration),
1461 );
1462
1463 let (sample, start, end) = match measure_iteration(monitor, || f(input)) {
1464 Ok(measurement) => measurement,
1465 Err(err) => {
1466 let _ = finish_semantic_phase_collection();
1467 return Err(err);
1468 }
1469 };
1470 samples.push(sample);
1471 push_timeline_span(
1472 &mut timeline,
1473 harness_origin,
1474 "measured-benchmark",
1475 start,
1476 end,
1477 Some(iteration),
1478 );
1479 }
1480 let phases = finish_semantic_phase_collection();
1481
1482 Ok(BenchReport {
1483 spec,
1484 samples,
1485 phases,
1486 timeline,
1487 })
1488}
1489
1490pub fn run_closure_with_setup_teardown<S, T, F, D>(
1519 spec: BenchSpec,
1520 setup: S,
1521 mut f: F,
1522 teardown: D,
1523) -> Result<BenchReport, TimingError>
1524where
1525 S: FnOnce() -> T,
1526 F: FnMut(&T) -> Result<(), TimingError>,
1527 D: FnOnce(T),
1528{
1529 let mut monitor = DefaultResourceMonitor::default();
1530 run_closure_with_setup_teardown_with_monitor(
1531 spec,
1532 &mut monitor,
1533 setup,
1534 move |input| f(input),
1535 teardown,
1536 )
1537}
1538
1539fn run_closure_with_setup_teardown_with_monitor<S, T, F, D, M>(
1540 spec: BenchSpec,
1541 monitor: &mut M,
1542 setup: S,
1543 mut f: F,
1544 teardown: D,
1545) -> Result<BenchReport, TimingError>
1546where
1547 S: FnOnce() -> T,
1548 F: FnMut(&T) -> Result<(), TimingError>,
1549 D: FnOnce(T),
1550 M: ResourceMonitor,
1551{
1552 spec.validate()?;
1553
1554 reset_semantic_phase_collection();
1555 let harness_origin = Instant::now();
1556 let mut timeline = Vec::new();
1557
1558 let setup_start = Instant::now();
1560 let input = setup();
1561 push_timeline_span(
1562 &mut timeline,
1563 harness_origin,
1564 "setup",
1565 setup_start,
1566 Instant::now(),
1567 None,
1568 );
1569
1570 let result = (|| {
1571 for iteration in 0..spec.warmup {
1573 let phase_start = Instant::now();
1574 f(&input)?;
1575 push_timeline_span(
1576 &mut timeline,
1577 harness_origin,
1578 "warmup-benchmark",
1579 phase_start,
1580 Instant::now(),
1581 Some(iteration),
1582 );
1583 }
1584
1585 begin_semantic_phase_collection();
1587 let mut samples = Vec::with_capacity(spec.iterations as usize);
1588 for iteration in 0..spec.iterations {
1589 let (sample, start, end) = match measure_iteration(monitor, || f(&input)) {
1590 Ok(measurement) => measurement,
1591 Err(err) => {
1592 let _ = finish_semantic_phase_collection();
1593 return Err(err);
1594 }
1595 };
1596 samples.push(sample);
1597 push_timeline_span(
1598 &mut timeline,
1599 harness_origin,
1600 "measured-benchmark",
1601 start,
1602 end,
1603 Some(iteration),
1604 );
1605 }
1606 let phases = finish_semantic_phase_collection();
1607
1608 Ok((samples, phases))
1609 })();
1610
1611 let teardown_start = Instant::now();
1613 teardown(input);
1614 push_timeline_span(
1615 &mut timeline,
1616 harness_origin,
1617 "teardown",
1618 teardown_start,
1619 Instant::now(),
1620 None,
1621 );
1622
1623 let (samples, phases) = result?;
1624 Ok(BenchReport {
1625 spec,
1626 samples,
1627 phases,
1628 timeline,
1629 })
1630}
1631
1632#[cfg(test)]
1633mod tests {
1634 use super::*;
1635
1636 #[derive(Default)]
1637 struct FakeResourceMonitor {
1638 samples: Vec<IterationResourceUsage>,
1639 started: usize,
1640 finished: usize,
1641 }
1642
1643 impl FakeResourceMonitor {
1644 fn new(samples: Vec<IterationResourceUsage>) -> Self {
1645 Self {
1646 samples,
1647 started: 0,
1648 finished: 0,
1649 }
1650 }
1651 }
1652
1653 impl ResourceMonitor for FakeResourceMonitor {
1654 type Token = usize;
1655
1656 fn start(&mut self) -> Self::Token {
1657 let token = self.started;
1658 self.started += 1;
1659 assert!(
1660 token < self.samples.len(),
1661 "resource capture should only run for measured iterations"
1662 );
1663 token
1664 }
1665
1666 fn finish(&mut self, token: Self::Token) -> IterationResourceUsage {
1667 self.finished += 1;
1668 self.samples
1669 .get(token)
1670 .cloned()
1671 .expect("resource usage for measured iteration")
1672 }
1673 }
1674
1675 #[cfg(unix)]
1676 #[test]
1677 fn process_cpu_time_snapshot_sums_user_and_kernel_time() {
1678 let snapshot = ProcessCpuTimeSnapshot::from_rusage_timevals(
1679 libc::timeval {
1680 tv_sec: 1,
1681 tv_usec: 250_000,
1682 },
1683 libc::timeval {
1684 tv_sec: 0,
1685 tv_usec: 750_000,
1686 },
1687 )
1688 .expect("valid snapshot");
1689
1690 assert_eq!(snapshot.total_ns(), 2_000_000_000);
1691 }
1692
1693 #[cfg(unix)]
1694 #[test]
1695 fn process_cpu_time_delta_ms_uses_user_and_kernel_time() {
1696 let start = ProcessCpuTimeSnapshot::from_rusage_timevals(
1697 libc::timeval {
1698 tv_sec: 1,
1699 tv_usec: 250_000,
1700 },
1701 libc::timeval {
1702 tv_sec: 0,
1703 tv_usec: 750_000,
1704 },
1705 )
1706 .expect("valid start snapshot");
1707 let end = ProcessCpuTimeSnapshot::from_rusage_timevals(
1708 libc::timeval {
1709 tv_sec: 1,
1710 tv_usec: 900_000,
1711 },
1712 libc::timeval {
1713 tv_sec: 1,
1714 tv_usec: 400_600,
1715 },
1716 )
1717 .expect("valid end snapshot");
1718
1719 assert_eq!(process_cpu_delta_ms(start, end), Some(1_301));
1720 }
1721
1722 #[test]
1723 fn runs_benchmark_collects_requested_samples() {
1724 let spec = BenchSpec::new("noop", 3, 1).unwrap();
1725 let report = run_closure(spec, || Ok(())).unwrap();
1726
1727 assert_eq!(report.samples.len(), 3);
1728 assert_eq!(report.spec.name, "noop");
1729 assert_eq!(report.spec.iterations, 3);
1730 }
1731
1732 #[test]
1733 fn rejects_zero_iterations() {
1734 let result = BenchSpec::new("test", 0, 10);
1735 assert!(matches!(
1736 result,
1737 Err(TimingError::NoIterations { count: 0 })
1738 ));
1739 }
1740
1741 #[test]
1742 fn allows_zero_warmup() {
1743 let spec = BenchSpec::new("test", 5, 0).unwrap();
1744 assert_eq!(spec.warmup, 0);
1745
1746 let report = run_closure(spec, || Ok(())).unwrap();
1747 assert_eq!(report.samples.len(), 5);
1748 }
1749
1750 #[test]
1751 fn rejects_counts_above_the_runtime_limit_at_every_sdk_boundary() {
1752 let out_of_range = MAX_BENCHMARK_COUNT + 1;
1753 assert!(BenchSpec::new("maximum", MAX_BENCHMARK_COUNT, MAX_BENCHMARK_COUNT).is_ok());
1754 assert!(matches!(
1755 BenchSpec::new("iterations", out_of_range, 0),
1756 Err(TimingError::Execution(message))
1757 if message.contains("iterations must not exceed")
1758 ));
1759 assert!(matches!(
1760 BenchSpec::new("warmup", 1, out_of_range),
1761 Err(TimingError::Execution(message)) if message.contains("warmup must not exceed")
1762 ));
1763
1764 let serialized =
1765 format!(r#"{{"name":"serialized","iterations":{out_of_range},"warmup":0}}"#);
1766 assert!(serde_json::from_str::<BenchSpec>(&serialized).is_err());
1767
1768 let mut executed = false;
1769 let direct = BenchSpec {
1770 name: "direct".to_string(),
1771 iterations: out_of_range,
1772 warmup: 0,
1773 };
1774 assert!(matches!(
1775 run_closure(direct, || {
1776 executed = true;
1777 Ok(())
1778 }),
1779 Err(TimingError::Execution(message))
1780 if message.contains("iterations must not exceed")
1781 ));
1782 assert!(!executed);
1783 }
1784
1785 #[test]
1786 fn serializes_to_json() {
1787 let report = BenchReport {
1788 spec: BenchSpec::new("test", 10, 2).unwrap(),
1789 samples: vec![BenchSample {
1790 duration_ns: 1_000_000,
1791 cpu_time_ms: Some(42),
1792 peak_memory_kb: Some(512),
1793 process_peak_memory_kb: Some(1536),
1794 }],
1795 phases: vec![SemanticPhase {
1796 name: "prove".to_string(),
1797 duration_ns: 1_000_000,
1798 }],
1799 timeline: vec![HarnessTimelineSpan {
1800 phase: "measured-benchmark".to_string(),
1801 start_offset_ns: 0,
1802 end_offset_ns: 1_000_000,
1803 iteration: Some(0),
1804 }],
1805 };
1806
1807 let json = serde_json::to_string(&report).unwrap();
1808 assert!(json.contains("\"peak_memory_kb\""));
1809 assert!(json.contains("\"process_peak_memory_kb\""));
1810 assert!(!json.contains("peak_memory_growth_kb"));
1811 let restored: BenchReport = serde_json::from_str(&json).unwrap();
1812
1813 assert_eq!(restored.spec.name, "test");
1814 assert_eq!(restored.samples.len(), 1);
1815 assert_eq!(restored.samples[0].cpu_time_ms, Some(42));
1816 assert_eq!(restored.samples[0].peak_memory_kb, Some(512));
1817 assert_eq!(restored.samples[0].process_peak_memory_kb, Some(1536));
1818 assert_eq!(restored.phases.len(), 1);
1819 assert_eq!(restored.phases[0].name, "prove");
1820 assert!(restored.phases[0].duration_ns > 0);
1821 }
1822
1823 #[test]
1824 fn bench_report_statistics_do_not_overflow_on_extreme_durations() {
1825 let report = BenchReport {
1826 spec: BenchSpec::new("extreme", 2, 0).expect("valid spec"),
1827 samples: [u64::MAX - 1, u64::MAX]
1828 .into_iter()
1829 .map(|duration_ns| BenchSample {
1830 duration_ns,
1831 cpu_time_ms: None,
1832 peak_memory_kb: None,
1833 process_peak_memory_kb: None,
1834 })
1835 .collect(),
1836 phases: Vec::new(),
1837 timeline: Vec::new(),
1838 };
1839
1840 assert!(report.mean_ns().is_finite());
1841 assert!(report.median_ns().is_finite());
1842 let summary = report.summary();
1843 assert_eq!(summary.max_ns, u64::MAX);
1844 assert_eq!(summary.p95_ns, u64::MAX as f64);
1845 }
1846
1847 #[test]
1848 fn bench_sample_saturates_durations_larger_than_the_wire_range() {
1849 let sample =
1850 BenchSample::from_measurement(Duration::MAX, IterationResourceUsage::default());
1851
1852 assert_eq!(sample.duration_ns, u64::MAX);
1853 }
1854
1855 #[test]
1856 fn bench_report_statistics_preserve_the_released_sdk_policy() {
1857 let report_for = |durations: &[u64]| BenchReport {
1858 spec: BenchSpec::new("policy", durations.len().max(1) as u32, 0).expect("valid spec"),
1859 samples: durations
1860 .iter()
1861 .map(|duration_ns| BenchSample {
1862 duration_ns: *duration_ns,
1863 cpu_time_ms: None,
1864 peak_memory_kb: None,
1865 process_peak_memory_kb: None,
1866 })
1867 .collect(),
1868 phases: Vec::new(),
1869 timeline: Vec::new(),
1870 };
1871
1872 let empty = report_for(&[]);
1873 assert_eq!(empty.mean_ns(), 0.0);
1874 assert_eq!(empty.median_ns(), 0.0);
1875 assert_eq!(empty.std_dev_ns(), 0.0);
1876 assert_eq!(empty.percentile_ns(95.0), 0.0);
1877 assert_eq!(empty.min_ns(), 0);
1878 assert_eq!(empty.max_ns(), 0);
1879
1880 let even = report_for(&[1, 2]);
1881 assert_eq!(even.mean_ns(), 1.5);
1882 assert_eq!(even.median_ns(), 1.5);
1883
1884 let samples = (1..=12).collect::<Vec<_>>();
1885 let report = report_for(&samples);
1886 let summary = report.summary();
1887 assert_eq!(summary.mean_ns, 6.5);
1888 assert_eq!(summary.median_ns, 6.5);
1889 assert_eq!(summary.p95_ns, 11.0);
1890 assert_eq!(summary.p99_ns, 12.0);
1891 }
1892
1893 #[test]
1894 fn profile_phase_records_only_measured_iterations() {
1895 let spec = BenchSpec::new("semantic", 2, 1).unwrap();
1896 let mut call_index = 0u32;
1897 let report = run_closure(spec, || {
1898 let phase_name = if call_index == 0 {
1899 "warmup-only"
1900 } else {
1901 "prove"
1902 };
1903 call_index += 1;
1904 profile_phase(phase_name, || std::thread::sleep(Duration::from_millis(1)));
1905 Ok(())
1906 })
1907 .unwrap();
1908
1909 assert!(
1910 !report
1911 .phases
1912 .iter()
1913 .any(|phase| phase.name == "warmup-only"),
1914 "warmup phases should not be recorded"
1915 );
1916 let prove = report
1917 .phases
1918 .iter()
1919 .find(|phase| phase.name == "prove")
1920 .expect("prove phase");
1921 assert!(prove.duration_ns > 0);
1922 }
1923
1924 #[test]
1925 fn profile_phase_keeps_the_v1_model_flat() {
1926 let spec = BenchSpec::new("semantic-flat", 1, 0).unwrap();
1927 let report = run_closure(spec, || {
1928 profile_phase("prove", || {
1929 std::thread::sleep(Duration::from_millis(1));
1930 profile_phase("inner", || std::thread::sleep(Duration::from_millis(1)));
1931 });
1932 Ok(())
1933 })
1934 .unwrap();
1935
1936 assert!(report.phases.iter().any(|phase| phase.name == "prove"));
1937 assert!(
1938 !report.phases.iter().any(|phase| phase.name == "inner"),
1939 "nested phases should not create a second flat phase entry"
1940 );
1941 }
1942
1943 #[test]
1944 fn measured_cpu_excludes_warmup_iterations() {
1945 let spec = BenchSpec::new("cpu", 2, 1).unwrap();
1946 let mut monitor = FakeResourceMonitor::new(vec![
1947 IterationResourceUsage {
1948 cpu_time_ms: Some(11),
1949 peak_memory_kb: Some(32),
1950 ..Default::default()
1951 },
1952 IterationResourceUsage {
1953 cpu_time_ms: Some(17),
1954 peak_memory_kb: Some(64),
1955 ..Default::default()
1956 },
1957 ]);
1958 let mut calls = 0_u32;
1959
1960 let report = run_closure_with_monitor(spec, &mut monitor, || {
1961 calls += 1;
1962 Ok(())
1963 })
1964 .unwrap();
1965
1966 assert_eq!(calls, 3);
1967 assert_eq!(monitor.started, 2);
1968 assert_eq!(monitor.finished, 2);
1969 assert_eq!(
1970 report
1971 .samples
1972 .iter()
1973 .map(|sample| sample.cpu_time_ms)
1974 .collect::<Vec<_>>(),
1975 vec![Some(11), Some(17)]
1976 );
1977 assert_eq!(report.cpu_total_ms(), Some(28));
1978 }
1979
1980 #[test]
1981 fn measured_cpu_excludes_outer_harness_and_report_overhead() {
1982 let spec = BenchSpec::new("cpu-harness", 2, 1).unwrap();
1983 let mut monitor = FakeResourceMonitor::new(vec![
1984 IterationResourceUsage {
1985 cpu_time_ms: Some(5),
1986 peak_memory_kb: Some(12),
1987 ..Default::default()
1988 },
1989 IterationResourceUsage {
1990 cpu_time_ms: Some(7),
1991 peak_memory_kb: Some(18),
1992 ..Default::default()
1993 },
1994 ]);
1995
1996 let mut setup_calls = 0_u32;
1997 let mut teardown_calls = 0_u32;
1998 let report = run_closure_with_setup_teardown_with_monitor(
1999 spec,
2000 &mut monitor,
2001 || {
2002 setup_calls += 1;
2003 vec![1_u8, 2, 3]
2004 },
2005 |_fixture| Ok(()),
2006 |_fixture| {
2007 teardown_calls += 1;
2008 },
2009 )
2010 .unwrap();
2011
2012 let _serialized = serde_json::to_string(&report).unwrap();
2013
2014 assert_eq!(setup_calls, 1);
2015 assert_eq!(teardown_calls, 1);
2016 assert_eq!(monitor.started, 2);
2017 assert_eq!(report.cpu_total_ms(), Some(12));
2018 assert_eq!(report.cpu_median_ms(), Some(6));
2019 }
2020
2021 #[test]
2022 fn setup_teardown_runs_teardown_when_warmup_fails() {
2023 let spec = BenchSpec::new("teardown-on-error", 1, 1).unwrap();
2024 let mut teardown_calls = 0_u32;
2025
2026 let result = run_closure_with_setup_teardown(
2027 spec,
2028 || vec![1_u8, 2, 3],
2029 |_fixture| Err(TimingError::Execution("warmup failed".to_string())),
2030 |_fixture| {
2031 teardown_calls += 1;
2032 },
2033 );
2034
2035 assert!(result.is_err());
2036 assert_eq!(teardown_calls, 1);
2037 }
2038
2039 #[test]
2040 fn single_iteration_cpu_median_matches_the_measured_iteration() {
2041 let spec = BenchSpec::new("single", 1, 0).unwrap();
2042 let mut monitor = FakeResourceMonitor::new(vec![IterationResourceUsage {
2043 cpu_time_ms: Some(42),
2044 peak_memory_kb: Some(24),
2045 ..Default::default()
2046 }]);
2047
2048 let report = run_closure_with_monitor(spec, &mut monitor, || Ok(())).unwrap();
2049
2050 assert_eq!(report.samples[0].cpu_time_ms, Some(42));
2051 assert_eq!(report.cpu_total_ms(), Some(42));
2052 assert_eq!(report.cpu_median_ms(), Some(42));
2053 }
2054
2055 #[test]
2056 fn multiple_iterations_export_the_median_cpu_sample() {
2057 let spec = BenchSpec::new("median", 3, 0).unwrap();
2058 let mut monitor = FakeResourceMonitor::new(vec![
2059 IterationResourceUsage {
2060 cpu_time_ms: Some(19),
2061 peak_memory_kb: Some(10),
2062 ..Default::default()
2063 },
2064 IterationResourceUsage {
2065 cpu_time_ms: Some(7),
2066 peak_memory_kb: Some(30),
2067 ..Default::default()
2068 },
2069 IterationResourceUsage {
2070 cpu_time_ms: Some(11),
2071 peak_memory_kb: Some(20),
2072 ..Default::default()
2073 },
2074 ]);
2075
2076 let report = run_closure_with_monitor(spec, &mut monitor, || Ok(())).unwrap();
2077
2078 assert_eq!(report.cpu_median_ms(), Some(11));
2079 assert_eq!(report.cpu_total_ms(), Some(37));
2080 }
2081
2082 #[test]
2083 fn peak_memory_excludes_harness_baseline_overhead() {
2084 let spec = BenchSpec::new("memory", 2, 1).unwrap();
2085 let mut monitor = FakeResourceMonitor::new(vec![
2086 IterationResourceUsage {
2087 cpu_time_ms: Some(3),
2088 peak_memory_kb: Some(48),
2089 process_peak_memory_kb: Some(1_048),
2090 },
2091 IterationResourceUsage {
2092 cpu_time_ms: Some(4),
2093 peak_memory_kb: Some(96),
2094 process_peak_memory_kb: Some(1_096),
2095 },
2096 ]);
2097
2098 let report = run_closure_with_setup_teardown_with_monitor(
2099 spec,
2100 &mut monitor,
2101 || vec![0_u8; 1024],
2102 |_fixture| Ok(()),
2103 |_fixture| {},
2104 )
2105 .unwrap();
2106
2107 assert_eq!(
2108 report
2109 .samples
2110 .iter()
2111 .map(|sample| sample.peak_memory_kb)
2112 .collect::<Vec<_>>(),
2113 vec![Some(48), Some(96)]
2114 );
2115 assert_eq!(report.peak_memory_kb(), Some(96));
2116 assert_eq!(report.peak_memory_growth_kb(), report.peak_memory_kb());
2117 assert_eq!(report.process_peak_memory_kb(), Some(1_096));
2118 }
2119
2120 #[test]
2121 #[cfg(not(target_arch = "wasm32"))]
2122 fn memory_peak_sampler_uses_the_first_post_startup_sample_as_its_baseline() {
2123 use std::collections::VecDeque;
2124 use std::sync::{Arc, Mutex};
2125
2126 let samples = Arc::new(Mutex::new(VecDeque::from([
2129 Some(80_u64),
2130 Some(100_u64),
2131 Some(140_u64),
2132 Some(120_u64),
2133 ])));
2134 let reader_samples = Arc::clone(&samples);
2135 let reader = Arc::new(move || {
2136 reader_samples
2137 .lock()
2138 .expect("sample queue")
2139 .pop_front()
2140 .unwrap_or(Some(120))
2141 });
2142
2143 let sampler = PersistentMemorySampler::start_with_reader(reader).expect("sampler");
2144 assert!(sampler.begin_window());
2145 let peak = sampler.end_window().expect("peak memory");
2146
2147 assert_eq!(
2148 peak,
2149 ProcessMemoryPeak {
2150 growth_kb: 40,
2151 process_peak_kb: 140,
2152 }
2153 );
2154 }
2155
2156 #[test]
2157 #[cfg(not(target_arch = "wasm32"))]
2158 fn persistent_memory_sampler_does_not_queue_result_when_begin_fails() {
2159 use std::collections::VecDeque;
2160 use std::sync::{Arc, Mutex};
2161
2162 let samples = Arc::new(Mutex::new(VecDeque::from([
2165 Some(80_u64),
2166 None,
2167 Some(100_u64),
2168 Some(130_u64),
2169 ])));
2170 let reader_samples = Arc::clone(&samples);
2171 let reader = Arc::new(move || {
2172 reader_samples
2173 .lock()
2174 .expect("sample queue")
2175 .pop_front()
2176 .unwrap_or(Some(130))
2177 });
2178
2179 let sampler = PersistentMemorySampler::start_with_reader(reader).expect("sampler");
2180 assert!(!sampler.begin_window());
2181 assert!(sampler.begin_window());
2182 let peak = sampler
2183 .end_window()
2184 .expect("second window should receive its own result");
2185
2186 assert_eq!(
2187 peak,
2188 ProcessMemoryPeak {
2189 growth_kb: 30,
2190 process_peak_kb: 130,
2191 }
2192 );
2193 }
2194
2195 #[test]
2196 #[cfg(not(target_arch = "wasm32"))]
2197 fn persistent_memory_sampler_waits_for_baseline_before_begin_returns() {
2198 use std::sync::atomic::{AtomicBool, Ordering};
2199 use std::sync::{Arc, Mutex};
2200
2201 let call_count = Arc::new(Mutex::new(0_u32));
2202 let (baseline_entered_tx, baseline_entered_rx) = mpsc::sync_channel(1);
2203 let (baseline_release_tx, baseline_release_rx) = mpsc::sync_channel(1);
2204 let baseline_release_rx = Arc::new(Mutex::new(baseline_release_rx));
2205 let baseline_released = Arc::new(AtomicBool::new(false));
2206
2207 let reader_calls = Arc::clone(&call_count);
2208 let reader_release = Arc::clone(&baseline_release_rx);
2209 let reader = Arc::new(move || {
2210 let mut calls = reader_calls.lock().expect("call count");
2211 *calls += 1;
2212 let current = *calls;
2213 drop(calls);
2214
2215 if current == 2 {
2216 baseline_entered_tx.send(()).expect("baseline entered");
2217 reader_release
2218 .lock()
2219 .expect("baseline release")
2220 .recv()
2221 .expect("release baseline");
2222 return Some(100);
2223 }
2224
2225 Some(120)
2226 });
2227
2228 let released = Arc::clone(&baseline_released);
2229 let release_handle = thread::spawn(move || {
2230 baseline_entered_rx.recv().expect("baseline read started");
2231 thread::sleep(Duration::from_millis(20));
2232 released.store(true, Ordering::SeqCst);
2233 baseline_release_tx.send(()).expect("release baseline");
2234 });
2235
2236 let sampler = PersistentMemorySampler::start_with_reader(reader).expect("sampler");
2237 assert!(sampler.begin_window());
2238 assert!(
2239 baseline_released.load(Ordering::SeqCst),
2240 "begin_window returned before the baseline sample completed"
2241 );
2242 release_handle.join().expect("join release thread");
2243
2244 let peak = sampler.end_window().expect("peak memory");
2245 assert_eq!(peak.growth_kb, 20);
2246 assert_eq!(peak.process_peak_kb, 120);
2247 }
2248
2249 #[test]
2250 #[cfg(not(target_arch = "wasm32"))]
2251 fn persistent_memory_sampler_supports_multiple_windows() {
2252 use std::collections::VecDeque;
2253 use std::sync::{Arc, Mutex};
2254
2255 let samples = Arc::new(Mutex::new(VecDeque::from([
2258 Some(50_u64), Some(200_u64), Some(260_u64), Some(190_u64), Some(250_u64), ])));
2264 let reader_samples = Arc::clone(&samples);
2265 let reader = Arc::new(move || {
2266 reader_samples
2267 .lock()
2268 .expect("sample queue")
2269 .pop_front()
2270 .unwrap_or(Some(0))
2271 });
2272
2273 let sampler = PersistentMemorySampler::start_with_reader(reader).expect("sampler");
2274
2275 assert!(sampler.begin_window());
2276 let first = sampler.end_window().expect("first peak");
2277 assert_eq!(first.process_peak_kb, 260);
2278 assert_eq!(first.growth_kb, 60);
2279
2280 assert!(sampler.begin_window());
2281 let second = sampler.end_window().expect("second peak");
2282 assert_eq!(second.process_peak_kb, 250);
2283 assert_eq!(second.growth_kb, 60);
2284 }
2285
2286 #[test]
2287 fn bench_report_deserializes_legacy_payload_without_phases_or_timeline() {
2288 let legacy = r#"{
2292 "spec": { "name": "legacy", "iterations": 2, "warmup": 0 },
2293 "samples": [
2294 { "duration_ns": 100 },
2295 { "duration_ns": 200 }
2296 ]
2297 }"#;
2298
2299 let report: BenchReport = serde_json::from_str(legacy).expect("legacy report parses");
2300 assert_eq!(report.samples.len(), 2);
2301 assert!(report.phases.is_empty());
2302 assert!(report.timeline.is_empty());
2303 assert!(report.samples[0].cpu_time_ms.is_none());
2304 assert!(report.samples[0].peak_memory_kb.is_none());
2305 assert!(report.samples[0].process_peak_memory_kb.is_none());
2306
2307 let json = serde_json::to_string(&report).expect("serialize");
2310 assert!(!json.contains("\"phases\""));
2311 assert!(!json.contains("\"timeline\""));
2312 }
2313
2314 #[test]
2315 fn run_with_setup_calls_setup_once() {
2316 use std::sync::atomic::{AtomicU32, Ordering};
2317
2318 static SETUP_COUNT: AtomicU32 = AtomicU32::new(0);
2319 static RUN_COUNT: AtomicU32 = AtomicU32::new(0);
2320
2321 let spec = BenchSpec::new("test", 5, 2).unwrap();
2322 let report = run_closure_with_setup(
2323 spec,
2324 || {
2325 SETUP_COUNT.fetch_add(1, Ordering::SeqCst);
2326 vec![1, 2, 3]
2327 },
2328 |data| {
2329 RUN_COUNT.fetch_add(1, Ordering::SeqCst);
2330 std::hint::black_box(data.len());
2331 Ok(())
2332 },
2333 )
2334 .unwrap();
2335
2336 assert_eq!(SETUP_COUNT.load(Ordering::SeqCst), 1); assert_eq!(RUN_COUNT.load(Ordering::SeqCst), 7); assert_eq!(report.samples.len(), 5);
2339 }
2340
2341 #[test]
2342 fn run_with_setup_per_iter_calls_setup_each_time() {
2343 use std::sync::atomic::{AtomicU32, Ordering};
2344
2345 static SETUP_COUNT: AtomicU32 = AtomicU32::new(0);
2346
2347 let spec = BenchSpec::new("test", 3, 1).unwrap();
2348 let report = run_closure_with_setup_per_iter(
2349 spec,
2350 || {
2351 SETUP_COUNT.fetch_add(1, Ordering::SeqCst);
2352 vec![1, 2, 3]
2353 },
2354 |data| {
2355 std::hint::black_box(data);
2356 Ok(())
2357 },
2358 )
2359 .unwrap();
2360
2361 assert_eq!(SETUP_COUNT.load(Ordering::SeqCst), 4); assert_eq!(report.samples.len(), 3);
2363 }
2364
2365 #[test]
2366 fn run_with_setup_teardown_calls_both() {
2367 use std::sync::atomic::{AtomicU32, Ordering};
2368
2369 static SETUP_COUNT: AtomicU32 = AtomicU32::new(0);
2370 static TEARDOWN_COUNT: AtomicU32 = AtomicU32::new(0);
2371
2372 let spec = BenchSpec::new("test", 3, 1).unwrap();
2373 let report = run_closure_with_setup_teardown(
2374 spec,
2375 || {
2376 SETUP_COUNT.fetch_add(1, Ordering::SeqCst);
2377 "resource"
2378 },
2379 |_resource| Ok(()),
2380 |_resource| {
2381 TEARDOWN_COUNT.fetch_add(1, Ordering::SeqCst);
2382 },
2383 )
2384 .unwrap();
2385
2386 assert_eq!(SETUP_COUNT.load(Ordering::SeqCst), 1);
2387 assert_eq!(TEARDOWN_COUNT.load(Ordering::SeqCst), 1);
2388 assert_eq!(report.samples.len(), 3);
2389 }
2390
2391 #[test]
2392 fn bench_report_serializes_exact_harness_timeline() {
2393 let spec = BenchSpec::new("timeline", 2, 1).unwrap();
2394 let report = run_closure_with_setup_teardown(
2395 spec,
2396 || {
2397 std::thread::sleep(Duration::from_millis(1));
2398 "resource"
2399 },
2400 |_resource| {
2401 std::thread::sleep(Duration::from_millis(1));
2402 Ok(())
2403 },
2404 |_resource| {
2405 std::thread::sleep(Duration::from_millis(1));
2406 },
2407 )
2408 .unwrap();
2409
2410 let json = serde_json::to_value(&report).unwrap();
2411 assert_eq!(json["timeline"][0]["phase"], "setup");
2412 assert_eq!(json["timeline"][1]["phase"], "warmup-benchmark");
2413 assert_eq!(json["timeline"][2]["phase"], "measured-benchmark");
2414 assert_eq!(json["timeline"][3]["phase"], "measured-benchmark");
2415 assert_eq!(json["timeline"][4]["phase"], "teardown");
2416 }
2417}