@@ -239,8 +239,15 @@ struct PgCache {
unsigned prefetch ; /* read-ahead window (pages); 0 = off */
LayerPin * lp ; size_t lp_n , lp_cap ; /* hot-layer pin bookkeeping */
size_t dirty_count ; /* # dirty frames, maintained incrementally (M5) */
/* stats ( introspection only — never affect semantics) */
/* Interoception. These were " introspection only — never affect semantics",
* and that was the bug: the pool could not feel itself thrash, so it could
* not correct, and neither could anyone watching from outside. The sensed
* state IS the corrective mechanism (see pc_adapt_budget) — the same way the
* engram's own boundary-beat/chronoception let it feel its own activity. */
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 */
} ;
/* ── little-endian scalar codecs ──────────────────────────────────────────── */
@@ -1781,35 +1788,112 @@ static void pc_remove(PgCache* c, PgEnt* e){
/* Reclaim clean unpinned frames from the LRU end until under budget, or until no
* evictable frame remains (a dirty/pinned-heavy pool may transiently exceed cap —
* that is the no-steal guarantee, not a bug: the next checkpoint frees them). */
/* Thrash detector.
/* ── Adaptive budget: close the loop ─────────────────────────────────────────
*
* Thrashing is not slowness; it is zero progress, and from th e outside it is
* indistinguishable from "loading a big store" — 100% CPU, flat RSS, no output.
* That ambiguity cost hours on 2026-08-15: the process was assumed to be busy
* when it was in fact evicting each page moments before needing it again.
* THE LESSON THIS ENCODES (2026-08-15). The engram spent hours down whil e f our
* separate theories were tried — bad binary, corrupt snapshot, WAL replay,
* feature flags — because nothing in the system said what was happening. It
* looked identical to "busy loading": 100% CPU, flat RSS, no output. Meanwhile
* hits/misses/evictions were ALREADY being counted, right here, and surfaced
* nowhere. One eviction-rate number would have ended it in seconds.
*
* The signature is unambiguous and cheap to watch: evictions climbing at a rate
* comparable to accesses, i.e. nearly every fetch pushing out a live frame.
* A cache doing useful work evicts far less often than it hits. Say so, once,
* loudly, with the numbers and the remedy — silence here is what made this
* expensive to find. */
static void pc_thrash_check ( PgCache * c ) {
static int warned = 0 ;
if ( warned ) return ;
* So the counters are not decoration. They are the control signal.
*
* A budget chosen once — a literal like 65536, or 60% of RAM read at startup —
* is a guess about the future. It cannot know the store grew, the working set
* shifted, or another process took the memory. The cache already MEASURES the
* only thing that matters (am I evicting pages I am about to want again), so it
* should act on that measurement instead of on a number someone typed.
*
* The controller: over a sliding window, if evictions are running at a rate
* comparable to accesses AND there is genuine reuse (hits are material), the
* working set exceeds the budget — grow it. Growth is geometric, bounded by a
* live re-read of physical memory rather than a value cached at boot, so it
* tracks the machine instead of a snapshot of it. It never shrinks on its own:
* cap is a ceiling, not an allocation, and frames are only ever held because a
* real access put them there.
*
* Two things this deliberately does NOT do: it does not attempt a cleverer
* eviction policy (when the working set does not fit, no policy helps — that is
* Denning, and it is why "tune the LRU" was never the fix), and it does not stay
* silent (pool_report exposes the same numbers outward, so a human or a metric
* pipeline sees the pressure the controller is reacting to). */
/* El's native telemetry, already in the runtime and already exporting to OTLP.
* Declared weak so engram_store.c still links standalone; when the runtime is
* present (every real build) the pool's interoception flows into the SAME
* pipeline as every other metric.
*
* ONE emission carrying the whole sensed state — not a function per stat, and
* not a bespoke per-subsystem endpoint. Both of those are the degenerate case:
* they make observability something you hand-write per noun instead of a
* uniform mechanism every component already has. el_val_t is int64_t; strings
* ride as pointers cast through it (see el_runtime.h's value model). */
__attribute__ ( ( weak ) ) int64_t emit_log ( int64_t level , int64_t msg , int64_t fields_json ) ;
static void pc_report ( const PgCache * c , const char * cause ) {
if ( ! emit_log ) return ; /* runtime not linked: no-op */
uint64_t acc = c - > hits + c - > misses ;
if ( acc < 200000 ) return ; /* need a real sample */
if ( c - > evictions * 2 < acc ) return ; /* evicting < half of accesses: healthy */
if ( c - > hits > c - > evictions ) return ; /* still getting real reuse */
warned = 1 ;
fprintf ( stderr ,
" [engram] THRASHING: %llu evictions across %llu accesses (hits %llu, misses %llu) "
" with a %zu-frame budget (%.1f GiB). The working set exceeds the cache, so pages are "
" evicted just before they are reused and the store makes no forward progress. "
" Raise the budget (unset ENGRAM_POOL_FRAMES to derive it from host RAM, or raise "
" ENGRAM_POOL_MEM_PCT); a different eviction policy cannot fix this. \n " ,
( unsigned long long ) c - > evictions , ( unsigned long long ) acc ,
char f [ 512 ] ;
snprintf ( f , sizeof f ,
" { \" component \" : \" engram.pool \" , \" cause \" : \" %s \" , \" hits \" :%llu, \" misses \" :%llu, "
" \" evictions \" :%llu, \" prefetch_reads \" :%llu, \" cap_frames \" :%zu, \" resident \" :%zu, "
" \" dirty \" :%zu, \" grows \" :%llu, \" hit_rate \" :%.4f, \" evict_ratio \" :%.4f, "
" \" cap_gib \" :%.3f, \" resident_gib \" :%.3f} " ,
cause ,
( unsigned long long ) c - > hits , ( unsigned long long ) c - > misses ,
c - > cap , ( double ) c - > cap * ( double ) STORE_PAGE_SIZE / ( 1024.0 * 1024.0 * 1024.0 ) ) ;
( unsigned long long ) c - > evictions , ( unsigned long long ) c - > prefetch_reads ,
c - > cap , c - > count , c - > dirty_count , ( unsigned long long ) c - > adapt_grows ,
acc ? ( double ) c - > hits / ( double ) acc : 0.0 ,
acc ? ( double ) c - > evictions / ( double ) acc : 0.0 ,
( double ) c - > cap * ( double ) STORE_PAGE_SIZE / ( 1024.0 * 1024.0 * 1024.0 ) ,
( double ) c - > count * ( double ) STORE_PAGE_SIZE / ( 1024.0 * 1024.0 * 1024.0 ) ) ;
emit_log ( ( int64_t ) ( uintptr_t ) " warn " , ( int64_t ) ( uintptr_t ) " engram.pool pressure " ,
( int64_t ) ( uintptr_t ) f ) ;
}
static uint64_t pc_ram_bytes_live ( void ) { return pc_physical_ram ( ) ; }
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 */
/* Sliding window so the signal reflects NOW, not lifetime totals. */
uint64_t acc = c - > hits + c - > misses ;
if ( acc - c - > adapt_last_acc < 100000 ) return ;
uint64_t d_acc = acc - c - > adapt_last_acc ;
uint64_t d_evic = c - > evictions - c - > adapt_last_evic ;
uint64_t d_hits = c - > hits - c - > adapt_last_hits ;
c - > adapt_last_acc = acc ; c - > adapt_last_evic = c - > evictions ; c - > adapt_last_hits = c - > hits ;
/* Pressure = evicting on a large fraction of accesses while still getting
* real reuse. Evictions alone are normal (a scan evicts and never returns);
* evictions WITH reuse means the working set genuinely does not fit. */
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 */
if ( ! ram ) return ;
unsigned pct = 80 ; /* hard ceiling for autonomous growth */
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 ) ;
if ( c - > cap > = ceiling ) return ; /* already at the machine's limit */
size_t want = c - > cap + ( c - > cap / 2 ) + 1 ; /* × 1.5, geometric */
if ( want > ceiling ) want = ceiling ;
size_t was = c - > cap ;
c - > cap = want ;
c - > adapt_grows + + ;
/* Emit the sensed state, not just the reaction. These are the numbers that
* would have diagnosed 2026-08-15 in seconds instead of hours. */
pc_report ( c , " budget-grow " ) ;
fprintf ( stderr ,
" [engram] pool pressure: %llu evictions / %llu accesses (%llu hits) at %zu frames "
" (%.2f GiB) — working set exceeds budget; growing to %zu frames (%.2f GiB). \n " ,
( unsigned long long ) d_evic , ( unsigned long long ) d_acc , ( unsigned long long ) d_hits ,
was , ( double ) was * ( double ) STORE_PAGE_SIZE / ( 1024.0 * 1024.0 * 1024.0 ) ,
c - > cap , ( double ) c - > cap * ( double ) STORE_PAGE_SIZE / ( 1024.0 * 1024.0 * 1024.0 ) ) ;
fflush ( stderr ) ;
}
@@ -1824,7 +1908,7 @@ static void pc_evict_to_budget(PgCache* c){
}
if ( ! freed ) break ; /* nothing evictable — allowed to exceed cap */
}
pc_thrash_check ( c ) ;
pc_adapt_budget ( c ) ;
}
static PgEnt * pc_get ( EngramPagedStore * s , uint64_t id ) {