use crate::math::{f64_u64, hash_index, u64_f64};
use crate::sketch::{DDSketch256, HyperLogLog10, Mergeable};
#[derive(Debug)]
pub struct RingBuffer<T: Copy + Default, const N: usize> {
buffer: [T; N],
write_pos: usize,
read_pos: usize,
dropped: u64,
}
impl<T: Copy + Default, const N: usize> RingBuffer<T, N> {
#[must_use]
pub fn new() -> Self {
Self {
buffer: [T::default(); N],
write_pos: 0,
read_pos: 0,
dropped: 0,
}
}
#[inline]
pub const fn push(&mut self, item: T) -> bool {
assert!(N > 0, "RingBuffer needs N > 0 (got a zero-capacity buffer)");
let next_write = (self.write_pos + 1) % N;
if next_write == self.read_pos {
self.dropped += 1;
return false;
}
self.buffer[self.write_pos] = item;
self.write_pos = next_write;
true
}
#[inline]
pub const fn pop(&mut self) -> Option<T> {
if self.read_pos == self.write_pos {
return None;
}
let item = self.buffer[self.read_pos];
self.read_pos = (self.read_pos + 1) % N;
Some(item)
}
#[inline]
pub const fn is_empty(&self) -> bool {
self.read_pos == self.write_pos
}
#[inline]
pub const fn is_full(&self) -> bool {
N <= 1 || (self.write_pos + 1) % N == self.read_pos
}
#[inline]
pub const fn len(&self) -> usize {
if self.write_pos >= self.read_pos {
self.write_pos - self.read_pos
} else {
N - self.read_pos + self.write_pos
}
}
#[inline]
pub const fn capacity(&self) -> usize {
N.saturating_sub(1)
}
#[inline]
pub const fn dropped(&self) -> u64 {
self.dropped
}
pub const fn clear(&mut self) {
self.write_pos = 0;
self.read_pos = 0;
self.dropped = 0;
}
}
impl<T: Copy + Default, const N: usize> Default for RingBuffer<T, N> {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum MetricType {
#[default]
Counter,
Gauge,
Histogram,
Unique,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct MetricEvent {
pub name_hash: u64,
pub metric_type: MetricType,
pub value: f64,
pub timestamp: u64,
}
impl MetricEvent {
#[inline]
#[must_use]
pub const fn counter(name_hash: u64, delta: f64) -> Self {
Self {
name_hash,
metric_type: MetricType::Counter,
value: delta,
timestamp: 0,
}
}
#[inline]
#[must_use]
pub const fn gauge(name_hash: u64, value: f64) -> Self {
Self {
name_hash,
metric_type: MetricType::Gauge,
value,
timestamp: 0,
}
}
#[inline]
#[must_use]
pub const fn histogram(name_hash: u64, value: f64) -> Self {
Self {
name_hash,
metric_type: MetricType::Histogram,
value,
timestamp: 0,
}
}
#[inline]
#[must_use]
pub const fn unique(name_hash: u64, item_hash: u64) -> Self {
Self {
name_hash,
metric_type: MetricType::Unique,
value: u64_f64(item_hash),
timestamp: 0,
}
}
#[inline]
#[must_use]
pub const fn with_timestamp(mut self, ts: u64) -> Self {
self.timestamp = ts;
self
}
}
#[derive(Clone, Debug)]
pub struct MetricSlot {
pub name_hash: u64,
pub counter: f64,
pub gauge: f64,
pub hll: HyperLogLog10,
pub ddsketch: DDSketch256,
pub event_count: u64,
pub last_update: u64,
}
impl MetricSlot {
#[must_use]
pub fn new(name_hash: u64, alpha: f64) -> Self {
Self {
name_hash,
counter: 0.0,
gauge: 0.0,
hll: HyperLogLog10::new(),
ddsketch: DDSketch256::new(alpha),
event_count: 0,
last_update: 0,
}
}
pub fn process(&mut self, event: &MetricEvent) {
self.event_count += 1;
if event.timestamp > self.last_update {
self.last_update = event.timestamp;
}
match event.metric_type {
MetricType::Counter => {
self.counter += event.value;
}
MetricType::Gauge => {
self.gauge = event.value;
}
MetricType::Histogram => {
self.ddsketch.insert(event.value);
}
MetricType::Unique => {
self.hll.insert_hash(f64_u64(event.value));
}
}
}
pub const fn reset(&mut self) {
self.counter = 0.0;
self.gauge = 0.0;
self.hll.clear();
self.ddsketch.clear();
self.event_count = 0;
self.last_update = 0;
}
}
impl Mergeable for MetricSlot {
fn merge(&mut self, other: &Self) {
self.counter += other.counter;
if other.last_update > self.last_update {
self.gauge = other.gauge;
self.last_update = other.last_update;
}
self.hll.merge(&other.hll);
self.ddsketch.merge(&other.ddsketch);
self.event_count += other.event_count;
}
}
#[derive(Debug)]
pub struct MetricPipeline<const SLOTS: usize, const QUEUE_SIZE: usize> {
slots: [Option<MetricSlot>; SLOTS],
queue: RingBuffer<MetricEvent, QUEUE_SIZE>,
alpha: f64,
total_events: u64,
}
impl<const SLOTS: usize, const QUEUE_SIZE: usize> MetricPipeline<SLOTS, QUEUE_SIZE> {
#[must_use]
pub fn new(alpha: f64) -> Self {
const NONE_SLOT: Option<MetricSlot> = None;
Self {
slots: [NONE_SLOT; SLOTS],
queue: RingBuffer::new(),
alpha,
total_events: 0,
}
}
#[inline]
pub const fn submit(&mut self, event: MetricEvent) -> bool {
self.queue.push(event)
}
pub fn flush(&mut self) {
while let Some(event) = self.queue.pop() {
self.process_event(event);
}
}
fn process_event(&mut self, event: MetricEvent) {
self.total_events += 1;
let slot_idx = hash_index(event.name_hash) % SLOTS;
if let Some(ref mut slot) = self.slots[slot_idx] {
if slot.name_hash == event.name_hash {
slot.process(&event);
return;
}
slot.process(&event);
} else {
let mut slot = MetricSlot::new(event.name_hash, self.alpha);
slot.process(&event);
self.slots[slot_idx] = Some(slot);
}
}
#[must_use]
pub fn get_slot(&self, name_hash: u64) -> Option<&MetricSlot> {
let slot_idx = hash_index(name_hash) % SLOTS;
self.slots[slot_idx]
.as_ref()
.filter(|s| s.name_hash == name_hash)
}
pub fn get_slot_mut(&mut self, name_hash: u64) -> Option<&mut MetricSlot> {
let slot_idx = hash_index(name_hash) % SLOTS;
self.slots[slot_idx]
.as_mut()
.filter(|s| s.name_hash == name_hash)
}
pub fn iter_slots(&self) -> impl Iterator<Item = &MetricSlot> {
self.slots.iter().filter_map(|s| s.as_ref())
}
#[inline]
#[must_use]
pub const fn total_events(&self) -> u64 {
self.total_events
}
#[inline]
#[must_use]
pub const fn dropped_events(&self) -> u64 {
self.queue.dropped()
}
#[inline]
#[must_use]
pub const fn queue_len(&self) -> usize {
self.queue.len()
}
pub fn reset(&mut self) {
for s in self.slots.iter_mut().flatten() {
s.reset();
}
self.queue.clear();
self.total_events = 0;
}
}
#[derive(Clone, Debug)]
pub struct MetricEntry {
pub name: [u8; 64],
pub name_len: usize,
pub hash: u64,
pub metric_type: MetricType,
}
impl MetricEntry {
#[must_use]
pub fn new(name: &str, metric_type: MetricType) -> Self {
use crate::sketch::FnvHasher;
let mut name_buf = [0u8; 64];
let len = name.len().min(64);
name_buf[..len].copy_from_slice(&name.as_bytes()[..len]);
Self {
name: name_buf,
name_len: len,
hash: FnvHasher::hash_bytes(name.as_bytes()),
metric_type,
}
}
#[must_use]
pub fn name_str(&self) -> &str {
core::str::from_utf8(&self.name[..self.name_len]).unwrap_or("")
}
}
#[derive(Clone, Debug)]
pub struct MetricRegistry<const N: usize> {
entries: [Option<MetricEntry>; N],
count: usize,
}
impl<const N: usize> MetricRegistry<N> {
#[must_use]
pub const fn new() -> Self {
const NONE_ENTRY: Option<MetricEntry> = None;
Self {
entries: [NONE_ENTRY; N],
count: 0,
}
}
pub fn register(&mut self, name: &str, metric_type: MetricType) -> Option<u64> {
if self.count >= N {
return None;
}
let entry = MetricEntry::new(name, metric_type);
let hash = entry.hash;
for slot in &mut self.entries {
if slot.is_none() {
*slot = Some(entry);
self.count += 1;
return Some(hash);
}
}
None
}
#[must_use]
pub fn lookup(&self, name: &str) -> Option<&MetricEntry> {
use crate::sketch::FnvHasher;
let hash = FnvHasher::hash_bytes(name.as_bytes());
self.lookup_by_hash(hash)
}
#[must_use]
pub fn lookup_by_hash(&self, hash: u64) -> Option<&MetricEntry> {
self.entries
.iter()
.filter_map(|e| e.as_ref())
.find(|e| e.hash == hash)
}
pub fn iter(&self) -> impl Iterator<Item = &MetricEntry> {
self.entries.iter().filter_map(|e| e.as_ref())
}
#[inline]
#[must_use]
pub const fn count(&self) -> usize {
self.count
}
}
impl<const N: usize> Default for MetricRegistry<N> {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug)]
pub struct MetricSnapshot {
pub name_hash: u64,
pub counter: f64,
pub gauge: f64,
pub cardinality: f64,
pub p50: f64,
pub p95: f64,
pub p99: f64,
pub mean: f64,
pub min: f64,
pub max: f64,
pub event_count: u64,
}
impl From<&MetricSlot> for MetricSnapshot {
fn from(slot: &MetricSlot) -> Self {
Self {
name_hash: slot.name_hash,
counter: slot.counter,
gauge: slot.gauge,
cardinality: slot.hll.cardinality(),
p50: slot.ddsketch.quantile(0.50),
p95: slot.ddsketch.quantile(0.95),
p99: slot.ddsketch.quantile(0.99),
mean: slot.ddsketch.mean(),
min: slot.ddsketch.min(),
max: slot.ddsketch.max(),
event_count: slot.event_count,
}
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::*;
use crate::sketch::FnvHasher;
fn hash(s: &str) -> u64 {
FnvHasher::hash_bytes(s.as_bytes())
}
#[test]
fn test_ring_buffer_basic() {
let mut rb = RingBuffer::<u32, 4>::new();
assert!(rb.is_empty());
assert!(!rb.is_full());
assert_eq!(rb.capacity(), 3);
assert!(rb.push(1));
assert!(rb.push(2));
assert!(rb.push(3));
assert!(!rb.push(4));
assert_eq!(rb.pop(), Some(1));
assert_eq!(rb.pop(), Some(2));
assert_eq!(rb.pop(), Some(3));
assert_eq!(rb.pop(), None);
}
#[test]
fn test_ring_buffer_wrap() {
let mut rb = RingBuffer::<u32, 4>::new();
rb.push(1);
rb.push(2);
assert_eq!(rb.pop(), Some(1));
rb.push(3);
rb.push(4);
assert_eq!(rb.pop(), Some(2));
assert_eq!(rb.pop(), Some(3));
assert_eq!(rb.pop(), Some(4));
assert_eq!(rb.pop(), None);
}
#[test]
fn test_metric_event() {
let counter = MetricEvent::counter(hash("requests"), 1.0);
assert_eq!(counter.metric_type, MetricType::Counter);
assert_eq!(counter.value, 1.0);
let hist = MetricEvent::histogram(hash("latency"), 42.5);
assert_eq!(hist.metric_type, MetricType::Histogram);
assert_eq!(hist.value, 42.5);
}
#[test]
fn test_metric_slot() {
let mut slot = MetricSlot::new(hash("test"), 0.05);
slot.process(&MetricEvent::counter(hash("test"), 5.0));
slot.process(&MetricEvent::counter(hash("test"), 3.0));
assert_eq!(slot.counter, 8.0);
slot.process(&MetricEvent::gauge(hash("test"), 100.0));
assert_eq!(slot.gauge, 100.0);
slot.process(&MetricEvent::histogram(hash("test"), 10.0));
slot.process(&MetricEvent::histogram(hash("test"), 20.0));
assert_eq!(slot.ddsketch.count(), 2);
}
#[test]
fn test_metric_pipeline() {
let mut pipeline = MetricPipeline::<64, 1024>::new(0.05);
let req_hash = hash("http.requests");
let lat_hash = hash("http.latency");
for _ in 0..100 {
pipeline.submit(MetricEvent::counter(req_hash, 1.0));
}
for lat in [10.0, 20.0, 30.0, 40.0, 50.0] {
pipeline.submit(MetricEvent::histogram(lat_hash, lat));
}
pipeline.flush();
let req_slot = pipeline.get_slot(req_hash).unwrap();
assert_eq!(req_slot.counter, 100.0);
let lat_slot = pipeline.get_slot(lat_hash).unwrap();
assert_eq!(lat_slot.ddsketch.count(), 5);
}
#[test]
fn test_metric_registry() {
let mut registry = MetricRegistry::<16>::new();
let h1 = registry
.register("http.requests", MetricType::Counter)
.unwrap();
let h2 = registry
.register("http.latency", MetricType::Histogram)
.unwrap();
assert_ne!(h1, h2);
assert_eq!(registry.count(), 2);
let entry = registry.lookup("http.requests").unwrap();
assert_eq!(entry.metric_type, MetricType::Counter);
}
#[test]
fn test_metric_snapshot() {
let mut slot = MetricSlot::new(hash("test"), 0.05);
slot.process(&MetricEvent::counter(hash("test"), 100.0));
for v in [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0] {
slot.process(&MetricEvent::histogram(hash("test"), v));
}
let snapshot = MetricSnapshot::from(&slot);
assert_eq!(snapshot.counter, 100.0);
assert_eq!(snapshot.event_count, 11);
assert!((snapshot.mean - 55.0).abs() < 1.0);
}
#[test]
fn test_ring_buffer_len() {
let mut rb = RingBuffer::<u32, 8>::new();
assert_eq!(rb.len(), 0);
rb.push(1);
rb.push(2);
assert_eq!(rb.len(), 2);
rb.pop();
assert_eq!(rb.len(), 1);
}
#[test]
fn test_ring_buffer_dropped() {
let mut rb = RingBuffer::<u32, 3>::new();
rb.push(1);
rb.push(2);
assert!(!rb.push(3)); assert_eq!(rb.dropped(), 1);
}
#[test]
fn test_ring_buffer_clear() {
let mut rb = RingBuffer::<u32, 8>::new();
rb.push(1);
rb.push(2);
rb.clear();
assert!(rb.is_empty());
assert_eq!(rb.dropped(), 0);
}
#[test]
fn test_ring_buffer_default() {
let rb = RingBuffer::<u32, 8>::default();
assert!(rb.is_empty());
}
#[test]
fn test_metric_event_with_timestamp() {
let ev = MetricEvent::counter(42, 1.0).with_timestamp(12345);
assert_eq!(ev.timestamp, 12345);
}
#[test]
fn test_metric_event_gauge() {
let ev = MetricEvent::gauge(1, 99.0);
assert_eq!(ev.metric_type, MetricType::Gauge);
assert_eq!(ev.value, 99.0);
}
#[test]
fn test_metric_event_unique() {
let ev = MetricEvent::unique(1, 42);
assert_eq!(ev.metric_type, MetricType::Unique);
}
#[test]
fn test_metric_slot_reset() {
let mut slot = MetricSlot::new(1, 0.05);
slot.process(&MetricEvent::counter(1, 10.0));
slot.reset();
assert_eq!(slot.counter, 0.0);
assert_eq!(slot.event_count, 0);
}
#[test]
fn test_metric_slot_merge() {
let mut s1 = MetricSlot::new(1, 0.05);
let mut s2 = MetricSlot::new(1, 0.05);
s1.process(&MetricEvent::counter(1, 10.0));
s2.process(&MetricEvent::counter(1, 20.0));
s1.merge(&s2);
assert_eq!(s1.counter, 30.0);
}
#[test]
fn test_pipeline_total_events() {
let mut pipeline = MetricPipeline::<16, 64>::new(0.05);
pipeline.submit(MetricEvent::counter(1, 1.0));
pipeline.submit(MetricEvent::counter(1, 1.0));
pipeline.flush();
assert_eq!(pipeline.total_events(), 2);
}
#[test]
fn test_pipeline_dropped_events() {
let mut pipeline = MetricPipeline::<16, 3>::new(0.05);
pipeline.submit(MetricEvent::counter(1, 1.0));
pipeline.submit(MetricEvent::counter(1, 1.0));
pipeline.submit(MetricEvent::counter(1, 1.0));
assert_eq!(pipeline.dropped_events(), 1);
}
#[test]
fn test_pipeline_queue_len() {
let mut pipeline = MetricPipeline::<16, 64>::new(0.05);
pipeline.submit(MetricEvent::counter(1, 1.0));
assert_eq!(pipeline.queue_len(), 1);
pipeline.flush();
assert_eq!(pipeline.queue_len(), 0);
}
#[test]
fn test_pipeline_get_slot_mut() {
let mut pipeline = MetricPipeline::<64, 128>::new(0.05);
let h = hash("test_metric");
pipeline.submit(MetricEvent::counter(h, 5.0));
pipeline.flush();
let slot = pipeline.get_slot_mut(h).unwrap();
assert_eq!(slot.counter, 5.0);
}
#[test]
fn test_pipeline_iter_slots() {
let mut pipeline = MetricPipeline::<64, 128>::new(0.05);
pipeline.submit(MetricEvent::counter(hash("a"), 1.0));
pipeline.submit(MetricEvent::counter(hash("b"), 2.0));
pipeline.flush();
let count = pipeline.iter_slots().count();
assert!(count >= 1);
}
#[test]
fn test_pipeline_reset() {
let mut pipeline = MetricPipeline::<16, 64>::new(0.05);
pipeline.submit(MetricEvent::counter(1, 1.0));
pipeline.flush();
pipeline.reset();
assert_eq!(pipeline.total_events(), 0);
}
#[test]
fn test_metric_registry_full() {
let mut registry = MetricRegistry::<2>::new();
registry.register("a", MetricType::Counter);
registry.register("b", MetricType::Counter);
assert!(registry.register("c", MetricType::Counter).is_none());
}
#[test]
fn test_metric_registry_default() {
let registry = MetricRegistry::<8>::default();
assert_eq!(registry.count(), 0);
}
#[test]
fn test_metric_registry_lookup_by_hash() {
let mut registry = MetricRegistry::<16>::new();
let h = registry.register("test", MetricType::Histogram).unwrap();
let entry = registry.lookup_by_hash(h).unwrap();
assert_eq!(entry.metric_type, MetricType::Histogram);
}
#[test]
fn test_metric_registry_iter() {
let mut registry = MetricRegistry::<16>::new();
registry.register("a", MetricType::Counter);
registry.register("b", MetricType::Gauge);
assert_eq!(registry.iter().count(), 2);
}
#[test]
fn test_metric_entry_name_str() {
let entry = MetricEntry::new("http.requests", MetricType::Counter);
assert_eq!(entry.name_str(), "http.requests");
}
#[cfg(feature = "std")]
#[test]
fn test_metric_snapshot_debug() {
let slot = MetricSlot::new(1, 0.05);
let snapshot = MetricSnapshot::from(&slot);
let s = format!("{snapshot:?}");
assert!(s.contains("name_hash"));
}
}