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.1.37", default-features = false, features = ["runner-only"] }
63//! ```
64
65use 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    /// Browser-backed monotonic instant.
88    ///
89    /// `std::time::Instant::now()` panics on `wasm32-unknown-unknown`, so the
90    /// timing harness uses the browser's high-resolution monotonic clock.
91    #[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/// Benchmark specification defining what and how to benchmark.
113///
114/// Contains the benchmark name, number of measurement iterations, and
115/// warmup iterations to perform before measuring.
116///
117/// # Example
118///
119/// ```
120/// use mobench_sdk::timing::BenchSpec;
121///
122/// // Create a spec for 100 iterations with 10 warmup runs
123/// let spec = BenchSpec::new("sorting_benchmark", 100, 10)?;
124///
125/// assert_eq!(spec.name, "sorting_benchmark");
126/// assert_eq!(spec.iterations, 100);
127/// assert_eq!(spec.warmup, 10);
128/// # Ok::<(), mobench_sdk::timing::TimingError>(())
129/// ```
130///
131/// # Serialization
132///
133/// `BenchSpec` implements `Serialize` and `Deserialize` for JSON persistence:
134///
135/// ```
136/// use mobench_sdk::timing::BenchSpec;
137///
138/// let spec = BenchSpec {
139///     name: "my_bench".to_string(),
140///     iterations: 50,
141///     warmup: 5,
142/// };
143///
144/// let json = serde_json::to_string(&spec)?;
145/// let restored: BenchSpec = serde_json::from_str(&json)?;
146///
147/// assert_eq!(spec.name, restored.name);
148/// # Ok::<(), serde_json::Error>(())
149/// ```
150#[derive(Clone, Debug, Serialize, Deserialize)]
151pub struct BenchSpec {
152    /// Name of the benchmark, typically the fully-qualified function name.
153    ///
154    /// Examples: `"my_crate::fibonacci"`, `"sorting_benchmark"`
155    pub name: String,
156
157    /// Number of iterations to measure.
158    ///
159    /// Each iteration produces one [`BenchSample`]. Must be greater than zero.
160    pub iterations: u32,
161
162    /// Number of warmup iterations before measurement.
163    ///
164    /// Warmup iterations are not recorded. They allow CPU caches to warm
165    /// and any JIT compilation to complete. Can be zero.
166    pub warmup: u32,
167}
168
169impl BenchSpec {
170    /// Creates a new benchmark specification.
171    ///
172    /// # Arguments
173    ///
174    /// * `name` - Name identifier for the benchmark
175    /// * `iterations` - Number of measured iterations (must be > 0)
176    /// * `warmup` - Number of warmup iterations (can be 0)
177    ///
178    /// # Errors
179    ///
180    /// Returns [`TimingError::NoIterations`] if `iterations` is zero.
181    ///
182    /// # Example
183    ///
184    /// ```
185    /// use mobench_sdk::timing::BenchSpec;
186    ///
187    /// let spec = BenchSpec::new("test", 100, 10)?;
188    /// assert_eq!(spec.iterations, 100);
189    ///
190    /// // Zero iterations is an error
191    /// let err = BenchSpec::new("test", 0, 10);
192    /// assert!(err.is_err());
193    /// # Ok::<(), mobench_sdk::timing::TimingError>(())
194    /// ```
195    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/// A single timing sample from a benchmark iteration.
209///
210/// Holds the elapsed wall time in nanoseconds plus optional per-iteration
211/// resource metrics (CPU time, peak memory growth, and process peak memory).
212/// The optional fields are only populated on platforms where the harness can
213/// capture them and are skipped from the JSON output when absent.
214///
215/// # Example
216///
217/// ```
218/// use mobench_sdk::timing::BenchSample;
219///
220/// let sample = BenchSample {
221///     duration_ns: 1_500_000,
222///     ..Default::default()
223/// };
224///
225/// // Convert to milliseconds
226/// let ms = sample.duration_ns as f64 / 1_000_000.0;
227/// assert_eq!(ms, 1.5);
228/// ```
229#[derive(Clone, Debug, Default, Serialize, Deserialize)]
230pub struct BenchSample {
231    /// Duration of the iteration in nanoseconds.
232    ///
233    /// Measured using [`std::time::Instant`] for monotonic, high-resolution timing.
234    pub duration_ns: u64,
235
236    /// CPU time consumed by the measured iteration in milliseconds.
237    ///
238    /// This is captured around the measured benchmark closure only and excludes
239    /// warmup, setup, teardown, and report generation overhead.
240    #[serde(default, skip_serializing_if = "Option::is_none")]
241    pub cpu_time_ms: Option<u64>,
242
243    /// Peak memory growth during the measured iteration in kilobytes.
244    ///
245    /// This legacy wire field is baseline-adjusted immediately before the
246    /// measured closure enters. It reports growth during the measured
247    /// iteration, not absolute process or device peak memory.
248    #[serde(default, skip_serializing_if = "Option::is_none")]
249    pub peak_memory_kb: Option<u64>,
250
251    /// Peak resident memory of the benchmark process during the measured iteration.
252    ///
253    /// This is sampled from the current process while the measured closure is
254    /// running. Unlike `peak_memory_kb`, it is not baseline-adjusted.
255    #[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/// Complete benchmark report with all timing samples.
271///
272/// Contains the original specification and all collected samples.
273/// Can be serialized to JSON for storage or transmission.
274///
275/// # Example
276///
277/// ```
278/// use mobench_sdk::timing::{BenchSpec, run_closure};
279///
280/// let spec = BenchSpec::new("example", 50, 5)?;
281/// let report = run_closure(spec, || {
282///     std::hint::black_box(42);
283///     Ok(())
284/// })?;
285///
286/// // Calculate statistics
287/// let samples: Vec<u64> = report.samples.iter()
288///     .map(|s| s.duration_ns)
289///     .collect();
290///
291/// let min = samples.iter().min().unwrap();
292/// let max = samples.iter().max().unwrap();
293/// let mean = samples.iter().sum::<u64>() / samples.len() as u64;
294///
295/// println!("Min: {} ns, Max: {} ns, Mean: {} ns", min, max, mean);
296/// # Ok::<(), mobench_sdk::timing::TimingError>(())
297/// ```
298#[derive(Clone, Debug, Serialize, Deserialize)]
299pub struct BenchReport {
300    /// The specification used for this benchmark run.
301    pub spec: BenchSpec,
302
303    /// All collected timing samples.
304    ///
305    /// The length equals `spec.iterations`. Samples are in execution order.
306    pub samples: Vec<BenchSample>,
307
308    /// Optional semantic phase timings captured during measured iterations.
309    ///
310    /// Defaults to an empty vector when deserializing reports produced by
311    /// older mobench versions that did not emit phase data.
312    #[serde(default, skip_serializing_if = "Vec::is_empty")]
313    pub phases: Vec<SemanticPhase>,
314
315    /// Exact harness timeline spans in execution order.
316    ///
317    /// Defaults to an empty vector when deserializing reports produced by
318    /// older mobench versions that did not emit timeline data.
319    #[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    /// Returns the mean (average) duration in nanoseconds.
333    #[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    /// Returns the median duration in nanoseconds.
343    #[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    /// Returns the standard deviation in nanoseconds (sample std dev, n-1).
359    #[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    /// Returns the given percentile (0-100) in nanoseconds.
378    #[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    /// Returns the minimum duration in nanoseconds.
391    #[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    /// Returns the maximum duration in nanoseconds.
401    #[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    /// Returns the total measured CPU time in milliseconds across all iterations.
411    #[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    /// Returns the median measured CPU time in milliseconds across all iterations.
429    #[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    /// Returns the maximum baseline-adjusted peak memory growth in kilobytes.
452    ///
453    /// This is the legacy accessor for the serialized `peak_memory_kb` sample
454    /// field. It does not report absolute process or device peak memory.
455    #[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    /// Returns the maximum baseline-adjusted peak memory growth in kilobytes.
464    ///
465    /// This is an explicit alias for [`BenchReport::peak_memory_kb`] to make the
466    /// growth semantics clear while preserving the legacy wire field.
467    #[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    /// Returns the maximum process resident memory peak in kilobytes.
474    ///
475    /// This reports the current benchmark process peak sampled during measured
476    /// iterations. It excludes BrowserStack/session-level provider memory.
477    #[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    /// Returns a statistical summary of the benchmark results.
486    #[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/// Statistical summary of benchmark results.
534#[derive(Clone, Debug, Serialize, Deserialize)]
535pub struct BenchSummary {
536    /// Name of the benchmark.
537    pub name: String,
538    /// Number of measured iterations.
539    pub iterations: u32,
540    /// Number of warmup iterations.
541    pub warmup: u32,
542    /// Mean duration in nanoseconds.
543    pub mean_ns: f64,
544    /// Median duration in nanoseconds.
545    pub median_ns: f64,
546    /// Standard deviation in nanoseconds.
547    pub std_dev_ns: f64,
548    /// Minimum duration in nanoseconds.
549    pub min_ns: u64,
550    /// Maximum duration in nanoseconds.
551    pub max_ns: u64,
552    /// 95th percentile in nanoseconds.
553    pub p95_ns: f64,
554    /// 99th percentile in nanoseconds.
555    pub p99_ns: f64,
556}
557
558/// Flat semantic phase timing captured during a benchmark run.
559#[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    /// Lazily-initialized long-lived sampler shared across measured iterations.
666    ///
667    /// We pay thread-spawn cost once per benchmark function rather than per
668    /// iteration. On constrained mobile devices (Android/Bionic) thread
669    /// creation is significantly more expensive than on desktop Linux, and
670    /// 1000+ iteration benchmarks would otherwise spawn 1000+ throwaway
671    /// threads.
672    #[cfg(not(target_arch = "wasm32"))]
673    memory_sampler: Option<PersistentMemorySampler>,
674    /// Set after the first attempt to start the sampler so we do not retry
675    /// on platforms where the sampler is not supported.
676    #[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    /// True if the persistent sampler accepted a `Begin` for this iteration
704    /// and we therefore expect a corresponding result on `finish`.
705    #[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    // 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    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    // Warmup phase - not measured
1212    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    // Measurement phase
1226    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
1256/// Runs a benchmark with setup that executes once before all iterations.
1257///
1258/// The setup function is called once before timing begins, then the benchmark
1259/// runs multiple times using a reference to the setup result. This is useful
1260/// for expensive initialization that shouldn't be included in timing.
1261///
1262/// # Arguments
1263///
1264/// * `spec` - Benchmark configuration specifying iterations and warmup
1265/// * `setup` - Function that creates the input data (called once, not timed)
1266/// * `f` - Benchmark closure that receives a reference to setup result
1267///
1268/// # Example
1269///
1270/// ```ignore
1271/// use mobench_sdk::timing::{BenchSpec, run_closure_with_setup};
1272///
1273/// fn setup_data() -> Vec<u8> {
1274///     vec![0u8; 1_000_000]  // Expensive allocation not measured
1275/// }
1276///
1277/// let spec = BenchSpec::new("hash_benchmark", 100, 10)?;
1278/// let report = run_closure_with_setup(spec, setup_data, |data| {
1279///     std::hint::black_box(compute_hash(data));
1280///     Ok(())
1281/// })?;
1282/// ```
1283pub 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    // Setup phase - not timed
1318    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    // Warmup phase - not recorded
1330    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    // Measurement phase
1344    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
1374/// Runs a benchmark with per-iteration setup.
1375///
1376/// Setup runs before each iteration and is not timed. The benchmark takes
1377/// ownership of the setup result, making this suitable for benchmarks that
1378/// mutate their input (e.g., sorting).
1379///
1380/// # Arguments
1381///
1382/// * `spec` - Benchmark configuration specifying iterations and warmup
1383/// * `setup` - Function that creates fresh input for each iteration (not timed)
1384/// * `f` - Benchmark closure that takes ownership of setup result
1385///
1386/// # Example
1387///
1388/// ```ignore
1389/// use mobench_sdk::timing::{BenchSpec, run_closure_with_setup_per_iter};
1390///
1391/// fn generate_random_vec() -> Vec<i32> {
1392///     (0..1000).map(|_| rand::random()).collect()
1393/// }
1394///
1395/// let spec = BenchSpec::new("sort_benchmark", 100, 10)?;
1396/// let report = run_closure_with_setup_per_iter(spec, generate_random_vec, |mut data| {
1397///     data.sort();
1398///     std::hint::black_box(data);
1399///     Ok(())
1400/// })?;
1401/// ```
1402pub 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    // Warmup phase
1437    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    // Measurement phase
1461    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(); // Not timed
1466        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
1502/// Runs a benchmark with setup and teardown.
1503///
1504/// Setup runs once before all iterations, teardown runs once after all
1505/// iterations complete. Neither is included in timing.
1506///
1507/// # Arguments
1508///
1509/// * `spec` - Benchmark configuration specifying iterations and warmup
1510/// * `setup` - Function that creates the input data (called once, not timed)
1511/// * `f` - Benchmark closure that receives a reference to setup result
1512/// * `teardown` - Function that cleans up the input (called once, not timed)
1513///
1514/// # Example
1515///
1516/// ```ignore
1517/// use mobench_sdk::timing::{BenchSpec, run_closure_with_setup_teardown};
1518///
1519/// fn setup_db() -> Database { Database::connect("test.db") }
1520/// fn cleanup_db(db: Database) { db.close(); std::fs::remove_file("test.db").ok(); }
1521///
1522/// let spec = BenchSpec::new("db_benchmark", 100, 10)?;
1523/// let report = run_closure_with_setup_teardown(
1524///     spec,
1525///     setup_db,
1526///     |db| { db.query("SELECT *"); Ok(()) },
1527///     cleanup_db,
1528/// )?;
1529/// ```
1530pub 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    // Setup phase - not timed
1575    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        // Warmup phase
1588        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        // Measurement phase
1602        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    // Teardown phase - not timed. It runs even when warmup/measurement fails.
1628    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        // Queue: [80=startup warmup, 100=baseline-on-Begin, 140, 120, ...]
2038        // After exhaustion the reader returns 120 forever, so peak stays 140.
2039        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        // Queue: [80=startup warmup, None=failed first baseline,
2074        // 100=second baseline, 130=final sample].
2075        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        // First window baseline=200 peak=260 (growth=60).
2167        // Second window baseline=190 peak=250 (growth=60).
2168        let samples = Arc::new(Mutex::new(VecDeque::from([
2169            Some(50_u64),  // startup warmup
2170            Some(200_u64), // window 1 baseline
2171            Some(260_u64), // window 1 peak
2172            Some(190_u64), // window 2 baseline
2173            Some(250_u64), // window 2 peak
2174        ])));
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        // Wire format produced by mobench <= 0.1.34 (no phases / timeline /
2200        // resource fields). Adding these fields to BenchReport must not
2201        // break consumers that still emit the older shape.
2202        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        // Round-trip the parsed report and confirm the empty optional
2219        // collections are skipped from the serialized output.
2220        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); // Setup called once
2248        assert_eq!(RUN_COUNT.load(Ordering::SeqCst), 7); // 2 warmup + 5 iterations
2249        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); // 1 warmup + 3 iterations
2273        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}