#[cfg(any(test, feature = "jemalloc"))]
use std::sync::Arc;
#[cfg(any(test, feature = "jemalloc"))]
#[derive(Clone, Copy, Debug)]
pub(crate) struct AllocatorSnapshot {
pub allocated: u64,
pub resident: u64,
pub active: u64,
pub mapped: u64,
}
#[cfg(any(test, feature = "jemalloc"))]
pub(crate) fn emit_allocator_snapshot(
read: impl Fn() -> Result<AllocatorSnapshot, String>,
metrics: &Arc<dyn camel_api::MetricsCollector>,
) -> bool {
use camel_api::metrics::AllocatorStat;
match read() {
Ok(snap) => {
metrics.set_allocator_memory(AllocatorStat::Allocated, snap.allocated);
metrics.set_allocator_memory(AllocatorStat::Resident, snap.resident);
metrics.set_allocator_memory(AllocatorStat::Active, snap.active);
metrics.set_allocator_memory(AllocatorStat::Mapped, snap.mapped);
true
}
Err(_) => {
tracing::warn!("jemalloc stats read failed; retrying next tick");
false
}
}
}
#[cfg(feature = "jemalloc")]
struct JemallocMibs {
epoch: tikv_jemalloc_ctl::epoch_mib,
allocated: tikv_jemalloc_ctl::stats::allocated_mib,
resident: tikv_jemalloc_ctl::stats::resident_mib,
active: tikv_jemalloc_ctl::stats::active_mib,
mapped: tikv_jemalloc_ctl::stats::mapped_mib,
}
#[cfg(feature = "jemalloc")]
fn mibs() -> Result<&'static JemallocMibs, String> {
use std::sync::OnceLock;
static MIBS: OnceLock<Result<JemallocMibs, String>> = OnceLock::new();
MIBS.get_or_init(|| {
Ok(JemallocMibs {
epoch: tikv_jemalloc_ctl::epoch::mib().map_err(|e| e.to_string())?,
allocated: tikv_jemalloc_ctl::stats::allocated::mib().map_err(|e| e.to_string())?,
resident: tikv_jemalloc_ctl::stats::resident::mib().map_err(|e| e.to_string())?,
active: tikv_jemalloc_ctl::stats::active::mib().map_err(|e| e.to_string())?,
mapped: tikv_jemalloc_ctl::stats::mapped::mib().map_err(|e| e.to_string())?,
})
})
.as_ref()
.map_err(Clone::clone)
}
#[cfg(feature = "jemalloc")]
pub(crate) fn read_jemalloc_snapshot() -> Result<AllocatorSnapshot, String> {
let mibs = mibs()?;
mibs.epoch.advance().map_err(|e| format!("epoch: {e}"))?;
Ok(AllocatorSnapshot {
allocated: mibs
.allocated
.read()
.map_err(|e| format!("stats.allocated: {e}"))? as u64,
resident: mibs
.resident
.read()
.map_err(|e| format!("stats.resident: {e}"))? as u64,
active: mibs
.active
.read()
.map_err(|e| format!("stats.active: {e}"))? as u64,
mapped: mibs
.mapped
.read()
.map_err(|e| format!("stats.mapped: {e}"))? as u64,
})
}
#[cfg(feature = "jemalloc")]
pub(crate) fn spawn_allocator_sampler(metrics: std::sync::Arc<dyn camel_api::MetricsCollector>) {
if let Err(err) = mibs() {
tracing::warn!(
error = %err,
"jemalloc stats MIB init failed; allocator sampler disabled"
);
return;
}
tokio::spawn(async move {
let mut tick = tokio::time::interval(std::time::Duration::from_secs(5));
loop {
tick.tick().await;
let _ = emit_allocator_snapshot(read_jemalloc_snapshot, &metrics);
}
});
}
#[cfg(test)]
mod tests {
use super::{AllocatorSnapshot, emit_allocator_snapshot};
use camel_api::MetricsCollector;
use camel_api::metrics::AllocatorStat;
use std::sync::{Arc, Mutex};
#[derive(Default)]
struct RecordingCollector {
allocator: Mutex<Vec<(AllocatorStat, u64)>>,
}
impl MetricsCollector for RecordingCollector {
fn record_exchange_duration(&self, _route_id: &str, _duration: std::time::Duration) {}
fn increment_errors(&self, _route_id: &str, _error_type: &str) {}
fn increment_exchanges(&self, _route_id: &str) {}
fn set_queue_depth(&self, _queue: &str, _depth: usize) {}
fn record_circuit_breaker_change(&self, _route_id: &str, _from: &str, _to: &str) {}
fn set_allocator_memory(&self, stat: AllocatorStat, bytes: u64) {
self.allocator
.lock()
.expect("allocator lock")
.push((stat, bytes));
}
}
#[test]
fn ok_snapshot_maps_to_four_exact_emissions() {
let recorder = Arc::new(RecordingCollector::default());
let metrics: Arc<dyn MetricsCollector> = recorder.clone();
let read = || {
Ok(AllocatorSnapshot {
allocated: 11,
resident: 22,
active: 33,
mapped: 44,
})
};
let emitted = emit_allocator_snapshot(read, &metrics);
assert!(emitted);
let captures = recorder.allocator.lock().expect("allocator lock").clone();
assert_eq!(
captures,
vec![
(AllocatorStat::Allocated, 11),
(AllocatorStat::Resident, 22),
(AllocatorStat::Active, 33),
(AllocatorStat::Mapped, 44),
]
);
}
#[test]
fn err_read_emits_nothing_and_returns_false() {
let recorder = Arc::new(RecordingCollector::default());
let metrics: Arc<dyn MetricsCollector> = recorder.clone();
let read = || Err("epoch".to_string());
let emitted = emit_allocator_snapshot(read, &metrics);
assert!(!emitted);
assert!(
recorder
.allocator
.lock()
.expect("allocator lock")
.is_empty()
);
}
#[test]
fn unwired_handle_snapshot_is_silent_noop() {
let metrics: Arc<dyn MetricsCollector> = Arc::new(camel_api::MetricsHandle::new());
let read = || {
Ok(AllocatorSnapshot {
allocated: 1,
resident: 2,
active: 3,
mapped: 4,
})
};
let emitted = emit_allocator_snapshot(read, &metrics);
assert!(emitted);
}
#[cfg(feature = "jemalloc")]
#[test]
fn real_read_closure_advances_epoch_and_returns_snapshot() {
let snapshot = super::read_jemalloc_snapshot();
assert!(
snapshot.is_ok(),
"real jemalloc read failed: {:?}",
snapshot.err()
);
}
}