Skip to main content

mobench_sdk/
timing.rs

1//! Lightweight benchmarking harness for mobile platforms.
2//!
3//! This module provides the core timing infrastructure for the mobench ecosystem.
4//! It was previously a separate crate (`mobench-runner`) but has been consolidated
5//! into `mobench-sdk` for a simpler dependency graph.
6//!
7//! The module is designed to be minimal and portable, with no platform-specific
8//! dependencies, making it suitable for compilation to Android and iOS targets.
9//!
10//! ## Overview
11//!
12//! The timing module executes benchmark functions with:
13//! - Configurable warmup iterations
14//! - Precise nanosecond-resolution timing
15//! - Simple, serializable results
16//!
17//! ## Usage
18//!
19//! Most users should use this via the higher-level [`crate::run_benchmark`] function
20//! or [`crate::BenchmarkBuilder`]. Direct usage is for custom integrations:
21//!
22//! ```
23//! use mobench_sdk::timing::{BenchSpec, run_closure, TimingError};
24//!
25//! // Define a benchmark specification
26//! let spec = BenchSpec::new("my_benchmark", 100, 10)?;
27//!
28//! // Run the benchmark
29//! let report = run_closure(spec, || {
30//!     // Your benchmark code
31//!     let sum: u64 = (0..1000).sum();
32//!     std::hint::black_box(sum);
33//!     Ok(())
34//! })?;
35//!
36//! // Analyze results
37//! let mean_ns = report.samples.iter()
38//!     .map(|s| s.duration_ns)
39//!     .sum::<u64>() / report.samples.len() as u64;
40//!
41//! println!("Mean: {} ns", mean_ns);
42//! # Ok::<(), TimingError>(())
43//! ```
44//!
45//! ## Types
46//!
47//! | Type | Description |
48//! |------|-------------|
49//! | [`BenchSpec`] | Benchmark configuration (name, iterations, warmup) |
50//! | [`BenchSample`] | Single timing measurement in nanoseconds |
51//! | [`BenchReport`] | Complete results with all samples |
52//! | [`TimingError`] | Error conditions during benchmarking |
53//!
54//! ## Feature Flags
55//!
56//! This module is always available. When using `mobench-sdk` with default features,
57//! you also get build automation and template generation. For minimal binary size
58//! (e.g., on mobile targets), use the `runner-only` feature:
59//!
60//! ```toml
61//! [dependencies]
62//! mobench-sdk = { version = "0.2.0", default-features = false, features = ["runner-only"] }
63//! ```
64
65use 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    /// Browser-backed monotonic instant.
92    ///
93    /// `std::time::Instant::now()` panics on `wasm32-unknown-unknown`, so the
94    /// timing harness uses the browser's high-resolution monotonic clock.
95    #[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/// Benchmark specification defining what and how to benchmark.
117///
118/// Contains the benchmark name, number of measurement iterations, and
119/// warmup iterations to perform before measuring.
120///
121/// # Example
122///
123/// ```
124/// use mobench_sdk::timing::BenchSpec;
125///
126/// // Create a spec for 100 iterations with 10 warmup runs
127/// let spec = BenchSpec::new("sorting_benchmark", 100, 10)?;
128///
129/// assert_eq!(spec.name, "sorting_benchmark");
130/// assert_eq!(spec.iterations, 100);
131/// assert_eq!(spec.warmup, 10);
132/// # Ok::<(), mobench_sdk::timing::TimingError>(())
133/// ```
134///
135/// # Serialization
136///
137/// `BenchSpec` implements `Serialize` and `Deserialize` for JSON persistence:
138///
139/// ```
140/// use mobench_sdk::timing::BenchSpec;
141///
142/// let spec = BenchSpec {
143///     name: "my_bench".to_string(),
144///     iterations: 50,
145///     warmup: 5,
146/// };
147///
148/// let json = serde_json::to_string(&spec)?;
149/// let restored: BenchSpec = serde_json::from_str(&json)?;
150///
151/// assert_eq!(spec.name, restored.name);
152/// # Ok::<(), serde_json::Error>(())
153/// ```
154#[derive(Clone, Debug, Serialize, Deserialize)]
155#[serde(try_from = "UncheckedBenchSpec")]
156pub struct BenchSpec {
157    /// Name of the benchmark, typically the fully-qualified function name.
158    ///
159    /// Examples: `"my_crate::fibonacci"`, `"sorting_benchmark"`
160    pub name: String,
161
162    /// Number of iterations to measure.
163    ///
164    /// Each iteration produces one [`BenchSample`]. Must be greater than zero.
165    pub iterations: u32,
166
167    /// Number of warmup iterations before measurement.
168    ///
169    /// Warmup iterations are not recorded. They allow CPU caches to warm
170    /// and any JIT compilation to complete. Can be zero.
171    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    /// Creates a new benchmark specification.
191    ///
192    /// # Arguments
193    ///
194    /// * `name` - Name identifier for the benchmark
195    /// * `iterations` - Number of measured iterations (must be > 0)
196    /// * `warmup` - Number of warmup iterations (can be 0)
197    ///
198    /// # Errors
199    ///
200    /// Returns [`TimingError::NoIterations`] if `iterations` is zero.
201    ///
202    /// # Example
203    ///
204    /// ```
205    /// use mobench_sdk::timing::BenchSpec;
206    ///
207    /// let spec = BenchSpec::new("test", 100, 10)?;
208    /// assert_eq!(spec.iterations, 100);
209    ///
210    /// // Zero iterations is an error
211    /// let err = BenchSpec::new("test", 0, 10);
212    /// assert!(err.is_err());
213    /// # Ok::<(), mobench_sdk::timing::TimingError>(())
214    /// ```
215    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    /// Validate the iteration counts on a specification built with public fields.
226    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/// A single timing sample from a benchmark iteration.
249///
250/// Holds the elapsed wall time in nanoseconds plus optional per-iteration
251/// resource metrics (CPU time, peak memory growth, and process peak memory).
252/// The optional fields are only populated on platforms where the harness can
253/// capture them and are skipped from the JSON output when absent.
254///
255/// # Example
256///
257/// ```
258/// use mobench_sdk::timing::BenchSample;
259///
260/// let sample = BenchSample {
261///     duration_ns: 1_500_000,
262///     ..Default::default()
263/// };
264///
265/// // Convert to milliseconds
266/// let ms = sample.duration_ns as f64 / 1_000_000.0;
267/// assert_eq!(ms, 1.5);
268/// ```
269#[derive(Clone, Debug, Default, Serialize, Deserialize)]
270pub struct BenchSample {
271    /// Duration of the iteration in nanoseconds.
272    ///
273    /// Measured using a platform monotonic clock with high-resolution timing.
274    pub duration_ns: u64,
275
276    /// CPU time consumed by the measured iteration in milliseconds.
277    ///
278    /// This is captured around the measured benchmark closure only and excludes
279    /// warmup, setup, teardown, and report generation overhead.
280    #[serde(default, skip_serializing_if = "Option::is_none")]
281    pub cpu_time_ms: Option<u64>,
282
283    /// Peak memory growth during the measured iteration in kilobytes.
284    ///
285    /// This legacy wire field is baseline-adjusted immediately before the
286    /// measured closure enters. It reports growth during the measured
287    /// iteration, not absolute process or device peak memory.
288    #[serde(default, skip_serializing_if = "Option::is_none")]
289    pub peak_memory_kb: Option<u64>,
290
291    /// Peak resident memory of the benchmark process during the measured iteration.
292    ///
293    /// This is sampled from the current process while the measured closure is
294    /// running. Unlike `peak_memory_kb`, it is not baseline-adjusted.
295    #[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/// Complete benchmark report with all timing samples.
311///
312/// Contains the original specification and all collected samples.
313/// Can be serialized to JSON for storage or transmission.
314///
315/// # Example
316///
317/// ```
318/// use mobench_sdk::timing::{BenchSpec, run_closure};
319///
320/// let spec = BenchSpec::new("example", 50, 5)?;
321/// let report = run_closure(spec, || {
322///     std::hint::black_box(42);
323///     Ok(())
324/// })?;
325///
326/// // Calculate statistics
327/// let samples: Vec<u64> = report.samples.iter()
328///     .map(|s| s.duration_ns)
329///     .collect();
330///
331/// let min = samples.iter().min().unwrap();
332/// let max = samples.iter().max().unwrap();
333/// let mean = samples.iter().sum::<u64>() / samples.len() as u64;
334///
335/// println!("Min: {} ns, Max: {} ns, Mean: {} ns", min, max, mean);
336/// # Ok::<(), mobench_sdk::timing::TimingError>(())
337/// ```
338#[derive(Clone, Debug, Serialize, Deserialize)]
339pub struct BenchReport {
340    /// The specification used for this benchmark run.
341    pub spec: BenchSpec,
342
343    /// All collected timing samples.
344    ///
345    /// The length equals `spec.iterations`. Samples are in execution order.
346    pub samples: Vec<BenchSample>,
347
348    /// Optional semantic phase timings captured during measured iterations.
349    ///
350    /// Defaults to an empty vector when deserializing reports produced by
351    /// older mobench versions that did not emit phase data.
352    #[serde(default, skip_serializing_if = "Vec::is_empty")]
353    pub phases: Vec<SemanticPhase>,
354
355    /// Exact harness timeline spans in execution order.
356    ///
357    /// Defaults to an empty vector when deserializing reports produced by
358    /// older mobench versions that did not emit timeline data.
359    #[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    /// Returns the mean (average) duration in nanoseconds.
373    #[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    /// Returns the median duration in nanoseconds.
379    #[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    /// Returns the standard deviation in nanoseconds (sample std dev, n-1).
386    #[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    /// Returns the given percentile (0-100) in nanoseconds.
392    #[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    /// Returns the minimum duration in nanoseconds.
399    #[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    /// Returns the maximum duration in nanoseconds.
409    #[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    /// Returns the total measured CPU time in milliseconds across all iterations.
419    #[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    /// Returns the median measured CPU time in milliseconds across all iterations.
431    #[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    /// Returns the maximum baseline-adjusted peak memory growth in kilobytes.
444    ///
445    /// This is the legacy accessor for the serialized `peak_memory_kb` sample
446    /// field. It does not report absolute process or device peak memory.
447    #[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    /// Returns the maximum baseline-adjusted peak memory growth in kilobytes.
456    ///
457    /// This is an explicit alias for [`BenchReport::peak_memory_kb`] to make the
458    /// growth semantics clear while preserving the legacy wire field.
459    #[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    /// Returns the maximum process resident memory peak in kilobytes.
466    ///
467    /// This reports the current benchmark process peak sampled during measured
468    /// iterations. It excludes BrowserStack/session-level provider memory.
469    #[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    /// Returns a statistical summary of the benchmark results.
478    #[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/// Statistical summary of benchmark results.
535#[derive(Clone, Debug, Serialize, Deserialize)]
536pub struct BenchSummary {
537    /// Name of the benchmark.
538    pub name: String,
539    /// Number of measured iterations.
540    pub iterations: u32,
541    /// Number of warmup iterations.
542    pub warmup: u32,
543    /// Mean duration in nanoseconds.
544    pub mean_ns: f64,
545    /// Median duration in nanoseconds.
546    pub median_ns: f64,
547    /// Standard deviation in nanoseconds.
548    pub std_dev_ns: f64,
549    /// Minimum duration in nanoseconds.
550    pub min_ns: u64,
551    /// Maximum duration in nanoseconds.
552    pub max_ns: u64,
553    /// 95th percentile in nanoseconds.
554    pub p95_ns: f64,
555    /// 99th percentile in nanoseconds.
556    pub p99_ns: f64,
557}
558
559/// Flat semantic phase timing captured during a benchmark run.
560#[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    /// Lazily-initialized long-lived sampler shared across measured iterations.
667    ///
668    /// We pay thread-spawn cost once per benchmark function rather than per
669    /// iteration. On constrained mobile devices (Android/Bionic) thread
670    /// creation is significantly more expensive than on desktop Linux, and
671    /// 1000+ iteration benchmarks would otherwise spawn 1000+ throwaway
672    /// threads.
673    #[cfg(not(target_arch = "wasm32"))]
674    memory_sampler: Option<PersistentMemorySampler>,
675    /// Set after the first attempt to start the sampler so we do not retry
676    /// on platforms where the sampler is not supported.
677    #[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    /// True if the persistent sampler accepted a `Begin` for this iteration
705    /// and we therefore expect a corresponding result on `finish`.
706    #[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    // SAFETY: `RUSAGE_SELF` is always a valid `who` value and the kernel
805    // writes a fully-initialized `rusage` into the provided pointer on
806    // success. We bail out via `rc != 0` before touching the buffer below.
807    let rc = unsafe { libc::getrusage(libc::RUSAGE_SELF, usage.as_mut_ptr()) };
808    if rc != 0 {
809        return None;
810    }
811
812    // SAFETY: `getrusage` returned 0, so the buffer is fully initialized.
813    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/// Long-lived memory sampler. Spawned once per benchmark function and reused
835/// across every measured iteration via `begin_window` / `end_window`.
836///
837/// Replaces the previous per-iteration design that spawned and joined a fresh
838/// thread for every sample. On Android (Bionic) and iOS that thread-creation
839/// overhead is non-trivial and would inflate harness wall time on
840/// high-iteration runs.
841#[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                // Touch the sampler thread's own stack and runtime state once
870                // before any window opens so its initialization cost cannot
871                // contaminate the first iteration's baseline measurement.
872                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            // Thread failed before sending readiness. Send Shutdown to make
884            // sure it does not get stuck on a later cmd recv, then join.
885            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                            // A stray Begin while a window is already open
921                            // means the producer side desynced — preserve
922                            // existing behavior by ignoring it.
923                            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                    // One last sample after the window closes so a final
937                    // allocation that happens between the last poll and the
938                    // End command is still accounted for.
939                    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                // End without an active Begin — ignore.
954                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    // SAFETY: `_SC_PAGESIZE` is a valid sysconf selector on every supported
992    // POSIX target; sysconf has no side effects and reports failures via -1.
993    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    // `mach_task_self` is marked deprecated by libc in favor of
1006    // `mach_task_self_`, but the replacement symbol is not yet exposed by the
1007    // libc crate's iOS/macOS bindings (libc < 0.3). The deprecation is purely
1008    // cosmetic — the function continues to be the documented way to obtain
1009    // the current task port and is what Apple's headers expand the macro to.
1010    #[allow(deprecated)]
1011    // SAFETY: `mach_task_self` always returns a valid task port for the
1012    // current process. `MACH_TASK_BASIC_INFO` matches the
1013    // `mach_task_basic_info_data_t` layout we pass; `count` carries the
1014    // capacity in 32-bit words and is updated by the kernel on success.
1015    // We check for `KERN_SUCCESS` before assuming the buffer is initialized.
1016    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    // SAFETY: `task_info` returned `KERN_SUCCESS`, so the basic info struct
1029    // is fully populated.
1030    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
1067/// Records a flat semantic phase when called inside an active benchmark measurement loop.
1068///
1069/// Phases are aggregated across measured iterations and ignored during warmup/setup.
1070/// Nested phases are intentionally collapsed in v1 to keep the output flat.
1071pub 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/// Errors that can occur during benchmark execution.
1094///
1095/// # Example
1096///
1097/// ```
1098/// use mobench_sdk::timing::{BenchSpec, TimingError};
1099///
1100/// // Zero iterations produces an error
1101/// let result = BenchSpec::new("test", 0, 10);
1102/// assert!(matches!(result, Err(TimingError::NoIterations { .. })));
1103/// ```
1104#[derive(Debug, Error)]
1105pub enum TimingError {
1106    /// The iteration count was zero or invalid.
1107    ///
1108    /// At least one iteration is required to produce a measurement.
1109    /// The error includes the actual value provided for diagnostic purposes.
1110    #[error("iterations must be greater than zero (got {count}). Minimum recommended: 10")]
1111    NoIterations {
1112        /// The invalid iteration count that was provided.
1113        count: u32,
1114    },
1115
1116    /// The benchmark function failed during execution.
1117    ///
1118    /// Contains a description of the failure.
1119    #[error("benchmark function failed: {0}")]
1120    Execution(String),
1121}
1122
1123/// Runs a benchmark by executing a closure repeatedly.
1124///
1125/// This is the core benchmarking function. It:
1126///
1127/// 1. Executes the closure `spec.warmup` times without recording
1128/// 2. Executes the closure `spec.iterations` times, recording each duration
1129/// 3. Returns a [`BenchReport`] with all samples
1130///
1131/// # Arguments
1132///
1133/// * `spec` - Benchmark configuration specifying iterations and warmup
1134/// * `f` - Closure to benchmark; must return `Result<(), TimingError>`
1135///
1136/// # Returns
1137///
1138/// A [`BenchReport`] containing all timing samples, or a [`TimingError`] if
1139/// the benchmark fails.
1140///
1141/// # Example
1142///
1143/// ```
1144/// use mobench_sdk::timing::{BenchSpec, run_closure, TimingError};
1145///
1146/// let spec = BenchSpec::new("sum_benchmark", 100, 10)?;
1147///
1148/// let report = run_closure(spec, || {
1149///     let sum: u64 = (0..1000).sum();
1150///     std::hint::black_box(sum);
1151///     Ok(())
1152/// })?;
1153///
1154/// assert_eq!(report.samples.len(), 100);
1155///
1156/// // Calculate mean duration
1157/// let total_ns: u64 = report.samples.iter().map(|s| s.duration_ns).sum();
1158/// let mean_ns = total_ns / report.samples.len() as u64;
1159/// println!("Mean: {} ns", mean_ns);
1160/// # Ok::<(), TimingError>(())
1161/// ```
1162///
1163/// # Error Handling
1164///
1165/// If the closure returns an error, the benchmark stops immediately:
1166///
1167/// ```
1168/// use mobench_sdk::timing::{BenchSpec, run_closure, TimingError};
1169///
1170/// let spec = BenchSpec::new("failing_bench", 100, 0)?;
1171///
1172/// let result = run_closure(spec, || {
1173///     Err(TimingError::Execution("simulated failure".into()))
1174/// });
1175///
1176/// assert!(result.is_err());
1177/// # Ok::<(), TimingError>(())
1178/// ```
1179///
1180/// # Timing Precision
1181///
1182/// Uses [`std::time::Instant`] for timing, which provides monotonic,
1183/// nanosecond-resolution measurements on most platforms.
1184pub 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    // Warmup phase - not measured
1208    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    // Measurement phase
1222    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
1252/// Runs a benchmark with setup that executes once before all iterations.
1253///
1254/// The setup function is called once before timing begins, then the benchmark
1255/// runs multiple times using a reference to the setup result. This is useful
1256/// for expensive initialization that shouldn't be included in timing.
1257///
1258/// # Arguments
1259///
1260/// * `spec` - Benchmark configuration specifying iterations and warmup
1261/// * `setup` - Function that creates the input data (called once, not timed)
1262/// * `f` - Benchmark closure that receives a reference to setup result
1263///
1264/// # Example
1265///
1266/// ```ignore
1267/// use mobench_sdk::timing::{BenchSpec, run_closure_with_setup};
1268///
1269/// fn setup_data() -> Vec<u8> {
1270///     vec![0u8; 1_000_000]  // Expensive allocation not measured
1271/// }
1272///
1273/// let spec = BenchSpec::new("hash_benchmark", 100, 10)?;
1274/// let report = run_closure_with_setup(spec, setup_data, |data| {
1275///     std::hint::black_box(compute_hash(data));
1276///     Ok(())
1277/// })?;
1278/// ```
1279pub 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    // Setup phase - not timed
1310    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    // Warmup phase - not recorded
1322    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    // Measurement phase
1336    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
1366/// Runs a benchmark with per-iteration setup.
1367///
1368/// Setup runs before each iteration and is not timed. The benchmark takes
1369/// ownership of the setup result, making this suitable for benchmarks that
1370/// mutate their input (e.g., sorting).
1371///
1372/// # Arguments
1373///
1374/// * `spec` - Benchmark configuration specifying iterations and warmup
1375/// * `setup` - Function that creates fresh input for each iteration (not timed)
1376/// * `f` - Benchmark closure that takes ownership of setup result
1377///
1378/// # Example
1379///
1380/// ```ignore
1381/// use mobench_sdk::timing::{BenchSpec, run_closure_with_setup_per_iter};
1382///
1383/// fn generate_random_vec() -> Vec<i32> {
1384///     (0..1000).map(|_| rand::random()).collect()
1385/// }
1386///
1387/// let spec = BenchSpec::new("sort_benchmark", 100, 10)?;
1388/// let report = run_closure_with_setup_per_iter(spec, generate_random_vec, |mut data| {
1389///     data.sort();
1390///     std::hint::black_box(data);
1391///     Ok(())
1392/// })?;
1393/// ```
1394pub 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    // Warmup phase
1425    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    // Measurement phase
1449    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(); // Not timed
1454        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
1490/// Runs a benchmark with setup and teardown.
1491///
1492/// Setup runs once before all iterations, teardown runs once after all
1493/// iterations complete. Neither is included in timing.
1494///
1495/// # Arguments
1496///
1497/// * `spec` - Benchmark configuration specifying iterations and warmup
1498/// * `setup` - Function that creates the input data (called once, not timed)
1499/// * `f` - Benchmark closure that receives a reference to setup result
1500/// * `teardown` - Function that cleans up the input (called once, not timed)
1501///
1502/// # Example
1503///
1504/// ```ignore
1505/// use mobench_sdk::timing::{BenchSpec, run_closure_with_setup_teardown};
1506///
1507/// fn setup_db() -> Database { Database::connect("test.db") }
1508/// fn cleanup_db(db: Database) { db.close(); std::fs::remove_file("test.db").ok(); }
1509///
1510/// let spec = BenchSpec::new("db_benchmark", 100, 10)?;
1511/// let report = run_closure_with_setup_teardown(
1512///     spec,
1513///     setup_db,
1514///     |db| { db.query("SELECT *"); Ok(()) },
1515///     cleanup_db,
1516/// )?;
1517/// ```
1518pub 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    // Setup phase - not timed
1559    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        // Warmup phase
1572        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        // Measurement phase
1586        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    // Teardown phase - not timed. It runs even when warmup/measurement fails.
1612    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        // Queue: [80=startup warmup, 100=baseline-on-Begin, 140, 120, ...]
2127        // After exhaustion the reader returns 120 forever, so peak stays 140.
2128        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        // Queue: [80=startup warmup, None=failed first baseline,
2163        // 100=second baseline, 130=final sample].
2164        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        // First window baseline=200 peak=260 (growth=60).
2256        // Second window baseline=190 peak=250 (growth=60).
2257        let samples = Arc::new(Mutex::new(VecDeque::from([
2258            Some(50_u64),  // startup warmup
2259            Some(200_u64), // window 1 baseline
2260            Some(260_u64), // window 1 peak
2261            Some(190_u64), // window 2 baseline
2262            Some(250_u64), // window 2 peak
2263        ])));
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        // Wire format produced by mobench <= 0.1.34 (no phases / timeline /
2289        // resource fields). Adding these fields to BenchReport must not
2290        // break consumers that still emit the older shape.
2291        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        // Round-trip the parsed report and confirm the empty optional
2308        // collections are skipped from the serialized output.
2309        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); // Setup called once
2337        assert_eq!(RUN_COUNT.load(Ordering::SeqCst), 7); // 2 warmup + 5 iterations
2338        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); // 1 warmup + 3 iterations
2362        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}