#![doc = include_str!("../README.md")]
#![expect(unsafe_code, reason = "Custom allocator is unsafe, the logic we add is all safe")]
use std::{
alloc::{GlobalAlloc, Layout, System},
fmt,
io::{self, Write},
process,
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
};
use monty_types::{BASELINE_MEMORY, LIVE_MEMORY, OOM_EXIT_CODE};
static SOFT_LIMIT: AtomicUsize = AtomicUsize::new(usize::MAX);
static HARD_LIMIT: AtomicUsize = AtomicUsize::new(usize::MAX);
const BASE_HEADROOM: usize = 4 * 1024 * 1024;
const TYPE_CHECK_HEADROOM: usize = 32 * 1024 * 1024;
pub fn set_limit(max_memory: Option<usize>, type_check: bool) -> Result<(), &'static str> {
let live = LIVE_MEMORY.load(Ordering::Relaxed);
if live == 0 {
return Err("monty-alloc is not installed as the global allocator");
}
let baseline = BASELINE_MEMORY.fetch_min(live, Ordering::Relaxed).min(live);
let (soft, hard) = match max_memory {
Some(bytes) => {
let soft = baseline.saturating_add(bytes);
let headroom = if type_check { TYPE_CHECK_HEADROOM } else { BASE_HEADROOM };
(soft, soft.saturating_add(headroom))
}
None => (usize::MAX, usize::MAX),
};
HARD_LIMIT.store(hard, Ordering::Relaxed);
SOFT_LIMIT.store(soft, Ordering::Relaxed);
Ok(())
}
pub struct LimitedAllocator;
unsafe impl GlobalAlloc for LimitedAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
charge(layout.size());
let ptr = unsafe { System.alloc(layout) };
if ptr.is_null() {
out_of_memory(format_args!(
"monty worker: allocation of {} bytes failed",
layout.size()
));
}
ptr
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
refund(layout.size());
unsafe { System.dealloc(ptr, layout) };
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
charge(layout.size());
let ptr = unsafe { System.alloc_zeroed(layout) };
if ptr.is_null() {
out_of_memory(format_args!(
"monty worker: allocation of {} bytes failed",
layout.size()
));
}
ptr
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
if let Some(size_change) = new_size.checked_sub(layout.size()) {
charge(size_change);
} else {
refund(layout.size() - new_size);
}
let new_ptr = unsafe { System.realloc(ptr, layout, new_size) };
if new_ptr.is_null() {
out_of_memory(format_args!("monty worker: allocation of {new_size} bytes failed"));
}
new_ptr
}
}
#[inline]
fn charge(size: usize) {
let live = LIVE_MEMORY.fetch_add(size, Ordering::Relaxed).saturating_add(size);
if live > SOFT_LIMIT.load(Ordering::Relaxed) && live > HARD_LIMIT.load(Ordering::Relaxed) {
out_of_memory(format_args!(
"monty worker: allocation of {size} bytes exceeds the memory limit"
));
}
}
#[inline]
fn refund(size: usize) {
LIVE_MEMORY.fetch_sub(size, Ordering::Relaxed);
}
#[cold]
#[inline(never)]
fn out_of_memory(reason: fmt::Arguments<'_>) -> ! {
static REPORTING: AtomicBool = AtomicBool::new(false);
SOFT_LIMIT.store(usize::MAX, Ordering::Relaxed);
HARD_LIMIT.store(usize::MAX, Ordering::Relaxed);
if !REPORTING.swap(true, Ordering::Relaxed) {
let _ = writeln!(io::stderr(), "{reason}");
}
#[cfg(feature = "exit-code")]
process::exit(OOM_EXIT_CODE);
#[cfg(not(feature = "exit-code"))]
process::abort();
}