benchmark 0.8.0

Nanosecond-precision benchmarking for dev, testing, and production. Zero-overhead core timing when disabled; optional std-powered collectors and zero-dependency metrics (Watch/Timer) for real service observability.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
#![cfg(all(feature = "std", feature = "metrics"))]

use core::marker::PhantomData;
#[cfg(feature = "parking-lot-locks")]
use parking_lot::RwLock;
use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
#[cfg(not(feature = "parking-lot-locks"))]
use std::sync::RwLock;
use std::time::Instant;

use crate::hist_backend::HistBackend;
#[cfg(feature = "trace")]
use crate::trace;

// Select concrete backend by feature flags
#[cfg(feature = "hdr")]
type Backend = crate::hist_hdr::Histogram;
#[cfg(not(feature = "hdr"))]
type Backend = crate::histogram::FastHistogram;

// Normalize guard types across lock backends at module scope
#[cfg(feature = "parking-lot-locks")]
type ReadGuard<'a, B> = parking_lot::RwLockReadGuard<'a, HashMap<Arc<str>, Arc<B>>>;
#[cfg(not(feature = "parking-lot-locks"))]
type ReadGuard<'a, B> = std::sync::RwLockReadGuard<'a, HashMap<Arc<str>, Arc<B>>>;

#[cfg(feature = "parking-lot-locks")]
type WriteGuard<'a, B> = parking_lot::RwLockWriteGuard<'a, HashMap<Arc<str>, Arc<B>>>;
#[cfg(not(feature = "parking-lot-locks"))]
type WriteGuard<'a, B> = std::sync::RwLockWriteGuard<'a, HashMap<Arc<str>, Arc<B>>>;

/// Default lowest discernible value (1ns)
const DEFAULT_LOWEST: u64 = 1;
/// Default highest trackable value (~1 hour in ns)
const DEFAULT_HIGHEST: u64 = 3_600_000_000_000;
// Note: precision is fixed internally for performance; no configurable sigfig.

/// Central, thread-safe metrics collector for production timing.
///
/// Holds per-metric internal `Histogram` instances and provides efficient
/// recording and percentile queries via `snapshot()`. Cheap to clone, safe to
/// share across threads and async tasks.
///
/// # Examples
/// Basic record and snapshot:
/// ```
/// use benchmark::Watch;
/// let w = Watch::new();
/// w.record("op", 1_500);
/// let s = &w.snapshot()["op"];
/// assert_eq!(s.count, 1);
/// assert_eq!(s.min, 1_500);
/// ```
pub struct WatchGeneric<B: HistBackend> {
    inner: Arc<Inner<B>>,
}

/// Feature-selected concrete `Watch` type using the active histogram backend.
pub type Watch = WatchGeneric<Backend>;

impl<B: HistBackend> Default for WatchGeneric<B> {
    #[inline]
    fn default() -> Self {
        Self::new()
    }
}

impl<B: HistBackend> Clone for WatchGeneric<B> {
    #[inline]
    fn clone(&self) -> Self {
        Self {
            inner: Arc::clone(&self.inner),
        }
    }
}

struct Inner<B: HistBackend> {
    // Store Arc<Histogram> to allow lock-free record on hot path
    // Keyed by Arc<str> to avoid repeated String allocations and enable cheap sharing.
    hist: RwLock<HashMap<Arc<str>, Arc<B>>>,
    lowest: u64,
    highest: u64,
}

/// Snapshot stats for a single metric.
#[derive(Debug, Clone, Copy)]
pub struct WatchStats {
    /// Number of recorded samples.
    pub count: u64,
    /// Minimum observed value (ns).
    pub min: u64,
    /// Maximum observed value (ns).
    pub max: u64,
    /// 50th percentile/median (ns).
    pub p50: u64,
    /// 90th percentile (ns).
    pub p90: u64,
    /// 95th percentile (ns).
    pub p95: u64,
    /// 99th percentile (ns).
    pub p99: u64,
    /// 99.9th percentile (ns).
    pub p999: u64,
    /// Arithmetic mean (ns).
    pub mean: f64,
}

impl<B: HistBackend> fmt::Debug for WatchGeneric<B> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let len = self.read_hist().len();
        f.debug_struct("Watch").field("metrics_len", &len).finish()
    }
}

