use alloc::string::String;
use core::sync::atomic::{AtomicU64, Ordering};
use super::{Distributio, Numerator};
#[derive(Debug)]
pub struct MensuraEffectus {
effect_id: u64,
effect_name: String,
operations: Numerator,
successes: Numerator,
failures: Numerator,
latency: Distributio,
in_flight: AtomicU64,
}
impl MensuraEffectus {
pub fn new(effect_id: u64, effect_name: impl Into<String>) -> Self {
MensuraEffectus {
effect_id,
effect_name: effect_name.into(),
operations: Numerator::new(),
successes: Numerator::new(),
failures: Numerator::new(),
latency: Distributio::new(),
in_flight: AtomicU64::new(0),
}
}
#[inline]
pub fn effect_id(&self) -> u64 {
self.effect_id
}
#[inline]
pub fn effect_name(&self) -> &str {
&self.effect_name
}
#[inline]
pub fn operation_start(&self) {
self.in_flight.fetch_add(1, Ordering::Relaxed);
}
#[inline]
pub fn record_success(&self, duration_ns: u64) {
let _ = self
.in_flight
.try_update(Ordering::Relaxed, Ordering::Relaxed, |v| v.checked_sub(1));
self.operations.increment();
self.successes.increment();
self.latency.record(duration_ns);
}
#[inline]
pub fn record_failure(&self, duration_ns: u64) {
let _ = self
.in_flight
.try_update(Ordering::Relaxed, Ordering::Relaxed, |v| v.checked_sub(1));
self.operations.increment();
self.failures.increment();
self.latency.record(duration_ns);
}
#[inline]
pub fn record_operation(&self, duration_ns: u64, success: bool) {
if success {
self.record_success(duration_ns);
} else {
self.record_failure(duration_ns);
}
}
#[inline]
pub fn total_operations(&self) -> u64 {
self.operations.value()
}
#[inline]
pub fn success_count(&self) -> u64 {
self.successes.value()
}
#[inline]
pub fn failure_count(&self) -> u64 {
self.failures.value()
}
#[inline]
pub fn in_flight(&self) -> u64 {
self.in_flight.load(Ordering::Relaxed)
}
#[inline]
#[allow(clippy::cast_precision_loss)]
pub fn success_rate(&self) -> f64 {
let total = self.total_operations();
if total == 0 {
1.0
} else {
self.success_count() as f64 / total as f64
}
}
#[inline]
pub fn error_rate(&self) -> f64 {
1.0 - self.success_rate()
}
#[inline]
pub fn mean_latency_ns(&self) -> f64 {
self.latency.mean()
}
#[inline]
pub fn mean_latency_us(&self) -> f64 {
self.latency.mean() / 1000.0
}
#[inline]
pub fn mean_latency_ms(&self) -> f64 {
self.latency.mean() / 1_000_000.0
}
#[inline]
pub fn latency_percentile_ns(&self, p: f64) -> Option<u64> {
self.latency.percentile(p)
}
#[inline]
pub fn latency_histogram(&self) -> &Distributio {
&self.latency
}
pub fn summary(&self) -> EffectMetricsSummary {
EffectMetricsSummary {
effect_id: self.effect_id,
effect_name: self.effect_name.clone(),
total_operations: self.total_operations(),
successes: self.success_count(),
failures: self.failure_count(),
in_flight: self.in_flight(),
mean_latency_ns: self.mean_latency_ns(),
p50_latency_ns: self.latency_percentile_ns(50.0),
p99_latency_ns: self.latency_percentile_ns(99.0),
}
}
}
#[derive(Debug, Clone)]
pub struct EffectMetricsSummary {
pub effect_id: u64,
pub effect_name: String,
pub total_operations: u64,
pub successes: u64,
pub failures: u64,
pub in_flight: u64,
pub mean_latency_ns: f64,
pub p50_latency_ns: Option<u64>,
pub p99_latency_ns: Option<u64>,
}
impl EffectMetricsSummary {
#[allow(clippy::cast_precision_loss)]
pub fn success_rate(&self) -> f64 {
if self.total_operations == 0 {
1.0
} else {
self.successes as f64 / self.total_operations as f64
}
}
}
pub struct OperationScope<'a> {
metrics: &'a MensuraEffectus,
start_ns: u64,
success: bool,
}
impl<'a> OperationScope<'a> {
pub fn new(metrics: &'a MensuraEffectus, start_ns: u64) -> Self {
metrics.operation_start();
OperationScope {
metrics,
start_ns,
success: true,
}
}
pub fn fail(&mut self) {
self.success = false;
}
pub fn complete(self, end_ns: u64) {
let duration = end_ns.saturating_sub(self.start_ns);
self.metrics.record_operation(duration, self.success);
core::mem::forget(self);
}
}
impl Drop for OperationScope<'_> {
fn drop(&mut self) {
self.metrics.record_operation(0, self.success);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mensura_effectus_new() {
let metrics = MensuraEffectus::new(1, "StateEffect");
assert_eq!(metrics.effect_id(), 1);
assert_eq!(metrics.effect_name(), "StateEffect");
assert_eq!(metrics.total_operations(), 0);
}
#[test]
fn test_mensura_effectus_record() {
let metrics = MensuraEffectus::new(1, "StateEffect");
metrics.operation_start();
metrics.record_success(1000);
assert_eq!(metrics.total_operations(), 1);
assert_eq!(metrics.success_count(), 1);
assert_eq!(metrics.failure_count(), 0);
}
#[test]
fn test_mensura_effectus_failure() {
let metrics = MensuraEffectus::new(1, "IO");
metrics.operation_start();
metrics.record_failure(5000);
assert_eq!(metrics.total_operations(), 1);
assert_eq!(metrics.success_count(), 0);
assert_eq!(metrics.failure_count(), 1);
assert_eq!(metrics.error_rate(), 1.0);
}
#[test]
fn test_mensura_effectus_latency() {
let metrics = MensuraEffectus::new(1, "DB");
for _ in 0..100 {
metrics.operation_start();
metrics.record_success(10_000); }
assert!(metrics.mean_latency_ns() > 0.0);
assert!(metrics.latency_percentile_ns(50.0).is_some());
}
#[test]
fn gauge_saturates_at_zero() {
let metrics = MensuraEffectus::new(1, "Unpaired");
metrics.record_success(10);
assert_eq!(metrics.in_flight(), 0); }
#[test]
fn test_effect_metrics_summary() {
let metrics = MensuraEffectus::new(1, "Cache");
metrics.operation_start();
metrics.record_success(1000);
metrics.operation_start();
metrics.record_failure(2000);
let summary = metrics.summary();
assert_eq!(summary.total_operations, 2);
assert_eq!(summary.successes, 1);
assert_eq!(summary.failures, 1);
assert_eq!(summary.success_rate(), 0.5);
}
}