1use std::collections::BTreeMap;
38use std::sync::atomic::{AtomicU64, Ordering};
39use std::sync::Arc;
40
41use dashmap::DashMap;
42
43#[derive(Debug, Clone, Default, PartialEq, Eq)]
45pub struct RoomStats {
46 pub messages_in: u64,
48 pub messages_out: u64,
50 pub bytes_in: u64,
52 pub bytes_out: u64,
54 pub drops: u64,
56}
57
58#[derive(Debug, Clone, Default, PartialEq, Eq)]
63pub struct Stats {
64 pub connections: u64,
66 pub rooms: u64,
68 pub messages_in: u64,
70 pub messages_out: u64,
72 pub bytes_in: u64,
74 pub bytes_out: u64,
76 pub drops: u64,
78 pub per_room: BTreeMap<String, RoomStats>,
80}
81
82#[derive(Debug, Default)]
83struct Globals {
84 connections: AtomicU64,
85 rooms: AtomicU64,
86 messages_in: AtomicU64,
87 messages_out: AtomicU64,
88 bytes_in: AtomicU64,
89 bytes_out: AtomicU64,
90 drops: AtomicU64,
91}
92
93#[derive(Debug, Default)]
94struct RoomCounters {
95 messages_in: AtomicU64,
96 messages_out: AtomicU64,
97 bytes_in: AtomicU64,
98 bytes_out: AtomicU64,
99 drops: AtomicU64,
100}
101
102#[derive(Debug, Default)]
103struct Inner {
104 globals: Globals,
105 rooms: DashMap<String, RoomCounters>,
106}
107
108#[derive(Clone, Debug, Default)]
113pub struct StatsRecorder {
114 inner: Arc<Inner>,
115}
116
117impl StatsRecorder {
118 pub fn new() -> Self {
120 Self::default()
121 }
122
123 pub fn inc_connections(&self) -> u64 {
125 let v = self
126 .inner
127 .globals
128 .connections
129 .fetch_add(1, Ordering::Relaxed)
130 + 1;
131 #[cfg(feature = "metrics")]
132 ::metrics::gauge!("ws_kit_connections").set(v as f64);
133 v
134 }
135
136 pub fn dec_connections(&self) -> u64 {
138 let v = saturating_dec(&self.inner.globals.connections);
139 #[cfg(feature = "metrics")]
140 ::metrics::gauge!("ws_kit_connections").set(v as f64);
141 v
142 }
143
144 pub fn room_opened(&self) {
146 let v = self.inner.globals.rooms.fetch_add(1, Ordering::Relaxed) + 1;
147 #[cfg(feature = "metrics")]
148 ::metrics::gauge!("ws_kit_rooms").set(v as f64);
149 #[cfg(not(feature = "metrics"))]
150 let _ = v;
151 }
152
153 pub fn room_closed(&self) {
155 let v = saturating_dec(&self.inner.globals.rooms);
156 #[cfg(feature = "metrics")]
157 ::metrics::gauge!("ws_kit_rooms").set(v as f64);
158 #[cfg(not(feature = "metrics"))]
159 let _ = v;
160 }
161
162 pub fn record_message_in(&self, bytes: u64) {
166 self.inner
167 .globals
168 .messages_in
169 .fetch_add(1, Ordering::Relaxed);
170 self.inner
171 .globals
172 .bytes_in
173 .fetch_add(bytes, Ordering::Relaxed);
174 #[cfg(feature = "metrics")]
175 {
176 ::metrics::counter!("ws_kit_messages_in_total").increment(1);
177 ::metrics::counter!("ws_kit_bytes_in_total").increment(bytes);
178 }
179 }
180
181 pub fn record_message_out(&self, count: u64, bytes: u64) {
184 self.inner
185 .globals
186 .messages_out
187 .fetch_add(count, Ordering::Relaxed);
188 self.inner
189 .globals
190 .bytes_out
191 .fetch_add(bytes, Ordering::Relaxed);
192 #[cfg(feature = "metrics")]
193 {
194 ::metrics::counter!("ws_kit_messages_out_total").increment(count);
195 ::metrics::counter!("ws_kit_bytes_out_total").increment(bytes);
196 }
197 }
198
199 pub fn record_drop(&self) {
201 self.inner.globals.drops.fetch_add(1, Ordering::Relaxed);
202 #[cfg(feature = "metrics")]
203 ::metrics::counter!("ws_kit_drops_total").increment(1);
204 }
205
206 pub fn record_room_message_in(&self, room: &str, bytes: u64) {
208 let counters = self.room_counters(room);
209 counters.messages_in.fetch_add(1, Ordering::Relaxed);
210 counters.bytes_in.fetch_add(bytes, Ordering::Relaxed);
211 drop(counters);
212 self.record_message_in(bytes);
213 }
214
215 pub fn record_room_message_out(&self, room: &str, count: u64, bytes: u64) {
218 let counters = self.room_counters(room);
219 counters.messages_out.fetch_add(count, Ordering::Relaxed);
220 counters.bytes_out.fetch_add(bytes, Ordering::Relaxed);
221 drop(counters);
222 self.record_message_out(count, bytes);
223 }
224
225 pub fn record_room_drop(&self, room: &str) {
227 let counters = self.room_counters(room);
228 counters.drops.fetch_add(1, Ordering::Relaxed);
229 drop(counters);
230 self.record_drop();
231 }
232
233 pub fn snapshot(&self) -> Stats {
236 let g = &self.inner.globals;
237 let per_room: BTreeMap<String, RoomStats> = self
238 .inner
239 .rooms
240 .iter()
241 .map(|e| {
242 let c = e.value();
243 (
244 e.key().clone(),
245 RoomStats {
246 messages_in: c.messages_in.load(Ordering::Relaxed),
247 messages_out: c.messages_out.load(Ordering::Relaxed),
248 bytes_in: c.bytes_in.load(Ordering::Relaxed),
249 bytes_out: c.bytes_out.load(Ordering::Relaxed),
250 drops: c.drops.load(Ordering::Relaxed),
251 },
252 )
253 })
254 .collect();
255 Stats {
256 connections: g.connections.load(Ordering::Relaxed),
257 rooms: g.rooms.load(Ordering::Relaxed),
258 messages_in: g.messages_in.load(Ordering::Relaxed),
259 messages_out: g.messages_out.load(Ordering::Relaxed),
260 bytes_in: g.bytes_in.load(Ordering::Relaxed),
261 bytes_out: g.bytes_out.load(Ordering::Relaxed),
262 drops: g.drops.load(Ordering::Relaxed),
263 per_room,
264 }
265 }
266
267 fn room_counters(&self, room: &str) -> dashmap::mapref::one::Ref<'_, String, RoomCounters> {
268 self.inner
269 .rooms
270 .entry(room.to_string())
271 .or_default()
272 .downgrade()
273 }
274}
275
276fn saturating_dec(counter: &AtomicU64) -> u64 {
277 let _ = counter.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| {
278 if v == 0 {
279 None
280 } else {
281 Some(v - 1)
282 }
283 });
284 counter.load(Ordering::Relaxed)
285}
286
287#[allow(
290 clippy::unwrap_used,
291 clippy::expect_used,
292 clippy::indexing_slicing,
293 clippy::panic
294)]
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn initial_snapshot_zeroed() {
301 let s = StatsRecorder::new().snapshot();
302 assert_eq!(s, Stats::default());
303 }
304
305 #[test]
306 fn connection_counter_saturates() {
307 let stats = StatsRecorder::new();
308 assert_eq!(stats.inc_connections(), 1);
309 assert_eq!(stats.inc_connections(), 2);
310 assert_eq!(stats.dec_connections(), 1);
311 assert_eq!(stats.dec_connections(), 0);
312 stats.dec_connections(); assert_eq!(stats.snapshot().connections, 0);
314 }
315
316 #[test]
317 fn room_lifecycle_gauge() {
318 let stats = StatsRecorder::new();
319 stats.room_opened();
320 stats.room_opened();
321 assert_eq!(stats.snapshot().rooms, 2);
322 stats.room_closed();
323 stats.room_closed();
324 stats.room_closed(); assert_eq!(stats.snapshot().rooms, 0);
326 }
327
328 #[test]
329 fn global_traffic_counters() {
330 let stats = StatsRecorder::new();
331 stats.record_message_in(128);
332 stats.record_message_out(3, 96);
333 stats.record_drop();
334 let s = stats.snapshot();
335 assert_eq!(s.messages_in, 1);
336 assert_eq!(s.bytes_in, 128);
337 assert_eq!(s.messages_out, 3);
338 assert_eq!(s.bytes_out, 96);
339 assert_eq!(s.drops, 1);
340 }
341
342 #[test]
343 fn room_counters_roll_up_to_globals() {
344 let stats = StatsRecorder::new();
345 stats.record_room_message_in("lobby", 10);
346 stats.record_room_message_out("lobby", 2, 20);
347 stats.record_room_drop("lobby");
348
349 let s = stats.snapshot();
350 let lobby = s.per_room.get("lobby").unwrap();
351 assert_eq!(lobby.messages_in, 1);
352 assert_eq!(lobby.bytes_in, 10);
353 assert_eq!(lobby.messages_out, 2);
354 assert_eq!(lobby.bytes_out, 20);
355 assert_eq!(lobby.drops, 1);
356
357 assert_eq!(s.messages_in, 1);
359 assert_eq!(s.messages_out, 2);
360 assert_eq!(s.bytes_out, 20);
361 assert_eq!(s.drops, 1);
362 }
363
364 #[test]
365 fn rooms_are_isolated_in_snapshot() {
366 let stats = StatsRecorder::new();
367 stats.record_room_message_out("a", 1, 5);
368 stats.record_room_message_out("b", 2, 10);
369 let s = stats.snapshot();
370 assert_eq!(s.per_room.len(), 2);
371 assert_eq!(s.per_room.get("a").unwrap().messages_out, 1);
372 assert_eq!(s.per_room.get("b").unwrap().messages_out, 2);
373 assert_eq!(s.messages_out, 3);
374 }
375
376 #[test]
377 fn clone_shares_counters() {
378 let stats = StatsRecorder::new();
379 let clone = stats.clone();
380 stats.record_message_in(7);
381 assert_eq!(clone.snapshot().messages_in, 1);
382 }
383
384 #[tokio::test]
385 async fn concurrent_recording_no_lost_updates() {
386 let stats = StatsRecorder::new();
387 let mut handles = Vec::new();
388 for _ in 0..8 {
389 let s = stats.clone();
390 handles.push(tokio::spawn(async move {
391 for _ in 0..1000 {
392 s.record_message_in(1);
393 }
394 }));
395 }
396 for h in handles {
397 h.await.unwrap();
398 }
399 assert_eq!(stats.snapshot().messages_in, 8000);
400 assert_eq!(stats.snapshot().bytes_in, 8000);
401 }
402}