impl<B: HistBackend> WatchGeneric<B> {
    #[cfg(feature = "parking-lot-locks")]
    #[inline]
    fn read_hist(&self) -> ReadGuard<'_, B> {
        self.inner.hist.read()
    }

    #[cfg(not(feature = "parking-lot-locks"))]
    #[inline]
    fn read_hist(&self) -> ReadGuard<'_, B> {
        self.inner
            .hist
            .read()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
    }

    #[cfg(feature = "parking-lot-locks")]
    #[inline]
    fn write_hist(&self) -> WriteGuard<'_, B> {
        self.inner.hist.write()
    }

    #[cfg(not(feature = "parking-lot-locks"))]
    #[inline]
    fn write_hist(&self) -> WriteGuard<'_, B> {
        self.inner
            .hist
            .write()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
    }

    /// Create a new Watch with sensible defaults.
    ///
    /// # Examples
    /// ```
    /// use benchmark::Watch;
    /// let w = Watch::new();
    /// // empty initially
    /// assert!(w.snapshot().is_empty());
    /// ```
    pub fn new() -> Self {
        Self::with_bounds(DEFAULT_LOWEST, DEFAULT_HIGHEST)
    }

    /// Create a builder to configure histogram bounds and precision.
    ///
    /// # Examples
    /// ```
    /// use benchmark::Watch;
    /// let w = Watch::builder().lowest(10).highest(1_000_000).build();
    /// let _ = w; // built successfully
    /// ```
    #[inline]
    pub fn builder() -> WatchBuilderGeneric<B> {
        WatchBuilderGeneric::new()
    }

    /// Create a Watch with custom histogram bounds.
    ///
    /// `lowest_discernible`: smallest value discernible (ns)
    /// `highest_trackable`: largest value tracked (ns)
    ///
    /// # Examples
    /// ```
    /// use benchmark::Watch;
    /// let w = Watch::with_bounds(5, 10_000);
    /// let _ = w.snapshot();
    /// ```
    pub fn with_bounds(lowest_discernible: u64, highest_trackable: u64) -> Self {
        let lowest = lowest_discernible.max(1);
        let highest = highest_trackable.max(lowest + 1);
        Self {
            inner: Arc::new(Inner {
                hist: RwLock::new(HashMap::new()),
                lowest,
                highest,
            }),
        }
    }

    /// Record a duration in nanoseconds for a metric name.
    ///
    /// Safe, thread-safe, and minimal overhead.
    ///
    /// # Poisoning
    /// If the internal lock has been poisoned due to a prior panic, this method
    /// will recover the inner data and continue operating to avoid panics in production.
    ///
    /// # Examples
    /// ```
    /// use benchmark::Watch;
    /// let w = Watch::new();
    /// w.record("t", 42);
    /// assert_eq!(w.snapshot()["t"].count, 1);
    /// ```
    pub fn record(&self, name: &str, duration_ns: u64) {
        // Clamp to histogram range to avoid errors.
        let ns = duration_ns.clamp(self.inner.lowest, self.inner.highest);

        // Fast path: try obtain Arc without write locking
        let existing: Option<Arc<B>> = {
            let map = self.read_hist();
            map.get(name).cloned()
        };
        if let Some(h) = existing {
            h.record(ns);
            #[cfg(feature = "trace")]
            trace::record_event(name, ns);
            return;
        }

        // Slow path: create the histogram under write lock if absent
        let mut map = self.write_hist();
        let key: Arc<str> = Arc::<str>::from(name);
        let h = map.entry(key).or_insert_with(|| Arc::new(B::new())).clone();
        h.record(ns);
        #[cfg(feature = "trace")]
        trace::record_event(name, ns);
    }

    /// Record elapsed time since `start` for a metric name.
    ///
    /// # Examples
    /// ```
    /// use benchmark::Watch;
    /// use std::time::Instant;
    /// let w = Watch::new();
    /// let start = Instant::now();
    /// // do work
    /// let ns = w.record_instant("io", start);
    /// assert!(ns >= 0);
    /// ```
    pub fn record_instant(&self, name: &str, start: Instant) -> u64 {
        let ns_u128 = start.elapsed().as_nanos();
        // Convert to u64 using infallible saturating via try_from to satisfy clippy
        let ns_u64 = if ns_u128 > u128::from(u64::MAX) {
            u64::MAX
        } else {
            u64::try_from(ns_u128).unwrap_or(u64::MAX)
        };
        self.record(name, ns_u64);
        ns_u64
    }

    /// Return a snapshot of all metrics with basic statistics.
    ///
    /// Implementation clones histograms under a read lock, then computes outside the lock
    /// to minimize lock hold times and contention.
    ///
    /// # Panics
    /// Panics if the internal lock is poisoned from a prior panic.
    ///
    /// # Examples
    /// ```
    /// use benchmark::Watch;
    /// let w = Watch::new();
    /// w.record("rpc", 10);
    /// w.record("rpc", 20);
    /// let m = &w.snapshot()["rpc"];
    /// assert_eq!(m.count, 2);
    /// assert!(m.min <= m.p50 && m.p50 <= m.max);
    /// ```
    pub fn snapshot(&self) -> HashMap<String, WatchStats> {
        let items: Vec<(Arc<str>, Arc<B>)> = {
            let map = self.read_hist();
            map.iter()
                .map(|(k, v)| (Arc::clone(k), Arc::clone(v)))
                .collect()
        };

        let mut out = HashMap::with_capacity(items.len());
        for (name, h) in items {
            let count = h.count();
            if count == 0 {
                out.insert(
                    name.to_string(),
                    WatchStats {
                        count: 0,
                        min: 0,
                        max: 0,
                        p50: 0,
                        p90: 0,
                        p95: 0,
                        p99: 0,
                        p999: 0,
                        mean: 0.0,
                    },
                );
                continue;
            }

            // Safe unwraps since count > 0
            let min = h.min().unwrap_or(0);
            let max = h.max().unwrap_or(0);
            let p50 = h.percentile(0.50).unwrap_or(min);
            let p90 = h.percentile(0.90).unwrap_or(max);
            let p95 = h.percentile(0.95).unwrap_or(max);
            let p99 = h.percentile(0.99).unwrap_or(max);
            let p999 = h.percentile(0.999).unwrap_or(max);
            let mean = h.mean().unwrap_or(0.0);

            out.insert(
                name.to_string(),
                WatchStats {
                    count,
                    min,
                    max,
                    p50,
                    p90,
                    p95,
                    p99,
                    p999,
                    mean,
                },
            );
        }
        out
    }

    /// Clear all metrics.
    ///
    /// # Panics
    /// Panics if the internal lock is poisoned from a prior panic.
    ///
    /// # Examples
    /// ```
    /// use benchmark::Watch;
    /// let w = Watch::new();
    /// w.record("a", 1);
    /// assert!(!w.snapshot().is_empty());
    /// w.clear();
    /// assert!(w.snapshot().is_empty());
    /// ```
    pub fn clear(&self) {
        let mut map = self.write_hist();
        map.clear();
    }

    /// Clear a specific metric by name.
    ///
    /// # Panics
    /// Panics if the internal lock is poisoned from a prior panic.
    ///
    /// # Examples
    /// ```
    /// use benchmark::Watch;
    /// let w = Watch::new();
    /// w.record("x", 1);
    /// w.clear_name("x");
    /// assert!(!w.snapshot().contains_key("x"));
    /// ```
    pub fn clear_name(&self, name: &str) {
        let mut map = self.write_hist();
        map.remove(name);
    }
}

