Every resident artifact has a sensible individual cap. Nothing caps their sum.
search_index SPIMI_SOFT_LIMIT_BYTES 128MB / HARD 256MB, MAX_ENTRIES 10M per root
semantic_index max_files per root
callgraph store SQLite + buffers per root
symbol cache disk-backed per root
borrowed cache BORROWED_INDEX_CACHE_CAPACITY = 4 process-wide ✓
Only the borrowed-index cache is bounded process-wide. The rest are per-root, and the process holds as many sets as it has bound roots. IDLE_ROOT_TTL (30 min) is the only thing that ever reclaims them, and it's a liveness rule, not a budget — a daemon with N actively-used roots keeps N full artifact sets resident by design, and no configuration expresses "this process should not exceed X".
Live instance right now, ~2 h uptime:
VmRSS 2.95 GB
VmData 10.97 GB
Threads 123
31 distinct sessions in the last 110 perf ticks
This is the same shape as #202, one layer up. There, InspectManager lived in the per-root AppContext, so N roots meant N rayon pools (88 inspect threads of 211). The fix made the pool process-wide. The artifacts have the identical structure — bounded per item, multiplied per root, unbounded in aggregate — and no equivalent fix.
Two reasons I think it's worth a bound rather than just an eviction TTL:
It's the allocation pattern that strands free space. Many large, long-lived, differently-sized buffers arriving and departing on independent schedules (bind, watcher churn, idle eviction, reconfigure) is the textbook way to interleave free chunks beneath live ones. That's the mechanism behind the fragmentation in #205 — where measurement showed 15 of 16 arenas at ≥0.86 free with one arena doing the work — and malloc_trim structurally can't reclaim it.
A multi-root daemon has no backpressure. Idle eviction is time-based, so 31 sessions across many roots in one process means the peak is set by how many roots happen to be warm at once, not by anything the operator can configure. On this host that contributed to an OOM kill of the daemon (Consumed … 29.5G memory peak, 38.6G memory swap peak at 41 h uptime).
What I'm not proposing: a specific mechanism. LRU eviction on a byte budget, a total-artifact-bytes config with eviction of the coldest root, or per-root caps scaled by root count are all plausible and have different failure modes — evicting a root someone is about to use costs a rebuild, which may be worse than the memory it saves. You'd know which fits the eviction machinery that already exists.
The concrete ask is whether an aggregate bound belongs here at all, or whether the intended answer is "run fewer roots per daemon". If it's the latter that's worth stating somewhere, because the daemon transport makes many-roots-per-process the default rather than the exception.
Related: #205 (fragmentation measurement), #202 (same shape, per-root thread pools).
Every resident artifact has a sensible individual cap. Nothing caps their sum.
Only the borrowed-index cache is bounded process-wide. The rest are per-root, and the process holds as many sets as it has bound roots.
IDLE_ROOT_TTL(30 min) is the only thing that ever reclaims them, and it's a liveness rule, not a budget — a daemon with N actively-used roots keeps N full artifact sets resident by design, and no configuration expresses "this process should not exceed X".Live instance right now, ~2 h uptime:
This is the same shape as #202, one layer up. There,
InspectManagerlived in the per-rootAppContext, so N roots meant N rayon pools (88 inspect threads of 211). The fix made the pool process-wide. The artifacts have the identical structure — bounded per item, multiplied per root, unbounded in aggregate — and no equivalent fix.Two reasons I think it's worth a bound rather than just an eviction TTL:
It's the allocation pattern that strands free space. Many large, long-lived, differently-sized buffers arriving and departing on independent schedules (bind, watcher churn, idle eviction, reconfigure) is the textbook way to interleave free chunks beneath live ones. That's the mechanism behind the fragmentation in #205 — where measurement showed 15 of 16 arenas at ≥0.86 free with one arena doing the work — and
malloc_trimstructurally can't reclaim it.A multi-root daemon has no backpressure. Idle eviction is time-based, so 31 sessions across many roots in one process means the peak is set by how many roots happen to be warm at once, not by anything the operator can configure. On this host that contributed to an OOM kill of the daemon (
Consumed … 29.5G memory peak, 38.6G memory swap peakat 41 h uptime).What I'm not proposing: a specific mechanism. LRU eviction on a byte budget, a total-artifact-bytes config with eviction of the coldest root, or per-root caps scaled by root count are all plausible and have different failure modes — evicting a root someone is about to use costs a rebuild, which may be worse than the memory it saves. You'd know which fits the eviction machinery that already exists.
The concrete ask is whether an aggregate bound belongs here at all, or whether the intended answer is "run fewer roots per daemon". If it's the latter that's worth stating somewhere, because the daemon transport makes many-roots-per-process the default rather than the exception.
Related: #205 (fragmentation measurement), #202 (same shape, per-root thread pools).