diff --git a/lang/runtime/engram_store.c b/lang/runtime/engram_store.c index 106cc6d..e525a4f 100644 --- a/lang/runtime/engram_store.c +++ b/lang/runtime/engram_store.c @@ -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. */ static uint64_t pc_available_ram(void){ #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(); vm_size_t page = 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; uint64_t avail = (uint64_t)vm.free_count + (uint64_t)vm.inactive_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; #else FILE* f = fopen("/proc/meminfo", "r");