memscope_rs/tracking/
stats.rs1use std::sync::atomic::{AtomicUsize, Ordering};
2use std::time::{Duration, Instant};
3use tracing::warn;
4
5#[derive(Debug)]
10pub struct TrackingStats {
11 pub total_attempts: AtomicUsize,
13 pub successful_tracks: AtomicUsize,
15 pub missed_due_to_contention: AtomicUsize,
17 pub last_warning_time: std::sync::Mutex<Option<Instant>>,
19}
20
21impl TrackingStats {
22 pub fn new() -> Self {
24 Self {
25 total_attempts: AtomicUsize::new(0),
26 successful_tracks: AtomicUsize::new(0),
27 missed_due_to_contention: AtomicUsize::new(0),
28 last_warning_time: std::sync::Mutex::new(None),
29 }
30 }
31
32 #[inline]
34 pub fn record_attempt(&self) {
35 self.total_attempts.fetch_add(1, Ordering::Relaxed);
36 }
37
38 #[inline]
40 pub fn record_success(&self) {
41 self.successful_tracks.fetch_add(1, Ordering::Relaxed);
42 }
43
44 #[inline]
46 pub fn record_miss(&self) {
47 self.missed_due_to_contention
48 .fetch_add(1, Ordering::Relaxed);
49 self.maybe_warn();
50 }
51
52 pub fn get_completeness(&self) -> f64 {
58 let attempts = self.total_attempts.load(Ordering::Relaxed);
59 let successful = self.successful_tracks.load(Ordering::Relaxed);
60 if attempts == 0 {
61 1.0
62 } else {
63 successful as f64 / attempts as f64
64 }
65 }
66
67 pub fn get_detailed_stats(&self) -> DetailedStats {
69 let attempts = self.total_attempts.load(Ordering::Relaxed);
70 let successful = self.successful_tracks.load(Ordering::Relaxed);
71 let missed = self.missed_due_to_contention.load(Ordering::Relaxed);
72
73 DetailedStats {
74 total_attempts: attempts,
75 successful_tracks: successful,
76 missed_due_to_contention: missed,
77 completeness: self.get_completeness(),
78 contention_rate: if attempts > 0 {
79 missed as f64 / attempts as f64
80 } else {
81 0.0
82 },
83 }
84 }
85
86 pub fn reset(&self) {
88 self.total_attempts.store(0, Ordering::Relaxed);
89 self.successful_tracks.store(0, Ordering::Relaxed);
90 self.missed_due_to_contention.store(0, Ordering::Relaxed);
91
92 if let Ok(mut last_warning) = self.last_warning_time.lock() {
93 *last_warning = None;
94 }
95 }
96
97 fn maybe_warn(&self) {
101 let completeness = self.get_completeness();
102
103 if completeness < 0.9 {
105 if let Ok(mut last_warning) = self.last_warning_time.lock() {
106 let now = Instant::now();
107 let should_warn = last_warning
108 .map(|last| now.duration_since(last) > Duration::from_secs(10))
109 .unwrap_or(true);
110
111 if should_warn {
112 let stats = self.get_detailed_stats();
113 warn!(
114 "Memory tracking completeness: {:.1}% ({}/{} successful, {} missed due to contention)",
115 completeness * 100.0,
116 stats.successful_tracks,
117 stats.total_attempts,
118 stats.missed_due_to_contention
119 );
120 *last_warning = Some(now);
121 }
122 }
123 }
124 }
125}
126
127impl Default for TrackingStats {
128 fn default() -> Self {
129 Self::new()
130 }
131}
132
133#[derive(Debug, Clone)]
135pub struct DetailedStats {
136 pub total_attempts: usize,
138 pub successful_tracks: usize,
140 pub missed_due_to_contention: usize,
142 pub completeness: f64,
144 pub contention_rate: f64,
146}
147
148impl DetailedStats {
149 pub fn is_healthy(&self) -> bool {
151 self.completeness >= 0.95 && self.contention_rate <= 0.05
152 }
153
154 pub fn quality_grade(&self) -> &'static str {
156 match self.completeness {
157 x if x >= 0.98 => "Excellent",
158 x if x >= 0.95 => "Good",
159 x if x >= 0.90 => "Fair",
160 x if x >= 0.80 => "Poor",
161 _ => "Critical",
162 }
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use std::thread;
170
171 #[test]
172 fn test_tracking_stats_basic() {
173 let stats = TrackingStats::new();
174
175 assert_eq!(stats.get_completeness(), 1.0);
177
178 stats.record_attempt();
180 stats.record_success();
181 assert_eq!(stats.get_completeness(), 1.0);
182
183 stats.record_attempt();
184 stats.record_miss();
185 assert_eq!(stats.get_completeness(), 0.5);
186 }
187
188 #[test]
189 fn test_detailed_stats() {
190 let stats = TrackingStats::new();
191
192 for _ in 0..100 {
194 stats.record_attempt();
195 stats.record_success();
196 }
197
198 for _ in 0..5 {
199 stats.record_attempt();
200 stats.record_miss();
201 }
202
203 let detailed = stats.get_detailed_stats();
204 assert_eq!(detailed.total_attempts, 105);
205 assert_eq!(detailed.successful_tracks, 100);
206 assert_eq!(detailed.missed_due_to_contention, 5);
207 assert!((detailed.completeness - 0.9523).abs() < 0.001);
208 assert!(detailed.is_healthy());
209 }
210
211 #[test]
212 fn test_quality_grades() {
213 let stats = TrackingStats::new();
214
215 let test_cases = vec![
217 (100, 100, "Excellent"),
218 (100, 96, "Good"),
219 (100, 92, "Fair"),
220 (100, 85, "Poor"),
221 (100, 70, "Critical"),
222 ];
223
224 for (attempts, successes, expected_grade) in test_cases {
225 stats.reset();
226
227 for _ in 0..attempts {
228 stats.record_attempt();
229 }
230 for _ in 0..successes {
231 stats.record_success();
232 }
233
234 let detailed = stats.get_detailed_stats();
235 assert_eq!(detailed.quality_grade(), expected_grade);
236 }
237 }
238
239 #[test]
240 fn test_concurrent_access() {
241 let stats = std::sync::Arc::new(TrackingStats::new());
242 let mut handles = vec![];
243
244 for _ in 0..4 {
246 let stats_clone = stats.clone();
247 let handle = thread::spawn(move || {
248 for _ in 0..1000 {
249 stats_clone.record_attempt();
250 #[allow(clippy::manual_is_multiple_of)]
251 if thread_local_random() % 10 != 0 {
252 stats_clone.record_success();
253 } else {
254 stats_clone.record_miss();
255 }
256 }
257 });
258 handles.push(handle);
259 }
260
261 for handle in handles {
263 handle.join().unwrap();
264 }
265
266 let detailed = stats.get_detailed_stats();
267 assert_eq!(detailed.total_attempts, 4000);
268 assert!(detailed.successful_tracks >= 3000); assert!(detailed.completeness >= 0.8);
270 }
271
272 fn thread_local_random() -> usize {
273 use std::cell::Cell;
274 thread_local! {
275 static RNG: Cell<usize> = const { Cell::new(1) };
276 }
277
278 RNG.with(|rng| {
279 let x = rng.get();
280 let next = x.wrapping_mul(1103515245).wrapping_add(12345);
281 rng.set(next);
282 next
283 })
284 }
285}