Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions turbopack/crates/turbo-tasks-malloc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ mimalloc = { version = "0.1.48", features = [
"local_dynamic_tls",
], optional = true }

[target.'cfg(target_os = "macos")'.dependencies]
libc = "0.2"

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.60", features = ["Win32_System_SystemInformation"] }

[features]
custom_allocator = ["dep:mimalloc", "dep:libmimalloc-sys"]
default = ["custom_allocator"]
51 changes: 51 additions & 0 deletions turbopack/crates/turbo-tasks-malloc/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod counter;
mod memory_pressure;

use std::{
alloc::{GlobalAlloc, Layout},
Expand Down Expand Up @@ -107,6 +108,23 @@ impl TurboMalloc {
pub fn reset_allocation_counters(start: AllocationCounters) {
self::counter::reset_allocation_counters(start);
}

/// Returns a memory pressure value in the range `0..=100`, or `None` when
/// the current platform does not expose a memory pressure signal or a
/// query for it failed.
///
/// `0` means no memory pressure, `100` means maximum pressure.
///
/// - On Linux this is derived from `/proc/pressure/memory` (the `some` `avg10` stall
/// percentage), falling back to `(MemTotal - MemAvailable) / MemTotal` from `/proc/meminfo`
/// when PSI is not available (older kernels, no `CONFIG_PSI`, or containers without access).
/// - On macOS this is derived from the `kern.memorystatus_level` sysctl (`100 -
/// free_memory_percentage`).
/// - On Windows this is `MEMORYSTATUSEX::dwMemoryLoad` (percentage of physical memory in use).
/// - On other platforms this returns `None`.
pub fn memory_pressure() -> Option<u8> {
memory_pressure::memory_pressure()
}
}

/// Get the allocator for this platform that we should wrap with TurboMalloc.
Expand Down Expand Up @@ -164,3 +182,36 @@ unsafe impl GlobalAlloc for TurboMalloc {
ret
}
}

#[cfg(test)]
mod tests {
use super::TurboMalloc;

#[test]
fn memory_pressure_is_in_range() {
let value = TurboMalloc::memory_pressure();

// On all supported platforms the value must be reported.
#[cfg(any(
all(target_os = "linux", not(target_family = "wasm")),
target_os = "macos",
windows,
))]
let value = value.expect("memory_pressure() should return Some on this platform");

// On unsupported platforms we expect None and have nothing further to assert.
#[cfg(not(any(
all(target_os = "linux", not(target_family = "wasm")),
target_os = "macos",
windows,
)))]
let Some(value) = value else {
return;
};

assert!(
value <= 100,
"memory_pressure() returned {value}, expected a value in 0..=100"
);
}
}
194 changes: 194 additions & 0 deletions turbopack/crates/turbo-tasks-malloc/src/memory_pressure.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
//! Per-OS memory pressure detection.
//!
//! All implementations return a value in the range `0..=100`, where `0` means
//! no memory pressure and `100` means maximum memory pressure. Platforms that
//! do not expose a memory pressure signal (or for which the query fails)
//! return `None`.

/// See [`super::TurboMalloc::memory_pressure`].
pub fn memory_pressure() -> Option<u8> {
platform::memory_pressure()
}

fn clamp_percent(value: f64) -> u8 {
if !value.is_finite() {
return 0;
}
value.round().clamp(0.0, 100.0) as u8
}

#[cfg(all(target_os = "linux", not(target_family = "wasm")))]
mod platform {
use super::clamp_percent;

/// Reads memory pressure on Linux. Prefers `/proc/pressure/memory` (the
/// `some avg10` stall percentage) when the kernel exposes PSI, and falls
/// back to `MemAvailable`/`MemTotal` from `/proc/meminfo` when it does
/// not (older kernels, containers without PSI access, or kernels built
/// without `CONFIG_PSI`).
pub fn memory_pressure() -> Option<u8> {
if let Ok(content) = std::fs::read_to_string("/proc/pressure/memory")
&& let Some(value) = parse_psi(&content)
{
return Some(value);
}
let content = std::fs::read_to_string("/proc/meminfo").ok()?;
parse_meminfo(&content)
}

fn parse_psi(content: &str) -> Option<u8> {
// Expected format:
// some avg10=0.00 avg60=0.00 avg300=0.00 total=...
// full avg10=0.00 avg60=0.00 avg300=0.00 total=...
for line in content.lines() {
let Some(rest) = line.strip_prefix("some ") else {
continue;
};
for field in rest.split_ascii_whitespace() {
if let Some(val) = field.strip_prefix("avg10=") {
let parsed: f64 = val.parse().ok()?;
return Some(clamp_percent(parsed));
}
}
}
None
}

fn parse_meminfo(content: &str) -> Option<u8> {
// Returns `(MemTotal - MemAvailable) / MemTotal * 100`, i.e. the
// percentage of physical memory currently unavailable — analogous to
// Windows' `dwMemoryLoad`.
let mut total: Option<u64> = None;
let mut available: Option<u64> = None;
for line in content.lines() {
if let Some(rest) = line.strip_prefix("MemTotal:") {
total = parse_kb(rest);
} else if let Some(rest) = line.strip_prefix("MemAvailable:") {
available = parse_kb(rest);
}
if total.is_some() && available.is_some() {
break;
}
}
let total = total?;
let available = available?;
if total == 0 {
return None;
}
let used = total.saturating_sub(available);
let pct = (used as f64) * 100.0 / (total as f64);
Some(clamp_percent(pct))
}

fn parse_kb(rest: &str) -> Option<u64> {
// Expected format: " 12345 kB"
let mut iter = rest.split_ascii_whitespace();
iter.next()?.parse().ok()
}

#[cfg(test)]
mod tests {
use super::{parse_meminfo, parse_psi};

#[test]
fn parses_typical_psi_content() {
let content = "some avg10=12.34 avg60=5.67 avg300=1.00 total=123456\nfull avg10=0.00 \
avg60=0.00 avg300=0.00 total=0\n";
assert_eq!(parse_psi(content), Some(12));
}

#[test]
fn returns_none_on_malformed_psi() {
assert_eq!(parse_psi(""), None);
assert_eq!(parse_psi("garbage"), None);
}

#[test]
fn clamps_psi_to_100() {
let content = "some avg10=150.00 avg60=0.00 avg300=0.00 total=0\n";
assert_eq!(parse_psi(content), Some(100));
}

#[test]
fn parses_meminfo() {
let content =
"MemTotal: 1000 kB\nMemFree: 500 kB\nMemAvailable: 750 kB\n";
// used = 250, 25%
assert_eq!(parse_meminfo(content), Some(25));
}

#[test]
fn returns_none_on_missing_meminfo_fields() {
assert_eq!(parse_meminfo("MemTotal: 1000 kB\n"), None);
assert_eq!(parse_meminfo(""), None);
}
}
}

