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