store: judge memory pressure by swap RATE, not level #130
@@ -1870,6 +1870,53 @@ static uint64_t pc_ram_bytes_live(void){ return pc_physical_ram(); }
|
|||||||
* Returns 0 when undeterminable — callers then refuse to grow, the safe way. */
|
* Returns 0 when undeterminable — callers then refuse to grow, the safe way. */
|
||||||
static uint64_t pc_available_ram(void){
|
static uint64_t pc_available_ram(void){
|
||||||
#if defined(__APPLE__) || defined(__MACH__)
|
#if defined(__APPLE__) || defined(__MACH__)
|
||||||
|
/* SWAP AND COMPRESSOR FIRST. free+inactive+purgeable is a LIE under memory
|
||||||
|
* pressure: a machine deep in swap still reports gigabytes "available",
|
||||||
|
* because inactive pages are only reclaimable by evicting them to swap.
|
||||||
|
* Observed 2026-08-15: this returned 9.43 GiB available while vm.swapusage
|
||||||
|
* showed 51.58 of 53.25 GiB used (97% full) and the compressor occupied
|
||||||
|
* 23.7 GiB — the host was thrashing to disk and the pool would have been
|
||||||
|
* cleared to grow into it. Growing a cache in that state is how a guard
|
||||||
|
* becomes the crash.
|
||||||
|
*
|
||||||
|
* So: if swap is nearly spent, report ZERO available. Callers refuse to
|
||||||
|
* grow on 0 and pc_relieve_pressure hands frames back. Only when the
|
||||||
|
* machine is genuinely not swapping do free+inactive+purgeable mean
|
||||||
|
* anything, and even then the compressor's footprint is subtracted because
|
||||||
|
* that RAM is already spoken for. */
|
||||||
|
/* RATE, NOT LEVEL. Swap *level* is a terrible signal: macOS grows swap files
|
||||||
|
* on demand and reclaims them lazily, so "47 of 48 GiB used" can mean the
|
||||||
|
* machine is dying OR that it recovered ten minutes ago and the file has not
|
||||||
|
* been trimmed yet. Measured both states on one host within minutes:
|
||||||
|
* 47.65/48.00 GiB used, 2047 swapouts/s -> genuinely thrashing
|
||||||
|
* 26.67/28.00 GiB used, 0 swapouts/s -> perfectly healthy, 15.6 GiB free
|
||||||
|
* A level check calls the second one an emergency and starves the pool for
|
||||||
|
* no reason. What distinguishes them is whether pages are moving NOW.
|
||||||
|
*
|
||||||
|
* So sample the swapout counter across calls and judge the delta. First call
|
||||||
|
* establishes the baseline and reports no pressure — one sample cannot have
|
||||||
|
* a rate, and guessing from a single reading is the whole mistake. */
|
||||||
|
{
|
||||||
|
static uint64_t prev_swapouts = 0;
|
||||||
|
static time_t prev_t = 0;
|
||||||
|
static int primed = 0;
|
||||||
|
mach_port_t h0 = mach_host_self();
|
||||||
|
vm_statistics64_data_t v0; mach_msg_type_number_t c0 = HOST_VM_INFO64_COUNT;
|
||||||
|
if (host_statistics64(h0, HOST_VM_INFO64, (host_info64_t)&v0, &c0) == KERN_SUCCESS){
|
||||||
|
uint64_t now_out = (uint64_t)v0.swapouts;
|
||||||
|
time_t now_t = time(NULL);
|
||||||
|
if (!primed){ prev_swapouts = now_out; prev_t = now_t; primed = 1; }
|
||||||
|
else if (now_t > prev_t){
|
||||||
|
double per_s = (double)(now_out - prev_swapouts) / (double)(now_t - prev_t);
|
||||||
|
prev_swapouts = now_out; prev_t = now_t;
|
||||||
|
/* Sustained outward paging with nothing coming back is the
|
||||||
|
* signature of a host being pushed into swap. ~200 pages/s is
|
||||||
|
* ~3 MiB/s — well above idle noise, well below the 2000+/s seen
|
||||||
|
* while actually thrashing. */
|
||||||
|
if (per_s > 200.0) return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
mach_port_t host = mach_host_self();
|
mach_port_t host = mach_host_self();
|
||||||
vm_size_t page = 0;
|
vm_size_t page = 0;
|
||||||
if (host_page_size(host, &page) != KERN_SUCCESS) return 0;
|
if (host_page_size(host, &page) != KERN_SUCCESS) return 0;
|
||||||
@@ -1877,6 +1924,10 @@ static uint64_t pc_available_ram(void){
|
|||||||
if (host_statistics64(host, HOST_VM_INFO64, (host_info64_t)&vm, &cnt) != KERN_SUCCESS) return 0;
|
if (host_statistics64(host, HOST_VM_INFO64, (host_info64_t)&vm, &cnt) != KERN_SUCCESS) return 0;
|
||||||
uint64_t avail = (uint64_t)vm.free_count + (uint64_t)vm.inactive_count
|
uint64_t avail = (uint64_t)vm.free_count + (uint64_t)vm.inactive_count
|
||||||
+ (uint64_t)vm.purgeable_count;
|
+ (uint64_t)vm.purgeable_count;
|
||||||
|
/* the compressor is holding real RAM that nobody can hand us */
|
||||||
|
uint64_t compressed = (uint64_t)vm.compressor_page_count;
|
||||||
|
if (compressed >= avail) return 0;
|
||||||
|
avail -= compressed;
|
||||||
return avail * (uint64_t)page;
|
return avail * (uint64_t)page;
|
||||||
#else
|
#else
|
||||||
FILE* f = fopen("/proc/meminfo", "r");
|
FILE* f = fopen("/proc/meminfo", "r");
|
||||||
|
|||||||
Reference in New Issue
Block a user