#[cfg(target_os = "macos")]
pub fn current_rss_bytes() -> Option<u64> {
#[allow(deprecated)] unsafe {
let mut info: libc::mach_task_basic_info_data_t = std::mem::zeroed();
let mut count = (std::mem::size_of::<libc::mach_task_basic_info_data_t>()
/ std::mem::size_of::<libc::natural_t>())
as libc::mach_msg_type_number_t;
let kr = libc::task_info(
libc::mach_task_self(),
libc::MACH_TASK_BASIC_INFO,
(&raw mut info).cast::<libc::integer_t>(),
&raw mut count,
);
if kr == libc::KERN_SUCCESS {
Some(info.resident_size) } else {
None
}
}
}
#[cfg(target_os = "linux")]
pub fn current_rss_bytes() -> Option<u64> {
let content = std::fs::read_to_string("/proc/self/status").ok()?;
for line in content.lines() {
if let Some(rest) = line.strip_prefix("VmRSS:") {
let kb: u64 = rest.split_whitespace().next()?.parse().ok()?;
return Some(kb * 1024);
}
}
None
}
#[cfg(target_os = "windows")]
pub fn current_rss_bytes() -> Option<u64> {
use std::mem::MaybeUninit;
unsafe {
let handle = windows_sys::Win32::System::Threading::GetCurrentProcess();
let mut pmc = MaybeUninit::<
windows_sys::Win32::System::ProcessStatus::PROCESS_MEMORY_COUNTERS,
>::zeroed();
let cb = std::mem::size_of_val(&pmc) as u32;
let ok = windows_sys::Win32::System::ProcessStatus::K32GetProcessMemoryInfo(
handle,
pmc.as_mut_ptr(),
cb,
);
if ok != 0 {
Some(pmc.assume_init().WorkingSetSize as u64)
} else {
None
}
}
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
pub fn current_rss_bytes() -> Option<u64> {
None
}
pub fn current_rss_mb() -> Option<u64> {
current_rss_bytes().map(|b| b / (1024 * 1024))
}
static PURGE_FN: std::sync::OnceLock<fn()> = std::sync::OnceLock::new();
pub fn register_purge_fn(f: fn()) {
let _ = PURGE_FN.set(f);
}
pub fn purge_allocator() {
if let Some(f) = PURGE_FN.get() {
f();
}
}
#[cfg(target_os = "macos")]
pub fn system_ram_bytes() -> Option<u64> {
unsafe {
let mut size: u64 = 0;
let mut len = std::mem::size_of::<u64>();
let name = c"hw.memsize";
let rc = libc::sysctlbyname(
name.as_ptr(),
(&raw mut size).cast::<libc::c_void>(),
&raw mut len,
std::ptr::null_mut(),
0,
);
if rc == 0 { Some(size) } else { None }
}
}
#[cfg(target_os = "linux")]
pub fn system_ram_bytes() -> Option<u64> {
let content = std::fs::read_to_string("/proc/meminfo").ok()?;
for line in content.lines() {
if let Some(rest) = line.strip_prefix("MemTotal:") {
let kb: u64 = rest.split_whitespace().next()?.parse().ok()?;
return Some(kb * 1024);
}
}
None
}
#[cfg(target_os = "windows")]
pub fn system_ram_bytes() -> Option<u64> {
use std::mem::MaybeUninit;
unsafe {
let mut status =
MaybeUninit::<windows_sys::Win32::System::SystemInformation::MEMORYSTATUSEX>::zeroed();
let p = status.as_mut_ptr();
(*p).dwLength = std::mem::size_of_val(&status) as u32;
let ok = windows_sys::Win32::System::SystemInformation::GlobalMemoryStatusEx(p);
if ok != 0 {
Some(status.assume_init().ullTotalPhys)
} else {
None
}
}
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
pub fn system_ram_bytes() -> Option<u64> {
None
}
pub fn system_ram_mb() -> Option<u64> {
system_ram_bytes().map(|b| b / (1024 * 1024))
}
pub const ABSOLUTE_FLOOR_MB: u64 = 1024;
pub const ABSOLUTE_CEILING_MB: u64 = 12 * 1024;
pub const KERNEL_AS_CEILING_MB: u64 = 96 * 1024;
pub fn soft_rss_limit_mb() -> u64 {
if let Ok(env) = std::env::var("SEMANTEX_MAX_RSS_MB")
&& let Ok(n) = env.trim().parse::<u64>()
{
return n;
}
let detected = system_ram_mb().unwrap_or(4 * 1024);
(detected / 2).clamp(ABSOLUTE_FLOOR_MB, ABSOLUTE_CEILING_MB)
}
pub fn kernel_rss_cap_bytes() -> u64 {
const AS_HEADROOM_MULTIPLIER: u64 = 4;
if let Ok(env) = std::env::var("SEMANTEX_MAX_RSS_MB")
&& let Ok(n) = env.trim().parse::<u64>()
&& n > 0
{
return (n * AS_HEADROOM_MULTIPLIER).clamp(ABSOLUTE_FLOOR_MB, KERNEL_AS_CEILING_MB)
* 1024
* 1024;
}
let detected = system_ram_mb().unwrap_or(4 * 1024);
let mb = (detected * AS_HEADROOM_MULTIPLIER).clamp(ABSOLUTE_FLOOR_MB, KERNEL_AS_CEILING_MB);
mb * 1024 * 1024
}
#[derive(Debug, Clone, Copy)]
pub enum KernelCapResult {
Installed(u64),
UnsupportedPlatform,
Disabled,
Failed(i32),
}
#[cfg(target_os = "linux")]
pub fn install_kernel_rss_cap() -> KernelCapResult {
if std::env::var("SEMANTEX_NO_RLIMIT").as_deref() == Ok("1") {
return KernelCapResult::Disabled;
}
let bytes = kernel_rss_cap_bytes();
unsafe {
let rlim = libc::rlimit {
rlim_cur: bytes,
rlim_max: bytes,
};
if libc::setrlimit(libc::RLIMIT_AS, &raw const rlim) == 0 {
KernelCapResult::Installed(bytes)
} else {
KernelCapResult::Failed(*libc::__errno_location())
}
}
}
#[cfg(not(target_os = "linux"))]
pub fn install_kernel_rss_cap() -> KernelCapResult {
if std::env::var("SEMANTEX_NO_RLIMIT").as_deref() == Ok("1") {
KernelCapResult::Disabled
} else {
KernelCapResult::UnsupportedPlatform
}
}
static OVERSHOOT_COUNT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
const ABORT_AFTER_CONSECUTIVE_OVERSHOOTS: u32 = 3;
pub fn check_rss_or_abort(label: &str) -> Result<(), String> {
use std::sync::atomic::Ordering;
let limit_mb = soft_rss_limit_mb();
if limit_mb == 0 {
return Ok(()); }
let Some(rss_mb) = current_rss_mb() else {
return Ok(()); };
if rss_mb <= limit_mb {
OVERSHOOT_COUNT.store(0, Ordering::Relaxed);
return Ok(());
}
tracing::warn!(
label,
rss_mb,
limit_mb,
"RSS exceeded soft cap — purging allocator and re-checking"
);
purge_allocator();
let after = current_rss_mb().unwrap_or(rss_mb);
if after <= limit_mb {
tracing::info!(
label,
before_mb = rss_mb,
after_mb = after,
limit_mb,
"RSS recovered after purge"
);
OVERSHOOT_COUNT.store(0, Ordering::Relaxed);
return Ok(());
}
let prev = OVERSHOOT_COUNT.fetch_add(1, Ordering::Relaxed);
let count = prev + 1;
if count >= ABORT_AFTER_CONSECUTIVE_OVERSHOOTS {
eprintln!(
"\n[semantex FATAL] RSS {after} MB exceeded SEMANTEX_MAX_RSS_MB={limit_mb} \
for {count} consecutive checks (last: {label}). \
Aborting process to protect host memory.\n\
To raise the cap, set `SEMANTEX_MAX_RSS_MB=<bigger>` (e.g. 8192). \
To opt out (NOT recommended), set `SEMANTEX_MAX_RSS_MB=0`.\n"
);
std::process::abort();
}
Err(format!(
"RSS {after} MB exceeds SEMANTEX_MAX_RSS_MB={limit_mb} (at {label}, \
consecutive overshoot {count}/{ABORT_AFTER_CONSECUTIVE_OVERSHOOTS}). \
Operation aborted. Raise the cap via `SEMANTEX_MAX_RSS_MB=<larger>` \
(e.g. 8192) or reindex a smaller subset. After \
{ABORT_AFTER_CONSECUTIVE_OVERSHOOTS} consecutive overshoots the \
process will hard-abort to protect host memory."
))
}
const WATCHDOG_HARD_MULTIPLIER: f64 = 1.5;
const WATCHDOG_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(500);
pub fn spawn_rss_watchdog() {
let soft_mb = soft_rss_limit_mb();
if soft_mb == 0 {
return;
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let hard_mb = (soft_mb as f64 * WATCHDOG_HARD_MULTIPLIER) as u64;
let spawned = std::thread::Builder::new()
.name("semantex-rss-watchdog".to_string())
.spawn(move || {
loop {
std::thread::sleep(WATCHDOG_POLL_INTERVAL);
let Some(rss_mb) = current_rss_mb() else {
continue;
};
if rss_mb <= hard_mb {
continue;
}
purge_allocator();
let after = current_rss_mb().unwrap_or(rss_mb);
if after > hard_mb {
eprintln!(
"\n[semantex FATAL] watchdog: RSS {after} MB exceeded the hard cap \
{hard_mb} MB ({WATCHDOG_HARD_MULTIPLIER}x SEMANTEX_MAX_RSS_MB={soft_mb}). \
Aborting to protect host memory. This is the out-of-band watchdog — it \
does not depend on any code path calling check_rss_or_abort.\n"
);
std::process::abort();
}
}
});
if let Err(e) = spawned {
tracing::warn!("Failed to spawn RSS watchdog thread: {e}");
}
}
#[cfg(test)]
mod cap_tests {
use super::*;
#[test]
fn system_ram_is_reasonable() {
if let Some(mb) = system_ram_mb() {
assert!(mb >= 1024, "system RAM too small: {mb} MB");
assert!(
mb < 4 * 1024 * 1024,
"system RAM implausibly large: {mb} MB"
);
}
}
#[test]
fn env_cap_behaviour_serial() {
let orig = std::env::var("SEMANTEX_MAX_RSS_MB").ok();
let restore = |v: &Option<String>| match v {
Some(s) => unsafe { std::env::set_var("SEMANTEX_MAX_RSS_MB", s) },
None => unsafe { std::env::remove_var("SEMANTEX_MAX_RSS_MB") },
};
unsafe { std::env::remove_var("SEMANTEX_MAX_RSS_MB") };
let limit = soft_rss_limit_mb();
assert!(limit >= ABSOLUTE_FLOOR_MB, "limit {limit} below floor");
assert!(limit <= ABSOLUTE_CEILING_MB, "limit {limit} above ceiling");
let soft_bytes = soft_rss_limit_mb() * 1024 * 1024;
let kernel_bytes = kernel_rss_cap_bytes();
assert!(
kernel_bytes >= soft_bytes,
"kernel cap {kernel_bytes} bytes < soft cap {soft_bytes} bytes — soft cap would never fire"
);
if let Some(ram_mb) = system_ram_mb() {
let old_formula_bytes =
(ram_mb * 3 / 4).clamp(ABSOLUTE_FLOOR_MB, 2 * 12 * 1024) * 1024 * 1024;
assert!(
kernel_bytes > old_formula_bytes,
"new default kernel cap {kernel_bytes} bytes must exceed the old, too-tight \
formula's {old_formula_bytes} bytes on a {ram_mb} MB host"
);
}
unsafe { std::env::set_var("SEMANTEX_MAX_RSS_MB", "2000") };
assert_eq!(soft_rss_limit_mb(), 2000);
assert_eq!(kernel_rss_cap_bytes(), 8000 * 1024 * 1024);
unsafe { std::env::set_var("SEMANTEX_MAX_RSS_MB", "10000") };
let kernel_bytes_10g = kernel_rss_cap_bytes();
assert!(
kernel_bytes_10g >= 30_000 * 1024 * 1024,
"kernel AS cap {kernel_bytes_10g} bytes gives too little headroom over a 10 GB \
RSS budget — mimalloc/ORT/mmap virtual-address overhead needs more than the old \
1.5x multiplier provided"
);
unsafe { std::env::set_var("SEMANTEX_MAX_RSS_MB", "1000000") };
assert_eq!(
kernel_rss_cap_bytes(),
KERNEL_AS_CEILING_MB * 1024 * 1024,
"an extreme override must clamp to KERNEL_AS_CEILING_MB, not multiply unbounded"
);
unsafe { std::env::set_var("SEMANTEX_MAX_RSS_MB", "0") };
assert_eq!(soft_rss_limit_mb(), 0);
unsafe { std::env::set_var("SEMANTEX_MAX_RSS_MB", "100000") };
assert!(check_rss_or_abort("test").is_ok());
let current = current_rss_mb().expect("RSS available on test platform");
assert!(current > 1, "RSS must be at least 1 MB to test");
unsafe { std::env::set_var("SEMANTEX_MAX_RSS_MB", "1") };
OVERSHOOT_COUNT.store(0, std::sync::atomic::Ordering::Relaxed);
let result = check_rss_or_abort("forced overshoot");
assert!(
result.is_err(),
"expected Err when current RSS ({current} MB) > cap (1 MB), got {result:?}"
);
let msg = result.unwrap_err();
assert!(
msg.contains("exceeds SEMANTEX_MAX_RSS_MB"),
"error message should reference the env var, got: {msg}"
);
OVERSHOOT_COUNT.store(0, std::sync::atomic::Ordering::Relaxed);
restore(&orig);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rss_returns_some_on_supported_platforms() {
if cfg!(any(
target_os = "macos",
target_os = "linux",
target_os = "windows"
)) {
let bytes = current_rss_bytes().expect("RSS should be available on this platform");
assert!(bytes > 1_000_000, "RSS too small: {bytes} bytes");
assert!(
bytes < 64 * 1024 * 1024 * 1024,
"RSS too large: {bytes} bytes"
);
}
}
#[test]
fn rss_mb_returns_reasonable_value() {
if let Some(mb) = current_rss_mb() {
assert!(mb >= 1, "RSS should be at least 1 MB, got {mb}");
assert!(mb < 64 * 1024, "RSS should be less than 64 GB, got {mb} MB");
}
}
#[test]
fn rss_decreases_after_large_allocation_is_dropped() {
let before = current_rss_mb();
let big: Vec<u8> = vec![42u8; 50 * 1024 * 1024];
let during = current_rss_mb();
assert_eq!(big[big.len() - 1], 42);
drop(big);
purge_allocator();
let after = current_rss_mb();
if let (Some(before), Some(during), Some(after)) = (before, during, after) {
assert!(
during >= before,
"RSS should not shrink during allocation: before={before}, during={during}"
);
eprintln!("RSS: before={before}MB, during={during}MB, after={after}MB");
}
}
}