use crate::{MetricsError, Result};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
#[repr(align(64))]
pub struct Gauge {
value: AtomicU64,
created_at: Instant,
}
#[derive(Debug, Clone)]
pub struct GaugeStats {
pub value: f64,
pub age: Duration,
pub updates: Option<u64>,
}
impl Gauge {
#[inline]
pub fn new() -> Self {
Self {
value: AtomicU64::new(0.0_f64.to_bits()),
created_at: Instant::now(),
}
}
#[inline]
pub fn try_add(&self, delta: f64) -> Result<()> {
if delta == 0.0 {
return Ok(());
}
if !delta.is_finite() {
return Err(MetricsError::InvalidValue {
reason: "delta is not finite",
});
}
loop {
let current_bits = self.value.load(Ordering::Relaxed);
let current_value = f64::from_bits(current_bits);
let new_value = current_value + delta;
if !new_value.is_finite() {
return Err(MetricsError::Overflow);
}
let new_bits = new_value.to_bits();
match self.value.compare_exchange_weak(
current_bits,
new_bits,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => return Ok(()),
Err(_) => continue,
}
}
}
#[inline]
pub fn try_sub(&self, delta: f64) -> Result<()> {
self.try_add(-delta)
}
#[inline]
pub fn with_value(initial: f64) -> Self {
Self {
value: AtomicU64::new(initial.to_bits()),
created_at: Instant::now(),
}
}
#[inline(always)]
pub fn set(&self, value: f64) {
self.value.store(value.to_bits(), Ordering::Relaxed);
}
#[inline]
pub fn try_set(&self, value: f64) -> Result<()> {
if !value.is_finite() {
return Err(MetricsError::InvalidValue {
reason: "value is not finite",
});
}
self.set(value);
Ok(())
}
#[must_use]
#[inline(always)]
pub fn get(&self) -> f64 {
f64::from_bits(self.value.load(Ordering::Relaxed))
}
#[inline]
pub fn add(&self, delta: f64) {
if delta == 0.0 || !delta.is_finite() {
return;
}
loop {
let current_bits = self.value.load(Ordering::Relaxed);
let current_value = f64::from_bits(current_bits);
let new_value = current_value + delta;
if !new_value.is_finite() {
return; }
let new_bits = new_value.to_bits();
match self.value.compare_exchange_weak(
current_bits,
new_bits,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(_) => continue, }
}
}
#[inline]
pub fn sub(&self, delta: f64) {
self.add(-delta);
}
#[inline]
pub fn set_max(&self, value: f64) {
loop {
let current_bits = self.value.load(Ordering::Relaxed);
let current_value = f64::from_bits(current_bits);
if value <= current_value {
break; }
let new_bits = value.to_bits();
match self.value.compare_exchange_weak(
current_bits,
new_bits,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(_) => continue, }
}
}
#[inline]
pub fn try_set_max(&self, value: f64) -> Result<()> {
if !value.is_finite() {
return Err(MetricsError::InvalidValue {
reason: "value is not finite",
});
}
loop {
let current_bits = self.value.load(Ordering::Relaxed);
let current_value = f64::from_bits(current_bits);
if value <= current_value {
return Ok(());
}
let new_bits = value.to_bits();
match self.value.compare_exchange_weak(
current_bits,
new_bits,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => return Ok(()),
Err(_) => continue,
}
}
}
#[inline]
pub fn set_min(&self, value: f64) {
loop {
let current_bits = self.value.load(Ordering::Relaxed);
let current_value = f64::from_bits(current_bits);
if value >= current_value {
break; }
let new_bits = value.to_bits();
match self.value.compare_exchange_weak(
current_bits,
new_bits,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(_) => continue, }
}
}
#[inline]
pub fn try_set_min(&self, value: f64) -> Result<()> {
if !value.is_finite() {
return Err(MetricsError::InvalidValue {
reason: "value is not finite",
});
}
loop {
let current_bits = self.value.load(Ordering::Relaxed);
let current_value = f64::from_bits(current_bits);
if value >= current_value {
return Ok(());
}
let new_bits = value.to_bits();
match self.value.compare_exchange_weak(
current_bits,
new_bits,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => return Ok(()),
Err(_) => continue,
}
}
}
#[inline]
pub fn compare_and_swap(&self, expected: f64, new: f64) -> core::result::Result<f64, f64> {
let expected_bits = expected.to_bits();
let new_bits = new.to_bits();
match self.value.compare_exchange(
expected_bits,
new_bits,
Ordering::SeqCst,
Ordering::SeqCst,
) {
Ok(prev_bits) => Ok(f64::from_bits(prev_bits)),
Err(current_bits) => Err(f64::from_bits(current_bits)),
}
}
#[inline]
pub fn reset(&self) {
self.set(0.0);
}
#[inline]
pub fn multiply(&self, factor: f64) {
if factor == 1.0 || !factor.is_finite() {
return;
}
loop {
let current_bits = self.value.load(Ordering::Relaxed);
let current_value = f64::from_bits(current_bits);
let new_value = current_value * factor;
if !new_value.is_finite() {
return; }
let new_bits = new_value.to_bits();
match self.value.compare_exchange_weak(
current_bits,
new_bits,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(_) => continue,
}
}
}
#[inline]
pub fn divide(&self, divisor: f64) {
if divisor == 0.0 || divisor == 1.0 || !divisor.is_finite() {
return;
}
self.multiply(1.0 / divisor);
}
#[inline]
pub fn abs(&self) {
loop {
let current_bits = self.value.load(Ordering::Relaxed);
let current_value = f64::from_bits(current_bits);
if current_value >= 0.0 {
break; }
let abs_value = current_value.abs();
let abs_bits = abs_value.to_bits();
match self.value.compare_exchange_weak(
current_bits,
abs_bits,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(_) => continue,
}
}
}
#[inline]
pub fn clamp(&self, min: f64, max: f64) {
loop {
let current_bits = self.value.load(Ordering::Relaxed);
let current_value = f64::from_bits(current_bits);
let clamped_value = current_value.clamp(min, max);
if (current_value - clamped_value).abs() < f64::EPSILON {
break; }
let clamped_bits = clamped_value.to_bits();
match self.value.compare_exchange_weak(
current_bits,
clamped_bits,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(_) => continue,
}
}
}
#[inline]
pub fn update_ema(&self, sample: f64, alpha: f64) {
let alpha = alpha.clamp(0.0, 1.0);
loop {
let current_bits = self.value.load(Ordering::Relaxed);
let current_value = f64::from_bits(current_bits);
let new_value = alpha * sample + (1.0 - alpha) * current_value;
let new_bits = new_value.to_bits();
match self.value.compare_exchange_weak(
current_bits,
new_bits,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(_) => continue,
}
}
}
#[must_use]
pub fn stats(&self) -> GaugeStats {
GaugeStats {
value: self.get(),
age: self.created_at.elapsed(),
updates: None, }
}
#[must_use]
#[inline]
pub fn age(&self) -> Duration {
self.created_at.elapsed()
}
#[must_use]
#[inline]
pub fn is_zero(&self) -> bool {
self.get() == 0.0
}
#[must_use]
#[inline]
pub fn is_positive(&self) -> bool {
self.get() > 0.0
}
#[must_use]
#[inline]
pub fn is_negative(&self) -> bool {
self.get() < 0.0
}
#[must_use]
#[inline]
pub fn is_finite(&self) -> bool {
self.get().is_finite()
}
}
impl Default for Gauge {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for Gauge {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Gauge({})", self.get())
}
}
impl std::fmt::Debug for Gauge {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Gauge")
.field("value", &self.get())
.field("age", &self.age())
.field("is_finite", &self.is_finite())
.finish()
}
}
pub mod specialized {
use super::*;
#[repr(align(64))]
pub struct PercentageGauge {
inner: Gauge,
}
impl PercentageGauge {
#[inline]
pub fn new() -> Self {
Self {
inner: Gauge::new(),
}
}
#[inline(always)]
pub fn set_percentage(&self, percentage: f64) {
let clamped = percentage.clamp(0.0, 100.0);
self.inner.set(clamped);
}
#[inline(always)]
pub fn get_percentage(&self) -> f64 {
self.inner.get()
}
#[inline(always)]
pub fn set_ratio(&self, ratio: f64) {
let percentage = (ratio * 100.0).clamp(0.0, 100.0);
self.inner.set(percentage);
}
#[inline(always)]
pub fn get_ratio(&self) -> f64 {
self.inner.get() / 100.0
}
#[inline]
pub fn is_full(&self) -> bool {
(self.inner.get() - 100.0).abs() < f64::EPSILON
}
#[inline]
pub fn is_empty(&self) -> bool {
self.inner.get() < f64::EPSILON
}
#[inline]
pub fn add_percentage(&self, delta: f64) {
loop {
let current = self.get_percentage();
let new_value = (current + delta).clamp(0.0, 100.0);
if (current - new_value).abs() < f64::EPSILON {
break; }
match self.inner.compare_and_swap(current, new_value) {
Ok(_) => break,
Err(_) => continue, }
}
}
pub fn stats(&self) -> GaugeStats {
self.inner.stats()
}
}
impl Default for PercentageGauge {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for PercentageGauge {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "PercentageGauge({}%)", self.get_percentage())
}
}
pub type CpuGauge = PercentageGauge;
#[repr(align(64))]
pub struct MemoryGauge {
bytes: Gauge,
}
impl MemoryGauge {
#[inline]
pub fn new() -> Self {
Self {
bytes: Gauge::new(),
}
}
#[inline(always)]
pub fn set_bytes(&self, bytes: u64) {
self.bytes.set(bytes as f64);
}
#[inline]
pub fn get_bytes(&self) -> u64 {
self.bytes.get() as u64
}
#[inline]
pub fn get_kb(&self) -> f64 {
self.bytes.get() / 1024.0
}
#[inline]
pub fn get_mb(&self) -> f64 {
self.bytes.get() / (1024.0 * 1024.0)
}
#[inline]
pub fn get_gb(&self) -> f64 {
self.bytes.get() / (1024.0 * 1024.0 * 1024.0)
}
#[inline]
pub fn add_bytes(&self, bytes: i64) {
self.bytes.add(bytes as f64);
}
pub fn stats(&self) -> GaugeStats {
self.bytes.stats()
}
}
impl Default for MemoryGauge {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for MemoryGauge {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mb = self.get_mb();
if mb >= 1024.0 {
write!(f, "MemoryGauge({:.2} GB)", self.get_gb())
} else {
write!(f, "MemoryGauge({mb:.2} MB)")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::thread;
#[test]
fn test_basic_operations() {
let gauge = Gauge::new();
assert_eq!(gauge.get(), 0.0);
assert!(gauge.is_zero());
assert!(gauge.is_finite());
gauge.set(42.5);
assert_eq!(gauge.get(), 42.5);
assert!(!gauge.is_zero());
assert!(gauge.is_positive());
gauge.add(7.5);
assert_eq!(gauge.get(), 50.0);
gauge.sub(10.0);
assert_eq!(gauge.get(), 40.0);
gauge.reset();
assert_eq!(gauge.get(), 0.0);
}
#[test]
fn test_mathematical_operations() {
let gauge = Gauge::with_value(10.0);
gauge.multiply(2.0);
assert_eq!(gauge.get(), 20.0);
gauge.divide(4.0);
assert_eq!(gauge.get(), 5.0);
gauge.set(-15.0);
assert!(gauge.is_negative());
gauge.abs();
assert_eq!(gauge.get(), 15.0);
assert!(gauge.is_positive());
gauge.clamp(5.0, 10.0);
assert_eq!(gauge.get(), 10.0);
}
#[test]
fn test_min_max_operations() {
let gauge = Gauge::with_value(10.0);
gauge.set_max(15.0);
assert_eq!(gauge.get(), 15.0);
gauge.set_max(12.0); assert_eq!(gauge.get(), 15.0);
gauge.set_min(8.0);
assert_eq!(gauge.get(), 8.0);
gauge.set_min(12.0); assert_eq!(gauge.get(), 8.0);
}
#[test]
fn test_compare_and_swap() {
let gauge = Gauge::with_value(10.0);
assert_eq!(gauge.compare_and_swap(10.0, 20.0), Ok(10.0));
assert_eq!(gauge.get(), 20.0);
assert_eq!(gauge.compare_and_swap(10.0, 30.0), Err(20.0));
assert_eq!(gauge.get(), 20.0);
}
#[test]
fn test_ema_update() {
let gauge = Gauge::with_value(10.0);
gauge.update_ema(20.0, 0.5);
assert_eq!(gauge.get(), 15.0);
gauge.update_ema(30.0, 0.3);
assert_eq!(gauge.get(), 19.5);
}
#[test]
fn test_percentage_gauge() {
let gauge = specialized::PercentageGauge::new();
gauge.set_percentage(75.5);
assert_eq!(gauge.get_percentage(), 75.5);
assert!((gauge.get_ratio() - 0.755).abs() < f64::EPSILON);
gauge.set_ratio(0.9);
assert_eq!(gauge.get_percentage(), 90.0);
gauge.set_percentage(150.0);
assert_eq!(gauge.get_percentage(), 100.0);
assert!(gauge.is_full());
gauge.set_percentage(-10.0);
assert_eq!(gauge.get_percentage(), 0.0);
assert!(gauge.is_empty());
gauge.set_percentage(95.0);
gauge.add_percentage(10.0);
assert_eq!(gauge.get_percentage(), 100.0);
}
#[test]
fn test_memory_gauge() {
let gauge = specialized::MemoryGauge::new();
gauge.set_bytes(1024 * 1024 * 1024);
assert_eq!(gauge.get_bytes(), 1024 * 1024 * 1024);
assert!((gauge.get_mb() - 1024.0).abs() < 0.1);
assert!((gauge.get_gb() - 1.0).abs() < 0.001);
gauge.add_bytes(1024 * 1024); assert!(gauge.get_mb() > 1024.0);
}
#[test]
fn test_statistics() {
let gauge = Gauge::with_value(42.0);
let stats = gauge.stats();
assert_eq!(stats.value, 42.0);
assert!(stats.age <= gauge.age());
assert!(stats.updates.is_none()); }
#[test]
fn test_high_concurrency() {
let gauge = Arc::new(Gauge::new());
let num_threads = 100;
let operations_per_thread = 1000;
let handles: Vec<_> = (0..num_threads)
.map(|thread_id| {
let gauge = Arc::clone(&gauge);
thread::spawn(move || {
for i in 0..operations_per_thread {
let value = (thread_id * operations_per_thread + i) as f64;
gauge.set(value);
gauge.add(0.1);
gauge.multiply(1.001);
}
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
let final_value = gauge.get();
assert!(final_value.is_finite());
let stats = gauge.stats();
assert!(stats.age <= gauge.age());
}
#[test]
fn test_special_values() {
let gauge = Gauge::new();
gauge.set(f64::INFINITY);
assert!(!gauge.is_finite());
gauge.set(f64::NAN);
assert!(!gauge.is_finite());
gauge.set(42.0);
assert!(gauge.is_finite());
}
#[test]
fn test_display_and_debug() {
let gauge = Gauge::with_value(42.5);
let display_str = format!("{gauge}");
assert!(display_str.contains("42.5"));
let debug_str = format!("{gauge:?}");
assert!(debug_str.contains("Gauge"));
assert!(debug_str.contains("42.5"));
}
#[test]
fn test_try_add_validation_and_overflow() {
let gauge = Gauge::new();
assert!(matches!(
gauge.try_add(f64::NAN),
Err(MetricsError::InvalidValue { .. })
));
assert!(matches!(
gauge.try_add(f64::INFINITY),
Err(MetricsError::InvalidValue { .. })
));
let gauge2 = Gauge::with_value(f64::MAX / 2.0);
assert!(matches!(
gauge2.try_add(f64::MAX),
Err(MetricsError::Overflow)
));
}
#[test]
fn test_try_set_and_min_max_validation() {
let gauge = Gauge::new();
assert!(matches!(
gauge.try_set(f64::NAN),
Err(MetricsError::InvalidValue { .. })
));
assert!(matches!(
gauge.try_set_max(f64::INFINITY),
Err(MetricsError::InvalidValue { .. })
));
assert!(matches!(
gauge.try_set_min(f64::NAN),
Err(MetricsError::InvalidValue { .. })
));
}
#[test]
fn test_ema_alpha_boundaries() {
let gauge = Gauge::with_value(10.0);
gauge.update_ema(100.0, -1.0);
assert_eq!(gauge.get(), 10.0);
gauge.update_ema(100.0, 2.0);
assert_eq!(gauge.get(), 100.0);
gauge.set(5.0);
gauge.update_ema(20.0, 0.0);
assert_eq!(gauge.get(), 5.0);
gauge.update_ema(20.0, 1.0);
assert_eq!(gauge.get(), 20.0);
}
#[test]
fn test_non_finite_math_helpers_are_noops() {
let gauge = Gauge::with_value(12.0);
gauge.add(f64::NAN);
assert_eq!(gauge.get(), 12.0);
gauge.multiply(f64::INFINITY);
assert_eq!(gauge.get(), 12.0);
gauge.divide(0.0);
assert_eq!(gauge.get(), 12.0);
let huge = Gauge::with_value(f64::MAX / 2.0);
huge.multiply(4.0);
assert_eq!(huge.get(), f64::MAX / 2.0);
}
}
#[cfg(all(test, feature = "bench-tests", not(tarpaulin)))]
#[allow(unused_imports)]
mod benchmarks {
use super::*;
use std::time::Instant;
#[cfg_attr(not(feature = "bench-tests"), ignore)]
#[test]
fn bench_gauge_set() {
let gauge = Gauge::new();
let iterations = 10_000_000;
let start = Instant::now();
for i in 0..iterations {
gauge.set(i as f64);
}
let elapsed = start.elapsed();
println!(
"Gauge set: {:.2} ns/op",
elapsed.as_nanos() as f64 / iterations as f64
);
assert!(elapsed.as_nanos() / iterations < 100);
assert_eq!(gauge.get(), (iterations - 1) as f64);
}
#[cfg_attr(not(feature = "bench-tests"), ignore)]
#[test]
fn bench_gauge_add() {
let gauge = Gauge::new();
let iterations = 1_000_000;
let start = Instant::now();
for _ in 0..iterations {
gauge.add(1.0);
}
let elapsed = start.elapsed();
println!(
"Gauge add: {:.2} ns/op",
elapsed.as_nanos() as f64 / iterations as f64
);
assert!(elapsed.as_nanos() / iterations < 300);
assert_eq!(gauge.get(), iterations as f64);
}
#[cfg_attr(not(feature = "bench-tests"), ignore)]
#[test]
fn bench_gauge_get() {
let gauge = Gauge::with_value(42.5);
let iterations = 100_000_000;
let start = Instant::now();
let mut sum = 0.0;
for _ in 0..iterations {
sum += gauge.get();
}
let elapsed = start.elapsed();
println!(
"Gauge get: {:.2} ns/op",
elapsed.as_nanos() as f64 / iterations as f64
);
assert_eq!(sum, 42.5 * iterations as f64);
assert!(elapsed.as_nanos() / iterations < 50);
}
}