1use crate::logging::{debug, info, warn};
9use evmlib::quoting_metrics::QuotingMetrics;
10use parking_lot::RwLock;
11use std::path::PathBuf;
12use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
13use std::time::Instant;
14
15const PERSIST_INTERVAL: usize = 10;
17
18#[derive(Debug)]
23pub struct QuotingMetricsTracker {
24 received_payment_count: AtomicUsize,
26 close_records_stored: AtomicUsize,
28 records_per_type: RwLock<Vec<(u32, u32)>>,
30 start_time: Instant,
32 persist_path: Option<PathBuf>,
34 network_size: AtomicU64,
36 ops_since_persist: AtomicUsize,
38}
39
40impl QuotingMetricsTracker {
41 #[must_use]
47 pub fn new(initial_records: usize) -> Self {
48 Self {
49 received_payment_count: AtomicUsize::new(0),
50 close_records_stored: AtomicUsize::new(initial_records),
51 records_per_type: RwLock::new(Vec::new()),
52 start_time: Instant::now(),
53 persist_path: None,
54 network_size: AtomicU64::new(500), ops_since_persist: AtomicUsize::new(0),
56 }
57 }
58
59 #[must_use]
65 pub fn with_persistence(persist_path: &std::path::Path) -> Self {
66 let mut tracker = Self::new(0);
67 tracker.persist_path = Some(persist_path.to_path_buf());
68
69 if let Some(loaded) = Self::load_from_disk(persist_path) {
71 tracker
72 .received_payment_count
73 .store(loaded.received_payment_count, Ordering::SeqCst);
74 tracker
75 .close_records_stored
76 .store(loaded.close_records_stored, Ordering::SeqCst);
77 *tracker.records_per_type.write() = loaded.records_per_type;
78 info!(
79 "Loaded persisted metrics: {} payments received",
80 loaded.received_payment_count
81 );
82 }
83
84 tracker
85 }
86
87 pub fn record_payment(&self) {
89 let count = self.received_payment_count.fetch_add(1, Ordering::SeqCst) + 1;
90 debug!("Payment received, total count: {count}");
91 self.maybe_persist();
92 }
93
94 pub fn record_store(&self, data_type: u32) {
100 self.close_records_stored.fetch_add(1, Ordering::SeqCst);
101
102 {
104 let mut records = self.records_per_type.write();
105 if let Some(entry) = records.iter_mut().find(|(t, _)| *t == data_type) {
106 entry.1 = entry.1.saturating_add(1);
107 } else {
108 records.push((data_type, 1));
109 }
110 }
111
112 self.maybe_persist();
113 }
114
115 #[must_use]
117 pub fn payment_count(&self) -> usize {
118 self.received_payment_count.load(Ordering::SeqCst)
119 }
120
121 #[must_use]
123 pub fn records_stored(&self) -> usize {
124 self.close_records_stored.load(Ordering::SeqCst)
125 }
126
127 #[must_use]
129 pub fn live_time_hours(&self) -> u64 {
130 self.start_time.elapsed().as_secs() / 3600
131 }
132
133 pub fn set_network_size(&self, size: u64) {
135 self.network_size.store(size, Ordering::SeqCst);
136 }
137
138 #[must_use]
145 pub fn get_metrics(&self, data_size: usize, data_type: u32) -> QuotingMetrics {
146 QuotingMetrics {
147 data_type,
148 data_size,
149 close_records_stored: self.close_records_stored.load(Ordering::SeqCst),
150 records_per_type: self.records_per_type.read().clone(),
151 received_payment_count: self.received_payment_count.load(Ordering::SeqCst),
152 live_time: self.live_time_hours(),
153 network_density: None, network_size: Some(self.network_size.load(Ordering::SeqCst)),
155 }
156 }
157
158 fn maybe_persist(&self) {
160 let ops = self.ops_since_persist.fetch_add(1, Ordering::Relaxed);
161 if ops % PERSIST_INTERVAL == 0 {
162 self.persist();
163 }
164 }
165
166 fn persist(&self) {
168 if let Some(ref path) = self.persist_path {
169 let data = PersistedMetrics {
170 received_payment_count: self.received_payment_count.load(Ordering::SeqCst),
171 close_records_stored: self.close_records_stored.load(Ordering::SeqCst),
172 records_per_type: self.records_per_type.read().clone(),
173 };
174
175 if let Ok(bytes) = rmp_serde::to_vec(&data) {
176 if let Err(e) = std::fs::write(path, bytes) {
177 warn!("Failed to persist metrics: {e}");
178 }
179 }
180 }
181 }
182
183 fn load_from_disk(path: &std::path::Path) -> Option<PersistedMetrics> {
185 let bytes = std::fs::read(path).ok()?;
186 rmp_serde::from_slice(&bytes).ok()
187 }
188}
189
190impl Drop for QuotingMetricsTracker {
191 fn drop(&mut self) {
192 self.persist();
193 }
194}
195
196#[derive(Debug, serde::Serialize, serde::Deserialize)]
198struct PersistedMetrics {
199 received_payment_count: usize,
200 close_records_stored: usize,
201 records_per_type: Vec<(u32, u32)>,
202}
203
204#[cfg(test)]
205#[allow(clippy::expect_used)]
206mod tests {
207 use super::*;
208 use tempfile::tempdir;
209
210 #[test]
211 fn test_new_tracker() {
212 let tracker = QuotingMetricsTracker::new(50);
213 assert_eq!(tracker.payment_count(), 0);
214 assert_eq!(tracker.records_stored(), 50);
215 }
216
217 #[test]
218 fn test_record_payment() {
219 let tracker = QuotingMetricsTracker::new(0);
220 assert_eq!(tracker.payment_count(), 0);
221
222 tracker.record_payment();
223 assert_eq!(tracker.payment_count(), 1);
224
225 tracker.record_payment();
226 assert_eq!(tracker.payment_count(), 2);
227 }
228
229 #[test]
230 fn test_record_store() {
231 let tracker = QuotingMetricsTracker::new(0);
232 assert_eq!(tracker.records_stored(), 0);
233
234 tracker.record_store(0); assert_eq!(tracker.records_stored(), 1);
236
237 tracker.record_store(0);
238 tracker.record_store(1); assert_eq!(tracker.records_stored(), 3);
240
241 let metrics = tracker.get_metrics(1024, 0);
242 assert_eq!(metrics.records_per_type.len(), 2);
243 }
244
245 #[test]
246 fn test_get_metrics() {
247 let tracker = QuotingMetricsTracker::new(100);
248 tracker.record_payment();
249 tracker.record_payment();
250
251 let metrics = tracker.get_metrics(2048, 0);
252 assert_eq!(metrics.data_size, 2048);
253 assert_eq!(metrics.data_type, 0);
254 assert_eq!(metrics.close_records_stored, 100);
255 assert_eq!(metrics.received_payment_count, 2);
256 }
257
258 #[test]
259 fn test_persistence() {
260 let dir = tempdir().expect("tempdir");
261 let path = dir.path().join("metrics.bin");
262
263 {
265 let tracker = QuotingMetricsTracker::with_persistence(&path);
266 tracker.record_payment();
267 tracker.record_payment();
268 tracker.record_store(0);
269 }
270
271 let tracker = QuotingMetricsTracker::with_persistence(&path);
273 assert_eq!(tracker.payment_count(), 2);
274 assert_eq!(tracker.records_stored(), 1);
275 }
276
277 #[test]
278 fn test_live_time_hours() {
279 let tracker = QuotingMetricsTracker::new(0);
280 assert_eq!(tracker.live_time_hours(), 0);
282 }
283
284 #[test]
285 fn test_set_network_size() {
286 let tracker = QuotingMetricsTracker::new(0);
287 tracker.set_network_size(1000);
288
289 let metrics = tracker.get_metrics(0, 0);
290 assert_eq!(metrics.network_size, Some(1000));
291 }
292
293 #[test]
294 fn test_records_per_type_multiple_types() {
295 let tracker = QuotingMetricsTracker::new(0);
296
297 tracker.record_store(0);
298 tracker.record_store(0);
299 tracker.record_store(1);
300 tracker.record_store(2);
301 tracker.record_store(1);
302
303 let metrics = tracker.get_metrics(0, 0);
304 assert_eq!(metrics.records_per_type.len(), 3);
305
306 let type_0 = metrics.records_per_type.iter().find(|(t, _)| *t == 0);
308 let type_1 = metrics.records_per_type.iter().find(|(t, _)| *t == 1);
309 let type_2 = metrics.records_per_type.iter().find(|(t, _)| *t == 2);
310
311 assert_eq!(type_0.expect("type 0 exists").1, 2);
312 assert_eq!(type_1.expect("type 1 exists").1, 2);
313 assert_eq!(type_2.expect("type 2 exists").1, 1);
314 }
315
316 #[test]
317 fn test_persistence_round_trip_with_types() {
318 let dir = tempdir().expect("tempdir");
319 let path = dir.path().join("metrics_types.bin");
320
321 {
322 let tracker = QuotingMetricsTracker::with_persistence(&path);
323 tracker.record_store(0);
324 tracker.record_store(0);
325 tracker.record_store(1);
326 tracker.record_payment();
327 }
328
329 let tracker = QuotingMetricsTracker::with_persistence(&path);
330 assert_eq!(tracker.payment_count(), 1);
331 assert_eq!(tracker.records_stored(), 3); let metrics = tracker.get_metrics(0, 0);
334 assert_eq!(metrics.records_per_type.len(), 2);
335 }
336
337 #[test]
338 fn test_with_persistence_nonexistent_path() {
339 let dir = tempdir().expect("tempdir");
340 let path = dir.path().join("nonexistent_subdir").join("metrics.bin");
341
342 let tracker = QuotingMetricsTracker::with_persistence(&path);
344 assert_eq!(tracker.payment_count(), 0);
345 assert_eq!(tracker.records_stored(), 0);
346 }
347
348 #[test]
349 fn test_get_metrics_passes_data_params() {
350 let tracker = QuotingMetricsTracker::new(0);
351 let metrics = tracker.get_metrics(4096, 3);
352 assert_eq!(metrics.data_size, 4096);
353 assert_eq!(metrics.data_type, 3);
354 }
355
356 #[test]
357 fn test_default_network_size() {
358 let tracker = QuotingMetricsTracker::new(0);
359 let metrics = tracker.get_metrics(0, 0);
360 assert_eq!(metrics.network_size, Some(500));
361 }
362}