1use serde::{Deserialize, Serialize};
66use std::cell::RefCell;
67#[cfg(not(target_arch = "wasm32"))]
68use std::sync::{Arc, mpsc};
69#[cfg(not(target_arch = "wasm32"))]
70use std::thread::{self, JoinHandle};
71use std::time::Duration;
72#[cfg(not(target_arch = "wasm32"))]
73use std::time::Instant;
74use thiserror::Error;
75
76#[cfg(target_arch = "wasm32")]
77mod browser_time {
78 use std::time::Duration;
79 use wasm_bindgen::prelude::*;
80
81 #[wasm_bindgen]
82 extern "C" {
83 #[wasm_bindgen(js_namespace = performance, js_name = now)]
84 fn performance_now_ms() -> f64;
85 }
86
87 #[derive(Clone, Copy)]
92 pub(super) struct Instant(f64);
93
94 impl Instant {
95 pub(super) fn now() -> Self {
96 Self(performance_now_ms())
97 }
98
99 pub(super) fn duration_since(self, earlier: Self) -> Duration {
100 Duration::from_secs_f64(((self.0 - earlier.0).max(0.0)) / 1_000.0)
101 }
102
103 pub(super) fn elapsed(self) -> Duration {
104 Self::now().duration_since(self)
105 }
106 }
107}
108
109#[cfg(target_arch = "wasm32")]
110use browser_time::Instant;
111
112#[derive(Clone, Debug, Serialize, Deserialize)]
151pub struct BenchSpec {
152 pub name: String,
156
157 pub iterations: u32,
161
162 pub warmup: u32,
167}
168
169impl BenchSpec {
170 pub fn new(name: impl Into<String>, iterations: u32, warmup: u32) -> Result<Self, TimingError> {
196 if iterations == 0 {
197 return Err(TimingError::NoIterations { count: iterations });
198 }
199
200 Ok(Self {
201 name: name.into(),
202 iterations,
203 warmup,
204 })
205 }
206}
207
208#[derive(Clone, Debug, Default, Serialize, Deserialize)]
230pub struct BenchSample {
231 pub duration_ns: u64,
235
236 #[serde(default, skip_serializing_if = "Option::is_none")]
241 pub cpu_time_ms: Option<u64>,
242
243 #[serde(default, skip_serializing_if = "Option::is_none")]
249 pub peak_memory_kb: Option<u64>,
250
251 #[serde(default, skip_serializing_if = "Option::is_none")]
256 pub process_peak_memory_kb: Option<u64>,
257}
258
259impl BenchSample {
260 fn from_measurement(duration: Duration, resources: IterationResourceUsage) -> Self {
261 Self {
262 duration_ns: duration.as_nanos() as u64,
263 cpu_time_ms: resources.cpu_time_ms,
264 peak_memory_kb: resources.peak_memory_kb,
265 process_peak_memory_kb: resources.process_peak_memory_kb,
266 }
267 }
268}
269
270#[derive(Clone, Debug, Serialize, Deserialize)]
299pub struct BenchReport {
300 pub spec: BenchSpec,
302
303 pub samples: Vec<BenchSample>,
307
308 #[serde(default, skip_serializing_if = "Vec::is_empty")]
313 pub phases: Vec<SemanticPhase>,
314
315 #[serde(default, skip_serializing_if = "Vec::is_empty")]
320 pub timeline: Vec<HarnessTimelineSpan>,
321}
322
323#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
324pub struct HarnessTimelineSpan {
325 pub phase: String,
326 pub start_offset_ns: u64,
327 pub end_offset_ns: u64,
328 pub iteration: Option<u32>,
329}
330
331impl BenchReport {
332 #[must_use]
334 pub fn mean_ns(&self) -> f64 {
335 if self.samples.is_empty() {
336 return 0.0;
337 }
338 let sum: u64 = self.samples.iter().map(|s| s.duration_ns).sum();
339 sum as f64 / self.samples.len() as f64
340 }
341
342 #[must_use]
344 pub fn median_ns(&self) -> f64 {
345 if self.samples.is_empty() {
346 return 0.0;
347 }
348 let mut sorted: Vec<u64> = self.samples.iter().map(|s| s.duration_ns).collect();
349 sorted.sort_unstable();
350 let len = sorted.len();
351 if len % 2 == 0 {
352 (sorted[len / 2 - 1] + sorted[len / 2]) as f64 / 2.0
353 } else {
354 sorted[len / 2] as f64
355 }
356 }
357
358 #[must_use]
360 pub fn std_dev_ns(&self) -> f64 {
361 if self.samples.len() < 2 {
362 return 0.0;
363 }
364 let mean = self.mean_ns();
365 let variance: f64 = self
366 .samples
367 .iter()
368 .map(|s| {
369 let diff = s.duration_ns as f64 - mean;
370 diff * diff
371 })
372 .sum::<f64>()
373 / (self.samples.len() - 1) as f64;
374 variance.sqrt()
375 }
376
377 #[must_use]
379 pub fn percentile_ns(&self, p: f64) -> f64 {
380 if self.samples.is_empty() {
381 return 0.0;
382 }
383 let mut sorted: Vec<u64> = self.samples.iter().map(|s| s.duration_ns).collect();
384 sorted.sort_unstable();
385 let p = p.clamp(0.0, 100.0) / 100.0;
386 let index = (p * (sorted.len() - 1) as f64).round() as usize;
387 sorted[index.min(sorted.len() - 1)] as f64
388 }
389
390 #[must_use]
392 pub fn min_ns(&self) -> u64 {
393 self.samples
394 .iter()
395 .map(|s| s.duration_ns)
396 .min()
397 .unwrap_or(0)
398 }
399
400 #[must_use]
402 pub fn max_ns(&self) -> u64 {
403 self.samples
404 .iter()
405 .map(|s| s.duration_ns)
406 .max()
407 .unwrap_or(0)
408 }
409
410 #[must_use]
412 pub fn cpu_total_ms(&self) -> Option<u64> {
413 let values = self
414 .samples
415 .iter()
416 .filter_map(|sample| sample.cpu_time_ms)
417 .collect::<Vec<_>>();
418 if values.is_empty() {
419 return None;
420 }
421
422 let total = values
423 .iter()
424 .fold(0_u128, |sum, value| sum.saturating_add(u128::from(*value)));
425 Some(total.min(u128::from(u64::MAX)) as u64)
426 }
427
428 #[must_use]
430 pub fn cpu_median_ms(&self) -> Option<u64> {
431 let mut values = self
432 .samples
433 .iter()
434 .filter_map(|sample| sample.cpu_time_ms)
435 .collect::<Vec<_>>();
436 if values.is_empty() {
437 return None;
438 }
439
440 values.sort_unstable();
441 let len = values.len();
442 Some(if len % 2 == 0 {
443 let lower = u128::from(values[(len / 2) - 1]);
444 let upper = u128::from(values[len / 2]);
445 ((lower + upper) / 2) as u64
446 } else {
447 values[len / 2]
448 })
449 }
450
451 #[must_use]
456 pub fn peak_memory_kb(&self) -> Option<u64> {
457 self.samples
458 .iter()
459 .filter_map(|sample| sample.peak_memory_kb)
460 .max()
461 }
462
463 #[must_use]
468 #[doc(alias = "peak_memory_kb")]
469 pub fn peak_memory_growth_kb(&self) -> Option<u64> {
470 self.peak_memory_kb()
471 }
472
473 #[must_use]
478 pub fn process_peak_memory_kb(&self) -> Option<u64> {
479 self.samples
480 .iter()
481 .filter_map(|sample| sample.process_peak_memory_kb)
482 .max()
483 }
484
485 #[must_use]
487 pub fn summary(&self) -> BenchSummary {
488 BenchSummary {
489 name: self.spec.name.clone(),
490 iterations: self.samples.len() as u32,
491 warmup: self.spec.warmup,
492 mean_ns: self.mean_ns(),
493 median_ns: self.median_ns(),
494 std_dev_ns: self.std_dev_ns(),
495 min_ns: self.min_ns(),
496 max_ns: self.max_ns(),
497 p95_ns: self.percentile_ns(95.0),
498 p99_ns: self.percentile_ns(99.0),
499 }
500 }
501}
502
503#[derive(Clone, Debug, Default)]
504struct IterationResourceUsage {
505 cpu_time_ms: Option<u64>,
506 peak_memory_kb: Option<u64>,
507 process_peak_memory_kb: Option<u64>,
508}
509
510fn instant_offset_ns(origin: Instant, instant: Instant) -> u64 {
511 instant
512 .duration_since(origin)
513 .as_nanos()
514 .min(u128::from(u64::MAX)) as u64
515}
516
517fn push_timeline_span(
518 timeline: &mut Vec<HarnessTimelineSpan>,
519 origin: Instant,
520 phase: &str,
521 started_at: Instant,
522 ended_at: Instant,
523 iteration: Option<u32>,
524) {
525 timeline.push(HarnessTimelineSpan {
526 phase: phase.to_string(),
527 start_offset_ns: instant_offset_ns(origin, started_at),
528 end_offset_ns: instant_offset_ns(origin, ended_at),
529 iteration,
530 });
531}
532
533#[derive(Clone, Debug, Serialize, Deserialize)]
535pub struct BenchSummary {
536 pub name: String,
538 pub iterations: u32,
540 pub warmup: u32,
542 pub mean_ns: f64,
544 pub median_ns: f64,
546 pub std_dev_ns: f64,
548 pub min_ns: u64,
550 pub max_ns: u64,
552 pub p95_ns: f64,
554 pub p99_ns: f64,
556}
557
558#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
560pub struct SemanticPhase {
561 pub name: String,
562 pub duration_ns: u64,
563}
564
565#[derive(Default)]
566struct SemanticPhaseCollector {
567 enabled: bool,
568 depth: usize,
569 phases: Vec<SemanticPhase>,
570}
571
572impl SemanticPhaseCollector {
573 fn reset(&mut self) {
574 self.enabled = false;
575 self.depth = 0;
576 self.phases.clear();
577 }
578
579 fn begin_measurement(&mut self) {
580 self.reset();
581 self.enabled = true;
582 }
583
584 fn finish(&mut self) -> Vec<SemanticPhase> {
585 self.enabled = false;
586 self.depth = 0;
587 std::mem::take(&mut self.phases)
588 }
589
590 fn enter_phase(&mut self) -> Option<bool> {
591 if !self.enabled {
592 return None;
593 }
594 let top_level = self.depth == 0;
595 self.depth += 1;
596 Some(top_level)
597 }
598
599 fn exit_phase(&mut self, name: &str, top_level: bool, elapsed: Duration) {
600 self.depth = self.depth.saturating_sub(1);
601 if !self.enabled || !top_level {
602 return;
603 }
604
605 let duration_ns = elapsed.as_nanos().min(u128::from(u64::MAX)) as u64;
606 if let Some(phase) = self.phases.iter_mut().find(|phase| phase.name == name) {
607 phase.duration_ns = phase.duration_ns.saturating_add(duration_ns);
608 } else {
609 self.phases.push(SemanticPhase {
610 name: name.to_string(),
611 duration_ns,
612 });
613 }
614 }
615}
616
617thread_local! {
618 static SEMANTIC_PHASE_COLLECTOR: RefCell<SemanticPhaseCollector> =
619 RefCell::new(SemanticPhaseCollector::default());
620}
621
622struct SemanticPhaseGuard {
623 name: String,
624 started_at: Option<Instant>,
625 top_level: bool,
626}
627
628impl Drop for SemanticPhaseGuard {
629 fn drop(&mut self) {
630 let Some(started_at) = self.started_at else {
631 return;
632 };
633
634 let elapsed = started_at.elapsed();
635 SEMANTIC_PHASE_COLLECTOR.with(|collector| {
636 collector
637 .borrow_mut()
638 .exit_phase(&self.name, self.top_level, elapsed);
639 });
640 }
641}
642
643fn reset_semantic_phase_collection() {
644 SEMANTIC_PHASE_COLLECTOR.with(|collector| collector.borrow_mut().reset());
645}
646
647fn begin_semantic_phase_collection() {
648 SEMANTIC_PHASE_COLLECTOR.with(|collector| collector.borrow_mut().begin_measurement());
649}
650
651fn finish_semantic_phase_collection() -> Vec<SemanticPhase> {
652 SEMANTIC_PHASE_COLLECTOR.with(|collector| collector.borrow_mut().finish())
653}
654
655trait ResourceMonitor {
656 type Token;
657
658 fn start(&mut self) -> Self::Token;
659
660 fn finish(&mut self, token: Self::Token) -> IterationResourceUsage;
661}
662
663#[derive(Default)]
664struct DefaultResourceMonitor {
665 #[cfg(not(target_arch = "wasm32"))]
673 memory_sampler: Option<PersistentMemorySampler>,
674 #[cfg(not(target_arch = "wasm32"))]
677 sampler_init_attempted: bool,
678}
679
680#[derive(Clone, Copy, Debug, PartialEq, Eq)]
681struct ProcessCpuTimeSnapshot {
682 user_ns: u64,
683 system_ns: u64,
684}
685
686impl ProcessCpuTimeSnapshot {
687 #[cfg(unix)]
688 fn from_rusage_timevals(user: libc::timeval, system: libc::timeval) -> Option<Self> {
689 Some(Self {
690 user_ns: timeval_to_ns(user)?,
691 system_ns: timeval_to_ns(system)?,
692 })
693 }
694
695 #[cfg(unix)]
696 fn total_ns(self) -> u64 {
697 self.user_ns.saturating_add(self.system_ns)
698 }
699}
700
701struct DefaultResourceToken {
702 cpu_time_start: Option<ProcessCpuTimeSnapshot>,
703 #[cfg(not(target_arch = "wasm32"))]
706 has_memory_window: bool,
707}
708
709impl ResourceMonitor for DefaultResourceMonitor {
710 type Token = DefaultResourceToken;
711
712 fn start(&mut self) -> Self::Token {
713 #[cfg(not(target_arch = "wasm32"))]
714 {
715 if !self.sampler_init_attempted {
716 self.memory_sampler = PersistentMemorySampler::start();
717 self.sampler_init_attempted = true;
718 }
719 let has_memory_window = self
720 .memory_sampler
721 .as_ref()
722 .is_some_and(PersistentMemorySampler::begin_window);
723 Self::Token {
724 cpu_time_start: current_process_cpu_time(),
725 has_memory_window,
726 }
727 }
728
729 #[cfg(target_arch = "wasm32")]
730 Self::Token {
731 cpu_time_start: None,
732 }
733 }
734
735 fn finish(&mut self, token: Self::Token) -> IterationResourceUsage {
736 let cpu_time_ms = token
737 .cpu_time_start
738 .zip(current_process_cpu_time())
739 .and_then(|(start, end)| process_cpu_delta_ms(start, end));
740
741 #[cfg(not(target_arch = "wasm32"))]
742 let memory_peak = if token.has_memory_window {
743 self.memory_sampler
744 .as_ref()
745 .and_then(PersistentMemorySampler::end_window)
746 } else {
747 None
748 };
749
750 #[cfg(not(target_arch = "wasm32"))]
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 if spec.iterations == 0 {
1202 return Err(TimingError::NoIterations {
1203 count: spec.iterations,
1204 });
1205 }
1206
1207 reset_semantic_phase_collection();
1208 let harness_origin = Instant::now();
1209 let mut timeline = Vec::new();
1210
1211 for iteration in 0..spec.warmup {
1213 let phase_start = Instant::now();
1214 f()?;
1215 push_timeline_span(
1216 &mut timeline,
1217 harness_origin,
1218 "warmup-benchmark",
1219 phase_start,
1220 Instant::now(),
1221 Some(iteration),
1222 );
1223 }
1224
1225 begin_semantic_phase_collection();
1227 let mut samples = Vec::with_capacity(spec.iterations as usize);
1228 for iteration in 0..spec.iterations {
1229 let (sample, start, end) = match measure_iteration(monitor, &mut f) {
1230 Ok(measurement) => measurement,
1231 Err(err) => {
1232 let _ = finish_semantic_phase_collection();
1233 return Err(err);
1234 }
1235 };
1236 samples.push(sample);
1237 push_timeline_span(
1238 &mut timeline,
1239 harness_origin,
1240 "measured-benchmark",
1241 start,
1242 end,
1243 Some(iteration),
1244 );
1245 }
1246 let phases = finish_semantic_phase_collection();
1247
1248 Ok(BenchReport {
1249 spec,
1250 samples,
1251 phases,
1252 timeline,
1253 })
1254}
1255
1256pub fn run_closure_with_setup<S, T, F>(
1284 spec: BenchSpec,
1285 setup: S,
1286 mut f: F,
1287) -> Result<BenchReport, TimingError>
1288where
1289 S: FnOnce() -> T,
1290 F: FnMut(&T) -> Result<(), TimingError>,
1291{
1292 let mut monitor = DefaultResourceMonitor::default();
1293 run_closure_with_setup_with_monitor(spec, &mut monitor, setup, move |input| f(input))
1294}
1295
1296fn run_closure_with_setup_with_monitor<S, T, F, M>(
1297 spec: BenchSpec,
1298 monitor: &mut M,
1299 setup: S,
1300 mut f: F,
1301) -> Result<BenchReport, TimingError>
1302where
1303 S: FnOnce() -> T,
1304 F: FnMut(&T) -> Result<(), TimingError>,
1305 M: ResourceMonitor,
1306{
1307 if spec.iterations == 0 {
1308 return Err(TimingError::NoIterations {
1309 count: spec.iterations,
1310 });
1311 }
1312
1313 reset_semantic_phase_collection();
1314 let harness_origin = Instant::now();
1315 let mut timeline = Vec::new();
1316
1317 let setup_start = Instant::now();
1319 let input = setup();
1320 push_timeline_span(
1321 &mut timeline,
1322 harness_origin,
1323 "setup",
1324 setup_start,
1325 Instant::now(),
1326 None,
1327 );
1328
1329 for iteration in 0..spec.warmup {
1331 let phase_start = Instant::now();
1332 f(&input)?;
1333 push_timeline_span(
1334 &mut timeline,
1335 harness_origin,
1336 "warmup-benchmark",
1337 phase_start,
1338 Instant::now(),
1339 Some(iteration),
1340 );
1341 }
1342
1343 begin_semantic_phase_collection();
1345 let mut samples = Vec::with_capacity(spec.iterations as usize);
1346 for iteration in 0..spec.iterations {
1347 let (sample, start, end) = match measure_iteration(monitor, || f(&input)) {
1348 Ok(measurement) => measurement,
1349 Err(err) => {
1350 let _ = finish_semantic_phase_collection();
1351 return Err(err);
1352 }
1353 };
1354 samples.push(sample);
1355 push_timeline_span(
1356 &mut timeline,
1357 harness_origin,
1358 "measured-benchmark",
1359 start,
1360 end,
1361 Some(iteration),
1362 );
1363 }
1364 let phases = finish_semantic_phase_collection();
1365
1366 Ok(BenchReport {
1367 spec,
1368 samples,
1369 phases,
1370 timeline,
1371 })
1372}
1373
1374pub fn run_closure_with_setup_per_iter<S, T, F>(
1403 spec: BenchSpec,
1404 setup: S,
1405 f: F,
1406) -> Result<BenchReport, TimingError>
1407where
1408 S: FnMut() -> T,
1409 F: FnMut(T) -> Result<(), TimingError>,
1410{
1411 let mut monitor = DefaultResourceMonitor::default();
1412 run_closure_with_setup_per_iter_with_monitor(spec, &mut monitor, setup, f)
1413}
1414
1415fn run_closure_with_setup_per_iter_with_monitor<S, T, F, M>(
1416 spec: BenchSpec,
1417 monitor: &mut M,
1418 mut setup: S,
1419 mut f: F,
1420) -> Result<BenchReport, TimingError>
1421where
1422 S: FnMut() -> T,
1423 F: FnMut(T) -> Result<(), TimingError>,
1424 M: ResourceMonitor,
1425{
1426 if spec.iterations == 0 {
1427 return Err(TimingError::NoIterations {
1428 count: spec.iterations,
1429 });
1430 }
1431
1432 reset_semantic_phase_collection();
1433 let harness_origin = Instant::now();
1434 let mut timeline = Vec::new();
1435
1436 for iteration in 0..spec.warmup {
1438 let setup_start = Instant::now();
1439 let input = setup();
1440 push_timeline_span(
1441 &mut timeline,
1442 harness_origin,
1443 "fixture-setup",
1444 setup_start,
1445 Instant::now(),
1446 Some(iteration),
1447 );
1448 let phase_start = Instant::now();
1449 f(input)?;
1450 push_timeline_span(
1451 &mut timeline,
1452 harness_origin,
1453 "warmup-benchmark",
1454 phase_start,
1455 Instant::now(),
1456 Some(iteration),
1457 );
1458 }
1459
1460 begin_semantic_phase_collection();
1462 let mut samples = Vec::with_capacity(spec.iterations as usize);
1463 for iteration in 0..spec.iterations {
1464 let setup_start = Instant::now();
1465 let input = setup(); push_timeline_span(
1467 &mut timeline,
1468 harness_origin,
1469 "fixture-setup",
1470 setup_start,
1471 Instant::now(),
1472 Some(iteration),
1473 );
1474
1475 let (sample, start, end) = match measure_iteration(monitor, || f(input)) {
1476 Ok(measurement) => measurement,
1477 Err(err) => {
1478 let _ = finish_semantic_phase_collection();
1479 return Err(err);
1480 }
1481 };
1482 samples.push(sample);
1483 push_timeline_span(
1484 &mut timeline,
1485 harness_origin,
1486 "measured-benchmark",
1487 start,
1488 end,
1489 Some(iteration),
1490 );
1491 }
1492 let phases = finish_semantic_phase_collection();
1493
1494 Ok(BenchReport {
1495 spec,
1496 samples,
1497 phases,
1498 timeline,
1499 })
1500}
1501
1502pub fn run_closure_with_setup_teardown<S, T, F, D>(
1531 spec: BenchSpec,
1532 setup: S,
1533 mut f: F,
1534 teardown: D,
1535) -> Result<BenchReport, TimingError>
1536where
1537 S: FnOnce() -> T,
1538 F: FnMut(&T) -> Result<(), TimingError>,
1539 D: FnOnce(T),
1540{
1541 let mut monitor = DefaultResourceMonitor::default();
1542 run_closure_with_setup_teardown_with_monitor(
1543 spec,
1544 &mut monitor,
1545 setup,
1546 move |input| f(input),
1547 teardown,
1548 )
1549}
1550
1551fn run_closure_with_setup_teardown_with_monitor<S, T, F, D, M>(
1552 spec: BenchSpec,
1553 monitor: &mut M,
1554 setup: S,
1555 mut f: F,
1556 teardown: D,
1557) -> Result<BenchReport, TimingError>
1558where
1559 S: FnOnce() -> T,
1560 F: FnMut(&T) -> Result<(), TimingError>,
1561 D: FnOnce(T),
1562 M: ResourceMonitor,
1563{
1564 if spec.iterations == 0 {
1565 return Err(TimingError::NoIterations {
1566 count: spec.iterations,
1567 });
1568 }
1569
1570 reset_semantic_phase_collection();
1571 let harness_origin = Instant::now();
1572 let mut timeline = Vec::new();
1573
1574 let setup_start = Instant::now();
1576 let input = setup();
1577 push_timeline_span(
1578 &mut timeline,
1579 harness_origin,
1580 "setup",
1581 setup_start,
1582 Instant::now(),
1583 None,
1584 );
1585
1586 let result = (|| {
1587 for iteration in 0..spec.warmup {
1589 let phase_start = Instant::now();
1590 f(&input)?;
1591 push_timeline_span(
1592 &mut timeline,
1593 harness_origin,
1594 "warmup-benchmark",
1595 phase_start,
1596 Instant::now(),
1597 Some(iteration),
1598 );
1599 }
1600
1601 begin_semantic_phase_collection();
1603 let mut samples = Vec::with_capacity(spec.iterations as usize);
1604 for iteration in 0..spec.iterations {
1605 let (sample, start, end) = match measure_iteration(monitor, || f(&input)) {
1606 Ok(measurement) => measurement,
1607 Err(err) => {
1608 let _ = finish_semantic_phase_collection();
1609 return Err(err);
1610 }
1611 };
1612 samples.push(sample);
1613 push_timeline_span(
1614 &mut timeline,
1615 harness_origin,
1616 "measured-benchmark",
1617 start,
1618 end,
1619 Some(iteration),
1620 );
1621 }
1622 let phases = finish_semantic_phase_collection();
1623
1624 Ok((samples, phases))
1625 })();
1626
1627 let teardown_start = Instant::now();
1629 teardown(input);
1630 push_timeline_span(
1631 &mut timeline,
1632 harness_origin,
1633 "teardown",
1634 teardown_start,
1635 Instant::now(),
1636 None,
1637 );
1638
1639 let (samples, phases) = result?;
1640 Ok(BenchReport {
1641 spec,
1642 samples,
1643 phases,
1644 timeline,
1645 })
1646}
1647
1648#[cfg(test)]
1649mod tests {
1650 use super::*;
1651
1652 #[derive(Default)]
1653 struct FakeResourceMonitor {
1654 samples: Vec<IterationResourceUsage>,
1655 started: usize,
1656 finished: usize,
1657 }
1658
1659 impl FakeResourceMonitor {
1660 fn new(samples: Vec<IterationResourceUsage>) -> Self {
1661 Self {
1662 samples,
1663 started: 0,
1664 finished: 0,
1665 }
1666 }
1667 }
1668
1669 impl ResourceMonitor for FakeResourceMonitor {
1670 type Token = usize;
1671
1672 fn start(&mut self) -> Self::Token {
1673 let token = self.started;
1674 self.started += 1;
1675 assert!(
1676 token < self.samples.len(),
1677 "resource capture should only run for measured iterations"
1678 );
1679 token
1680 }
1681
1682 fn finish(&mut self, token: Self::Token) -> IterationResourceUsage {
1683 self.finished += 1;
1684 self.samples
1685 .get(token)
1686 .cloned()
1687 .expect("resource usage for measured iteration")
1688 }
1689 }
1690
1691 #[cfg(unix)]
1692 #[test]
1693 fn process_cpu_time_snapshot_sums_user_and_kernel_time() {
1694 let snapshot = ProcessCpuTimeSnapshot::from_rusage_timevals(
1695 libc::timeval {
1696 tv_sec: 1,
1697 tv_usec: 250_000,
1698 },
1699 libc::timeval {
1700 tv_sec: 0,
1701 tv_usec: 750_000,
1702 },
1703 )
1704 .expect("valid snapshot");
1705
1706 assert_eq!(snapshot.total_ns(), 2_000_000_000);
1707 }
1708
1709 #[cfg(unix)]
1710 #[test]
1711 fn process_cpu_time_delta_ms_uses_user_and_kernel_time() {
1712 let start = ProcessCpuTimeSnapshot::from_rusage_timevals(
1713 libc::timeval {
1714 tv_sec: 1,
1715 tv_usec: 250_000,
1716 },
1717 libc::timeval {
1718 tv_sec: 0,
1719 tv_usec: 750_000,
1720 },
1721 )
1722 .expect("valid start snapshot");
1723 let end = ProcessCpuTimeSnapshot::from_rusage_timevals(
1724 libc::timeval {
1725 tv_sec: 1,
1726 tv_usec: 900_000,
1727 },
1728 libc::timeval {
1729 tv_sec: 1,
1730 tv_usec: 400_600,
1731 },
1732 )
1733 .expect("valid end snapshot");
1734
1735 assert_eq!(process_cpu_delta_ms(start, end), Some(1_301));
1736 }
1737
1738 #[test]
1739 fn runs_benchmark_collects_requested_samples() {
1740 let spec = BenchSpec::new("noop", 3, 1).unwrap();
1741 let report = run_closure(spec, || Ok(())).unwrap();
1742
1743 assert_eq!(report.samples.len(), 3);
1744 assert_eq!(report.spec.name, "noop");
1745 assert_eq!(report.spec.iterations, 3);
1746 }
1747
1748 #[test]
1749 fn rejects_zero_iterations() {
1750 let result = BenchSpec::new("test", 0, 10);
1751 assert!(matches!(
1752 result,
1753 Err(TimingError::NoIterations { count: 0 })
1754 ));
1755 }
1756
1757 #[test]
1758 fn allows_zero_warmup() {
1759 let spec = BenchSpec::new("test", 5, 0).unwrap();
1760 assert_eq!(spec.warmup, 0);
1761
1762 let report = run_closure(spec, || Ok(())).unwrap();
1763 assert_eq!(report.samples.len(), 5);
1764 }
1765
1766 #[test]
1767 fn serializes_to_json() {
1768 let report = BenchReport {
1769 spec: BenchSpec::new("test", 10, 2).unwrap(),
1770 samples: vec![BenchSample {
1771 duration_ns: 1_000_000,
1772 cpu_time_ms: Some(42),
1773 peak_memory_kb: Some(512),
1774 process_peak_memory_kb: Some(1536),
1775 }],
1776 phases: vec![SemanticPhase {
1777 name: "prove".to_string(),
1778 duration_ns: 1_000_000,
1779 }],
1780 timeline: vec![HarnessTimelineSpan {
1781 phase: "measured-benchmark".to_string(),
1782 start_offset_ns: 0,
1783 end_offset_ns: 1_000_000,
1784 iteration: Some(0),
1785 }],
1786 };
1787
1788 let json = serde_json::to_string(&report).unwrap();
1789 assert!(json.contains("\"peak_memory_kb\""));
1790 assert!(json.contains("\"process_peak_memory_kb\""));
1791 assert!(!json.contains("peak_memory_growth_kb"));
1792 let restored: BenchReport = serde_json::from_str(&json).unwrap();
1793
1794 assert_eq!(restored.spec.name, "test");
1795 assert_eq!(restored.samples.len(), 1);
1796 assert_eq!(restored.samples[0].cpu_time_ms, Some(42));
1797 assert_eq!(restored.samples[0].peak_memory_kb, Some(512));
1798 assert_eq!(restored.samples[0].process_peak_memory_kb, Some(1536));
1799 assert_eq!(restored.phases.len(), 1);
1800 assert_eq!(restored.phases[0].name, "prove");
1801 assert!(restored.phases[0].duration_ns > 0);
1802 }
1803
1804 #[test]
1805 fn profile_phase_records_only_measured_iterations() {
1806 let spec = BenchSpec::new("semantic", 2, 1).unwrap();
1807 let mut call_index = 0u32;
1808 let report = run_closure(spec, || {
1809 let phase_name = if call_index == 0 {
1810 "warmup-only"
1811 } else {
1812 "prove"
1813 };
1814 call_index += 1;
1815 profile_phase(phase_name, || std::thread::sleep(Duration::from_millis(1)));
1816 Ok(())
1817 })
1818 .unwrap();
1819
1820 assert!(
1821 !report
1822 .phases
1823 .iter()
1824 .any(|phase| phase.name == "warmup-only"),
1825 "warmup phases should not be recorded"
1826 );
1827 let prove = report
1828 .phases
1829 .iter()
1830 .find(|phase| phase.name == "prove")
1831 .expect("prove phase");
1832 assert!(prove.duration_ns > 0);
1833 }
1834
1835 #[test]
1836 fn profile_phase_keeps_the_v1_model_flat() {
1837 let spec = BenchSpec::new("semantic-flat", 1, 0).unwrap();
1838 let report = run_closure(spec, || {
1839 profile_phase("prove", || {
1840 std::thread::sleep(Duration::from_millis(1));
1841 profile_phase("inner", || std::thread::sleep(Duration::from_millis(1)));
1842 });
1843 Ok(())
1844 })
1845 .unwrap();
1846
1847 assert!(report.phases.iter().any(|phase| phase.name == "prove"));
1848 assert!(
1849 !report.phases.iter().any(|phase| phase.name == "inner"),
1850 "nested phases should not create a second flat phase entry"
1851 );
1852 }
1853
1854 #[test]
1855 fn measured_cpu_excludes_warmup_iterations() {
1856 let spec = BenchSpec::new("cpu", 2, 1).unwrap();
1857 let mut monitor = FakeResourceMonitor::new(vec![
1858 IterationResourceUsage {
1859 cpu_time_ms: Some(11),
1860 peak_memory_kb: Some(32),
1861 ..Default::default()
1862 },
1863 IterationResourceUsage {
1864 cpu_time_ms: Some(17),
1865 peak_memory_kb: Some(64),
1866 ..Default::default()
1867 },
1868 ]);
1869 let mut calls = 0_u32;
1870
1871 let report = run_closure_with_monitor(spec, &mut monitor, || {
1872 calls += 1;
1873 Ok(())
1874 })
1875 .unwrap();
1876
1877 assert_eq!(calls, 3);
1878 assert_eq!(monitor.started, 2);
1879 assert_eq!(monitor.finished, 2);
1880 assert_eq!(
1881 report
1882 .samples
1883 .iter()
1884 .map(|sample| sample.cpu_time_ms)
1885 .collect::<Vec<_>>(),
1886 vec![Some(11), Some(17)]
1887 );
1888 assert_eq!(report.cpu_total_ms(), Some(28));
1889 }
1890
1891 #[test]
1892 fn measured_cpu_excludes_outer_harness_and_report_overhead() {
1893 let spec = BenchSpec::new("cpu-harness", 2, 1).unwrap();
1894 let mut monitor = FakeResourceMonitor::new(vec![
1895 IterationResourceUsage {
1896 cpu_time_ms: Some(5),
1897 peak_memory_kb: Some(12),
1898 ..Default::default()
1899 },
1900 IterationResourceUsage {
1901 cpu_time_ms: Some(7),
1902 peak_memory_kb: Some(18),
1903 ..Default::default()
1904 },
1905 ]);
1906
1907 let mut setup_calls = 0_u32;
1908 let mut teardown_calls = 0_u32;
1909 let report = run_closure_with_setup_teardown_with_monitor(
1910 spec,
1911 &mut monitor,
1912 || {
1913 setup_calls += 1;
1914 vec![1_u8, 2, 3]
1915 },
1916 |_fixture| Ok(()),
1917 |_fixture| {
1918 teardown_calls += 1;
1919 },
1920 )
1921 .unwrap();
1922
1923 let _serialized = serde_json::to_string(&report).unwrap();
1924
1925 assert_eq!(setup_calls, 1);
1926 assert_eq!(teardown_calls, 1);
1927 assert_eq!(monitor.started, 2);
1928 assert_eq!(report.cpu_total_ms(), Some(12));
1929 assert_eq!(report.cpu_median_ms(), Some(6));
1930 }
1931
1932 #[test]
1933 fn setup_teardown_runs_teardown_when_warmup_fails() {
1934 let spec = BenchSpec::new("teardown-on-error", 1, 1).unwrap();
1935 let mut teardown_calls = 0_u32;
1936
1937 let result = run_closure_with_setup_teardown(
1938 spec,
1939 || vec![1_u8, 2, 3],
1940 |_fixture| Err(TimingError::Execution("warmup failed".to_string())),
1941 |_fixture| {
1942 teardown_calls += 1;
1943 },
1944 );
1945
1946 assert!(result.is_err());
1947 assert_eq!(teardown_calls, 1);
1948 }
1949
1950 #[test]
1951 fn single_iteration_cpu_median_matches_the_measured_iteration() {
1952 let spec = BenchSpec::new("single", 1, 0).unwrap();
1953 let mut monitor = FakeResourceMonitor::new(vec![IterationResourceUsage {
1954 cpu_time_ms: Some(42),
1955 peak_memory_kb: Some(24),
1956 ..Default::default()
1957 }]);
1958
1959 let report = run_closure_with_monitor(spec, &mut monitor, || Ok(())).unwrap();
1960
1961 assert_eq!(report.samples[0].cpu_time_ms, Some(42));
1962 assert_eq!(report.cpu_total_ms(), Some(42));
1963 assert_eq!(report.cpu_median_ms(), Some(42));
1964 }
1965
1966 #[test]
1967 fn multiple_iterations_export_the_median_cpu_sample() {
1968 let spec = BenchSpec::new("median", 3, 0).unwrap();
1969 let mut monitor = FakeResourceMonitor::new(vec![
1970 IterationResourceUsage {
1971 cpu_time_ms: Some(19),
1972 peak_memory_kb: Some(10),
1973 ..Default::default()
1974 },
1975 IterationResourceUsage {
1976 cpu_time_ms: Some(7),
1977 peak_memory_kb: Some(30),
1978 ..Default::default()
1979 },
1980 IterationResourceUsage {
1981 cpu_time_ms: Some(11),
1982 peak_memory_kb: Some(20),
1983 ..Default::default()
1984 },
1985 ]);
1986
1987 let report = run_closure_with_monitor(spec, &mut monitor, || Ok(())).unwrap();
1988
1989 assert_eq!(report.cpu_median_ms(), Some(11));
1990 assert_eq!(report.cpu_total_ms(), Some(37));
1991 }
1992
1993 #[test]
1994 fn peak_memory_excludes_harness_baseline_overhead() {
1995 let spec = BenchSpec::new("memory", 2, 1).unwrap();
1996 let mut monitor = FakeResourceMonitor::new(vec![
1997 IterationResourceUsage {
1998 cpu_time_ms: Some(3),
1999 peak_memory_kb: Some(48),
2000 process_peak_memory_kb: Some(1_048),
2001 },
2002 IterationResourceUsage {
2003 cpu_time_ms: Some(4),
2004 peak_memory_kb: Some(96),
2005 process_peak_memory_kb: Some(1_096),
2006 },
2007 ]);
2008
2009 let report = run_closure_with_setup_teardown_with_monitor(
2010 spec,
2011 &mut monitor,
2012 || vec![0_u8; 1024],
2013 |_fixture| Ok(()),
2014 |_fixture| {},
2015 )
2016 .unwrap();
2017
2018 assert_eq!(
2019 report
2020 .samples
2021 .iter()
2022 .map(|sample| sample.peak_memory_kb)
2023 .collect::<Vec<_>>(),
2024 vec![Some(48), Some(96)]
2025 );
2026 assert_eq!(report.peak_memory_kb(), Some(96));
2027 assert_eq!(report.peak_memory_growth_kb(), report.peak_memory_kb());
2028 assert_eq!(report.process_peak_memory_kb(), Some(1_096));
2029 }
2030
2031 #[cfg(not(target_arch = "wasm32"))]
2032 #[test]
2033 fn memory_peak_sampler_uses_the_first_post_startup_sample_as_its_baseline() {
2034 use std::collections::VecDeque;
2035 use std::sync::{Arc, Mutex};
2036
2037 let samples = Arc::new(Mutex::new(VecDeque::from([
2040 Some(80_u64),
2041 Some(100_u64),
2042 Some(140_u64),
2043 Some(120_u64),
2044 ])));
2045 let reader_samples = Arc::clone(&samples);
2046 let reader = Arc::new(move || {
2047 reader_samples
2048 .lock()
2049 .expect("sample queue")
2050 .pop_front()
2051 .unwrap_or(Some(120))
2052 });
2053
2054 let sampler = PersistentMemorySampler::start_with_reader(reader).expect("sampler");
2055 assert!(sampler.begin_window());
2056 let peak = sampler.end_window().expect("peak memory");
2057
2058 assert_eq!(
2059 peak,
2060 ProcessMemoryPeak {
2061 growth_kb: 40,
2062 process_peak_kb: 140,
2063 }
2064 );
2065 }
2066
2067 #[cfg(not(target_arch = "wasm32"))]
2068 #[test]
2069 fn persistent_memory_sampler_does_not_queue_result_when_begin_fails() {
2070 use std::collections::VecDeque;
2071 use std::sync::{Arc, Mutex};
2072
2073 let samples = Arc::new(Mutex::new(VecDeque::from([
2076 Some(80_u64),
2077 None,
2078 Some(100_u64),
2079 Some(130_u64),
2080 ])));
2081 let reader_samples = Arc::clone(&samples);
2082 let reader = Arc::new(move || {
2083 reader_samples
2084 .lock()
2085 .expect("sample queue")
2086 .pop_front()
2087 .unwrap_or(Some(130))
2088 });
2089
2090 let sampler = PersistentMemorySampler::start_with_reader(reader).expect("sampler");
2091 assert!(!sampler.begin_window());
2092 assert!(sampler.begin_window());
2093 let peak = sampler
2094 .end_window()
2095 .expect("second window should receive its own result");
2096
2097 assert_eq!(
2098 peak,
2099 ProcessMemoryPeak {
2100 growth_kb: 30,
2101 process_peak_kb: 130,
2102 }
2103 );
2104 }
2105
2106 #[cfg(not(target_arch = "wasm32"))]
2107 #[test]
2108 fn persistent_memory_sampler_waits_for_baseline_before_begin_returns() {
2109 use std::sync::atomic::{AtomicBool, Ordering};
2110 use std::sync::{Arc, Mutex};
2111
2112 let call_count = Arc::new(Mutex::new(0_u32));
2113 let (baseline_entered_tx, baseline_entered_rx) = mpsc::sync_channel(1);
2114 let (baseline_release_tx, baseline_release_rx) = mpsc::sync_channel(1);
2115 let baseline_release_rx = Arc::new(Mutex::new(baseline_release_rx));
2116 let baseline_released = Arc::new(AtomicBool::new(false));
2117
2118 let reader_calls = Arc::clone(&call_count);
2119 let reader_release = Arc::clone(&baseline_release_rx);
2120 let reader = Arc::new(move || {
2121 let mut calls = reader_calls.lock().expect("call count");
2122 *calls += 1;
2123 let current = *calls;
2124 drop(calls);
2125
2126 if current == 2 {
2127 baseline_entered_tx.send(()).expect("baseline entered");
2128 reader_release
2129 .lock()
2130 .expect("baseline release")
2131 .recv()
2132 .expect("release baseline");
2133 return Some(100);
2134 }
2135
2136 Some(120)
2137 });
2138
2139 let released = Arc::clone(&baseline_released);
2140 let release_handle = thread::spawn(move || {
2141 baseline_entered_rx.recv().expect("baseline read started");
2142 thread::sleep(Duration::from_millis(20));
2143 released.store(true, Ordering::SeqCst);
2144 baseline_release_tx.send(()).expect("release baseline");
2145 });
2146
2147 let sampler = PersistentMemorySampler::start_with_reader(reader).expect("sampler");
2148 assert!(sampler.begin_window());
2149 assert!(
2150 baseline_released.load(Ordering::SeqCst),
2151 "begin_window returned before the baseline sample completed"
2152 );
2153 release_handle.join().expect("join release thread");
2154
2155 let peak = sampler.end_window().expect("peak memory");
2156 assert_eq!(peak.growth_kb, 20);
2157 assert_eq!(peak.process_peak_kb, 120);
2158 }
2159
2160 #[cfg(not(target_arch = "wasm32"))]
2161 #[test]
2162 fn persistent_memory_sampler_supports_multiple_windows() {
2163 use std::collections::VecDeque;
2164 use std::sync::{Arc, Mutex};
2165
2166 let samples = Arc::new(Mutex::new(VecDeque::from([
2169 Some(50_u64), Some(200_u64), Some(260_u64), Some(190_u64), Some(250_u64), ])));
2175 let reader_samples = Arc::clone(&samples);
2176 let reader = Arc::new(move || {
2177 reader_samples
2178 .lock()
2179 .expect("sample queue")
2180 .pop_front()
2181 .unwrap_or(Some(0))
2182 });
2183
2184 let sampler = PersistentMemorySampler::start_with_reader(reader).expect("sampler");
2185
2186 assert!(sampler.begin_window());
2187 let first = sampler.end_window().expect("first peak");
2188 assert_eq!(first.process_peak_kb, 260);
2189 assert_eq!(first.growth_kb, 60);
2190
2191 assert!(sampler.begin_window());
2192 let second = sampler.end_window().expect("second peak");
2193 assert_eq!(second.process_peak_kb, 250);
2194 assert_eq!(second.growth_kb, 60);
2195 }
2196
2197 #[test]
2198 fn bench_report_deserializes_legacy_payload_without_phases_or_timeline() {
2199 let legacy = r#"{
2203 "spec": { "name": "legacy", "iterations": 2, "warmup": 0 },
2204 "samples": [
2205 { "duration_ns": 100 },
2206 { "duration_ns": 200 }
2207 ]
2208 }"#;
2209
2210 let report: BenchReport = serde_json::from_str(legacy).expect("legacy report parses");
2211 assert_eq!(report.samples.len(), 2);
2212 assert!(report.phases.is_empty());
2213 assert!(report.timeline.is_empty());
2214 assert!(report.samples[0].cpu_time_ms.is_none());
2215 assert!(report.samples[0].peak_memory_kb.is_none());
2216 assert!(report.samples[0].process_peak_memory_kb.is_none());
2217
2218 let json = serde_json::to_string(&report).expect("serialize");
2221 assert!(!json.contains("\"phases\""));
2222 assert!(!json.contains("\"timeline\""));
2223 }
2224
2225 #[test]
2226 fn run_with_setup_calls_setup_once() {
2227 use std::sync::atomic::{AtomicU32, Ordering};
2228
2229 static SETUP_COUNT: AtomicU32 = AtomicU32::new(0);
2230 static RUN_COUNT: AtomicU32 = AtomicU32::new(0);
2231
2232 let spec = BenchSpec::new("test", 5, 2).unwrap();
2233 let report = run_closure_with_setup(
2234 spec,
2235 || {
2236 SETUP_COUNT.fetch_add(1, Ordering::SeqCst);
2237 vec![1, 2, 3]
2238 },
2239 |data| {
2240 RUN_COUNT.fetch_add(1, Ordering::SeqCst);
2241 std::hint::black_box(data.len());
2242 Ok(())
2243 },
2244 )
2245 .unwrap();
2246
2247 assert_eq!(SETUP_COUNT.load(Ordering::SeqCst), 1); assert_eq!(RUN_COUNT.load(Ordering::SeqCst), 7); assert_eq!(report.samples.len(), 5);
2250 }
2251
2252 #[test]
2253 fn run_with_setup_per_iter_calls_setup_each_time() {
2254 use std::sync::atomic::{AtomicU32, Ordering};
2255
2256 static SETUP_COUNT: AtomicU32 = AtomicU32::new(0);
2257
2258 let spec = BenchSpec::new("test", 3, 1).unwrap();
2259 let report = run_closure_with_setup_per_iter(
2260 spec,
2261 || {
2262 SETUP_COUNT.fetch_add(1, Ordering::SeqCst);
2263 vec![1, 2, 3]
2264 },
2265 |data| {
2266 std::hint::black_box(data);
2267 Ok(())
2268 },
2269 )
2270 .unwrap();
2271
2272 assert_eq!(SETUP_COUNT.load(Ordering::SeqCst), 4); assert_eq!(report.samples.len(), 3);
2274 }
2275
2276 #[test]
2277 fn run_with_setup_teardown_calls_both() {
2278 use std::sync::atomic::{AtomicU32, Ordering};
2279
2280 static SETUP_COUNT: AtomicU32 = AtomicU32::new(0);
2281 static TEARDOWN_COUNT: AtomicU32 = AtomicU32::new(0);
2282
2283 let spec = BenchSpec::new("test", 3, 1).unwrap();
2284 let report = run_closure_with_setup_teardown(
2285 spec,
2286 || {
2287 SETUP_COUNT.fetch_add(1, Ordering::SeqCst);
2288 "resource"
2289 },
2290 |_resource| Ok(()),
2291 |_resource| {
2292 TEARDOWN_COUNT.fetch_add(1, Ordering::SeqCst);
2293 },
2294 )
2295 .unwrap();
2296
2297 assert_eq!(SETUP_COUNT.load(Ordering::SeqCst), 1);
2298 assert_eq!(TEARDOWN_COUNT.load(Ordering::SeqCst), 1);
2299 assert_eq!(report.samples.len(), 3);
2300 }
2301
2302 #[test]
2303 fn bench_report_serializes_exact_harness_timeline() {
2304 let spec = BenchSpec::new("timeline", 2, 1).unwrap();
2305 let report = run_closure_with_setup_teardown(
2306 spec,
2307 || {
2308 std::thread::sleep(Duration::from_millis(1));
2309 "resource"
2310 },
2311 |_resource| {
2312 std::thread::sleep(Duration::from_millis(1));
2313 Ok(())
2314 },
2315 |_resource| {
2316 std::thread::sleep(Duration::from_millis(1));
2317 },
2318 )
2319 .unwrap();
2320
2321 let json = serde_json::to_value(&report).unwrap();
2322 assert_eq!(json["timeline"][0]["phase"], "setup");
2323 assert_eq!(json["timeline"][1]["phase"], "warmup-benchmark");
2324 assert_eq!(json["timeline"][2]["phase"], "measured-benchmark");
2325 assert_eq!(json["timeline"][3]["phase"], "measured-benchmark");
2326 assert_eq!(json["timeline"][4]["phase"], "teardown");
2327 }
2328}