diff --git a/lang/runtime/engram_store.c b/lang/runtime/engram_store.c index aae7419..106cc6d 100644 --- a/lang/runtime/engram_store.c +++ b/lang/runtime/engram_store.c @@ -46,6 +46,8 @@ #include #if defined(__APPLE__) || defined(__MACH__) #include +#include +#include #endif #include #include @@ -247,7 +249,8 @@ struct PgCache { uint64_t hits, misses, evictions, prefetch_reads; /* sliding-window marks so pressure reflects NOW, not lifetime totals */ uint64_t adapt_last_acc, adapt_last_evic, adapt_last_hits; - uint64_t adapt_grows; /* how many times the budget corrected upward */ + uint64_t adapt_grows; /* budget corrections upward */ + uint64_t adapt_shrinks; /* budget corrections downward (memory pressure) */ }; /* ── little-endian scalar codecs ──────────────────────────────────────────── */ @@ -1685,6 +1688,8 @@ int store_scan_edges(EngramPagedStore* s, StoreEdgeScanCb cb, void* ctx){ #define ENGRAM_POOL_FRAMES_FALLBACK (1u<<20) /* ~1M frames × 16KiB = 16 GiB */ #endif +static uint64_t pc_available_ram(void); /* fwd — defined with the controller */ + /* Physical RAM in bytes, 0 when it cannot be determined. */ static uint64_t pc_physical_ram(void){ #if defined(__APPLE__) || defined(__MACH__) @@ -1707,6 +1712,9 @@ static size_t pc_default_cap(void){ uint64_t ram = pc_physical_ram(); if (!ram) return ENGRAM_POOL_FRAMES_FALLBACK; uint64_t budget_bytes = (ram / 100u) * pct; + /* Never start above what the machine can actually spare right now. */ + uint64_t avail = pc_available_ram(); + if (avail > (1ull<<30) && budget_bytes > avail - (1ull<<30)) budget_bytes = avail - (1ull<<30); uint64_t frames = budget_bytes / (uint64_t)STORE_PAGE_SIZE; if (frames < 4096) frames = 4096; /* never absurdly small */ return (size_t)frames; @@ -1854,6 +1862,67 @@ static void pc_report(const PgCache* c, const char* cause){ static uint64_t pc_ram_bytes_live(void){ return pc_physical_ram(); } +/* AVAILABLE memory right now — free + reclaimable, not total. + * + * Sizing a cache against TOTAL ram is what turns a cache into a memory leak: + * total does not shrink when other processes need memory, so a pool that only + * grows never notices it is starving the machine it runs on. Availability does. + * Returns 0 when undeterminable — callers then refuse to grow, the safe way. */ +static uint64_t pc_available_ram(void){ +#if defined(__APPLE__) || defined(__MACH__) + mach_port_t host = mach_host_self(); + vm_size_t page = 0; + if (host_page_size(host, &page) != KERN_SUCCESS) return 0; + vm_statistics64_data_t vm; mach_msg_type_number_t cnt = HOST_VM_INFO64_COUNT; + 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; + return avail * (uint64_t)page; +#else + FILE* f = fopen("/proc/meminfo", "r"); + if (!f) return 0; + char line[256]; unsigned long long kb = 0; + while (fgets(line, sizeof line, f)) + if (sscanf(line, "MemAvailable: %llu kB", &kb) == 1) break; + fclose(f); + return (uint64_t)kb * 1024ull; +#endif +} + +/* Shrink the budget when the machine is short on memory. + * + * A pool that can only grow is a leak with extra steps. This is the other half + * of the control loop: if free memory drops below a floor, hand frames back. + * The resident set follows on the next eviction pass, so the memory is actually + * returned rather than merely re-labelled. */ +#ifndef ENGRAM_POOL_FREE_FLOOR_BYTES +#define ENGRAM_POOL_FREE_FLOOR_BYTES (2ull*1024ull*1024ull*1024ull) /* 2 GiB */ +#endif +static int pc_relieve_pressure(PgCache* c){ + uint64_t avail = pc_available_ram(); + if (!avail) return 0; + uint64_t floor_b = ENGRAM_POOL_FREE_FLOOR_BYTES; + const char* fe = getenv("ENGRAM_POOL_FREE_FLOOR_MB"); + if (fe && *fe){ unsigned long v = strtoul(fe, NULL, 10); if (v) floor_b = (uint64_t)v * 1024ull * 1024ull; } + if (avail >= floor_b) return 0; /* machine has room */ + if (!c->cap || c->count == 0) return 0; + size_t was = c->cap; + size_t want = c->count - (c->count / 4); /* give back ~25% of what we hold */ + if (want < 4096) want = 4096; + if (want >= c->cap) return 0; + c->cap = want; + c->adapt_shrinks++; + fprintf(stderr, + "[engram] memory pressure: %.2f GiB available (floor %.2f GiB) — shrinking pool " + "budget %zu -> %zu frames (%.2f -> %.2f GiB) and releasing frames.\n", + (double)avail/(1024.0*1024.0*1024.0), (double)floor_b/(1024.0*1024.0*1024.0), + was, c->cap, + (double)was * (double)STORE_PAGE_SIZE/(1024.0*1024.0*1024.0), + (double)c->cap* (double)STORE_PAGE_SIZE/(1024.0*1024.0*1024.0)); + fflush(stderr); + return 1; +} + static void pc_adapt_budget(PgCache* c){ if (!c->cap) return; /* unlimited: nothing to adapt */ if (getenv("ENGRAM_POOL_FRAMES")) return; /* explicit operator override wins */ @@ -1872,12 +1941,26 @@ static void pc_adapt_budget(PgCache* c){ if (d_evic * 3 < d_acc) return; /* < 1/3 of accesses evict: healthy */ if (d_hits * 4 < d_acc) return; /* little reuse: a scan, not pressure */ - uint64_t ram = pc_ram_bytes_live(); /* live, not a boot-time constant */ + /* Growth is bounded by what is AVAILABLE, never by total RAM. Sizing against + * total is how a cache starves its own host: total never shrinks when other + * processes need memory. Refuse to grow at all if availability is unknown or + * already under the floor — a cache is never worth swapping the machine. */ + uint64_t avail = pc_available_ram(); + uint64_t floor_b = ENGRAM_POOL_FREE_FLOOR_BYTES; + const char* fe = getenv("ENGRAM_POOL_FREE_FLOOR_MB"); + if (fe && *fe){ unsigned long v = strtoul(fe, NULL, 10); if (v) floor_b = (uint64_t)v * 1024ull * 1024ull; } + if (!avail || avail <= floor_b) return; + uint64_t ram = pc_ram_bytes_live(); if (!ram) return; - unsigned pct = 80; /* hard ceiling for autonomous growth */ + unsigned pct = 50; /* ceiling as a share of TOTAL, belt-and-braces */ const char* mp = getenv("ENGRAM_POOL_MAX_PCT"); if (mp && *mp){ unsigned long v = strtoul(mp, NULL, 10); if (v > 0 && v <= 95) pct = (unsigned)v; } size_t ceiling = (size_t)(((ram / 100u) * pct) / (uint64_t)STORE_PAGE_SIZE); + /* and never grow into the free-memory floor */ + uint64_t headroom = avail - floor_b; + size_t ceil_avail = (size_t)((c->count * (uint64_t)STORE_PAGE_SIZE + headroom) + / (uint64_t)STORE_PAGE_SIZE); + if (ceil_avail < ceiling) ceiling = ceil_avail; if (c->cap >= ceiling) return; /* already at the machine's limit */ size_t want = c->cap + (c->cap / 2) + 1; /* ×1.5, geometric */ @@ -1908,7 +1991,7 @@ static void pc_evict_to_budget(PgCache* c){ } if (!freed) break; /* nothing evictable — allowed to exceed cap */ } - pc_adapt_budget(c); + if (!pc_relieve_pressure(c)) pc_adapt_budget(c); } static PgEnt* pc_get(EngramPagedStore* s, uint64_t id){