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