Skip to main content

metrics_lib/
gauge.rs

1//! # Atomic Gauge with IEEE 754 Optimization
2//!
3//! Ultra-fast atomic gauge using bit manipulation for f64 values.
4//!
5//! ## Features
6//!
7//! - **Atomic f64 operations** - Using IEEE 754 bit manipulation
8//! - **Sub-5ns updates** - Direct bit-level atomic operations
9//! - **Zero allocations** - Pure stack operations
10//! - **Lock-free** - Never blocks, never waits
11//! - **Cache optimized** - Aligned to prevent false sharing
12
13use crate::{MetricsError, Result};
14use std::sync::atomic::{AtomicU64, Ordering};
15use std::time::{Duration, Instant};
16
17/// Ultra-fast atomic gauge for f64 values
18///
19/// Uses IEEE 754 bit manipulation for atomic operations on floating-point values.
20/// Cache-line aligned to prevent false sharing.
21#[repr(align(64))]
22pub struct Gauge {
23    /// Gauge value stored as IEEE 754 bits in atomic u64
24    value: AtomicU64,
25    /// Creation timestamp for statistics
26    created_at: Instant,
27}
28
29/// Gauge statistics
30#[derive(Debug, Clone)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize))]
32pub struct GaugeStats {
33    /// Current gauge value
34    pub value: f64,
35    /// Time since gauge creation
36    pub age: Duration,
37    /// Number of updates (not tracked by default for performance)
38    pub updates: Option<u64>,
39}
40
41impl Gauge {
42    /// Create new gauge starting at zero
43    #[inline]
44    pub fn new() -> Self {
45        Self {
46            value: AtomicU64::new(0.0_f64.to_bits()),
47            created_at: Instant::now(),
48        }
49    }
50
51    /// Try to add to current value with validation
52    ///
53    /// Returns `Err(MetricsError::InvalidValue)` if `delta` is not finite (NaN or ±inf)
54    /// or if the resulting value would become non-finite. Returns
55    /// `Err(MetricsError::Overflow)` if the sum overflows to a non-finite value.
56    ///
57    /// Example
58    /// ```
59    /// use metrics_lib::{Gauge, MetricsError};
60    /// let g = Gauge::with_value(1.5);
61    /// g.try_add(2.5).unwrap();
62    /// assert_eq!(g.get(), 4.0);
63    /// assert!(matches!(g.try_add(f64::INFINITY), Err(MetricsError::InvalidValue{..})));
64    /// ```
65    #[inline]
66    pub fn try_add(&self, delta: f64) -> Result<()> {
67        if delta == 0.0 {
68            return Ok(());
69        }
70        if !delta.is_finite() {
71            return Err(MetricsError::InvalidValue {
72                reason: "delta is not finite",
73            });
74        }
75
76        loop {
77            let current_bits = self.value.load(Ordering::Relaxed);
78            let current_value = f64::from_bits(current_bits);
79            let new_value = current_value + delta;
80            if !new_value.is_finite() {
81                return Err(MetricsError::Overflow);
82            }
83            let new_bits = new_value.to_bits();
84
85            match self.value.compare_exchange_weak(
86                current_bits,
87                new_bits,
88                Ordering::Relaxed,
89                Ordering::Relaxed,
90            ) {
91                Ok(_) => return Ok(()),
92                Err(_) => continue,
93            }
94        }
95    }
96
97    /// Try to subtract from current value (validated)
98    ///
99    /// Validation semantics are identical to [`Gauge::try_add`] but apply to
100    /// subtraction (`-delta`).
101    ///
102    /// Example
103    /// ```
104    /// use metrics_lib::Gauge;
105    /// let g = Gauge::with_value(10.0);
106    /// g.try_sub(4.0).unwrap();
107    /// assert_eq!(g.get(), 6.0);
108    /// ```
109    #[inline]
110    pub fn try_sub(&self, delta: f64) -> Result<()> {
111        self.try_add(-delta)
112    }
113
114    /// Create gauge with initial value
115    #[inline]
116    pub fn with_value(initial: f64) -> Self {
117        Self {
118            value: AtomicU64::new(initial.to_bits()),
119            created_at: Instant::now(),
120        }
121    }
122
123    /// Set gauge value - THE FASTEST PATH
124    ///
125    /// This is optimized for maximum speed:
126    /// - Convert f64 to IEEE 754 bits
127    /// - Single atomic store instruction
128    /// - Relaxed memory ordering for speed
129    /// - Inlined for zero function call overhead
130    #[inline(always)]
131    pub fn set(&self, value: f64) {
132        self.value.store(value.to_bits(), Ordering::Relaxed);
133    }
134
135    /// Try to set gauge value with validation
136    ///
137    /// Returns `Err(MetricsError::InvalidValue)` if `value` is not finite (NaN or ±inf).
138    ///
139    /// Example
140    /// ```
141    /// use metrics_lib::{Gauge, MetricsError};
142    /// let g = Gauge::new();
143    /// assert!(g.try_set(42.0).is_ok());
144    /// assert!(matches!(g.try_set(f64::NAN), Err(MetricsError::InvalidValue{..})));
145    /// ```
146    #[inline]
147    pub fn try_set(&self, value: f64) -> Result<()> {
148        if !value.is_finite() {
149            return Err(MetricsError::InvalidValue {
150                reason: "value is not finite",
151            });
152        }
153        self.set(value);
154        Ok(())
155    }
156
157    /// Get current value - single atomic load
158    ///
159    /// Note: `#[must_use]`. The returned value represents current gauge state;
160    /// ignoring it may indicate a logic bug.
161    #[must_use]
162    #[inline(always)]
163    pub fn get(&self) -> f64 {
164        f64::from_bits(self.value.load(Ordering::Relaxed))
165    }
166
167    /// Add to current value - atomic compare-and-swap loop
168    ///
169    /// # Non-finite protection
170    /// If `delta` is non-finite (NaN, ±∞), this is a **no-op**.
171    /// If the resulting sum would also be non-finite the gauge retains its
172    /// current value (silent no-op). Use [`Gauge::try_add`] to receive an
173    /// explicit error instead.
174    #[inline]
175    pub fn add(&self, delta: f64) {
176        if delta == 0.0 || !delta.is_finite() {
177            return;
178        }
179
180        loop {
181            let current_bits = self.value.load(Ordering::Relaxed);
182            let current_value = f64::from_bits(current_bits);
183            let new_value = current_value + delta;
184            if !new_value.is_finite() {
185                return; // Silently no-op — use try_add for error reporting
186            }
187            let new_bits = new_value.to_bits();
188
189            match self.value.compare_exchange_weak(
190                current_bits,
191                new_bits,
192                Ordering::Relaxed,
193                Ordering::Relaxed,
194            ) {
195                Ok(_) => break,
196                Err(_) => continue, // Retry with latest value
197            }
198        }
199    }
200
201    /// Subtract from current value
202    ///
203    /// Non-finite and overflow protection is identical to [`Gauge::add`].
204    #[inline]
205    pub fn sub(&self, delta: f64) {
206        self.add(-delta);
207    }
208
209    /// Set to maximum of current value and new value
210    #[inline]
211    pub fn set_max(&self, value: f64) {
212        loop {
213            let current_bits = self.value.load(Ordering::Relaxed);
214            let current_value = f64::from_bits(current_bits);
215
216            if value <= current_value {
217                break; // Current value is already larger
218            }
219
220            let new_bits = value.to_bits();
221            match self.value.compare_exchange_weak(
222                current_bits,
223                new_bits,
224                Ordering::Relaxed,
225                Ordering::Relaxed,
226            ) {
227                Ok(_) => break,
228                Err(_) => continue, // Retry
229            }
230        }
231    }
232
233    /// Try to set to maximum of current value and new value
234    ///
235    /// Returns `Err(MetricsError::InvalidValue)` if `value` is not finite.
236    /// Otherwise sets the gauge to `max(current, value)` and returns `Ok(())`.
237    ///
238    /// Example
239    /// ```
240    /// use metrics_lib::{Gauge, MetricsError};
241    /// let g = Gauge::with_value(5.0);
242    /// g.try_set_max(10.0).unwrap();
243    /// assert_eq!(g.get(), 10.0);
244    /// assert!(matches!(g.try_set_max(f64::INFINITY), Err(MetricsError::InvalidValue{..})));
245    /// ```
246    #[inline]
247    pub fn try_set_max(&self, value: f64) -> Result<()> {
248        if !value.is_finite() {
249            return Err(MetricsError::InvalidValue {
250                reason: "value is not finite",
251            });
252        }
253        loop {
254            let current_bits = self.value.load(Ordering::Relaxed);
255            let current_value = f64::from_bits(current_bits);
256            if value <= current_value {
257                return Ok(());
258            }
259            let new_bits = value.to_bits();
260            match self.value.compare_exchange_weak(
261                current_bits,
262                new_bits,
263                Ordering::Relaxed,
264                Ordering::Relaxed,
265            ) {
266                Ok(_) => return Ok(()),
267                Err(_) => continue,
268            }
269        }
270    }
271
272    /// Set to minimum of current value and new value
273    #[inline]
274    pub fn set_min(&self, value: f64) {
275        loop {
276            let current_bits = self.value.load(Ordering::Relaxed);
277            let current_value = f64::from_bits(current_bits);
278
279            if value >= current_value {
280                break; // Current value is already smaller
281            }
282
283            let new_bits = value.to_bits();
284            match self.value.compare_exchange_weak(
285                current_bits,
286                new_bits,
287                Ordering::Relaxed,
288                Ordering::Relaxed,
289            ) {
290                Ok(_) => break,
291                Err(_) => continue, // Retry
292            }
293        }
294    }
295
296    /// Try to set to minimum of current value and new value
297    ///
298    /// Returns `Err(MetricsError::InvalidValue)` if `value` is not finite.
299    /// Otherwise sets the gauge to `min(current, value)` and returns `Ok(())`.
300    ///
301    /// Example
302    /// ```
303    /// use metrics_lib::{Gauge, MetricsError};
304    /// let g = Gauge::with_value(10.0);
305    /// g.try_set_min(7.0).unwrap();
306    /// assert_eq!(g.get(), 7.0);
307    /// assert!(matches!(g.try_set_min(f64::NAN), Err(MetricsError::InvalidValue{..})));
308    /// ```
309    #[inline]
310    pub fn try_set_min(&self, value: f64) -> Result<()> {
311        if !value.is_finite() {
312            return Err(MetricsError::InvalidValue {
313                reason: "value is not finite",
314            });
315        }
316        loop {
317            let current_bits = self.value.load(Ordering::Relaxed);
318            let current_value = f64::from_bits(current_bits);
319            if value >= current_value {
320                return Ok(());
321            }
322            let new_bits = value.to_bits();
323            match self.value.compare_exchange_weak(
324                current_bits,
325                new_bits,
326                Ordering::Relaxed,
327                Ordering::Relaxed,
328            ) {
329                Ok(_) => return Ok(()),
330                Err(_) => continue,
331            }
332        }
333    }
334
335    /// Atomic compare-and-swap
336    ///
337    /// Returns Ok(previous_value) if successful, Err(current_value) if failed
338    #[inline]
339    pub fn compare_and_swap(&self, expected: f64, new: f64) -> core::result::Result<f64, f64> {
340        let expected_bits = expected.to_bits();
341        let new_bits = new.to_bits();
342
343        match self.value.compare_exchange(
344            expected_bits,
345            new_bits,
346            Ordering::SeqCst,
347            Ordering::SeqCst,
348        ) {
349            Ok(prev_bits) => Ok(f64::from_bits(prev_bits)),
350            Err(current_bits) => Err(f64::from_bits(current_bits)),
351        }
352    }
353
354    /// Reset to zero
355    #[inline]
356    pub fn reset(&self) {
357        self.set(0.0);
358    }
359
360    /// Multiply current value by factor
361    ///
362    /// If `factor` is non-finite, or if the product would be non-finite,
363    /// this is a **no-op** (the gauge retains its current value).
364    #[inline]
365    pub fn multiply(&self, factor: f64) {
366        if factor == 1.0 || !factor.is_finite() {
367            return;
368        }
369
370        loop {
371            let current_bits = self.value.load(Ordering::Relaxed);
372            let current_value = f64::from_bits(current_bits);
373            let new_value = current_value * factor;
374            if !new_value.is_finite() {
375                return; // Guard against overflow to ±Inf or NaN propagation
376            }
377            let new_bits = new_value.to_bits();
378
379            match self.value.compare_exchange_weak(
380                current_bits,
381                new_bits,
382                Ordering::Relaxed,
383                Ordering::Relaxed,
384            ) {
385                Ok(_) => break,
386                Err(_) => continue,
387            }
388        }
389    }
390
391    /// Divide current value by divisor
392    ///
393    /// If `divisor` is zero, non-finite, or the result would be non-finite,
394    /// this is a **no-op** (the gauge retains its current value). Non-finite
395    /// product protection is handled transitively by [`Gauge::multiply`].
396    #[inline]
397    pub fn divide(&self, divisor: f64) {
398        if divisor == 0.0 || divisor == 1.0 || !divisor.is_finite() {
399            return;
400        }
401        self.multiply(1.0 / divisor);
402    }
403
404    /// Set to absolute value of current value
405    #[inline]
406    pub fn abs(&self) {
407        loop {
408            let current_bits = self.value.load(Ordering::Relaxed);
409            let current_value = f64::from_bits(current_bits);
410
411            if current_value >= 0.0 {
412                break; // Already positive
413            }
414
415            let abs_value = current_value.abs();
416            let abs_bits = abs_value.to_bits();
417
418            match self.value.compare_exchange_weak(
419                current_bits,
420                abs_bits,
421                Ordering::Relaxed,
422                Ordering::Relaxed,
423            ) {
424                Ok(_) => break,
425                Err(_) => continue,
426            }
427        }
428    }
429
430    /// Clamp value to range [min, max]
431    #[inline]
432    pub fn clamp(&self, min: f64, max: f64) {
433        loop {
434            let current_bits = self.value.load(Ordering::Relaxed);
435            let current_value = f64::from_bits(current_bits);
436            let clamped_value = current_value.clamp(min, max);
437
438            if (current_value - clamped_value).abs() < f64::EPSILON {
439                break; // Already in range
440            }
441
442            let clamped_bits = clamped_value.to_bits();
443
444            match self.value.compare_exchange_weak(
445                current_bits,
446                clamped_bits,
447                Ordering::Relaxed,
448                Ordering::Relaxed,
449            ) {
450                Ok(_) => break,
451                Err(_) => continue,
452            }
453        }
454    }
455
456    /// Exponential moving average update
457    ///
458    /// new_value = alpha * sample + (1 - alpha) * old_value
459    #[inline]
460    pub fn update_ema(&self, sample: f64, alpha: f64) {
461        let alpha = alpha.clamp(0.0, 1.0);
462
463        loop {
464            let current_bits = self.value.load(Ordering::Relaxed);
465            let current_value = f64::from_bits(current_bits);
466            let new_value = alpha * sample + (1.0 - alpha) * current_value;
467            let new_bits = new_value.to_bits();
468
469            match self.value.compare_exchange_weak(
470                current_bits,
471                new_bits,
472                Ordering::Relaxed,
473                Ordering::Relaxed,
474            ) {
475                Ok(_) => break,
476                Err(_) => continue,
477            }
478        }
479    }
480
481    /// Get comprehensive statistics
482    ///
483    /// Note: `#[must_use]`. Statistics summarize current state; dropping the
484    /// result may indicate a logic bug.
485    #[must_use]
486    pub fn stats(&self) -> GaugeStats {
487        GaugeStats {
488            value: self.get(),
489            age: self.created_at.elapsed(),
490            updates: None, // Not tracked by default for performance
491        }
492    }
493
494    /// Get age since creation
495    ///
496    /// Note: `#[must_use]`. Age is often used for derived metrics; don't call
497    /// this for side effects.
498    #[must_use]
499    #[inline]
500    pub fn age(&self) -> Duration {
501        self.created_at.elapsed()
502    }
503
504    /// Check if gauge is zero
505    ///
506    /// Note: `#[must_use]`. The boolean result determines control flow.
507    #[must_use]
508    #[inline]
509    pub fn is_zero(&self) -> bool {
510        self.get() == 0.0
511    }
512
513    /// Check if value is positive
514    ///
515    /// Note: `#[must_use]`. The boolean result determines control flow.
516    #[must_use]
517    #[inline]
518    pub fn is_positive(&self) -> bool {
519        self.get() > 0.0
520    }
521
522    /// Check if value is negative
523    ///
524    /// Note: `#[must_use]`. The boolean result determines control flow.
525    #[must_use]
526    #[inline]
527    pub fn is_negative(&self) -> bool {
528        self.get() < 0.0
529    }
530
531    /// Check if value is finite (not NaN or infinity)
532    ///
533    /// Note: `#[must_use]`. The boolean result determines control flow.
534    #[must_use]
535    #[inline]
536    pub fn is_finite(&self) -> bool {
537        self.get().is_finite()
538    }
539}
540
541impl Default for Gauge {
542    #[inline]
543    fn default() -> Self {
544        Self::new()
545    }
546}
547
548impl std::fmt::Display for Gauge {
549    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
550        write!(f, "Gauge({})", self.get())
551    }
552}
553
554impl std::fmt::Debug for Gauge {
555    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
556        f.debug_struct("Gauge")
557            .field("value", &self.get())
558            .field("age", &self.age())
559            .field("is_finite", &self.is_finite())
560            .finish()
561    }
562}
563
564// Thread safety - Gauge is Send + Sync
565// Gauge is composed of `AtomicU64` (Send + Sync) and `Instant` (Send + Sync).
566// The compiler derives Send + Sync automatically; no explicit unsafe impl needed.
567
568/// Specialized gauge types for common use cases
569pub mod specialized {
570    use super::*;
571
572    /// Percentage gauge (0.0 to 100.0)
573    #[repr(align(64))]
574    pub struct PercentageGauge {
575        inner: Gauge,
576    }
577
578    impl PercentageGauge {
579        /// Create new percentage gauge
580        #[inline]
581        pub fn new() -> Self {
582            Self {
583                inner: Gauge::new(),
584            }
585        }
586
587        /// Set percentage (automatically clamped to 0.0-100.0)
588        #[inline(always)]
589        pub fn set_percentage(&self, percentage: f64) {
590            let clamped = percentage.clamp(0.0, 100.0);
591            self.inner.set(clamped);
592        }
593
594        /// Get percentage
595        #[inline(always)]
596        pub fn get_percentage(&self) -> f64 {
597            self.inner.get()
598        }
599
600        /// Set from ratio (0.0-1.0 becomes 0.0-100.0)
601        #[inline(always)]
602        pub fn set_ratio(&self, ratio: f64) {
603            let percentage = (ratio * 100.0).clamp(0.0, 100.0);
604            self.inner.set(percentage);
605        }
606
607        /// Get as ratio (0.0-1.0)
608        #[inline(always)]
609        pub fn get_ratio(&self) -> f64 {
610            self.inner.get() / 100.0
611        }
612
613        /// Check if at maximum (100%)
614        #[inline]
615        pub fn is_full(&self) -> bool {
616            (self.inner.get() - 100.0).abs() < f64::EPSILON
617        }
618
619        /// Check if at minimum (0%)
620        #[inline]
621        pub fn is_empty(&self) -> bool {
622            self.inner.get() < f64::EPSILON
623        }
624
625        /// Add percentage (clamped to valid range)
626        #[inline]
627        pub fn add_percentage(&self, delta: f64) {
628            loop {
629                let current = self.get_percentage();
630                let new_value = (current + delta).clamp(0.0, 100.0);
631
632                if (current - new_value).abs() < f64::EPSILON {
633                    break; // No change needed
634                }
635
636                match self.inner.compare_and_swap(current, new_value) {
637                    Ok(_) => break,
638                    Err(_) => continue, // Retry
639                }
640            }
641        }
642
643        /// Get gauge statistics
644        pub fn stats(&self) -> GaugeStats {
645            self.inner.stats()
646        }
647    }
648
649    impl Default for PercentageGauge {
650        fn default() -> Self {
651            Self::new()
652        }
653    }
654
655    impl std::fmt::Display for PercentageGauge {
656        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
657            write!(f, "PercentageGauge({}%)", self.get_percentage())
658        }
659    }
660
661    /// CPU usage gauge (specialized percentage gauge)
662    pub type CpuGauge = PercentageGauge;
663
664    /// Memory gauge for byte values
665    #[repr(align(64))]
666    pub struct MemoryGauge {
667        bytes: Gauge,
668    }
669
670    impl MemoryGauge {
671        /// Create new memory gauge
672        #[inline]
673        pub fn new() -> Self {
674            Self {
675                bytes: Gauge::new(),
676            }
677        }
678
679        /// Set memory usage in bytes
680        #[inline(always)]
681        pub fn set_bytes(&self, bytes: u64) {
682            self.bytes.set(bytes as f64);
683        }
684
685        /// Get memory usage in bytes
686        #[inline]
687        pub fn get_bytes(&self) -> u64 {
688            self.bytes.get() as u64
689        }
690
691        /// Get memory usage in KB
692        #[inline]
693        pub fn get_kb(&self) -> f64 {
694            self.bytes.get() / 1024.0
695        }
696
697        /// Get memory usage in MB
698        #[inline]
699        pub fn get_mb(&self) -> f64 {
700            self.bytes.get() / (1024.0 * 1024.0)
701        }
702
703        /// Get memory usage in GB
704        #[inline]
705        pub fn get_gb(&self) -> f64 {
706            self.bytes.get() / (1024.0 * 1024.0 * 1024.0)
707        }
708
709        /// Add bytes
710        #[inline]
711        pub fn add_bytes(&self, bytes: i64) {
712            self.bytes.add(bytes as f64);
713        }
714
715        /// Get gauge statistics
716        pub fn stats(&self) -> GaugeStats {
717            self.bytes.stats()
718        }
719    }
720
721    impl Default for MemoryGauge {
722        fn default() -> Self {
723            Self::new()
724        }
725    }
726
727    impl std::fmt::Display for MemoryGauge {
728        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
729            let mb = self.get_mb();
730            if mb >= 1024.0 {
731                write!(f, "MemoryGauge({:.2} GB)", self.get_gb())
732            } else {
733                write!(f, "MemoryGauge({mb:.2} MB)")
734            }
735        }
736    }
737}
738
739#[cfg(test)]
740mod tests {
741    use super::*;
742    use std::sync::Arc;
743    use std::thread;
744
745    #[test]
746    fn test_basic_operations() {
747        let gauge = Gauge::new();
748
749        assert_eq!(gauge.get(), 0.0);
750        assert!(gauge.is_zero());
751        assert!(gauge.is_finite());
752
753        gauge.set(42.5);
754        assert_eq!(gauge.get(), 42.5);
755        assert!(!gauge.is_zero());
756        assert!(gauge.is_positive());
757
758        gauge.add(7.5);
759        assert_eq!(gauge.get(), 50.0);
760
761        gauge.sub(10.0);
762        assert_eq!(gauge.get(), 40.0);
763
764        gauge.reset();
765        assert_eq!(gauge.get(), 0.0);
766    }
767
768    #[test]
769    fn test_mathematical_operations() {
770        let gauge = Gauge::with_value(10.0);
771
772        gauge.multiply(2.0);
773        assert_eq!(gauge.get(), 20.0);
774
775        gauge.divide(4.0);
776        assert_eq!(gauge.get(), 5.0);
777
778        gauge.set(-15.0);
779        assert!(gauge.is_negative());
780
781        gauge.abs();
782        assert_eq!(gauge.get(), 15.0);
783        assert!(gauge.is_positive());
784
785        gauge.clamp(5.0, 10.0);
786        assert_eq!(gauge.get(), 10.0);
787    }
788
789    #[test]
790    fn test_min_max_operations() {
791        let gauge = Gauge::with_value(10.0);
792
793        gauge.set_max(15.0);
794        assert_eq!(gauge.get(), 15.0);
795
796        gauge.set_max(12.0); // Should not change
797        assert_eq!(gauge.get(), 15.0);
798
799        gauge.set_min(8.0);
800        assert_eq!(gauge.get(), 8.0);
801
802        gauge.set_min(12.0); // Should not change
803        assert_eq!(gauge.get(), 8.0);
804    }
805
806    #[test]
807    fn test_compare_and_swap() {
808        let gauge = Gauge::with_value(10.0);
809
810        // Successful swap
811        assert_eq!(gauge.compare_and_swap(10.0, 20.0), Ok(10.0));
812        assert_eq!(gauge.get(), 20.0);
813
814        // Failed swap
815        assert_eq!(gauge.compare_and_swap(10.0, 30.0), Err(20.0));
816        assert_eq!(gauge.get(), 20.0);
817    }
818
819    #[test]
820    fn test_ema_update() {
821        let gauge = Gauge::with_value(10.0);
822
823        // EMA with alpha = 0.5: 0.5 * 20 + 0.5 * 10 = 15
824        gauge.update_ema(20.0, 0.5);
825        assert_eq!(gauge.get(), 15.0);
826
827        // EMA with alpha = 0.3: 0.3 * 30 + 0.7 * 15 = 19.5
828        gauge.update_ema(30.0, 0.3);
829        assert_eq!(gauge.get(), 19.5);
830    }
831
832    #[test]
833    fn test_percentage_gauge() {
834        let gauge = specialized::PercentageGauge::new();
835
836        gauge.set_percentage(75.5);
837        assert_eq!(gauge.get_percentage(), 75.5);
838        assert!((gauge.get_ratio() - 0.755).abs() < f64::EPSILON);
839
840        gauge.set_ratio(0.9);
841        assert_eq!(gauge.get_percentage(), 90.0);
842
843        // Test clamping
844        gauge.set_percentage(150.0);
845        assert_eq!(gauge.get_percentage(), 100.0);
846        assert!(gauge.is_full());
847
848        gauge.set_percentage(-10.0);
849        assert_eq!(gauge.get_percentage(), 0.0);
850        assert!(gauge.is_empty());
851
852        // Test add with clamping
853        gauge.set_percentage(95.0);
854        gauge.add_percentage(10.0);
855        assert_eq!(gauge.get_percentage(), 100.0);
856    }
857
858    #[test]
859    fn test_memory_gauge() {
860        let gauge = specialized::MemoryGauge::new();
861
862        gauge.set_bytes(1024 * 1024 * 1024); // 1GB
863
864        assert_eq!(gauge.get_bytes(), 1024 * 1024 * 1024);
865        assert!((gauge.get_mb() - 1024.0).abs() < 0.1);
866        assert!((gauge.get_gb() - 1.0).abs() < 0.001);
867
868        gauge.add_bytes(1024 * 1024); // Add 1MB
869        assert!(gauge.get_mb() > 1024.0);
870    }
871
872    #[test]
873    fn test_statistics() {
874        let gauge = Gauge::with_value(42.0);
875
876        let stats = gauge.stats();
877        assert_eq!(stats.value, 42.0);
878        assert!(stats.age <= gauge.age());
879        assert!(stats.updates.is_none()); // Not tracked by default
880    }
881
882    #[test]
883    fn test_high_concurrency() {
884        let gauge = Arc::new(Gauge::new());
885        let num_threads = 100;
886        let operations_per_thread = 1000;
887
888        let handles: Vec<_> = (0..num_threads)
889            .map(|thread_id| {
890                let gauge = Arc::clone(&gauge);
891                thread::spawn(move || {
892                    for i in 0..operations_per_thread {
893                        let value = (thread_id * operations_per_thread + i) as f64;
894                        gauge.set(value);
895                        gauge.add(0.1);
896                        gauge.multiply(1.001);
897                    }
898                })
899            })
900            .collect();
901
902        for handle in handles {
903            handle.join().unwrap();
904        }
905
906        // Just check that we can read the final value without panicking
907        let final_value = gauge.get();
908        assert!(final_value.is_finite());
909
910        let stats = gauge.stats();
911        assert!(stats.age <= gauge.age());
912    }
913
914    #[test]
915    fn test_special_values() {
916        let gauge = Gauge::new();
917
918        // Test infinity handling
919        gauge.set(f64::INFINITY);
920        assert!(!gauge.is_finite());
921
922        // Test NaN handling
923        gauge.set(f64::NAN);
924        assert!(!gauge.is_finite());
925
926        // Reset to normal value
927        gauge.set(42.0);
928        assert!(gauge.is_finite());
929    }
930
931    #[test]
932    fn test_display_and_debug() {
933        let gauge = Gauge::with_value(42.5);
934
935        let display_str = format!("{gauge}");
936        assert!(display_str.contains("42.5"));
937
938        let debug_str = format!("{gauge:?}");
939        assert!(debug_str.contains("Gauge"));
940        assert!(debug_str.contains("42.5"));
941    }
942
943    #[test]
944    fn test_try_add_validation_and_overflow() {
945        let gauge = Gauge::new();
946
947        // Invalid deltas
948        assert!(matches!(
949            gauge.try_add(f64::NAN),
950            Err(MetricsError::InvalidValue { .. })
951        ));
952        assert!(matches!(
953            gauge.try_add(f64::INFINITY),
954            Err(MetricsError::InvalidValue { .. })
955        ));
956
957        // Overflow on result becoming non-finite
958        let gauge2 = Gauge::with_value(f64::MAX / 2.0);
959        assert!(matches!(
960            gauge2.try_add(f64::MAX),
961            Err(MetricsError::Overflow)
962        ));
963    }
964
965    #[test]
966    fn test_try_set_and_min_max_validation() {
967        let gauge = Gauge::new();
968        assert!(matches!(
969            gauge.try_set(f64::NAN),
970            Err(MetricsError::InvalidValue { .. })
971        ));
972
973        // try_set_max invalid
974        assert!(matches!(
975            gauge.try_set_max(f64::INFINITY),
976            Err(MetricsError::InvalidValue { .. })
977        ));
978
979        // try_set_min invalid
980        assert!(matches!(
981            gauge.try_set_min(f64::NAN),
982            Err(MetricsError::InvalidValue { .. })
983        ));
984    }
985
986    #[test]
987    fn test_ema_alpha_boundaries() {
988        let gauge = Gauge::with_value(10.0);
989
990        // alpha < 0 should clamp to 0 -> unchanged
991        gauge.update_ema(100.0, -1.0);
992        assert_eq!(gauge.get(), 10.0);
993
994        // alpha > 1 should clamp to 1 -> equals sample
995        gauge.update_ema(100.0, 2.0);
996        assert_eq!(gauge.get(), 100.0);
997
998        // alpha 0 -> unchanged; alpha 1 -> exact sample
999        gauge.set(5.0);
1000        gauge.update_ema(20.0, 0.0);
1001        assert_eq!(gauge.get(), 5.0);
1002        gauge.update_ema(20.0, 1.0);
1003        assert_eq!(gauge.get(), 20.0);
1004    }
1005
1006    #[test]
1007    fn test_non_finite_math_helpers_are_noops() {
1008        let gauge = Gauge::with_value(12.0);
1009
1010        gauge.add(f64::NAN);
1011        assert_eq!(gauge.get(), 12.0);
1012
1013        gauge.multiply(f64::INFINITY);
1014        assert_eq!(gauge.get(), 12.0);
1015
1016        gauge.divide(0.0);
1017        assert_eq!(gauge.get(), 12.0);
1018
1019        let huge = Gauge::with_value(f64::MAX / 2.0);
1020        huge.multiply(4.0);
1021        assert_eq!(huge.get(), f64::MAX / 2.0);
1022    }
1023}
1024
1025#[cfg(all(test, feature = "bench-tests", not(tarpaulin), not(coverage)))]
1026#[allow(unused_imports)]
1027mod benchmarks {
1028    use super::*;
1029    use std::time::Instant;
1030
1031    #[cfg_attr(not(feature = "bench-tests"), ignore)]
1032    #[test]
1033    fn bench_gauge_set() {
1034        let gauge = Gauge::new();
1035        let iterations = 10_000_000;
1036
1037        let start = Instant::now();
1038        for i in 0..iterations {
1039            gauge.set(i as f64);
1040        }
1041        let elapsed = start.elapsed();
1042
1043        println!(
1044            "Gauge set: {:.2} ns/op",
1045            elapsed.as_nanos() as f64 / iterations as f64
1046        );
1047
1048        // Throughput-only smoke check; Criterion is the regression detector.
1049        assert_eq!(gauge.get(), (iterations - 1) as f64);
1050    }
1051
1052    #[cfg_attr(not(feature = "bench-tests"), ignore)]
1053    #[test]
1054    fn bench_gauge_add() {
1055        let gauge = Gauge::new();
1056        let iterations = 1_000_000;
1057
1058        let start = Instant::now();
1059        for _ in 0..iterations {
1060            gauge.add(1.0);
1061        }
1062        let elapsed = start.elapsed();
1063
1064        println!(
1065            "Gauge add: {:.2} ns/op",
1066            elapsed.as_nanos() as f64 / iterations as f64
1067        );
1068
1069        // Throughput-only smoke check; Criterion is the regression detector.
1070        assert_eq!(gauge.get(), iterations as f64);
1071    }
1072
1073    #[cfg_attr(not(feature = "bench-tests"), ignore)]
1074    #[test]
1075    fn bench_gauge_get() {
1076        let gauge = Gauge::with_value(42.5);
1077        let iterations = 100_000_000;
1078
1079        let start = Instant::now();
1080        let mut sum = 0.0;
1081        for _ in 0..iterations {
1082            sum += gauge.get();
1083        }
1084        let elapsed = start.elapsed();
1085
1086        println!(
1087            "Gauge get: {:.2} ns/op",
1088            elapsed.as_nanos() as f64 / iterations as f64
1089        );
1090
1091        // Prevent optimization elision; Criterion is the regression detector.
1092        assert_eq!(sum, 42.5 * iterations as f64);
1093    }
1094}