use std::ffi::{CString, c_char};
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
const DEFAULT_FLOOR_BYTES: u64 = 2 * 1024 * 1024 * 1024;
const DEFAULT_STEP_BYTES: u64 = 1024 * 1024 * 1024;
const DIR_ENV: &str = "MEMORY_BUDGET_HEAP_PROFILE_DIR";
const FLOOR_GB_ENV: &str = "MEMORY_BUDGET_HEAP_PROFILE_FLOOR_GB";
const STEP_GB_ENV: &str = "MEMORY_BUDGET_HEAP_PROFILE_STEP_GB";
pub(crate) struct HeapProfiler {
dir: PathBuf,
step_bytes: u64,
next_threshold: AtomicU64,
seq: AtomicU64,
}
impl HeapProfiler {
pub(crate) fn from_env() -> Option<Self> {
let dir = std::env::var_os(DIR_ENV)
.map(PathBuf::from)
.unwrap_or_else(|| {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join("logs")
});
let floor = gib_env(FLOOR_GB_ENV).unwrap_or(DEFAULT_FLOOR_BYTES);
let step = gib_env(STEP_GB_ENV).unwrap_or(DEFAULT_STEP_BYTES).max(1);
let prof_on = prof_is_active();
Self::log_symbolization_anchor();
tracing::info!(
target: "memory_budget::heap_profile",
dir = %dir.display(),
floor_bytes = floor,
step_bytes = step,
prof_active = prof_on,
"heap profiler armed{}",
if prof_on {
""
} else {
" — but jemalloc opt.prof is false; dumps will fail (set \
_RJEM_MALLOC_CONF=prof:true,prof_active:true and build with \
--features heap-profiling)"
}
);
Some(Self {
dir,
step_bytes: step,
next_threshold: AtomicU64::new(floor),
seq: AtomicU64::new(0),
})
}
pub(crate) fn maybe_dump(&self, resident_bytes: u64) {
if resident_bytes < self.next_threshold.load(Ordering::Relaxed) {
return;
}
let seq = self.seq.fetch_add(1, Ordering::Relaxed);
self.dump(seq, resident_bytes);
self.next_threshold
.store(resident_bytes + self.step_bytes, Ordering::Relaxed);
}
fn dump(&self, seq: u64, resident_bytes: u64) {
let gib = resident_bytes / (1024 * 1024 * 1024);
let path = self.dir.join(format!("heap-{seq:04}-{gib}gib.heap"));
let Ok(c_path) = CString::new(path.as_os_str().as_encoded_bytes()) else {
tracing::warn!(
target: "memory_budget::heap_profile",
path = %path.display(),
"heap dump path contained a NUL byte; skipping"
);
return;
};
let name_ptr: *const c_char = c_path.as_ptr();
let result = unsafe { tikv_jemalloc_ctl::raw::write(b"prof.dump\0", name_ptr) };
match result {
Ok(()) => tracing::warn!(
target: "memory_budget::heap_profile",
seq,
resident_bytes,
path = %path.display(),
"heap profile dumped"
),
Err(e) => tracing::warn!(
target: "memory_budget::heap_profile",
seq,
resident_bytes,
error = %e,
"heap profile dump failed (is jemalloc built with profiling and started \
with prof:true?)"
),
}
}
fn log_symbolization_anchor() {
#[cfg(target_os = "macos")]
{
let slide = unsafe { dyld_image_vmaddr_slide_0() };
tracing::info!(
target: "memory_budget::heap_profile",
dyld_slide = format!("0x{slide:x}"),
symbolize = "atos -o <application> -l 0x100000000+<dyld_slide> <addr>...",
"heap profile symbolization anchor (macOS: no /proc maps)"
);
}
}
}
fn gib_env(name: &str) -> Option<u64> {
let gib = std::env::var(name).ok()?.trim().parse::<u64>().ok()?;
Some(gib.saturating_mul(1024 * 1024 * 1024))
}
fn prof_is_active() -> bool {
unsafe { tikv_jemalloc_ctl::raw::read::<bool>(b"opt.prof\0") }.unwrap_or(false)
}
#[cfg(target_os = "macos")]
unsafe extern "C" {
#[link_name = "_dyld_get_image_vmaddr_slide"]
fn dyld_image_vmaddr_slide_0_raw(image_index: u32) -> isize;
}
#[cfg(target_os = "macos")]
unsafe fn dyld_image_vmaddr_slide_0() -> isize {
unsafe { dyld_image_vmaddr_slide_0_raw(0) }
}