use crate::logging::{debug, info, warn};
use evmlib::quoting_metrics::QuotingMetrics;
use parking_lot::RwLock;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::time::Instant;
const PERSIST_INTERVAL: usize = 10;
#[derive(Debug)]
pub struct QuotingMetricsTracker {
received_payment_count: AtomicUsize,
close_records_stored: AtomicUsize,
records_per_type: RwLock<Vec<(u32, u32)>>,
start_time: Instant,
persist_path: Option<PathBuf>,
network_size: AtomicU64,
ops_since_persist: AtomicUsize,
}
impl QuotingMetricsTracker {
#[must_use]
pub fn new(initial_records: usize) -> Self {
Self {
received_payment_count: AtomicUsize::new(0),
close_records_stored: AtomicUsize::new(initial_records),
records_per_type: RwLock::new(Vec::new()),
start_time: Instant::now(),
persist_path: None,
network_size: AtomicU64::new(500), ops_since_persist: AtomicUsize::new(0),
}
}
#[must_use]
pub fn with_persistence(persist_path: &std::path::Path) -> Self {
let mut tracker = Self::new(0);
tracker.persist_path = Some(persist_path.to_path_buf());
if let Some(loaded) = Self::load_from_disk(persist_path) {
tracker
.received_payment_count
.store(loaded.received_payment_count, Ordering::SeqCst);
tracker
.close_records_stored
.store(loaded.close_records_stored, Ordering::SeqCst);
*tracker.records_per_type.write() = loaded.records_per_type;
info!(
"Loaded persisted metrics: {} payments received",
loaded.received_payment_count
);
}
tracker
}
pub fn record_payment(&self) {
let count = self.received_payment_count.fetch_add(1, Ordering::SeqCst) + 1;
debug!("Payment received, total count: {count}");
self.maybe_persist();
}
pub fn record_store(&self, data_type: u32) {
self.close_records_stored.fetch_add(1, Ordering::SeqCst);
{
let mut records = self.records_per_type.write();
if let Some(entry) = records.iter_mut().find(|(t, _)| *t == data_type) {
entry.1 = entry.1.saturating_add(1);
} else {
records.push((data_type, 1));
}
}
self.maybe_persist();
}
#[must_use]
pub fn payment_count(&self) -> usize {
self.received_payment_count.load(Ordering::SeqCst)
}
#[must_use]
pub fn records_stored(&self) -> usize {
self.close_records_stored.load(Ordering::SeqCst)
}
#[must_use]
pub fn live_time_hours(&self) -> u64 {
self.start_time.elapsed().as_secs() / 3600
}
pub fn set_network_size(&self, size: u64) {
self.network_size.store(size, Ordering::SeqCst);
}
#[must_use]
pub fn get_metrics(&self, data_size: usize, data_type: u32) -> QuotingMetrics {
QuotingMetrics {
data_type,
data_size,
close_records_stored: self.close_records_stored.load(Ordering::SeqCst),
records_per_type: self.records_per_type.read().clone(),
received_payment_count: self.received_payment_count.load(Ordering::SeqCst),
live_time: self.live_time_hours(),
network_density: None, network_size: Some(self.network_size.load(Ordering::SeqCst)),
}
}
fn maybe_persist(&self) {
let ops = self.ops_since_persist.fetch_add(1, Ordering::Relaxed);
if ops % PERSIST_INTERVAL == 0 {
self.persist();
}
}
fn persist(&self) {
if let Some(ref path) = self.persist_path {
let data = PersistedMetrics {
received_payment_count: self.received_payment_count.load(Ordering::SeqCst),
close_records_stored: self.close_records_stored.load(Ordering::SeqCst),
records_per_type: self.records_per_type.read().clone(),
};
if let Ok(bytes) = rmp_serde::to_vec(&data) {
if let Err(e) = std::fs::write(path, bytes) {
warn!("Failed to persist metrics: {e}");
}
}
}
}
fn load_from_disk(path: &std::path::Path) -> Option<PersistedMetrics> {
let bytes = std::fs::read(path).ok()?;
rmp_serde::from_slice(&bytes).ok()
}
}
impl Drop for QuotingMetricsTracker {
fn drop(&mut self) {
self.persist();
}
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct PersistedMetrics {
received_payment_count: usize,
close_records_stored: usize,
records_per_type: Vec<(u32, u32)>,
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_new_tracker() {
let tracker = QuotingMetricsTracker::new(50);
assert_eq!(tracker.payment_count(), 0);
assert_eq!(tracker.records_stored(), 50);
}
#[test]
fn test_record_payment() {
let tracker = QuotingMetricsTracker::new(0);
assert_eq!(tracker.payment_count(), 0);
tracker.record_payment();
assert_eq!(tracker.payment_count(), 1);
tracker.record_payment();
assert_eq!(tracker.payment_count(), 2);
}
#[test]
fn test_record_store() {
let tracker = QuotingMetricsTracker::new(0);
assert_eq!(tracker.records_stored(), 0);
tracker.record_store(0); assert_eq!(tracker.records_stored(), 1);
tracker.record_store(0);
tracker.record_store(1); assert_eq!(tracker.records_stored(), 3);
let metrics = tracker.get_metrics(1024, 0);
assert_eq!(metrics.records_per_type.len(), 2);
}
#[test]
fn test_get_metrics() {
let tracker = QuotingMetricsTracker::new(100);
tracker.record_payment();
tracker.record_payment();
let metrics = tracker.get_metrics(2048, 0);
assert_eq!(metrics.data_size, 2048);
assert_eq!(metrics.data_type, 0);
assert_eq!(metrics.close_records_stored, 100);
assert_eq!(metrics.received_payment_count, 2);
}
#[test]
fn test_persistence() {
let dir = tempdir().expect("tempdir");
let path = dir.path().join("metrics.bin");
{
let tracker = QuotingMetricsTracker::with_persistence(&path);
tracker.record_payment();
tracker.record_payment();
tracker.record_store(0);
}
let tracker = QuotingMetricsTracker::with_persistence(&path);
assert_eq!(tracker.payment_count(), 2);
assert_eq!(tracker.records_stored(), 1);
}
#[test]
fn test_live_time_hours() {
let tracker = QuotingMetricsTracker::new(0);
assert_eq!(tracker.live_time_hours(), 0);
}
#[test]
fn test_set_network_size() {
let tracker = QuotingMetricsTracker::new(0);
tracker.set_network_size(1000);
let metrics = tracker.get_metrics(0, 0);
assert_eq!(metrics.network_size, Some(1000));
}
#[test]
fn test_records_per_type_multiple_types() {
let tracker = QuotingMetricsTracker::new(0);
tracker.record_store(0);
tracker.record_store(0);
tracker.record_store(1);
tracker.record_store(2);
tracker.record_store(1);
let metrics = tracker.get_metrics(0, 0);
assert_eq!(metrics.records_per_type.len(), 3);
let type_0 = metrics.records_per_type.iter().find(|(t, _)| *t == 0);
let type_1 = metrics.records_per_type.iter().find(|(t, _)| *t == 1);
let type_2 = metrics.records_per_type.iter().find(|(t, _)| *t == 2);
assert_eq!(type_0.expect("type 0 exists").1, 2);
assert_eq!(type_1.expect("type 1 exists").1, 2);
assert_eq!(type_2.expect("type 2 exists").1, 1);
}
#[test]
fn test_persistence_round_trip_with_types() {
let dir = tempdir().expect("tempdir");
let path = dir.path().join("metrics_types.bin");
{
let tracker = QuotingMetricsTracker::with_persistence(&path);
tracker.record_store(0);
tracker.record_store(0);
tracker.record_store(1);
tracker.record_payment();
}
let tracker = QuotingMetricsTracker::with_persistence(&path);
assert_eq!(tracker.payment_count(), 1);
assert_eq!(tracker.records_stored(), 3);
let metrics = tracker.get_metrics(0, 0);
assert_eq!(metrics.records_per_type.len(), 2);
}
#[test]
fn test_with_persistence_nonexistent_path() {
let dir = tempdir().expect("tempdir");
let path = dir.path().join("nonexistent_subdir").join("metrics.bin");
let tracker = QuotingMetricsTracker::with_persistence(&path);
assert_eq!(tracker.payment_count(), 0);
assert_eq!(tracker.records_stored(), 0);
}
#[test]
fn test_get_metrics_passes_data_params() {
let tracker = QuotingMetricsTracker::new(0);
let metrics = tracker.get_metrics(4096, 3);
assert_eq!(metrics.data_size, 4096);
assert_eq!(metrics.data_type, 3);
}
#[test]
fn test_default_network_size() {
let tracker = QuotingMetricsTracker::new(0);
let metrics = tracker.get_metrics(0, 0);
assert_eq!(metrics.network_size, Some(500));
}
}