use std::alloc::{GlobalAlloc, Layout};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
pub struct Allocator;
static INNER_ALLOCATOR: mimalloc_pprof::MiMalloc = mimalloc_pprof::MiMalloc;
impl Allocator {
pub const fn new() -> Self {
Self
}
}
impl Default for Allocator {
fn default() -> Self {
Self::new()
}
}
unsafe impl GlobalAlloc for Allocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
unsafe { INNER_ALLOCATOR.alloc(layout) }
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
unsafe { INNER_ALLOCATOR.alloc_zeroed(layout) }
}
unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
unsafe { INNER_ALLOCATOR.dealloc(pointer, layout) };
}
unsafe fn realloc(&self, pointer: *mut u8, layout: Layout, size: usize) -> *mut u8 {
unsafe { INNER_ALLOCATOR.realloc(pointer, layout, size) }
}
}
pub const HEAP_PROFILE_ENV: &str = "KERNAL_API_HEAP_PROFILE";
pub const DEFAULT_SAMPLE_RATE: usize = 512 * 1024;
static DUMP_SEQUENCE: AtomicU64 = AtomicU64::new(0);
pub fn sample_rate_from(value: &str) -> Option<usize> {
match value.trim().to_ascii_lowercase().as_str() {
"" | "0" | "false" | "off" | "no" => None,
"1" | "true" | "on" | "yes" => Some(DEFAULT_SAMPLE_RATE),
other => other.parse::<usize>().ok().filter(|rate| *rate > 0),
}
}
pub fn start_from_named_env(env_var: &str) -> Option<usize> {
let rate = sample_rate_from(&std::env::var(env_var).ok()?)?;
mimalloc_pprof::prof::start(rate).then_some(rate)
}
pub fn start_from_env() -> Option<usize> {
start_from_named_env(HEAP_PROFILE_ENV)
}
pub fn start(sample_rate: usize) -> bool {
sample_rate > 0 && mimalloc_pprof::prof::start(sample_rate)
}
pub fn stop() {
mimalloc_pprof::prof::stop();
}
pub fn is_enabled() -> bool {
mimalloc_pprof::prof::is_enabled()
}
pub fn live_sample_count() -> usize {
mimalloc_pprof::prof::stats().live_samples
}
#[derive(Clone, Debug, Default)]
pub struct ProfilerStats {
inner: mimalloc_pprof::prof::ProfStats,
}
impl ProfilerStats {
pub fn enabled(&self) -> bool {
self.inner.enabled
}
pub fn accumulating(&self) -> bool {
self.inner.accum
}
pub fn sample_rate(&self) -> usize {
self.inner.sample_rate
}
pub fn live_samples(&self) -> usize {
self.inner.live_samples
}
pub fn live_bytes(&self) -> usize {
self.inner.live_bytes
}
pub fn accumulated_samples(&self) -> usize {
self.inner.accum_samples
}
pub fn accumulated_bytes(&self) -> usize {
self.inner.accum_bytes
}
pub fn unique_stacks(&self) -> usize {
self.inner.unique_stacks
}
pub fn profiler_committed_bytes(&self) -> usize {
self.inner.arena_committed
}
pub fn stack_table_overflows(&self) -> usize {
self.inner.stack_table_overflows
}
pub fn dropped_samples(&self) -> usize {
self.inner.dropped_samples
}
pub fn heap(&self) -> HeapStats {
HeapStats {
inner: self.inner.heap.clone(),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct HeapStats {
inner: mimalloc_pprof::prof::HeapStats,
}
impl HeapStats {
pub fn committed(&self) -> usize {
self.inner.committed
}
pub fn reserved(&self) -> usize {
self.inner.reserved
}
pub fn malloc_requested(&self) -> usize {
self.inner.malloc_requested
}
pub fn pages(&self) -> usize {
self.inner.pages
}
pub fn pages_abandoned(&self) -> usize {
self.inner.pages_abandoned
}
pub fn heaps(&self) -> usize {
self.inner.heaps
}
pub fn thread_heaps(&self) -> usize {
self.inner.theaps
}
pub fn purged(&self) -> usize {
self.inner.purged
}
pub fn detailed(&self) -> bool {
self.inner.detailed
}
}
pub fn stats() -> ProfilerStats {
ProfilerStats {
inner: mimalloc_pprof::prof::stats(),
}
}
pub fn next_dump_name() -> String {
let sequence = DUMP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
let millis = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|elapsed| elapsed.as_millis())
.unwrap_or_default();
format!("heap-{}-{millis}-{sequence}.pb", std::process::id())
}
pub fn dump_to(path: impl AsRef<Path>) -> std::io::Result<()> {
mimalloc_pprof::prof::dump_proto_file(path.as_ref())
}
pub fn dump_file(path: impl AsRef<Path>) -> std::io::Result<()> {
mimalloc_pprof::prof::dump_file(path.as_ref())
}
pub fn dump_to_vec() -> Vec<u8> {
mimalloc_pprof::prof::dump_proto_to_vec()
}
pub async fn dump_in(directory: impl AsRef<Path>) -> std::io::Result<PathBuf> {
let directory = directory.as_ref();
tokio::fs::create_dir_all(directory).await?;
let path = directory.join(next_dump_name());
let dump_path = path.clone();
tokio::task::spawn_blocking(move || dump_to(&dump_path))
.await
.map_err(std::io::Error::other)??;
Ok(path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn settings_are_explicit_and_zero_never_means_sample_everything() {
for off in ["", "0", "00", "false", "off", "no"] {
assert_eq!(sample_rate_from(off), None, "{off:?}");
}
for on in ["1", "true", "on", "yes"] {
assert_eq!(sample_rate_from(on), Some(DEFAULT_SAMPLE_RATE), "{on:?}");
}
assert_eq!(sample_rate_from("65536"), Some(65_536));
}
#[test]
fn dump_names_do_not_collide() {
let first = next_dump_name();
let second = next_dump_name();
assert_ne!(first, second);
assert!(first.ends_with(".pb"));
}
}