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