#[cfg(target_os = "macos")]
mod platform {
use std::{ffi::c_void, mem::size_of};

use super::clamp_percent;

/// Reads the `kern.memorystatus_level` sysctl, which exposes the percentage
/// of free memory available (0..=100). The returned memory pressure is
/// `100 - free_percentage`.
pub fn memory_pressure() -> Option<u8> {
// `kern.memorystatus_level` returns an `int` (percentage of free
// memory, 0..=100).
let mut level: libc::c_int = 0;
let mut size: libc::size_t = size_of::<libc::c_int>() as libc::size_t;
let name = c"kern.memorystatus_level";

// Safety: `sysctlbyname` writes up to `size` bytes into `&mut level`;
// the buffer is large enough for a `c_int`. We pass a valid,
// NUL-terminated C string as the first argument.
let ret = unsafe {
libc::sysctlbyname(
name.as_ptr(),
&mut level as *mut libc::c_int as *mut c_void,
&mut size,
std::ptr::null_mut(),
0,
)
};

if ret != 0 || size != size_of::<libc::c_int>() as libc::size_t {
return None;
}

let pressure = 100.0 - f64::from(level);
Some(clamp_percent(pressure))
}
}

#[cfg(windows)]
mod platform {
use windows_sys::Win32::System::SystemInformation::{GlobalMemoryStatusEx, MEMORYSTATUSEX};

/// Reads `MEMORYSTATUSEX::dwMemoryLoad`, which is the approximate
/// percentage of physical memory in use (0..=100).
pub fn memory_pressure() -> Option<u8> {
let mut status: MEMORYSTATUSEX = unsafe { std::mem::zeroed() };
status.dwLength = std::mem::size_of::<MEMORYSTATUSEX>() as u32;
// Safety: `status` is a properly sized and initialized MEMORYSTATUSEX.
let ok = unsafe { GlobalMemoryStatusEx(&mut status) };
if ok == 0 {
return None;
}
let load = status.dwMemoryLoad;
Some(load.min(100) as u8)
}
}

#[cfg(not(any(
all(target_os = "linux", not(target_family = "wasm")),
target_os = "macos",
windows,
)))]
mod platform {
pub fn memory_pressure() -> Option<u8> {
None
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -290,9 +290,13 @@ impl TurbopackFormat {
}
}
}
TraceRow::MemorySample { ts, memory } => {
TraceRow::MemorySample {
ts,
memory,
memory_pressure,
} => {
let ts = Timestamp::from_micros(ts);
store.add_memory_sample(ts, memory);
store.add_memory_sample(ts, memory, memory_pressure);
}
TraceRow::AllocationCounters {
ts: _,
Expand Down
5 changes: 5 additions & 0 deletions turbopack/crates/turbopack-trace-server/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub enum ServerToClientMessage {
args: Vec<(String, String)>,
path: Vec<String>,
memory_samples: Vec<u64>,
memory_pressure_samples: Vec<u8>,
},
}

Expand Down Expand Up @@ -294,6 +295,8 @@ fn handle_connection(
path.reverse();
let memory_samples =
store.memory_samples_for_range(span.start(), span.end());
let memory_pressure_samples = store
.memory_pressure_samples_for_range(span.start(), span.end());
ServerToClientMessage::QueryResult {
id,
is_graph,
Expand All @@ -308,6 +311,7 @@ fn handle_connection(
args,
path,
memory_samples,
memory_pressure_samples,
}
} else {
ServerToClientMessage::QueryResult {
Expand All @@ -324,6 +328,7 @@ fn handle_connection(
args: Vec::new(),
path: Vec::new(),
memory_samples: Vec::new(),
memory_pressure_samples: Vec::new(),
}
}
};
Expand Down
Loading
Loading