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::sync::{
68    Arc,
69    atomic::{AtomicBool, AtomicU64, Ordering},
70    mpsc,
71};
72use std::thread::{self, JoinHandle};
73use std::time::{Duration, Instant};
74use thiserror::Error;
75
76/// Benchmark specification defining what and how to benchmark.
77///
78/// Contains the benchmark name, number of measurement iterations, and
79/// warmup iterations to perform before measuring.
80///
81/// # Example
82///
83/// ```
84/// use mobench_sdk::timing::BenchSpec;
85///
86/// // Create a spec for 100 iterations with 10 warmup runs
87/// let spec = BenchSpec::new("sorting_benchmark", 100, 10)?;
88///
89/// assert_eq!(spec.name, "sorting_benchmark");
90/// assert_eq!(spec.iterations, 100);
91/// assert_eq!(spec.warmup, 10);
92/// # Ok::<(), mobench_sdk::timing::TimingError>(())
93/// ```
94///
95/// # Serialization
96///
97/// `BenchSpec` implements `Serialize` and `Deserialize` for JSON persistence:
98///
99/// ```
100/// use mobench_sdk::timing::BenchSpec;
101///
102/// let spec = BenchSpec {
103///     name: "my_bench".to_string(),
104///     iterations: 50,
105///     warmup: 5,
106/// };
107///
108/// let json = serde_json::to_string(&spec)?;
109/// let restored: BenchSpec = serde_json::from_str(&json)?;
110///
111/// assert_eq!(spec.name, restored.name);
112/// # Ok::<(), serde_json::Error>(())
113/// ```
114#[derive(Clone, Debug, Serialize, Deserialize)]
115pub struct BenchSpec {
116    /// Name of the benchmark, typically the fully-qualified function name.
117    ///
118    /// Examples: `"my_crate::fibonacci"`, `"sorting_benchmark"`
119    pub name: String,
120
121    /// Number of iterations to measure.
122    ///
123    /// Each iteration produces one [`BenchSample`]. Must be greater than zero.
124    pub iterations: u32,
125
126    /// Number of warmup iterations before measurement.
127    ///
128    /// Warmup iterations are not recorded. They allow CPU caches to warm
129    /// and any JIT compilation to complete. Can be zero.
130    pub warmup: u32,
131}
132
133impl BenchSpec {
134    /// Creates a new benchmark specification.
135    ///
136    /// # Arguments
137    ///
138    /// * `name` - Name identifier for the benchmark
139    /// * `iterations` - Number of measured iterations (must be > 0)
140    /// * `warmup` - Number of warmup iterations (can be 0)
141    ///
142    /// # Errors
143    ///
144    /// Returns [`TimingError::NoIterations`] if `iterations` is zero.
145    ///
146    /// # Example
147    ///
148    /// ```
149    /// use mobench_sdk::timing::BenchSpec;
150    ///
151    /// let spec = BenchSpec::new("test", 100, 10)?;
152    /// assert_eq!(spec.iterations, 100);
153    ///
154    /// // Zero iterations is an error
155    /// let err = BenchSpec::new("test", 0, 10);
156    /// assert!(err.is_err());
157    /// # Ok::<(), mobench_sdk::timing::TimingError>(())
158    /// ```
159    pub fn new(name: impl Into<String>, iterations: u32, warmup: u32) -> Result<Self, TimingError> {
160        if iterations == 0 {
161            return Err(TimingError::NoIterations { count: iterations });
162        }
163
164        Ok(Self {
165            name: name.into(),
166            iterations,
167            warmup,
168        })
169    }
170}
171
172/// A single timing sample from a benchmark iteration.
173///
174/// Contains the elapsed time in nanoseconds for one execution of the
175/// benchmark function.
176///
177/// # Example
178///
179/// ```
180/// use mobench_sdk::timing::BenchSample;
181///
182/// let sample = BenchSample {
183///     duration_ns: 1_500_000,
184///     ..Default::default()
185/// };
186///
187/// // Convert to milliseconds
188/// let ms = sample.duration_ns as f64 / 1_000_000.0;
189/// assert_eq!(ms, 1.5);
190/// ```
191#[derive(Clone, Debug, Default, Serialize, Deserialize)]
192pub struct BenchSample {
193    /// Duration of the iteration in nanoseconds.
194    ///
195    /// Measured using [`std::time::Instant`] for monotonic, high-resolution timing.
196    pub duration_ns: u64,
197
198    /// CPU time consumed by the measured iteration in milliseconds.
199    ///
200    /// This is captured around the measured benchmark closure only and excludes
201    /// warmup, setup, teardown, and report generation overhead.
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    pub cpu_time_ms: Option<u64>,
204
205    /// Peak memory growth during the measured iteration in kilobytes.
206    ///
207    /// Values are baseline-adjusted immediately before the measured closure
208    /// enters so harness footprint is not counted.
209    #[serde(default, skip_serializing_if = "Option::is_none")]
210    pub peak_memory_kb: Option<u64>,
211}
212
213impl BenchSample {
214    fn from_measurement(duration: Duration, resources: IterationResourceUsage) -> Self {
215        Self {
216            duration_ns: duration.as_nanos() as u64,
217            cpu_time_ms: resources.cpu_time_ms,
218            peak_memory_kb: resources.peak_memory_kb,
219        }
220    }
221}
222
223/// Complete benchmark report with all timing samples.
224///
225/// Contains the original specification and all collected samples.
226/// Can be serialized to JSON for storage or transmission.
227///
228/// # Example
229///
230/// ```
231/// use mobench_sdk::timing::{BenchSpec, run_closure};
232///
233/// let spec = BenchSpec::new("example", 50, 5)?;
234/// let report = run_closure(spec, || {
235///     std::hint::black_box(42);
236///     Ok(())
237/// })?;
238///
239/// // Calculate statistics
240/// let samples: Vec<u64> = report.samples.iter()
241///     .map(|s| s.duration_ns)
242///     .collect();
243///
244/// let min = samples.iter().min().unwrap();
245/// let max = samples.iter().max().unwrap();
246/// let mean = samples.iter().sum::<u64>() / samples.len() as u64;
247///
248/// println!("Min: {} ns, Max: {} ns, Mean: {} ns", min, max, mean);
249/// # Ok::<(), mobench_sdk::timing::TimingError>(())
250/// ```
251#[derive(Clone, Debug, Serialize, Deserialize)]
252pub struct BenchReport {
253    /// The specification used for this benchmark run.
254    pub spec: BenchSpec,
255
256    /// All collected timing samples.
257    ///
258    /// The length equals `spec.iterations`. Samples are in execution order.
259    pub samples: Vec<BenchSample>,
260
261    /// Optional semantic phase timings captured during measured iterations.
262    pub phases: Vec<SemanticPhase>,
263
264    /// Exact harness timeline spans in execution order.
265    pub timeline: Vec<HarnessTimelineSpan>,
266}
267
268#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
269pub struct HarnessTimelineSpan {
270    pub phase: String,
271    pub start_offset_ns: u64,
272    pub end_offset_ns: u64,
273    pub iteration: Option<u32>,
274}
275
276impl BenchReport {
277    /// Returns the mean (average) duration in nanoseconds.
278    #[must_use]
279    pub fn mean_ns(&self) -> f64 {
280        if self.samples.is_empty() {
281            return 0.0;
282        }
283        let sum: u64 = self.samples.iter().map(|s| s.duration_ns).sum();
284        sum as f64 / self.samples.len() as f64
285    }
286
287    /// Returns the median duration in nanoseconds.
288    #[must_use]
289    pub fn median_ns(&self) -> f64 {
290        if self.samples.is_empty() {
291            return 0.0;
292        }
293        let mut sorted: Vec<u64> = self.samples.iter().map(|s| s.duration_ns).collect();
294        sorted.sort_unstable();
295        let len = sorted.len();
296        if len % 2 == 0 {
297            (sorted[len / 2 - 1] + sorted[len / 2]) as f64 / 2.0
298        } else {
299            sorted[len / 2] as f64
300        }
301    }
302
303    /// Returns the standard deviation in nanoseconds (sample std dev, n-1).
304    #[must_use]
305    pub fn std_dev_ns(&self) -> f64 {
306        if self.samples.len() < 2 {
307            return 0.0;
308        }
309        let mean = self.mean_ns();
310        let variance: f64 = self
311            .samples
312            .iter()
313            .map(|s| {
314                let diff = s.duration_ns as f64 - mean;
315                diff * diff
316            })
317            .sum::<f64>()
318            / (self.samples.len() - 1) as f64;
319        variance.sqrt()
320    }
321
322    /// Returns the given percentile (0-100) in nanoseconds.
323    #[must_use]
324    pub fn percentile_ns(&self, p: f64) -> f64 {
325        if self.samples.is_empty() {
326            return 0.0;
327        }
328        let mut sorted: Vec<u64> = self.samples.iter().map(|s| s.duration_ns).collect();
329        sorted.sort_unstable();
330        let p = p.clamp(0.0, 100.0) / 100.0;
331        let index = (p * (sorted.len() - 1) as f64).round() as usize;
332        sorted[index.min(sorted.len() - 1)] as f64
333    }
334
335    /// Returns the minimum duration in nanoseconds.
336    #[must_use]
337    pub fn min_ns(&self) -> u64 {
338        self.samples
339            .iter()
340            .map(|s| s.duration_ns)
341            .min()
342            .unwrap_or(0)
343    }
344
345    /// Returns the maximum duration in nanoseconds.
346    #[must_use]
347    pub fn max_ns(&self) -> u64 {
348        self.samples
349            .iter()
350            .map(|s| s.duration_ns)
351            .max()
352            .unwrap_or(0)
353    }
354
355    /// Returns the total measured CPU time in milliseconds across all iterations.
356    #[must_use]
357    pub fn cpu_total_ms(&self) -> Option<u64> {
358        let values = self
359            .samples
360            .iter()
361            .filter_map(|sample| sample.cpu_time_ms)
362            .collect::<Vec<_>>();
363        if values.is_empty() {
364            return None;
365        }
366
367        let total = values
368            .iter()
369            .fold(0_u128, |sum, value| sum.saturating_add(u128::from(*value)));
370        Some(total.min(u128::from(u64::MAX)) as u64)
371    }
372
373    /// Returns the median measured CPU time in milliseconds across all iterations.
374    #[must_use]
375    pub fn cpu_median_ms(&self) -> Option<u64> {
376        let mut values = self
377            .samples
378            .iter()
379            .filter_map(|sample| sample.cpu_time_ms)
380            .collect::<Vec<_>>();
381        if values.is_empty() {
382            return None;
383        }
384
385        values.sort_unstable();
386        let len = values.len();
387        Some(if len % 2 == 0 {
388            let lower = u128::from(values[(len / 2) - 1]);
389            let upper = u128::from(values[len / 2]);
390            ((lower + upper) / 2) as u64
391        } else {
392            values[len / 2]
393        })
394    }
395
396    /// Returns the maximum baseline-adjusted peak memory growth in kilobytes.
397    #[must_use]
398    pub fn peak_memory_kb(&self) -> Option<u64> {
399        self.samples
400            .iter()
401            .filter_map(|sample| sample.peak_memory_kb)
402            .max()
403    }
404
405    /// Returns a statistical summary of the benchmark results.
406    #[must_use]
407    pub fn summary(&self) -> BenchSummary {
408        BenchSummary {
409            name: self.spec.name.clone(),
410            iterations: self.samples.len() as u32,
411            warmup: self.spec.warmup,
412            mean_ns: self.mean_ns(),
413            median_ns: self.median_ns(),
414            std_dev_ns: self.std_dev_ns(),
415            min_ns: self.min_ns(),
416            max_ns: self.max_ns(),
417            p95_ns: self.percentile_ns(95.0),
418            p99_ns: self.percentile_ns(99.0),
419        }
420    }
421}
422
423#[derive(Clone, Debug, Default)]
424struct IterationResourceUsage {
425    cpu_time_ms: Option<u64>,
426    peak_memory_kb: Option<u64>,
427}
428
429fn instant_offset_ns(origin: Instant, instant: Instant) -> u64 {
430    instant
431        .duration_since(origin)
432        .as_nanos()
433        .min(u128::from(u64::MAX)) as u64
434}
435
436fn push_timeline_span(
437    timeline: &mut Vec<HarnessTimelineSpan>,
438    origin: Instant,
439    phase: &str,
440    started_at: Instant,
441    ended_at: Instant,
442    iteration: Option<u32>,
443) {
444    timeline.push(HarnessTimelineSpan {
445        phase: phase.to_string(),
446        start_offset_ns: instant_offset_ns(origin, started_at),
447        end_offset_ns: instant_offset_ns(origin, ended_at),
448        iteration,
449    });
450}
451
452/// Statistical summary of benchmark results.
453#[derive(Clone, Debug, Serialize, Deserialize)]
454pub struct BenchSummary {
455    /// Name of the benchmark.
456    pub name: String,
457    /// Number of measured iterations.
458    pub iterations: u32,
459    /// Number of warmup iterations.
460    pub warmup: u32,
461    /// Mean duration in nanoseconds.
462    pub mean_ns: f64,
463    /// Median duration in nanoseconds.
464    pub median_ns: f64,
465    /// Standard deviation in nanoseconds.
466    pub std_dev_ns: f64,
467    /// Minimum duration in nanoseconds.
468    pub min_ns: u64,
469    /// Maximum duration in nanoseconds.
470    pub max_ns: u64,
471    /// 95th percentile in nanoseconds.
472    pub p95_ns: f64,
473    /// 99th percentile in nanoseconds.
474    pub p99_ns: f64,
475}
476
477/// Flat semantic phase timing captured during a benchmark run.
478#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
479pub struct SemanticPhase {
480    pub name: String,
481    pub duration_ns: u64,
482}
483
484#[derive(Default)]
485struct SemanticPhaseCollector {
486    enabled: bool,
487    depth: usize,
488    phases: Vec<SemanticPhase>,
489}
490
491impl SemanticPhaseCollector {
492    fn reset(&mut self) {
493        self.enabled = false;
494        self.depth = 0;
495        self.phases.clear();
496    }
497
498    fn begin_measurement(&mut self) {
499        self.reset();
500        self.enabled = true;
501    }
502
503    fn finish(&mut self) -> Vec<SemanticPhase> {
504        self.enabled = false;
505        self.depth = 0;
506        std::mem::take(&mut self.phases)
507    }
508
509    fn enter_phase(&mut self) -> Option<bool> {
510        if !self.enabled {
511            return None;
512        }
513        let top_level = self.depth == 0;
514        self.depth += 1;
515        Some(top_level)
516    }
517
518    fn exit_phase(&mut self, name: &str, top_level: bool, elapsed: Duration) {
519        self.depth = self.depth.saturating_sub(1);
520        if !self.enabled || !top_level {
521            return;
522        }
523
524        let duration_ns = elapsed.as_nanos().min(u128::from(u64::MAX)) as u64;
525        if let Some(phase) = self.phases.iter_mut().find(|phase| phase.name == name) {
526            phase.duration_ns = phase.duration_ns.saturating_add(duration_ns);
527        } else {
528            self.phases.push(SemanticPhase {
529                name: name.to_string(),
530                duration_ns,
531            });
532        }
533    }
534}
535
536thread_local! {
537    static SEMANTIC_PHASE_COLLECTOR: RefCell<SemanticPhaseCollector> =
538        RefCell::new(SemanticPhaseCollector::default());
539}
540
541struct SemanticPhaseGuard {
542    name: String,
543    started_at: Option<Instant>,
544    top_level: bool,
545}
546
547impl Drop for SemanticPhaseGuard {
548    fn drop(&mut self) {
549        let Some(started_at) = self.started_at else {
550            return;
551        };
552
553        let elapsed = started_at.elapsed();
554        SEMANTIC_PHASE_COLLECTOR.with(|collector| {
555            collector
556                .borrow_mut()
557                .exit_phase(&self.name, self.top_level, elapsed);
558        });
559    }
560}
561
562fn reset_semantic_phase_collection() {
563    SEMANTIC_PHASE_COLLECTOR.with(|collector| collector.borrow_mut().reset());
564}
565
566fn begin_semantic_phase_collection() {
567    SEMANTIC_PHASE_COLLECTOR.with(|collector| collector.borrow_mut().begin_measurement());
568}
569
570fn finish_semantic_phase_collection() -> Vec<SemanticPhase> {
571    SEMANTIC_PHASE_COLLECTOR.with(|collector| collector.borrow_mut().finish())
572}
573
574trait ResourceMonitor {
575    type Token;
576
577    fn start(&mut self) -> Self::Token;
578
579    fn finish(&mut self, token: Self::Token) -> IterationResourceUsage;
580}
581
582#[derive(Default)]
583struct DefaultResourceMonitor;
584
585struct DefaultResourceToken {
586    cpu_time_start_ns: Option<u64>,
587    memory_sampler: Option<MemoryPeakSampler>,
588}
589
590impl ResourceMonitor for DefaultResourceMonitor {
591    type Token = DefaultResourceToken;
592
593    fn start(&mut self) -> Self::Token {
594        Self::Token {
595            cpu_time_start_ns: current_thread_cpu_time_ns(),
596            memory_sampler: MemoryPeakSampler::start(),
597        }
598    }
599
600    fn finish(&mut self, token: Self::Token) -> IterationResourceUsage {
601        let cpu_time_ms = match (token.cpu_time_start_ns, current_thread_cpu_time_ns()) {
602            (Some(start_ns), Some(end_ns)) if end_ns >= start_ns => {
603                Some(round_ns_to_ms(end_ns - start_ns))
604            }
605            _ => None,
606        };
607
608        IterationResourceUsage {
609            cpu_time_ms,
610            peak_memory_kb: token
611                .memory_sampler
612                .and_then(MemoryPeakSampler::stop)
613                .filter(|value| *value > 0),
614        }
615    }
616}
617
618fn round_ns_to_ms(ns: u64) -> u64 {
619    ((u128::from(ns) + 500_000) / 1_000_000) as u64
620}
621
622#[cfg(unix)]
623fn current_thread_cpu_time_ns() -> Option<u64> {
624    let mut ts = std::mem::MaybeUninit::<libc::timespec>::uninit();
625    let rc = unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, ts.as_mut_ptr()) };
626    if rc != 0 {
627        return None;
628    }
629
630    let ts = unsafe { ts.assume_init() };
631    let secs = u64::try_from(ts.tv_sec).ok()?;
632    let nanos = u64::try_from(ts.tv_nsec).ok()?;
633    Some(secs.saturating_mul(1_000_000_000).saturating_add(nanos))
634}
635
636#[cfg(not(unix))]
637fn current_thread_cpu_time_ns() -> Option<u64> {
638    None
639}
640
641const MEMORY_SAMPLER_INTERVAL: Duration = Duration::from_millis(1);
642type MemoryReader = Arc<dyn Fn() -> Option<u64> + Send + Sync + 'static>;
643
644struct MemoryPeakSampler {
645    baseline_kb: u64,
646    stop_flag: Arc<AtomicBool>,
647    peak_kb: Arc<AtomicU64>,
648    handle: JoinHandle<()>,
649}
650
651impl MemoryPeakSampler {
652    fn start() -> Option<Self> {
653        Self::start_with_reader(Arc::new(|| current_process_memory_kb()))
654    }
655
656    fn start_with_reader(reader: MemoryReader) -> Option<Self> {
657        let stop_flag = Arc::new(AtomicBool::new(false));
658        let peak_kb = Arc::new(AtomicU64::new(0));
659        let (ready_tx, ready_rx) = mpsc::sync_channel(1);
660        let (baseline_tx, baseline_rx) = mpsc::sync_channel(1);
661        let sampler_stop = Arc::clone(&stop_flag);
662        let sampler_peak = Arc::clone(&peak_kb);
663        let sampler_reader = Arc::clone(&reader);
664
665        let handle = thread::Builder::new()
666            .name("mobench-memory-sampler".to_string())
667            .spawn(move || {
668                // Touch the sampler thread's own stack and runtime state before the
669                // benchmark baseline is captured so its overhead is not reported as
670                // measured benchmark memory.
671                let _ = sampler_reader();
672                let _ = ready_tx.send(());
673
674                let Some(baseline_kb) = baseline_rx.recv().ok().flatten() else {
675                    return;
676                };
677                sampler_peak.store(baseline_kb, Ordering::Release);
678
679                while !sampler_stop.load(Ordering::Acquire) {
680                    if let Some(current_kb) = sampler_reader() {
681                        update_atomic_max(&sampler_peak, current_kb);
682                    }
683                    thread::sleep(MEMORY_SAMPLER_INTERVAL);
684                }
685
686                if let Some(current_kb) = sampler_reader() {
687                    update_atomic_max(&sampler_peak, current_kb);
688                }
689            })
690            .ok()?;
691
692        if ready_rx.recv().is_err() {
693            stop_flag.store(true, Ordering::Release);
694            let _ = handle.join();
695            return None;
696        }
697
698        let baseline_kb = match reader() {
699            Some(value) => value,
700            None => {
701                let _ = baseline_tx.send(None);
702                stop_flag.store(true, Ordering::Release);
703                let _ = handle.join();
704                return None;
705            }
706        };
707        if baseline_tx.send(Some(baseline_kb)).is_err() {
708            stop_flag.store(true, Ordering::Release);
709            let _ = handle.join();
710            return None;
711        }
712
713        Some(Self {
714            baseline_kb,
715            stop_flag,
716            peak_kb,
717            handle,
718        })
719    }
720
721    fn stop(self) -> Option<u64> {
722        self.stop_flag.store(true, Ordering::Release);
723        let _ = self.handle.join();
724        let peak_kb = self.peak_kb.load(Ordering::Acquire);
725        Some(peak_kb.saturating_sub(self.baseline_kb))
726    }
727}
728
729fn update_atomic_max(target: &AtomicU64, value: u64) {
730    let mut current = target.load(Ordering::Relaxed);
731    while value > current {
732        match target.compare_exchange_weak(current, value, Ordering::Relaxed, Ordering::Relaxed) {
733            Ok(_) => break,
734            Err(observed) => current = observed,
735        }
736    }
737}
738
739#[cfg(any(target_os = "android", target_os = "linux"))]
740fn current_process_memory_kb() -> Option<u64> {
741    let statm = std::fs::read_to_string("/proc/self/statm").ok()?;
742    let resident_pages = statm
743        .split_whitespace()
744        .nth(1)
745        .and_then(|value| value.parse::<u64>().ok())?;
746    let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
747    if page_size <= 0 {
748        return None;
749    }
750    let page_size = u64::try_from(page_size).ok()?;
751    Some(resident_pages.saturating_mul(page_size) / 1024)
752}
753
754#[cfg(any(target_os = "ios", target_os = "macos"))]
755fn current_process_memory_kb() -> Option<u64> {
756    let mut info = std::mem::MaybeUninit::<libc::mach_task_basic_info_data_t>::uninit();
757    let mut count = libc::MACH_TASK_BASIC_INFO_COUNT;
758    #[allow(deprecated)]
759    let rc = unsafe {
760        libc::task_info(
761            libc::mach_task_self(),
762            libc::MACH_TASK_BASIC_INFO,
763            info.as_mut_ptr().cast::<libc::integer_t>(),
764            &mut count,
765        )
766    };
767    if rc != libc::KERN_SUCCESS {
768        return None;
769    }
770
771    let info = unsafe { info.assume_init() };
772    Some((info.resident_size / 1024) as u64)
773}
774
775#[cfg(not(any(
776    target_os = "android",
777    target_os = "linux",
778    target_os = "ios",
779    target_os = "macos"
780)))]
781fn current_process_memory_kb() -> Option<u64> {
782    None
783}
784
785fn measure_iteration<M, F>(
786    monitor: &mut M,
787    f: F,
788) -> Result<(BenchSample, Instant, Instant), TimingError>
789where
790    M: ResourceMonitor,
791    F: FnOnce() -> Result<(), TimingError>,
792{
793    let token = monitor.start();
794    let started_at = Instant::now();
795    let result = f();
796    let ended_at = Instant::now();
797    let resources = monitor.finish(token);
798    result.map(|_| {
799        (
800            BenchSample::from_measurement(ended_at.duration_since(started_at), resources),
801            started_at,
802            ended_at,
803        )
804    })
805}
806
807/// Records a flat semantic phase when called inside an active benchmark measurement loop.
808///
809/// Phases are aggregated across measured iterations and ignored during warmup/setup.
810/// Nested phases are intentionally collapsed in v1 to keep the output flat.
811pub fn profile_phase<T>(name: &str, f: impl FnOnce() -> T) -> T {
812    let guard = SEMANTIC_PHASE_COLLECTOR.with(|collector| {
813        let mut collector = collector.borrow_mut();
814        match collector.enter_phase() {
815            Some(top_level) => SemanticPhaseGuard {
816                name: name.to_string(),
817                started_at: Some(Instant::now()),
818                top_level,
819            },
820            None => SemanticPhaseGuard {
821                name: String::new(),
822                started_at: None,
823                top_level: false,
824            },
825        }
826    });
827
828    let result = f();
829    drop(guard);
830    result
831}
832
833/// Errors that can occur during benchmark execution.
834///
835/// # Example
836///
837/// ```
838/// use mobench_sdk::timing::{BenchSpec, TimingError};
839///
840/// // Zero iterations produces an error
841/// let result = BenchSpec::new("test", 0, 10);
842/// assert!(matches!(result, Err(TimingError::NoIterations { .. })));
843/// ```
844#[derive(Debug, Error)]
845pub enum TimingError {
846    /// The iteration count was zero or invalid.
847    ///
848    /// At least one iteration is required to produce a measurement.
849    /// The error includes the actual value provided for diagnostic purposes.
850    #[error("iterations must be greater than zero (got {count}). Minimum recommended: 10")]
851    NoIterations {
852        /// The invalid iteration count that was provided.
853        count: u32,
854    },
855
856    /// The benchmark function failed during execution.
857    ///
858    /// Contains a description of the failure.
859    #[error("benchmark function failed: {0}")]
860    Execution(String),
861}
862
863/// Runs a benchmark by executing a closure repeatedly.
864///
865/// This is the core benchmarking function. It:
866///
867/// 1. Executes the closure `spec.warmup` times without recording
868/// 2. Executes the closure `spec.iterations` times, recording each duration
869/// 3. Returns a [`BenchReport`] with all samples
870///
871/// # Arguments
872///
873/// * `spec` - Benchmark configuration specifying iterations and warmup
874/// * `f` - Closure to benchmark; must return `Result<(), TimingError>`
875///
876/// # Returns
877///
878/// A [`BenchReport`] containing all timing samples, or a [`TimingError`] if
879/// the benchmark fails.
880///
881/// # Example
882///
883/// ```
884/// use mobench_sdk::timing::{BenchSpec, run_closure, TimingError};
885///
886/// let spec = BenchSpec::new("sum_benchmark", 100, 10)?;
887///
888/// let report = run_closure(spec, || {
889///     let sum: u64 = (0..1000).sum();
890///     std::hint::black_box(sum);
891///     Ok(())
892/// })?;
893///
894/// assert_eq!(report.samples.len(), 100);
895///
896/// // Calculate mean duration
897/// let total_ns: u64 = report.samples.iter().map(|s| s.duration_ns).sum();
898/// let mean_ns = total_ns / report.samples.len() as u64;
899/// println!("Mean: {} ns", mean_ns);
900/// # Ok::<(), TimingError>(())
901/// ```
902///
903/// # Error Handling
904///
905/// If the closure returns an error, the benchmark stops immediately:
906///
907/// ```
908/// use mobench_sdk::timing::{BenchSpec, run_closure, TimingError};
909///
910/// let spec = BenchSpec::new("failing_bench", 100, 0)?;
911///
912/// let result = run_closure(spec, || {
913///     Err(TimingError::Execution("simulated failure".into()))
914/// });
915///
916/// assert!(result.is_err());
917/// # Ok::<(), TimingError>(())
918/// ```
919///
920/// # Timing Precision
921///
922/// Uses [`std::time::Instant`] for timing, which provides monotonic,
923/// nanosecond-resolution measurements on most platforms.
924pub fn run_closure<F>(spec: BenchSpec, mut f: F) -> Result<BenchReport, TimingError>
925where
926    F: FnMut() -> Result<(), TimingError>,
927{
928    let mut monitor = DefaultResourceMonitor;
929    run_closure_with_monitor(spec, &mut monitor, move || f())
930}
931
932fn run_closure_with_monitor<F, M>(
933    spec: BenchSpec,
934    monitor: &mut M,
935    mut f: F,
936) -> Result<BenchReport, TimingError>
937where
938    F: FnMut() -> Result<(), TimingError>,
939    M: ResourceMonitor,
940{
941    if spec.iterations == 0 {
942        return Err(TimingError::NoIterations {
943            count: spec.iterations,
944        });
945    }
946
947    reset_semantic_phase_collection();
948    let harness_origin = Instant::now();
949    let mut timeline = Vec::new();
950
951    // Warmup phase - not measured
952    for iteration in 0..spec.warmup {
953        let phase_start = Instant::now();
954        f()?;
955        push_timeline_span(
956            &mut timeline,
957            harness_origin,
958            "warmup-benchmark",
959            phase_start,
960            Instant::now(),
961            Some(iteration),
962        );
963    }
964
965    // Measurement phase
966    begin_semantic_phase_collection();
967    let mut samples = Vec::with_capacity(spec.iterations as usize);
968    for iteration in 0..spec.iterations {
969        let (sample, start, end) = match measure_iteration(monitor, || f()) {
970            Ok(measurement) => measurement,
971            Err(err) => {
972                let _ = finish_semantic_phase_collection();
973                return Err(err);
974            }
975        };
976        samples.push(sample);
977        push_timeline_span(
978            &mut timeline,
979            harness_origin,
980            "measured-benchmark",
981            start,
982            end,
983            Some(iteration),
984        );
985    }
986    let phases = finish_semantic_phase_collection();
987
988    Ok(BenchReport {
989        spec,
990        samples,
991        phases,
992        timeline,
993    })
994}
995
996/// Runs a benchmark with setup that executes once before all iterations.
997///
998/// The setup function is called once before timing begins, then the benchmark
999/// runs multiple times using a reference to the setup result. This is useful
1000/// for expensive initialization that shouldn't be included in timing.
1001///
1002/// # Arguments
1003///
1004/// * `spec` - Benchmark configuration specifying iterations and warmup
1005/// * `setup` - Function that creates the input data (called once, not timed)
1006/// * `f` - Benchmark closure that receives a reference to setup result
1007///
1008/// # Example
1009///
1010/// ```ignore
1011/// use mobench_sdk::timing::{BenchSpec, run_closure_with_setup};
1012///
1013/// fn setup_data() -> Vec<u8> {
1014///     vec![0u8; 1_000_000]  // Expensive allocation not measured
1015/// }
1016///
1017/// let spec = BenchSpec::new("hash_benchmark", 100, 10)?;
1018/// let report = run_closure_with_setup(spec, setup_data, |data| {
1019///     std::hint::black_box(compute_hash(data));
1020///     Ok(())
1021/// })?;
1022/// ```
1023pub fn run_closure_with_setup<S, T, F>(
1024    spec: BenchSpec,
1025    setup: S,
1026    mut f: F,
1027) -> Result<BenchReport, TimingError>
1028where
1029    S: FnOnce() -> T,
1030    F: FnMut(&T) -> Result<(), TimingError>,
1031{
1032    let mut monitor = DefaultResourceMonitor;
1033    run_closure_with_setup_with_monitor(spec, &mut monitor, setup, move |input| f(input))
1034}
1035
1036fn run_closure_with_setup_with_monitor<S, T, F, M>(
1037    spec: BenchSpec,
1038    monitor: &mut M,
1039    setup: S,
1040    mut f: F,
1041) -> Result<BenchReport, TimingError>
1042where
1043    S: FnOnce() -> T,
1044    F: FnMut(&T) -> Result<(), TimingError>,
1045    M: ResourceMonitor,
1046{
1047    if spec.iterations == 0 {
1048        return Err(TimingError::NoIterations {
1049            count: spec.iterations,
1050        });
1051    }
1052
1053    reset_semantic_phase_collection();
1054    let harness_origin = Instant::now();
1055    let mut timeline = Vec::new();
1056
1057    // Setup phase - not timed
1058    let setup_start = Instant::now();
1059    let input = setup();
1060    push_timeline_span(
1061        &mut timeline,
1062        harness_origin,
1063        "setup",
1064        setup_start,
1065        Instant::now(),
1066        None,
1067    );
1068
1069    // Warmup phase - not recorded
1070    for iteration in 0..spec.warmup {
1071        let phase_start = Instant::now();
1072        f(&input)?;
1073        push_timeline_span(
1074            &mut timeline,
1075            harness_origin,
1076            "warmup-benchmark",
1077            phase_start,
1078            Instant::now(),
1079            Some(iteration),
1080        );
1081    }
1082
1083    // Measurement phase
1084    begin_semantic_phase_collection();
1085    let mut samples = Vec::with_capacity(spec.iterations as usize);
1086    for iteration in 0..spec.iterations {
1087        let (sample, start, end) = match measure_iteration(monitor, || f(&input)) {
1088            Ok(measurement) => measurement,
1089            Err(err) => {
1090                let _ = finish_semantic_phase_collection();
1091                return Err(err);
1092            }
1093        };
1094        samples.push(sample);
1095        push_timeline_span(
1096            &mut timeline,
1097            harness_origin,
1098            "measured-benchmark",
1099            start,
1100            end,
1101            Some(iteration),
1102        );
1103    }
1104    let phases = finish_semantic_phase_collection();
1105
1106    Ok(BenchReport {
1107        spec,
1108        samples,
1109        phases,
1110        timeline,
1111    })
1112}
1113
1114/// Runs a benchmark with per-iteration setup.
1115///
1116/// Setup runs before each iteration and is not timed. The benchmark takes
1117/// ownership of the setup result, making this suitable for benchmarks that
1118/// mutate their input (e.g., sorting).
1119///
1120/// # Arguments
1121///
1122/// * `spec` - Benchmark configuration specifying iterations and warmup
1123/// * `setup` - Function that creates fresh input for each iteration (not timed)
1124/// * `f` - Benchmark closure that takes ownership of setup result
1125///
1126/// # Example
1127///
1128/// ```ignore
1129/// use mobench_sdk::timing::{BenchSpec, run_closure_with_setup_per_iter};
1130///
1131/// fn generate_random_vec() -> Vec<i32> {
1132///     (0..1000).map(|_| rand::random()).collect()
1133/// }
1134///
1135/// let spec = BenchSpec::new("sort_benchmark", 100, 10)?;
1136/// let report = run_closure_with_setup_per_iter(spec, generate_random_vec, |mut data| {
1137///     data.sort();
1138///     std::hint::black_box(data);
1139///     Ok(())
1140/// })?;
1141/// ```
1142pub fn run_closure_with_setup_per_iter<S, T, F>(
1143    spec: BenchSpec,
1144    mut setup: S,
1145    mut f: F,
1146) -> Result<BenchReport, TimingError>
1147where
1148    S: FnMut() -> T,
1149    F: FnMut(T) -> Result<(), TimingError>,
1150{
1151    let mut monitor = DefaultResourceMonitor;
1152    run_closure_with_setup_per_iter_with_monitor(
1153        spec,
1154        &mut monitor,
1155        move || setup(),
1156        move |input| f(input),
1157    )
1158}
1159
1160fn run_closure_with_setup_per_iter_with_monitor<S, T, F, M>(
1161    spec: BenchSpec,
1162    monitor: &mut M,
1163    mut setup: S,
1164    mut f: F,
1165) -> Result<BenchReport, TimingError>
1166where
1167    S: FnMut() -> T,
1168    F: FnMut(T) -> Result<(), TimingError>,
1169    M: ResourceMonitor,
1170{
1171    if spec.iterations == 0 {
1172        return Err(TimingError::NoIterations {
1173            count: spec.iterations,
1174        });
1175    }
1176
1177    reset_semantic_phase_collection();
1178    let harness_origin = Instant::now();
1179    let mut timeline = Vec::new();
1180
1181    // Warmup phase
1182    for iteration in 0..spec.warmup {
1183        let setup_start = Instant::now();
1184        let input = setup();
1185        push_timeline_span(
1186            &mut timeline,
1187            harness_origin,
1188            "fixture-setup",
1189            setup_start,
1190            Instant::now(),
1191            Some(iteration),
1192        );
1193        let phase_start = Instant::now();
1194        f(input)?;
1195        push_timeline_span(
1196            &mut timeline,
1197            harness_origin,
1198            "warmup-benchmark",
1199            phase_start,
1200            Instant::now(),
1201            Some(iteration),
1202        );
1203    }
1204
1205    // Measurement phase
1206    begin_semantic_phase_collection();
1207    let mut samples = Vec::with_capacity(spec.iterations as usize);
1208    for iteration in 0..spec.iterations {
1209        let setup_start = Instant::now();
1210        let input = setup(); // Not timed
1211        push_timeline_span(
1212            &mut timeline,
1213            harness_origin,
1214            "fixture-setup",
1215            setup_start,
1216            Instant::now(),
1217            Some(iteration),
1218        );
1219
1220        let (sample, start, end) = match measure_iteration(monitor, || f(input)) {
1221            Ok(measurement) => measurement,
1222            Err(err) => {
1223                let _ = finish_semantic_phase_collection();
1224                return Err(err);
1225            }
1226        };
1227        samples.push(sample);
1228        push_timeline_span(
1229            &mut timeline,
1230            harness_origin,
1231            "measured-benchmark",
1232            start,
1233            end,
1234            Some(iteration),
1235        );
1236    }
1237    let phases = finish_semantic_phase_collection();
1238
1239    Ok(BenchReport {
1240        spec,
1241        samples,
1242        phases,
1243        timeline,
1244    })
1245}
1246
1247/// Runs a benchmark with setup and teardown.
1248///
1249/// Setup runs once before all iterations, teardown runs once after all
1250/// iterations complete. Neither is included in timing.
1251///
1252/// # Arguments
1253///
1254/// * `spec` - Benchmark configuration specifying iterations and warmup
1255/// * `setup` - Function that creates the input data (called once, not timed)
1256/// * `f` - Benchmark closure that receives a reference to setup result
1257/// * `teardown` - Function that cleans up the input (called once, not timed)
1258///
1259/// # Example
1260///
1261/// ```ignore
1262/// use mobench_sdk::timing::{BenchSpec, run_closure_with_setup_teardown};
1263///
1264/// fn setup_db() -> Database { Database::connect("test.db") }
1265/// fn cleanup_db(db: Database) { db.close(); std::fs::remove_file("test.db").ok(); }
1266///
1267/// let spec = BenchSpec::new("db_benchmark", 100, 10)?;
1268/// let report = run_closure_with_setup_teardown(
1269///     spec,
1270///     setup_db,
1271///     |db| { db.query("SELECT *"); Ok(()) },
1272///     cleanup_db,
1273/// )?;
1274/// ```
1275pub fn run_closure_with_setup_teardown<S, T, F, D>(
1276    spec: BenchSpec,
1277    setup: S,
1278    mut f: F,
1279    teardown: D,
1280) -> Result<BenchReport, TimingError>
1281where
1282    S: FnOnce() -> T,
1283    F: FnMut(&T) -> Result<(), TimingError>,
1284    D: FnOnce(T),
1285{
1286    let mut monitor = DefaultResourceMonitor;
1287    run_closure_with_setup_teardown_with_monitor(
1288        spec,
1289        &mut monitor,
1290        setup,
1291        move |input| f(input),
1292        teardown,
1293    )
1294}
1295
1296fn run_closure_with_setup_teardown_with_monitor<S, T, F, D, M>(
1297    spec: BenchSpec,
1298    monitor: &mut M,
1299    setup: S,
1300    mut f: F,
1301    teardown: D,
1302) -> Result<BenchReport, TimingError>
1303where
1304    S: FnOnce() -> T,
1305    F: FnMut(&T) -> Result<(), TimingError>,
1306    D: FnOnce(T),
1307    M: ResourceMonitor,
1308{
1309    if spec.iterations == 0 {
1310        return Err(TimingError::NoIterations {
1311            count: spec.iterations,
1312        });
1313    }
1314
1315    reset_semantic_phase_collection();
1316    let harness_origin = Instant::now();
1317    let mut timeline = Vec::new();
1318
1319    // Setup phase - not timed
1320    let setup_start = Instant::now();
1321    let input = setup();
1322    push_timeline_span(
1323        &mut timeline,
1324        harness_origin,
1325        "setup",
1326        setup_start,
1327        Instant::now(),
1328        None,
1329    );
1330
1331    // Warmup phase
1332    for iteration in 0..spec.warmup {
1333        let phase_start = Instant::now();
1334        f(&input)?;
1335        push_timeline_span(
1336            &mut timeline,
1337            harness_origin,
1338            "warmup-benchmark",
1339            phase_start,
1340            Instant::now(),
1341            Some(iteration),
1342        );
1343    }
1344
1345    // Measurement phase
1346    begin_semantic_phase_collection();
1347    let mut samples = Vec::with_capacity(spec.iterations as usize);
1348    for iteration in 0..spec.iterations {
1349        let (sample, start, end) = match measure_iteration(monitor, || f(&input)) {
1350            Ok(measurement) => measurement,
1351            Err(err) => {
1352                let _ = finish_semantic_phase_collection();
1353                return Err(err);
1354            }
1355        };
1356        samples.push(sample);
1357        push_timeline_span(
1358            &mut timeline,
1359            harness_origin,
1360            "measured-benchmark",
1361            start,
1362            end,
1363            Some(iteration),
1364        );
1365    }
1366    let phases = finish_semantic_phase_collection();
1367
1368    // Teardown phase - not timed
1369    let teardown_start = Instant::now();
1370    teardown(input);
1371    push_timeline_span(
1372        &mut timeline,
1373        harness_origin,
1374        "teardown",
1375        teardown_start,
1376        Instant::now(),
1377        None,
1378    );
1379
1380    Ok(BenchReport {
1381        spec,
1382        samples,
1383        phases,
1384        timeline,
1385    })
1386}
1387
1388#[cfg(test)]
1389mod tests {
1390    use super::*;
1391
1392    #[derive(Default)]
1393    struct FakeResourceMonitor {
1394        samples: Vec<IterationResourceUsage>,
1395        started: usize,
1396        finished: usize,
1397    }
1398
1399    impl FakeResourceMonitor {
1400        fn new(samples: Vec<IterationResourceUsage>) -> Self {
1401            Self {
1402                samples,
1403                started: 0,
1404                finished: 0,
1405            }
1406        }
1407    }
1408
1409    impl ResourceMonitor for FakeResourceMonitor {
1410        type Token = usize;
1411
1412        fn start(&mut self) -> Self::Token {
1413            let token = self.started;
1414            self.started += 1;
1415            assert!(
1416                token < self.samples.len(),
1417                "resource capture should only run for measured iterations"
1418            );
1419            token
1420        }
1421
1422        fn finish(&mut self, token: Self::Token) -> IterationResourceUsage {
1423            self.finished += 1;
1424            self.samples
1425                .get(token)
1426                .cloned()
1427                .expect("resource usage for measured iteration")
1428        }
1429    }
1430
1431    #[test]
1432    fn runs_benchmark_collects_requested_samples() {
1433        let spec = BenchSpec::new("noop", 3, 1).unwrap();
1434        let report = run_closure(spec, || Ok(())).unwrap();
1435
1436        assert_eq!(report.samples.len(), 3);
1437        assert_eq!(report.spec.name, "noop");
1438        assert_eq!(report.spec.iterations, 3);
1439    }
1440
1441    #[test]
1442    fn rejects_zero_iterations() {
1443        let result = BenchSpec::new("test", 0, 10);
1444        assert!(matches!(
1445            result,
1446            Err(TimingError::NoIterations { count: 0 })
1447        ));
1448    }
1449
1450    #[test]
1451    fn allows_zero_warmup() {
1452        let spec = BenchSpec::new("test", 5, 0).unwrap();
1453        assert_eq!(spec.warmup, 0);
1454
1455        let report = run_closure(spec, || Ok(())).unwrap();
1456        assert_eq!(report.samples.len(), 5);
1457    }
1458
1459    #[test]
1460    fn serializes_to_json() {
1461        let report = BenchReport {
1462            spec: BenchSpec::new("test", 10, 2).unwrap(),
1463            samples: vec![BenchSample {
1464                duration_ns: 1_000_000,
1465                cpu_time_ms: Some(42),
1466                peak_memory_kb: Some(512),
1467            }],
1468            phases: vec![SemanticPhase {
1469                name: "prove".to_string(),
1470                duration_ns: 1_000_000,
1471            }],
1472            timeline: vec![HarnessTimelineSpan {
1473                phase: "measured-benchmark".to_string(),
1474                start_offset_ns: 0,
1475                end_offset_ns: 1_000_000,
1476                iteration: Some(0),
1477            }],
1478        };
1479
1480        let json = serde_json::to_string(&report).unwrap();
1481        let restored: BenchReport = serde_json::from_str(&json).unwrap();
1482
1483        assert_eq!(restored.spec.name, "test");
1484        assert_eq!(restored.samples.len(), 1);
1485        assert_eq!(restored.samples[0].cpu_time_ms, Some(42));
1486        assert_eq!(restored.samples[0].peak_memory_kb, Some(512));
1487        assert_eq!(restored.phases.len(), 1);
1488        assert_eq!(restored.phases[0].name, "prove");
1489        assert!(restored.phases[0].duration_ns > 0);
1490    }
1491
1492    #[test]
1493    fn profile_phase_records_only_measured_iterations() {
1494        let spec = BenchSpec::new("semantic", 2, 1).unwrap();
1495        let mut call_index = 0u32;
1496        let report = run_closure(spec, || {
1497            let phase_name = if call_index == 0 {
1498                "warmup-only"
1499            } else {
1500                "prove"
1501            };
1502            call_index += 1;
1503            profile_phase(phase_name, || std::thread::sleep(Duration::from_millis(1)));
1504            Ok(())
1505        })
1506        .unwrap();
1507
1508        assert!(
1509            !report
1510                .phases
1511                .iter()
1512                .any(|phase| phase.name == "warmup-only"),
1513            "warmup phases should not be recorded"
1514        );
1515        let prove = report
1516            .phases
1517            .iter()
1518            .find(|phase| phase.name == "prove")
1519            .expect("prove phase");
1520        assert!(prove.duration_ns > 0);
1521    }
1522
1523    #[test]
1524    fn profile_phase_keeps_the_v1_model_flat() {
1525        let spec = BenchSpec::new("semantic-flat", 1, 0).unwrap();
1526        let report = run_closure(spec, || {
1527            profile_phase("prove", || {
1528                std::thread::sleep(Duration::from_millis(1));
1529                profile_phase("inner", || std::thread::sleep(Duration::from_millis(1)));
1530            });
1531            Ok(())
1532        })
1533        .unwrap();
1534
1535        assert!(report.phases.iter().any(|phase| phase.name == "prove"));
1536        assert!(
1537            !report.phases.iter().any(|phase| phase.name == "inner"),
1538            "nested phases should not create a second flat phase entry"
1539        );
1540    }
1541
1542    #[test]
1543    fn measured_cpu_excludes_warmup_iterations() {
1544        let spec = BenchSpec::new("cpu", 2, 1).unwrap();
1545        let mut monitor = FakeResourceMonitor::new(vec![
1546            IterationResourceUsage {
1547                cpu_time_ms: Some(11),
1548                peak_memory_kb: Some(32),
1549            },
1550            IterationResourceUsage {
1551                cpu_time_ms: Some(17),
1552                peak_memory_kb: Some(64),
1553            },
1554        ]);
1555        let mut calls = 0_u32;
1556
1557        let report = run_closure_with_monitor(spec, &mut monitor, || {
1558            calls += 1;
1559            Ok(())
1560        })
1561        .unwrap();
1562
1563        assert_eq!(calls, 3);
1564        assert_eq!(monitor.started, 2);
1565        assert_eq!(monitor.finished, 2);
1566        assert_eq!(
1567            report
1568                .samples
1569                .iter()
1570                .map(|sample| sample.cpu_time_ms)
1571                .collect::<Vec<_>>(),
1572            vec![Some(11), Some(17)]
1573        );
1574        assert_eq!(report.cpu_total_ms(), Some(28));
1575    }
1576
1577    #[test]
1578    fn measured_cpu_excludes_outer_harness_and_report_overhead() {
1579        let spec = BenchSpec::new("cpu-harness", 2, 1).unwrap();
1580        let mut monitor = FakeResourceMonitor::new(vec![
1581            IterationResourceUsage {
1582                cpu_time_ms: Some(5),
1583                peak_memory_kb: Some(12),
1584            },
1585            IterationResourceUsage {
1586                cpu_time_ms: Some(7),
1587                peak_memory_kb: Some(18),
1588            },
1589        ]);
1590
1591        let mut setup_calls = 0_u32;
1592        let mut teardown_calls = 0_u32;
1593        let report = run_closure_with_setup_teardown_with_monitor(
1594            spec,
1595            &mut monitor,
1596            || {
1597                setup_calls += 1;
1598                vec![1_u8, 2, 3]
1599            },
1600            |_fixture| Ok(()),
1601            |_fixture| {
1602                teardown_calls += 1;
1603            },
1604        )
1605        .unwrap();
1606
1607        let _serialized = serde_json::to_string(&report).unwrap();
1608
1609        assert_eq!(setup_calls, 1);
1610        assert_eq!(teardown_calls, 1);
1611        assert_eq!(monitor.started, 2);
1612        assert_eq!(report.cpu_total_ms(), Some(12));
1613        assert_eq!(report.cpu_median_ms(), Some(6));
1614    }
1615
1616    #[test]
1617    fn single_iteration_cpu_median_matches_the_measured_iteration() {
1618        let spec = BenchSpec::new("single", 1, 0).unwrap();
1619        let mut monitor = FakeResourceMonitor::new(vec![IterationResourceUsage {
1620            cpu_time_ms: Some(42),
1621            peak_memory_kb: Some(24),
1622        }]);
1623
1624        let report = run_closure_with_monitor(spec, &mut monitor, || Ok(())).unwrap();
1625
1626        assert_eq!(report.samples[0].cpu_time_ms, Some(42));
1627        assert_eq!(report.cpu_total_ms(), Some(42));
1628        assert_eq!(report.cpu_median_ms(), Some(42));
1629    }
1630
1631    #[test]
1632    fn multiple_iterations_export_the_median_cpu_sample() {
1633        let spec = BenchSpec::new("median", 3, 0).unwrap();
1634        let mut monitor = FakeResourceMonitor::new(vec![
1635            IterationResourceUsage {
1636                cpu_time_ms: Some(19),
1637                peak_memory_kb: Some(10),
1638            },
1639            IterationResourceUsage {
1640                cpu_time_ms: Some(7),
1641                peak_memory_kb: Some(30),
1642            },
1643            IterationResourceUsage {
1644                cpu_time_ms: Some(11),
1645                peak_memory_kb: Some(20),
1646            },
1647        ]);
1648
1649        let report = run_closure_with_monitor(spec, &mut monitor, || Ok(())).unwrap();
1650
1651        assert_eq!(report.cpu_median_ms(), Some(11));
1652        assert_eq!(report.cpu_total_ms(), Some(37));
1653    }
1654
1655    #[test]
1656    fn peak_memory_excludes_harness_baseline_overhead() {
1657        let spec = BenchSpec::new("memory", 2, 1).unwrap();
1658        let mut monitor = FakeResourceMonitor::new(vec![
1659            IterationResourceUsage {
1660                cpu_time_ms: Some(3),
1661                peak_memory_kb: Some(48),
1662            },
1663            IterationResourceUsage {
1664                cpu_time_ms: Some(4),
1665                peak_memory_kb: Some(96),
1666            },
1667        ]);
1668
1669        let report = run_closure_with_setup_teardown_with_monitor(
1670            spec,
1671            &mut monitor,
1672            || vec![0_u8; 1024],
1673            |_fixture| Ok(()),
1674            |_fixture| {},
1675        )
1676        .unwrap();
1677
1678        assert_eq!(
1679            report
1680                .samples
1681                .iter()
1682                .map(|sample| sample.peak_memory_kb)
1683                .collect::<Vec<_>>(),
1684            vec![Some(48), Some(96)]
1685        );
1686        assert_eq!(report.peak_memory_kb(), Some(96));
1687    }
1688
1689    #[test]
1690    fn memory_peak_sampler_uses_the_first_post_startup_sample_as_its_baseline() {
1691        use std::collections::VecDeque;
1692        use std::sync::{Arc, Mutex};
1693
1694        let samples = Arc::new(Mutex::new(VecDeque::from([
1695            Some(80_u64),
1696            Some(100_u64),
1697            Some(140_u64),
1698            Some(120_u64),
1699        ])));
1700        let reader_samples = Arc::clone(&samples);
1701        let reader = Arc::new(move || {
1702            reader_samples
1703                .lock()
1704                .expect("sample queue")
1705                .pop_front()
1706                .unwrap_or(Some(120))
1707        });
1708
1709        let sampler = MemoryPeakSampler::start_with_reader(reader).expect("sampler");
1710        let peak_kb = sampler.stop().expect("peak memory");
1711
1712        assert_eq!(peak_kb, 40);
1713    }
1714
1715    #[test]
1716    fn run_with_setup_calls_setup_once() {
1717        use std::sync::atomic::{AtomicU32, Ordering};
1718
1719        static SETUP_COUNT: AtomicU32 = AtomicU32::new(0);
1720        static RUN_COUNT: AtomicU32 = AtomicU32::new(0);
1721
1722        let spec = BenchSpec::new("test", 5, 2).unwrap();
1723        let report = run_closure_with_setup(
1724            spec,
1725            || {
1726                SETUP_COUNT.fetch_add(1, Ordering::SeqCst);
1727                vec![1, 2, 3]
1728            },
1729            |data| {
1730                RUN_COUNT.fetch_add(1, Ordering::SeqCst);
1731                std::hint::black_box(data.len());
1732                Ok(())
1733            },
1734        )
1735        .unwrap();
1736
1737        assert_eq!(SETUP_COUNT.load(Ordering::SeqCst), 1); // Setup called once
1738        assert_eq!(RUN_COUNT.load(Ordering::SeqCst), 7); // 2 warmup + 5 iterations
1739        assert_eq!(report.samples.len(), 5);
1740    }
1741
1742    #[test]
1743    fn run_with_setup_per_iter_calls_setup_each_time() {
1744        use std::sync::atomic::{AtomicU32, Ordering};
1745
1746        static SETUP_COUNT: AtomicU32 = AtomicU32::new(0);
1747
1748        let spec = BenchSpec::new("test", 3, 1).unwrap();
1749        let report = run_closure_with_setup_per_iter(
1750            spec,
1751            || {
1752                SETUP_COUNT.fetch_add(1, Ordering::SeqCst);
1753                vec![1, 2, 3]
1754            },
1755            |data| {
1756                std::hint::black_box(data);
1757                Ok(())
1758            },
1759        )
1760        .unwrap();
1761
1762        assert_eq!(SETUP_COUNT.load(Ordering::SeqCst), 4); // 1 warmup + 3 iterations
1763        assert_eq!(report.samples.len(), 3);
1764    }
1765
1766    #[test]
1767    fn run_with_setup_teardown_calls_both() {
1768        use std::sync::atomic::{AtomicU32, Ordering};
1769
1770        static SETUP_COUNT: AtomicU32 = AtomicU32::new(0);
1771        static TEARDOWN_COUNT: AtomicU32 = AtomicU32::new(0);
1772
1773        let spec = BenchSpec::new("test", 3, 1).unwrap();
1774        let report = run_closure_with_setup_teardown(
1775            spec,
1776            || {
1777                SETUP_COUNT.fetch_add(1, Ordering::SeqCst);
1778                "resource"
1779            },
1780            |_resource| Ok(()),
1781            |_resource| {
1782                TEARDOWN_COUNT.fetch_add(1, Ordering::SeqCst);
1783            },
1784        )
1785        .unwrap();
1786
1787        assert_eq!(SETUP_COUNT.load(Ordering::SeqCst), 1);
1788        assert_eq!(TEARDOWN_COUNT.load(Ordering::SeqCst), 1);
1789        assert_eq!(report.samples.len(), 3);
1790    }
1791
1792    #[test]
1793    fn bench_report_serializes_exact_harness_timeline() {
1794        let spec = BenchSpec::new("timeline", 2, 1).unwrap();
1795        let report = run_closure_with_setup_teardown(
1796            spec,
1797            || {
1798                std::thread::sleep(Duration::from_millis(1));
1799                "resource"
1800            },
1801            |_resource| {
1802                std::thread::sleep(Duration::from_millis(1));
1803                Ok(())
1804            },
1805            |_resource| {
1806                std::thread::sleep(Duration::from_millis(1));
1807            },
1808        )
1809        .unwrap();
1810
1811        let json = serde_json::to_value(&report).unwrap();
1812        assert_eq!(json["timeline"][0]["phase"], "setup");
1813        assert_eq!(json["timeline"][1]["phase"], "warmup-benchmark");
1814        assert_eq!(json["timeline"][2]["phase"], "measured-benchmark");
1815        assert_eq!(json["timeline"][3]["phase"], "measured-benchmark");
1816        assert_eq!(json["timeline"][4]["phase"], "teardown");
1817    }
1818}