/// Builder for configuring and constructing a `Watch`.
#[derive(Debug, Clone, Copy)]
pub struct WatchBuilderGeneric<B: HistBackend> {
    lowest: u64,
    highest: u64,
    _marker: PhantomData<B>,
}

/// Feature-selected concrete `WatchBuilder` using the active histogram backend.
pub type WatchBuilder = WatchBuilderGeneric<Backend>;

impl<B: HistBackend> Default for WatchBuilderGeneric<B> {
    #[inline]
    fn default() -> Self {
        Self::new()
    }
}

impl<B: HistBackend> WatchBuilderGeneric<B> {
    /// Start a builder with default bounds: 1ns..~1h, 3 significant figures.
    ///
    /// # Examples
    /// ```
    /// use benchmark::WatchBuilder;
    /// let _w = WatchBuilder::new().lowest(1).highest(1_000_000).build();
    /// ```
    #[inline]
    pub fn new() -> Self {
        Self {
            lowest: DEFAULT_LOWEST,
            highest: DEFAULT_HIGHEST,
            _marker: PhantomData,
        }
    }

    /// Set the lowest discernible value in nanoseconds (min 1ns).
    #[inline]
    #[must_use]
    pub fn lowest(mut self, ns: u64) -> Self {
        self.lowest = ns.max(1);
        self
    }

    /// Set the highest trackable value in nanoseconds (must be > lowest).
    #[inline]
    #[must_use]
    pub fn highest(mut self, ns: u64) -> Self {
        self.highest = ns;
        self
    }

    /// Build the `Watch` with the configured settings.
    #[inline]
    pub fn build(self) -> WatchGeneric<B> {
        let lowest = self.lowest.max(1);
        let highest = self.highest.max(lowest + 1);
        WatchGeneric::<B>::with_bounds(lowest, highest)
    }
}