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", default-features = false, features = ["runner-only"] }
63//! ```
64
65use serde::{Deserialize, Serialize};
66use std::cell::RefCell;
67use std::time::{Duration, Instant};
68use thiserror::Error;
69
70/// Benchmark specification defining what and how to benchmark.
71///
72/// Contains the benchmark name, number of measurement iterations, and
73/// warmup iterations to perform before measuring.
74///
75/// # Example
76///
77/// ```
78/// use mobench_sdk::timing::BenchSpec;
79///
80/// // Create a spec for 100 iterations with 10 warmup runs
81/// let spec = BenchSpec::new("sorting_benchmark", 100, 10)?;
82///
83/// assert_eq!(spec.name, "sorting_benchmark");
84/// assert_eq!(spec.iterations, 100);
85/// assert_eq!(spec.warmup, 10);
86/// # Ok::<(), mobench_sdk::timing::TimingError>(())
87/// ```
88///
89/// # Serialization
90///
91/// `BenchSpec` implements `Serialize` and `Deserialize` for JSON persistence:
92///
93/// ```
94/// use mobench_sdk::timing::BenchSpec;
95///
96/// let spec = BenchSpec {
97///     name: "my_bench".to_string(),
98///     iterations: 50,
99///     warmup: 5,
100/// };
101///
102/// let json = serde_json::to_string(&spec)?;
103/// let restored: BenchSpec = serde_json::from_str(&json)?;
104///
105/// assert_eq!(spec.name, restored.name);
106/// # Ok::<(), serde_json::Error>(())
107/// ```
108#[derive(Clone, Debug, Serialize, Deserialize)]
109pub struct BenchSpec {
110    /// Name of the benchmark, typically the fully-qualified function name.
111    ///
112    /// Examples: `"my_crate::fibonacci"`, `"sorting_benchmark"`
113    pub name: String,
114
115    /// Number of iterations to measure.
116    ///
117    /// Each iteration produces one [`BenchSample`]. Must be greater than zero.
118    pub iterations: u32,
119
120    /// Number of warmup iterations before measurement.
121    ///
122    /// Warmup iterations are not recorded. They allow CPU caches to warm
123    /// and any JIT compilation to complete. Can be zero.
124    pub warmup: u32,
125}
126
127impl BenchSpec {
128    /// Creates a new benchmark specification.
129    ///
130    /// # Arguments
131    ///
132    /// * `name` - Name identifier for the benchmark
133    /// * `iterations` - Number of measured iterations (must be > 0)
134    /// * `warmup` - Number of warmup iterations (can be 0)
135    ///
136    /// # Errors
137    ///
138    /// Returns [`TimingError::NoIterations`] if `iterations` is zero.
139    ///
140    /// # Example
141    ///
142    /// ```
143    /// use mobench_sdk::timing::BenchSpec;
144    ///
145    /// let spec = BenchSpec::new("test", 100, 10)?;
146    /// assert_eq!(spec.iterations, 100);
147    ///
148    /// // Zero iterations is an error
149    /// let err = BenchSpec::new("test", 0, 10);
150    /// assert!(err.is_err());
151    /// # Ok::<(), mobench_sdk::timing::TimingError>(())
152    /// ```
153    pub fn new(name: impl Into<String>, iterations: u32, warmup: u32) -> Result<Self, TimingError> {
154        if iterations == 0 {
155            return Err(TimingError::NoIterations { count: iterations });
156        }
157
158        Ok(Self {
159            name: name.into(),
160            iterations,
161            warmup,
162        })
163    }
164}
165
166/// A single timing sample from a benchmark iteration.
167///
168/// Contains the elapsed time in nanoseconds for one execution of the
169/// benchmark function.
170///
171/// # Example
172///
173/// ```
174/// use mobench_sdk::timing::BenchSample;
175///
176/// let sample = BenchSample { duration_ns: 1_500_000 };
177///
178/// // Convert to milliseconds
179/// let ms = sample.duration_ns as f64 / 1_000_000.0;
180/// assert_eq!(ms, 1.5);
181/// ```
182#[derive(Clone, Debug, Serialize, Deserialize)]
183pub struct BenchSample {
184    /// Duration of the iteration in nanoseconds.
185    ///
186    /// Measured using [`std::time::Instant`] for monotonic, high-resolution timing.
187    pub duration_ns: u64,
188}
189
190impl BenchSample {
191    /// Creates a sample from a [`Duration`].
192    fn from_duration(duration: Duration) -> Self {
193        Self {
194            duration_ns: duration.as_nanos() as u64,
195        }
196    }
197}
198
199/// Complete benchmark report with all timing samples.
200///
201/// Contains the original specification and all collected samples.
202/// Can be serialized to JSON for storage or transmission.
203///
204/// # Example
205///
206/// ```
207/// use mobench_sdk::timing::{BenchSpec, run_closure};
208///
209/// let spec = BenchSpec::new("example", 50, 5)?;
210/// let report = run_closure(spec, || {
211///     std::hint::black_box(42);
212///     Ok(())
213/// })?;
214///
215/// // Calculate statistics
216/// let samples: Vec<u64> = report.samples.iter()
217///     .map(|s| s.duration_ns)
218///     .collect();
219///
220/// let min = samples.iter().min().unwrap();
221/// let max = samples.iter().max().unwrap();
222/// let mean = samples.iter().sum::<u64>() / samples.len() as u64;
223///
224/// println!("Min: {} ns, Max: {} ns, Mean: {} ns", min, max, mean);
225/// # Ok::<(), mobench_sdk::timing::TimingError>(())
226/// ```
227#[derive(Clone, Debug, Serialize, Deserialize)]
228pub struct BenchReport {
229    /// The specification used for this benchmark run.
230    pub spec: BenchSpec,
231
232    /// All collected timing samples.
233    ///
234    /// The length equals `spec.iterations`. Samples are in execution order.
235    pub samples: Vec<BenchSample>,
236
237    /// Optional semantic phase timings captured during measured iterations.
238    pub phases: Vec<SemanticPhase>,
239
240    /// Optional resource usage scoped to measured iterations.
241    #[serde(default)]
242    pub resource_usage: Option<BenchResourceUsage>,
243}
244
245/// Resource usage captured during the measured portion of a benchmark run.
246#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
247pub struct BenchResourceUsage {
248    /// Median process CPU time per measured iteration, in milliseconds.
249    pub cpu_median_ms: Option<u64>,
250    /// Peak process memory above the measurement baseline, in kilobytes.
251    pub peak_memory_kb: Option<u64>,
252}
253
254impl BenchResourceUsage {
255    #[must_use]
256    pub fn is_empty(&self) -> bool {
257        self.cpu_median_ms.is_none() && self.peak_memory_kb.is_none()
258    }
259}
260
261impl BenchReport {
262    /// Returns the mean (average) duration in nanoseconds.
263    #[must_use]
264    pub fn mean_ns(&self) -> f64 {
265        if self.samples.is_empty() {
266            return 0.0;
267        }
268        let sum: u64 = self.samples.iter().map(|s| s.duration_ns).sum();
269        sum as f64 / self.samples.len() as f64
270    }
271
272    /// Returns the median duration in nanoseconds.
273    #[must_use]
274    pub fn median_ns(&self) -> f64 {
275        if self.samples.is_empty() {
276            return 0.0;
277        }
278        let mut sorted: Vec<u64> = self.samples.iter().map(|s| s.duration_ns).collect();
279        sorted.sort_unstable();
280        let len = sorted.len();
281        if len % 2 == 0 {
282            (sorted[len / 2 - 1] + sorted[len / 2]) as f64 / 2.0
283        } else {
284            sorted[len / 2] as f64
285        }
286    }
287
288    /// Returns the standard deviation in nanoseconds (sample std dev, n-1).
289    #[must_use]
290    pub fn std_dev_ns(&self) -> f64 {
291        if self.samples.len() < 2 {
292            return 0.0;
293        }
294        let mean = self.mean_ns();
295        let variance: f64 = self
296            .samples
297            .iter()
298            .map(|s| {
299                let diff = s.duration_ns as f64 - mean;
300                diff * diff
301            })
302            .sum::<f64>()
303            / (self.samples.len() - 1) as f64;
304        variance.sqrt()
305    }
306
307    /// Returns the given percentile (0-100) in nanoseconds.
308    #[must_use]
309    pub fn percentile_ns(&self, p: f64) -> f64 {
310        if self.samples.is_empty() {
311            return 0.0;
312        }
313        let mut sorted: Vec<u64> = self.samples.iter().map(|s| s.duration_ns).collect();
314        sorted.sort_unstable();
315        let p = p.clamp(0.0, 100.0) / 100.0;
316        let index = (p * (sorted.len() - 1) as f64).round() as usize;
317        sorted[index.min(sorted.len() - 1)] as f64
318    }
319
320    /// Returns the minimum duration in nanoseconds.
321    #[must_use]
322    pub fn min_ns(&self) -> u64 {
323        self.samples
324            .iter()
325            .map(|s| s.duration_ns)
326            .min()
327            .unwrap_or(0)
328    }
329
330    /// Returns the maximum duration in nanoseconds.
331    #[must_use]
332    pub fn max_ns(&self) -> u64 {
333        self.samples
334            .iter()
335            .map(|s| s.duration_ns)
336            .max()
337            .unwrap_or(0)
338    }
339
340    /// Returns a statistical summary of the benchmark results.
341    #[must_use]
342    pub fn summary(&self) -> BenchSummary {
343        BenchSummary {
344            name: self.spec.name.clone(),
345            iterations: self.samples.len() as u32,
346            warmup: self.spec.warmup,
347            mean_ns: self.mean_ns(),
348            median_ns: self.median_ns(),
349            std_dev_ns: self.std_dev_ns(),
350            min_ns: self.min_ns(),
351            max_ns: self.max_ns(),
352            p95_ns: self.percentile_ns(95.0),
353            p99_ns: self.percentile_ns(99.0),
354        }
355    }
356}
357
358/// Statistical summary of benchmark results.
359#[derive(Clone, Debug, Serialize, Deserialize)]
360pub struct BenchSummary {
361    /// Name of the benchmark.
362    pub name: String,
363    /// Number of measured iterations.
364    pub iterations: u32,
365    /// Number of warmup iterations.
366    pub warmup: u32,
367    /// Mean duration in nanoseconds.
368    pub mean_ns: f64,
369    /// Median duration in nanoseconds.
370    pub median_ns: f64,
371    /// Standard deviation in nanoseconds.
372    pub std_dev_ns: f64,
373    /// Minimum duration in nanoseconds.
374    pub min_ns: u64,
375    /// Maximum duration in nanoseconds.
376    pub max_ns: u64,
377    /// 95th percentile in nanoseconds.
378    pub p95_ns: f64,
379    /// 99th percentile in nanoseconds.
380    pub p99_ns: f64,
381}
382
383/// Flat semantic phase timing captured during a benchmark run.
384#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
385pub struct SemanticPhase {
386    pub name: String,
387    pub duration_ns: u64,
388}
389
390#[derive(Default)]
391struct SemanticPhaseCollector {
392    enabled: bool,
393    depth: usize,
394    phases: Vec<SemanticPhase>,
395}
396
397impl SemanticPhaseCollector {
398    fn reset(&mut self) {
399        self.enabled = false;
400        self.depth = 0;
401        self.phases.clear();
402    }
403
404    fn begin_measurement(&mut self) {
405        self.reset();
406        self.enabled = true;
407    }
408
409    fn finish(&mut self) -> Vec<SemanticPhase> {
410        self.enabled = false;
411        self.depth = 0;
412        std::mem::take(&mut self.phases)
413    }
414
415    fn enter_phase(&mut self) -> Option<bool> {
416        if !self.enabled {
417            return None;
418        }
419        let top_level = self.depth == 0;
420        self.depth += 1;
421        Some(top_level)
422    }
423
424    fn exit_phase(&mut self, name: &str, top_level: bool, elapsed: Duration) {
425        self.depth = self.depth.saturating_sub(1);
426        if !self.enabled || !top_level {
427            return;
428        }
429
430        let duration_ns = elapsed.as_nanos().min(u128::from(u64::MAX)) as u64;
431        if let Some(phase) = self.phases.iter_mut().find(|phase| phase.name == name) {
432            phase.duration_ns = phase.duration_ns.saturating_add(duration_ns);
433        } else {
434            self.phases.push(SemanticPhase {
435                name: name.to_string(),
436                duration_ns,
437            });
438        }
439    }
440}
441
442#[derive(Default)]
443struct ResourceUsageCollector {
444    enabled: bool,
445    current_iteration_cpu_start_ms: Option<u64>,
446    cpu_samples_ms: Vec<u64>,
447    baseline_memory_kb: Option<u64>,
448    peak_memory_kb: Option<u64>,
449}
450
451impl ResourceUsageCollector {
452    fn reset(&mut self) {
453        self.enabled = false;
454        self.current_iteration_cpu_start_ms = None;
455        self.cpu_samples_ms.clear();
456        self.baseline_memory_kb = None;
457        self.peak_memory_kb = None;
458    }
459
460    fn begin_measurement(&mut self) {
461        self.reset();
462        self.enabled = true;
463        self.refresh_baseline();
464    }
465
466    fn refresh_baseline(&mut self) {
467        if !self.enabled {
468            return;
469        }
470
471        self.current_iteration_cpu_start_ms = None;
472        self.baseline_memory_kb = current_process_memory_kb();
473        if self.baseline_memory_kb.is_some() {
474            self.peak_memory_kb.get_or_insert(0);
475        }
476    }
477
478    fn begin_iteration(&mut self) {
479        if !self.enabled {
480            return;
481        }
482
483        self.current_iteration_cpu_start_ms = current_process_cpu_ms();
484    }
485
486    fn sample(&mut self) {
487        if !self.enabled {
488            return;
489        }
490
491        let Some(baseline_memory_kb) = self.baseline_memory_kb else {
492            return;
493        };
494        let Some(current_memory_kb) = current_process_memory_kb() else {
495            return;
496        };
497        let current_peak_kb = current_memory_kb.saturating_sub(baseline_memory_kb);
498
499        match self.peak_memory_kb {
500            Some(existing_peak_kb) if existing_peak_kb >= current_peak_kb => {}
501            _ => self.peak_memory_kb = Some(current_peak_kb),
502        }
503    }
504
505    fn end_iteration(&mut self) {
506        if !self.enabled {
507            return;
508        }
509
510        let Some(start_cpu_ms) = self.current_iteration_cpu_start_ms.take() else {
511            return;
512        };
513        let Some(end_cpu_ms) = current_process_cpu_ms() else {
514            return;
515        };
516        if end_cpu_ms < start_cpu_ms {
517            return;
518        }
519
520        self.cpu_samples_ms.push(end_cpu_ms - start_cpu_ms);
521    }
522
523    fn finish(&mut self) -> Option<BenchResourceUsage> {
524        self.enabled = false;
525        let cpu_median_ms = if self.cpu_samples_ms.iter().any(|sample_ms| *sample_ms > 0) {
526            median_u64(&self.cpu_samples_ms)
527        } else {
528            None
529        };
530        let resource_usage = Some(BenchResourceUsage {
531            cpu_median_ms,
532            peak_memory_kb: self.peak_memory_kb,
533        });
534        self.current_iteration_cpu_start_ms = None;
535        self.cpu_samples_ms.clear();
536        self.baseline_memory_kb = None;
537        self.peak_memory_kb = None;
538        resource_usage.filter(|usage| !usage.is_empty())
539    }
540}
541
542thread_local! {
543    static SEMANTIC_PHASE_COLLECTOR: RefCell<SemanticPhaseCollector> =
544        RefCell::new(SemanticPhaseCollector::default());
545    static RESOURCE_USAGE_COLLECTOR: RefCell<ResourceUsageCollector> =
546        RefCell::new(ResourceUsageCollector::default());
547}
548
549struct SemanticPhaseGuard {
550    name: String,
551    started_at: Option<Instant>,
552    top_level: bool,
553}
554
555impl Drop for SemanticPhaseGuard {
556    fn drop(&mut self) {
557        let Some(started_at) = self.started_at else {
558            return;
559        };
560
561        let elapsed = started_at.elapsed();
562        SEMANTIC_PHASE_COLLECTOR.with(|collector| {
563            collector
564                .borrow_mut()
565                .exit_phase(&self.name, self.top_level, elapsed);
566        });
567    }
568}
569
570fn reset_semantic_phase_collection() {
571    SEMANTIC_PHASE_COLLECTOR.with(|collector| collector.borrow_mut().reset());
572}
573
574fn begin_semantic_phase_collection() {
575    SEMANTIC_PHASE_COLLECTOR.with(|collector| collector.borrow_mut().begin_measurement());
576}
577
578fn finish_semantic_phase_collection() -> Vec<SemanticPhase> {
579    SEMANTIC_PHASE_COLLECTOR.with(|collector| collector.borrow_mut().finish())
580}
581
582fn reset_resource_usage_collection() {
583    RESOURCE_USAGE_COLLECTOR.with(|collector| collector.borrow_mut().reset());
584}
585
586fn begin_resource_usage_collection() {
587    RESOURCE_USAGE_COLLECTOR.with(|collector| collector.borrow_mut().begin_measurement());
588}
589
590fn refresh_resource_usage_baseline() {
591    RESOURCE_USAGE_COLLECTOR.with(|collector| collector.borrow_mut().refresh_baseline());
592}
593
594fn begin_resource_usage_iteration() {
595    RESOURCE_USAGE_COLLECTOR.with(|collector| collector.borrow_mut().begin_iteration());
596}
597
598fn sample_resource_usage() {
599    RESOURCE_USAGE_COLLECTOR.with(|collector| collector.borrow_mut().sample());
600}
601
602fn end_resource_usage_iteration() {
603    RESOURCE_USAGE_COLLECTOR.with(|collector| collector.borrow_mut().end_iteration());
604}
605
606fn finish_resource_usage_collection() -> Option<BenchResourceUsage> {
607    RESOURCE_USAGE_COLLECTOR.with(|collector| collector.borrow_mut().finish())
608}
609
610fn median_u64(values: &[u64]) -> Option<u64> {
611    if values.is_empty() {
612        return None;
613    }
614
615    let mut sorted = values.to_vec();
616    sorted.sort_unstable();
617    let len = sorted.len();
618    if len % 2 == 0 {
619        Some(((u128::from(sorted[len / 2 - 1]) + u128::from(sorted[len / 2])) / 2) as u64)
620    } else {
621        Some(sorted[len / 2])
622    }
623}
624
625/// Records a flat semantic phase when called inside an active benchmark measurement loop.
626///
627/// Phases are aggregated across measured iterations and ignored during warmup/setup.
628/// Nested phases are intentionally collapsed in v1 to keep the output flat.
629pub fn profile_phase<T>(name: &str, f: impl FnOnce() -> T) -> T {
630    sample_resource_usage();
631    let guard = SEMANTIC_PHASE_COLLECTOR.with(|collector| {
632        let mut collector = collector.borrow_mut();
633        match collector.enter_phase() {
634            Some(top_level) => SemanticPhaseGuard {
635                name: name.to_string(),
636                started_at: Some(Instant::now()),
637                top_level,
638            },
639            None => SemanticPhaseGuard {
640                name: String::new(),
641                started_at: None,
642                top_level: false,
643            },
644        }
645    });
646
647    let result = f();
648    drop(guard);
649    sample_resource_usage();
650    result
651}
652
653/// Errors that can occur during benchmark execution.
654///
655/// # Example
656///
657/// ```
658/// use mobench_sdk::timing::{BenchSpec, TimingError};
659///
660/// // Zero iterations produces an error
661/// let result = BenchSpec::new("test", 0, 10);
662/// assert!(matches!(result, Err(TimingError::NoIterations { .. })));
663/// ```
664#[derive(Debug, Error)]
665pub enum TimingError {
666    /// The iteration count was zero or invalid.
667    ///
668    /// At least one iteration is required to produce a measurement.
669    /// The error includes the actual value provided for diagnostic purposes.
670    #[error("iterations must be greater than zero (got {count}). Minimum recommended: 10")]
671    NoIterations {
672        /// The invalid iteration count that was provided.
673        count: u32,
674    },
675
676    /// The benchmark function failed during execution.
677    ///
678    /// Contains a description of the failure.
679    #[error("benchmark function failed: {0}")]
680    Execution(String),
681}
682
683/// Runs a benchmark by executing a closure repeatedly.
684///
685/// This is the core benchmarking function. It:
686///
687/// 1. Executes the closure `spec.warmup` times without recording
688/// 2. Executes the closure `spec.iterations` times, recording each duration
689/// 3. Returns a [`BenchReport`] with all samples
690///
691/// # Arguments
692///
693/// * `spec` - Benchmark configuration specifying iterations and warmup
694/// * `f` - Closure to benchmark; must return `Result<(), TimingError>`
695///
696/// # Returns
697///
698/// A [`BenchReport`] containing all timing samples, or a [`TimingError`] if
699/// the benchmark fails.
700///
701/// # Example
702///
703/// ```
704/// use mobench_sdk::timing::{BenchSpec, run_closure, TimingError};
705///
706/// let spec = BenchSpec::new("sum_benchmark", 100, 10)?;
707///
708/// let report = run_closure(spec, || {
709///     let sum: u64 = (0..1000).sum();
710///     std::hint::black_box(sum);
711///     Ok(())
712/// })?;
713///
714/// assert_eq!(report.samples.len(), 100);
715///
716/// // Calculate mean duration
717/// let total_ns: u64 = report.samples.iter().map(|s| s.duration_ns).sum();
718/// let mean_ns = total_ns / report.samples.len() as u64;
719/// println!("Mean: {} ns", mean_ns);
720/// # Ok::<(), TimingError>(())
721/// ```
722///
723/// # Error Handling
724///
725/// If the closure returns an error, the benchmark stops immediately:
726///
727/// ```
728/// use mobench_sdk::timing::{BenchSpec, run_closure, TimingError};
729///
730/// let spec = BenchSpec::new("failing_bench", 100, 0)?;
731///
732/// let result = run_closure(spec, || {
733///     Err(TimingError::Execution("simulated failure".into()))
734/// });
735///
736/// assert!(result.is_err());
737/// # Ok::<(), TimingError>(())
738/// ```
739///
740/// # Timing Precision
741///
742/// Uses [`std::time::Instant`] for timing, which provides monotonic,
743/// nanosecond-resolution measurements on most platforms.
744pub fn run_closure<F>(spec: BenchSpec, mut f: F) -> Result<BenchReport, TimingError>
745where
746    F: FnMut() -> Result<(), TimingError>,
747{
748    if spec.iterations == 0 {
749        return Err(TimingError::NoIterations {
750            count: spec.iterations,
751        });
752    }
753
754    reset_semantic_phase_collection();
755    reset_resource_usage_collection();
756
757    // Warmup phase - not measured
758    for _ in 0..spec.warmup {
759        f()?;
760    }
761
762    // Measurement phase
763    begin_semantic_phase_collection();
764    begin_resource_usage_collection();
765    let mut samples = Vec::with_capacity(spec.iterations as usize);
766    for _ in 0..spec.iterations {
767        begin_resource_usage_iteration();
768        sample_resource_usage();
769        let start = Instant::now();
770        if let Err(err) = f() {
771            let _ = finish_semantic_phase_collection();
772            let _ = finish_resource_usage_collection();
773            return Err(err);
774        }
775        sample_resource_usage();
776        end_resource_usage_iteration();
777        samples.push(BenchSample::from_duration(start.elapsed()));
778    }
779    let phases = finish_semantic_phase_collection();
780    let resource_usage = finish_resource_usage_collection();
781
782    Ok(BenchReport {
783        spec,
784        samples,
785        phases,
786        resource_usage,
787    })
788}
789
790/// Runs a benchmark with setup that executes once before all iterations.
791///
792/// The setup function is called once before timing begins, then the benchmark
793/// runs multiple times using a reference to the setup result. This is useful
794/// for expensive initialization that shouldn't be included in timing.
795///
796/// # Arguments
797///
798/// * `spec` - Benchmark configuration specifying iterations and warmup
799/// * `setup` - Function that creates the input data (called once, not timed)
800/// * `f` - Benchmark closure that receives a reference to setup result
801///
802/// # Example
803///
804/// ```ignore
805/// use mobench_sdk::timing::{BenchSpec, run_closure_with_setup};
806///
807/// fn setup_data() -> Vec<u8> {
808///     vec![0u8; 1_000_000]  // Expensive allocation not measured
809/// }
810///
811/// let spec = BenchSpec::new("hash_benchmark", 100, 10)?;
812/// let report = run_closure_with_setup(spec, setup_data, |data| {
813///     std::hint::black_box(compute_hash(data));
814///     Ok(())
815/// })?;
816/// ```
817pub fn run_closure_with_setup<S, T, F>(
818    spec: BenchSpec,
819    setup: S,
820    mut f: F,
821) -> Result<BenchReport, TimingError>
822where
823    S: FnOnce() -> T,
824    F: FnMut(&T) -> Result<(), TimingError>,
825{
826    if spec.iterations == 0 {
827        return Err(TimingError::NoIterations {
828            count: spec.iterations,
829        });
830    }
831
832    reset_semantic_phase_collection();
833    reset_resource_usage_collection();
834
835    // Setup phase - not timed
836    let input = setup();
837
838    // Warmup phase - not recorded
839    for _ in 0..spec.warmup {
840        f(&input)?;
841    }
842
843    // Measurement phase
844    begin_semantic_phase_collection();
845    begin_resource_usage_collection();
846    let mut samples = Vec::with_capacity(spec.iterations as usize);
847    for _ in 0..spec.iterations {
848        begin_resource_usage_iteration();
849        sample_resource_usage();
850        let start = Instant::now();
851        if let Err(err) = f(&input) {
852            let _ = finish_semantic_phase_collection();
853            let _ = finish_resource_usage_collection();
854            return Err(err);
855        }
856        sample_resource_usage();
857        end_resource_usage_iteration();
858        samples.push(BenchSample::from_duration(start.elapsed()));
859    }
860    let phases = finish_semantic_phase_collection();
861    let resource_usage = finish_resource_usage_collection();
862
863    Ok(BenchReport {
864        spec,
865        samples,
866        phases,
867        resource_usage,
868    })
869}
870
871/// Runs a benchmark with per-iteration setup.
872///
873/// Setup runs before each iteration and is not timed. The benchmark takes
874/// ownership of the setup result, making this suitable for benchmarks that
875/// mutate their input (e.g., sorting).
876///
877/// # Arguments
878///
879/// * `spec` - Benchmark configuration specifying iterations and warmup
880/// * `setup` - Function that creates fresh input for each iteration (not timed)
881/// * `f` - Benchmark closure that takes ownership of setup result
882///
883/// # Example
884///
885/// ```ignore
886/// use mobench_sdk::timing::{BenchSpec, run_closure_with_setup_per_iter};
887///
888/// fn generate_random_vec() -> Vec<i32> {
889///     (0..1000).map(|_| rand::random()).collect()
890/// }
891///
892/// let spec = BenchSpec::new("sort_benchmark", 100, 10)?;
893/// let report = run_closure_with_setup_per_iter(spec, generate_random_vec, |mut data| {
894///     data.sort();
895///     std::hint::black_box(data);
896///     Ok(())
897/// })?;
898/// ```
899pub fn run_closure_with_setup_per_iter<S, T, F>(
900    spec: BenchSpec,
901    mut setup: S,
902    mut f: F,
903) -> Result<BenchReport, TimingError>
904where
905    S: FnMut() -> T,
906    F: FnMut(T) -> Result<(), TimingError>,
907{
908    if spec.iterations == 0 {
909        return Err(TimingError::NoIterations {
910            count: spec.iterations,
911        });
912    }
913
914    reset_semantic_phase_collection();
915    reset_resource_usage_collection();
916
917    // Warmup phase
918    for _ in 0..spec.warmup {
919        let input = setup();
920        f(input)?;
921    }
922
923    // Measurement phase
924    begin_semantic_phase_collection();
925    begin_resource_usage_collection();
926    let mut samples = Vec::with_capacity(spec.iterations as usize);
927    for _ in 0..spec.iterations {
928        let input = setup(); // Not timed
929
930        refresh_resource_usage_baseline();
931        begin_resource_usage_iteration();
932        sample_resource_usage();
933        let start = Instant::now();
934        if let Err(err) = f(input) {
935            let _ = finish_semantic_phase_collection();
936            let _ = finish_resource_usage_collection();
937            return Err(err);
938        }
939        sample_resource_usage();
940        end_resource_usage_iteration();
941        samples.push(BenchSample::from_duration(start.elapsed()));
942    }
943    let phases = finish_semantic_phase_collection();
944    let resource_usage = finish_resource_usage_collection();
945
946    Ok(BenchReport {
947        spec,
948        samples,
949        phases,
950        resource_usage,
951    })
952}
953
954/// Runs a benchmark with setup and teardown.
955///
956/// Setup runs once before all iterations, teardown runs once after all
957/// iterations complete. Neither is included in timing.
958///
959/// # Arguments
960///
961/// * `spec` - Benchmark configuration specifying iterations and warmup
962/// * `setup` - Function that creates the input data (called once, not timed)
963/// * `f` - Benchmark closure that receives a reference to setup result
964/// * `teardown` - Function that cleans up the input (called once, not timed)
965///
966/// # Example
967///
968/// ```ignore
969/// use mobench_sdk::timing::{BenchSpec, run_closure_with_setup_teardown};
970///
971/// fn setup_db() -> Database { Database::connect("test.db") }
972/// fn cleanup_db(db: Database) { db.close(); std::fs::remove_file("test.db").ok(); }
973///
974/// let spec = BenchSpec::new("db_benchmark", 100, 10)?;
975/// let report = run_closure_with_setup_teardown(
976///     spec,
977///     setup_db,
978///     |db| { db.query("SELECT *"); Ok(()) },
979///     cleanup_db,
980/// )?;
981/// ```
982pub fn run_closure_with_setup_teardown<S, T, F, D>(
983    spec: BenchSpec,
984    setup: S,
985    mut f: F,
986    teardown: D,
987) -> Result<BenchReport, TimingError>
988where
989    S: FnOnce() -> T,
990    F: FnMut(&T) -> Result<(), TimingError>,
991    D: FnOnce(T),
992{
993    if spec.iterations == 0 {
994        return Err(TimingError::NoIterations {
995            count: spec.iterations,
996        });
997    }
998
999    reset_semantic_phase_collection();
1000    reset_resource_usage_collection();
1001
1002    // Setup phase - not timed
1003    let input = setup();
1004
1005    // Warmup phase
1006    for _ in 0..spec.warmup {
1007        f(&input)?;
1008    }
1009
1010    // Measurement phase
1011    begin_semantic_phase_collection();
1012    begin_resource_usage_collection();
1013    let mut samples = Vec::with_capacity(spec.iterations as usize);
1014    for _ in 0..spec.iterations {
1015        begin_resource_usage_iteration();
1016        sample_resource_usage();
1017        let start = Instant::now();
1018        if let Err(err) = f(&input) {
1019            let _ = finish_semantic_phase_collection();
1020            let _ = finish_resource_usage_collection();
1021            return Err(err);
1022        }
1023        sample_resource_usage();
1024        end_resource_usage_iteration();
1025        samples.push(BenchSample::from_duration(start.elapsed()));
1026    }
1027    let phases = finish_semantic_phase_collection();
1028    let resource_usage = finish_resource_usage_collection();
1029
1030    // Teardown phase - not timed
1031    teardown(input);
1032
1033    Ok(BenchReport {
1034        spec,
1035        samples,
1036        phases,
1037        resource_usage,
1038    })
1039}
1040
1041#[cfg(any(target_os = "ios", target_os = "macos"))]
1042fn platform_current_process_memory_kb() -> Option<u64> {
1043    unsafe extern "C" {
1044        fn proc_pid_rusage(
1045            pid: libc::c_int,
1046            flavor: libc::c_int,
1047            buffer: *mut libc::c_void,
1048        ) -> libc::c_int;
1049    }
1050
1051    let mut info = std::mem::MaybeUninit::<libc::rusage_info_v4>::zeroed();
1052    let status = unsafe {
1053        // SAFETY: We pass the current PID, a valid flavor constant for the selected
1054        // rusage_info_v4 layout, and a properly sized writable buffer.
1055        proc_pid_rusage(
1056            libc::getpid(),
1057            libc::RUSAGE_INFO_V4,
1058            info.as_mut_ptr().cast(),
1059        )
1060    };
1061    if status != 0 {
1062        return None;
1063    }
1064
1065    // SAFETY: proc_pid_rusage returned success, so the kernel initialized `info`.
1066    let info = unsafe { info.assume_init() };
1067    Some(info.ri_phys_footprint / 1024)
1068}
1069
1070#[cfg(target_os = "android")]
1071fn platform_current_process_memory_kb() -> Option<u64> {
1072    std::fs::read_to_string("/proc/self/status")
1073        .ok()
1074        .and_then(|status| parse_proc_status_memory_kb(&status))
1075}
1076
1077#[cfg(any(test, target_os = "android"))]
1078fn parse_proc_status_memory_kb(status: &str) -> Option<u64> {
1079    status.lines().find_map(|line| {
1080        let value = line.strip_prefix("VmRSS:")?;
1081        value.split_whitespace().next()?.parse().ok()
1082    })
1083}
1084
1085#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
1086fn platform_current_process_memory_kb() -> Option<u64> {
1087    None
1088}
1089
1090#[cfg(unix)]
1091fn platform_current_process_cpu_ms() -> Option<u64> {
1092    let mut usage = std::mem::MaybeUninit::<libc::rusage>::zeroed();
1093    let status = unsafe {
1094        // SAFETY: We pass a valid pointer to writable storage for `rusage` and use
1095        // the standard `RUSAGE_SELF` selector for the current process.
1096        libc::getrusage(libc::RUSAGE_SELF, usage.as_mut_ptr())
1097    };
1098    if status != 0 {
1099        return None;
1100    }
1101
1102    // SAFETY: getrusage returned success, so `usage` has been fully initialized.
1103    let usage = unsafe { usage.assume_init() };
1104    let user_ms = timeval_to_ms(usage.ru_utime)?;
1105    let system_ms = timeval_to_ms(usage.ru_stime)?;
1106    Some(user_ms.saturating_add(system_ms))
1107}
1108
1109#[cfg(not(unix))]
1110fn platform_current_process_cpu_ms() -> Option<u64> {
1111    None
1112}
1113
1114#[cfg(unix)]
1115fn timeval_to_ms(value: libc::timeval) -> Option<u64> {
1116    if value.tv_sec < 0 || value.tv_usec < 0 {
1117        return None;
1118    }
1119
1120    Some((value.tv_sec as u64).saturating_mul(1000) + (value.tv_usec as u64) / 1000)
1121}
1122
1123#[cfg(test)]
1124thread_local! {
1125    static TEST_MEMORY_SAMPLES_KB: RefCell<Option<std::collections::VecDeque<Option<u64>>>> =
1126        const { RefCell::new(None) };
1127    static TEST_CPU_SAMPLES_MS: RefCell<Option<std::collections::VecDeque<Option<u64>>>> =
1128        const { RefCell::new(None) };
1129}
1130
1131#[cfg(test)]
1132fn take_test_memory_sample_kb() -> Option<Option<u64>> {
1133    TEST_MEMORY_SAMPLES_KB.with(|samples| {
1134        samples
1135            .borrow_mut()
1136            .as_mut()
1137            .and_then(std::collections::VecDeque::pop_front)
1138    })
1139}
1140
1141#[cfg(test)]
1142fn take_test_cpu_sample_ms() -> Option<Option<u64>> {
1143    TEST_CPU_SAMPLES_MS.with(|samples| {
1144        samples
1145            .borrow_mut()
1146            .as_mut()
1147            .and_then(std::collections::VecDeque::pop_front)
1148    })
1149}
1150
1151#[cfg(test)]
1152fn set_test_memory_samples_kb<I>(samples: I)
1153where
1154    I: IntoIterator<Item = Option<u64>>,
1155{
1156    TEST_MEMORY_SAMPLES_KB.with(|state| {
1157        *state.borrow_mut() = Some(samples.into_iter().collect());
1158    });
1159}
1160
1161#[cfg(test)]
1162fn set_test_cpu_samples_ms<I>(samples: I)
1163where
1164    I: IntoIterator<Item = Option<u64>>,
1165{
1166    TEST_CPU_SAMPLES_MS.with(|state| {
1167        *state.borrow_mut() = Some(samples.into_iter().collect());
1168    });
1169}
1170
1171#[cfg(test)]
1172fn clear_test_memory_samples_kb() {
1173    TEST_MEMORY_SAMPLES_KB.with(|state| {
1174        *state.borrow_mut() = None;
1175    });
1176}
1177
1178#[cfg(test)]
1179fn clear_test_cpu_samples_ms() {
1180    TEST_CPU_SAMPLES_MS.with(|state| {
1181        *state.borrow_mut() = None;
1182    });
1183}
1184
1185fn current_process_memory_kb() -> Option<u64> {
1186    #[cfg(test)]
1187    if let Some(sample) = take_test_memory_sample_kb() {
1188        return sample;
1189    }
1190
1191    platform_current_process_memory_kb()
1192}
1193
1194fn current_process_cpu_ms() -> Option<u64> {
1195    #[cfg(test)]
1196    if let Some(sample) = take_test_cpu_sample_ms() {
1197        return sample;
1198    }
1199
1200    platform_current_process_cpu_ms()
1201}
1202
1203#[cfg(test)]
1204mod tests {
1205    use super::*;
1206
1207    struct TestMemorySamplesGuard;
1208
1209    impl Drop for TestMemorySamplesGuard {
1210        fn drop(&mut self) {
1211            clear_test_memory_samples_kb();
1212            clear_test_cpu_samples_ms();
1213            reset_resource_usage_collection();
1214        }
1215    }
1216
1217    #[test]
1218    fn runs_benchmark_collects_requested_samples() {
1219        let spec = BenchSpec::new("noop", 3, 1).unwrap();
1220        let report = run_closure(spec, || Ok(())).unwrap();
1221
1222        assert_eq!(report.samples.len(), 3);
1223        assert_eq!(report.spec.name, "noop");
1224        assert_eq!(report.spec.iterations, 3);
1225    }
1226
1227    #[test]
1228    fn rejects_zero_iterations() {
1229        let result = BenchSpec::new("test", 0, 10);
1230        assert!(matches!(
1231            result,
1232            Err(TimingError::NoIterations { count: 0 })
1233        ));
1234    }
1235
1236    #[test]
1237    fn allows_zero_warmup() {
1238        let spec = BenchSpec::new("test", 5, 0).unwrap();
1239        assert_eq!(spec.warmup, 0);
1240
1241        let report = run_closure(spec, || Ok(())).unwrap();
1242        assert_eq!(report.samples.len(), 5);
1243    }
1244
1245    #[test]
1246    fn serializes_to_json() {
1247        let spec = BenchSpec::new("test", 10, 2).unwrap();
1248        let report = run_closure(spec, || {
1249            profile_phase("prove", || std::thread::sleep(Duration::from_millis(1)));
1250            Ok(())
1251        })
1252        .unwrap();
1253
1254        let json = serde_json::to_string(&report).unwrap();
1255        let restored: BenchReport = serde_json::from_str(&json).unwrap();
1256
1257        assert_eq!(restored.spec.name, "test");
1258        assert_eq!(restored.samples.len(), 10);
1259        assert_eq!(restored.phases.len(), 1);
1260        assert_eq!(restored.phases[0].name, "prove");
1261        assert!(restored.phases[0].duration_ns > 0);
1262    }
1263
1264    #[test]
1265    fn measured_peak_memory_uses_iteration_baseline_only() {
1266        let _guard = TestMemorySamplesGuard;
1267        set_test_memory_samples_kb([
1268            Some(100),
1269            Some(104),
1270            Some(120),
1271            Some(112),
1272            Some(130),
1273        ]);
1274
1275        let spec = BenchSpec::new("mem", 2, 1).unwrap();
1276        let report = run_closure(spec, || Ok(())).unwrap();
1277
1278        assert_eq!(
1279            report.resource_usage,
1280            Some(BenchResourceUsage {
1281                cpu_median_ms: None,
1282                peak_memory_kb: Some(30),
1283            })
1284        );
1285    }
1286
1287    #[test]
1288    fn measured_peak_memory_excludes_one_time_setup_and_teardown() {
1289        let _guard = TestMemorySamplesGuard;
1290        set_test_memory_samples_kb([Some(220), Some(225), Some(250)]);
1291
1292        let spec = BenchSpec::new("mem-setup", 1, 0).unwrap();
1293        let report = run_closure_with_setup_teardown(
1294            spec,
1295            || vec![0u8; 1024],
1296            |_buffer| Ok(()),
1297            |_buffer| {},
1298        )
1299        .unwrap();
1300
1301        assert_eq!(
1302            report.resource_usage,
1303            Some(BenchResourceUsage {
1304                cpu_median_ms: None,
1305                peak_memory_kb: Some(30),
1306            })
1307        );
1308    }
1309
1310    #[test]
1311    fn measured_peak_memory_excludes_per_iteration_setup() {
1312        let _guard = TestMemorySamplesGuard;
1313        set_test_memory_samples_kb([
1314            Some(100),
1315            Some(150),
1316            Some(150),
1317            Some(160),
1318            Some(170),
1319            Some(170),
1320            Some(190),
1321        ]);
1322
1323        let spec = BenchSpec::new("mem-setup-per-iter", 2, 0).unwrap();
1324        let report = run_closure_with_setup_per_iter(spec, || vec![0u8; 1024], |_buffer| Ok(()))
1325            .unwrap();
1326
1327        assert_eq!(
1328            report.resource_usage,
1329            Some(BenchResourceUsage {
1330                cpu_median_ms: None,
1331                peak_memory_kb: Some(20),
1332            })
1333        );
1334    }
1335
1336    #[test]
1337    fn measured_peak_memory_preserves_zero_delta() {
1338        let _guard = TestMemorySamplesGuard;
1339        set_test_memory_samples_kb([Some(100), Some(100), Some(100)]);
1340
1341        let spec = BenchSpec::new("mem-zero", 1, 0).unwrap();
1342        let report = run_closure(spec, || Ok(())).unwrap();
1343
1344        assert_eq!(
1345            report.resource_usage,
1346            Some(BenchResourceUsage {
1347                cpu_median_ms: None,
1348                peak_memory_kb: Some(0),
1349            })
1350        );
1351    }
1352
1353    #[test]
1354    fn measured_cpu_median_uses_per_iteration_deltas_only() {
1355        let _guard = TestMemorySamplesGuard;
1356        set_test_memory_samples_kb([Some(100); 7]);
1357        set_test_cpu_samples_ms([
1358            Some(100),
1359            Some(106),
1360            Some(130),
1361            Some(142),
1362            Some(200),
1363            Some(219),
1364        ]);
1365
1366        let spec = BenchSpec::new("cpu", 3, 1).unwrap();
1367        let report = run_closure(spec, || Ok(())).unwrap();
1368
1369        assert_eq!(
1370            report.resource_usage,
1371            Some(BenchResourceUsage {
1372                cpu_median_ms: Some(12),
1373                peak_memory_kb: Some(0),
1374            })
1375        );
1376    }
1377
1378    #[test]
1379    fn measured_cpu_median_excludes_per_iteration_setup() {
1380        let _guard = TestMemorySamplesGuard;
1381        set_test_memory_samples_kb([Some(100); 10]);
1382        set_test_cpu_samples_ms([
1383            Some(150),
1384            Some(158),
1385            Some(210),
1386            Some(221),
1387            Some(280),
1388            Some(286),
1389        ]);
1390
1391        let spec = BenchSpec::new("cpu-setup-per-iter", 3, 0).unwrap();
1392        let report = run_closure_with_setup_per_iter(spec, || vec![0u8; 1024], |_buffer| Ok(()))
1393            .unwrap();
1394
1395        assert_eq!(
1396            report.resource_usage,
1397            Some(BenchResourceUsage {
1398                cpu_median_ms: Some(8),
1399                peak_memory_kb: Some(0),
1400            })
1401        );
1402    }
1403
1404    #[test]
1405    fn parse_proc_status_memory_kb_reads_vm_rss() {
1406        let status = "\
1407Name:\ttest\n\
1408VmPeak:\t   90304 kB\n\
1409VmRSS:\t   21537 kB\n\
1410RssAnon:\t   10144 kB\n";
1411
1412        assert_eq!(parse_proc_status_memory_kb(status), Some(21_537));
1413    }
1414
1415    #[test]
1416    fn parse_proc_status_memory_kb_returns_none_without_vm_rss() {
1417        let status = "\
1418Name:\ttest\n\
1419VmPeak:\t   90304 kB\n\
1420VmHWM:\t   24000 kB\n";
1421
1422        assert_eq!(parse_proc_status_memory_kb(status), None);
1423    }
1424
1425    #[test]
1426    fn profile_phase_records_only_measured_iterations() {
1427        let spec = BenchSpec::new("semantic", 2, 1).unwrap();
1428        let mut call_index = 0u32;
1429        let report = run_closure(spec, || {
1430            let phase_name = if call_index == 0 {
1431                "warmup-only"
1432            } else {
1433                "prove"
1434            };
1435            call_index += 1;
1436            profile_phase(phase_name, || std::thread::sleep(Duration::from_millis(1)));
1437            Ok(())
1438        })
1439        .unwrap();
1440
1441        assert!(
1442            !report
1443                .phases
1444                .iter()
1445                .any(|phase| phase.name == "warmup-only"),
1446            "warmup phases should not be recorded"
1447        );
1448        let prove = report
1449            .phases
1450            .iter()
1451            .find(|phase| phase.name == "prove")
1452            .expect("prove phase");
1453        assert!(prove.duration_ns > 0);
1454    }
1455
1456    #[test]
1457    fn profile_phase_keeps_the_v1_model_flat() {
1458        let spec = BenchSpec::new("semantic-flat", 1, 0).unwrap();
1459        let report = run_closure(spec, || {
1460            profile_phase("prove", || {
1461                std::thread::sleep(Duration::from_millis(1));
1462                profile_phase("inner", || std::thread::sleep(Duration::from_millis(1)));
1463            });
1464            Ok(())
1465        })
1466        .unwrap();
1467
1468        assert!(report.phases.iter().any(|phase| phase.name == "prove"));
1469        assert!(
1470            !report.phases.iter().any(|phase| phase.name == "inner"),
1471            "nested phases should not create a second flat phase entry"
1472        );
1473    }
1474
1475    #[test]
1476    fn run_with_setup_calls_setup_once() {
1477        use std::sync::atomic::{AtomicU32, Ordering};
1478
1479        static SETUP_COUNT: AtomicU32 = AtomicU32::new(0);
1480        static RUN_COUNT: AtomicU32 = AtomicU32::new(0);
1481
1482        let spec = BenchSpec::new("test", 5, 2).unwrap();
1483        let report = run_closure_with_setup(
1484            spec,
1485            || {
1486                SETUP_COUNT.fetch_add(1, Ordering::SeqCst);
1487                vec![1, 2, 3]
1488            },
1489            |data| {
1490                RUN_COUNT.fetch_add(1, Ordering::SeqCst);
1491                std::hint::black_box(data.len());
1492                Ok(())
1493            },
1494        )
1495        .unwrap();
1496
1497        assert_eq!(SETUP_COUNT.load(Ordering::SeqCst), 1); // Setup called once
1498        assert_eq!(RUN_COUNT.load(Ordering::SeqCst), 7); // 2 warmup + 5 iterations
1499        assert_eq!(report.samples.len(), 5);
1500    }
1501
1502    #[test]
1503    fn run_with_setup_per_iter_calls_setup_each_time() {
1504        use std::sync::atomic::{AtomicU32, Ordering};
1505
1506        static SETUP_COUNT: AtomicU32 = AtomicU32::new(0);
1507
1508        let spec = BenchSpec::new("test", 3, 1).unwrap();
1509        let report = run_closure_with_setup_per_iter(
1510            spec,
1511            || {
1512                SETUP_COUNT.fetch_add(1, Ordering::SeqCst);
1513                vec![1, 2, 3]
1514            },
1515            |data| {
1516                std::hint::black_box(data);
1517                Ok(())
1518            },
1519        )
1520        .unwrap();
1521
1522        assert_eq!(SETUP_COUNT.load(Ordering::SeqCst), 4); // 1 warmup + 3 iterations
1523        assert_eq!(report.samples.len(), 3);
1524    }
1525
1526    #[test]
1527    fn run_with_setup_teardown_calls_both() {
1528        use std::sync::atomic::{AtomicU32, Ordering};
1529
1530        static SETUP_COUNT: AtomicU32 = AtomicU32::new(0);
1531        static TEARDOWN_COUNT: AtomicU32 = AtomicU32::new(0);
1532
1533        let spec = BenchSpec::new("test", 3, 1).unwrap();
1534        let report = run_closure_with_setup_teardown(
1535            spec,
1536            || {
1537                SETUP_COUNT.fetch_add(1, Ordering::SeqCst);
1538                "resource"
1539            },
1540            |_resource| Ok(()),
1541            |_resource| {
1542                TEARDOWN_COUNT.fetch_add(1, Ordering::SeqCst);
1543            },
1544        )
1545        .unwrap();
1546
1547        assert_eq!(SETUP_COUNT.load(Ordering::SeqCst), 1);
1548        assert_eq!(TEARDOWN_COUNT.load(Ordering::SeqCst), 1);
1549        assert_eq!(report.samples.len(), 3);
1550    }
1551}