use std::{
sync::{
OnceLock,
atomic::{AtomicU64, Ordering},
},
thread,
time::Duration,
};
use dashmap::DashMap;
const WINDOW_MS: u64 = 1000;
#[derive(Default)]
struct Counts {
set: AtomicU64,
del: AtomicU64,
}
fn counts() -> &'static DashMap<String, Counts> {
static C: OnceLock<DashMap<String, Counts>> = OnceLock::new();
C.get_or_init(DashMap::new)
}
#[inline]
pub fn record_set(entity_type: &str) {
counts()
.entry(entity_type.to_string())
.or_default()
.set
.fetch_add(1, Ordering::Relaxed);
}
#[inline]
pub fn record_del(entity_type: &str) {
counts()
.entry(entity_type.to_string())
.or_default()
.del
.fetch_add(1, Ordering::Relaxed);
}
pub fn start_periodic_logger() {
static STARTED: OnceLock<()> = OnceLock::new();
if STARTED.set(()).is_err() {
return;
}
let _ = thread::Builder::new()
.name("myko-entity-set-stats".to_string())
.spawn(run_loop)
.map_err(|e| {
log::warn!(
target: "myko::server::entity_set_stats",
"Failed to spawn entity_set_stats thread: {}", e
)
});
}
fn run_loop() {
loop {
thread::sleep(Duration::from_millis(WINDOW_MS));
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(emit_window));
}
}
fn emit_window() {
let mut snap: Vec<(String, u64, u64)> = counts()
.iter()
.filter_map(|e| {
let sets = e.value().set.swap(0, Ordering::Relaxed);
let dels = e.value().del.swap(0, Ordering::Relaxed);
if sets == 0 && dels == 0 {
None
} else {
Some((e.key().clone(), sets, dels))
}
})
.collect();
if snap.is_empty() {
return;
}
snap.sort_by_key(|b| std::cmp::Reverse(b.1 + b.2));
let set_total: u64 = snap.iter().map(|s| s.1).sum();
let del_total: u64 = snap.iter().map(|s| s.2).sum();
let detail = snap
.iter()
.map(|(et, s, d)| format!("{}=set:{} del:{}", et, s, d))
.collect::<Vec<_>>()
.join(", ");
log::debug!(
target: "myko::server::entity_set_stats",
"[entity] window={}ms set_total={} del_total={} [{}]",
WINDOW_MS,
set_total,
del_total,
detail,
);
}