Follow-on from #201, where the shipped malloc_trim(0) relief turned out not to reclaim anything on a long-lived Linux daemon. The free space is pinned beneath live allocations, so trim structurally can't return it.
The open question is which of two things that is:
| mechanism |
remedy |
whose code |
| ordinary heap fragmentation |
allocator with decay-based purging |
aft's #[global_allocator] |
| one pathological allocation pattern |
fix the site |
aft, specific |
mallinfo2 gives aggregates only. The per-arena and per-bin free-space distribution separates the two, and that's malloc_info(3) — which the crate doesn't bind today.
I've designed an experiment and I'd rather ask before building it, because you may already know the answer or want it shaped differently. Specifically:
-
Do you have prior context on the allocation pattern? Long-lived per-root artifacts (search index, semantic index, callgraph store, symbol cache) are each individually capped but have no aggregate ceiling across roots — the daemon here held 7+. Many large, long-lived, differently-sized buffers arriving and departing on independent schedules is a textbook way to strand free space. If that's a known shape to you, the experiment may be unnecessary.
-
Has a non-default allocator been considered and rejected? jemalloc/mimalloc with decay purging is the obvious remedy for fragmentation-pinned space, and it's a #[global_allocator] change with real consequences — binary size, the panic = "unwind" interaction with ONNX Runtime, platform matrix. If there's a standing reason not to, I'd like to know before I spend a run producing evidence for it.
-
Would you shape the measurement differently? My rig is subc_storm_test.rs — it already drives binds, tool traffic, watcher churn, and semantic collection across N roots via AFT_STORM_SCALE, and it spawns its own ephemeral daemon so nothing production-facing is touched. One known weakness: it runs the module in-process, so malloc_info measures the whole test binary. I'm handling that with start/end deltas plus a zero-workload control arm to measure the scaffolding's own contribution, with a dedicated bin target as fallback if that control shows the scaffolding isn't constant.
Design in brief — four arms (scaffolding-only, stock, MALLOC_ARENA_MAX=2 as a negative control on the rig, decay-purging allocator), malloc_info + mallinfo2 + smaps census at start and end, predictions registered before running so the outcome can't be interpreted after the fact. Full design doc available if useful; happy to paste it here or open it as a PR against a docs path if you'd prefer it in-repo.
What I'm not doing: proposing an allocator change. That would need this evidence first, its own gate cycle, and your agreement that it's a direction worth taking at all.
Context on severity, so this is priced correctly: after #202 landed (inspect threads 88 → 8 on the same daemon) the live instance sits at ~2.9 GB swap after 12 h, against 15.3 GB at 18 h before. It's churn-bound rather than time-linear — regions and VmData go flat across idle hours. So this isn't urgent, and I'd rather spend the effort in the direction you'd actually use.
Follow-on from #201, where the shipped
malloc_trim(0)relief turned out not to reclaim anything on a long-lived Linux daemon. The free space is pinned beneath live allocations, so trim structurally can't return it.The open question is which of two things that is:
#[global_allocator]mallinfo2gives aggregates only. The per-arena and per-bin free-space distribution separates the two, and that'smalloc_info(3)— which the crate doesn't bind today.I've designed an experiment and I'd rather ask before building it, because you may already know the answer or want it shaped differently. Specifically:
Do you have prior context on the allocation pattern? Long-lived per-root artifacts (search index, semantic index, callgraph store, symbol cache) are each individually capped but have no aggregate ceiling across roots — the daemon here held 7+. Many large, long-lived, differently-sized buffers arriving and departing on independent schedules is a textbook way to strand free space. If that's a known shape to you, the experiment may be unnecessary.
Has a non-default allocator been considered and rejected? jemalloc/mimalloc with decay purging is the obvious remedy for fragmentation-pinned space, and it's a
#[global_allocator]change with real consequences — binary size, thepanic = "unwind"interaction with ONNX Runtime, platform matrix. If there's a standing reason not to, I'd like to know before I spend a run producing evidence for it.Would you shape the measurement differently? My rig is
subc_storm_test.rs— it already drives binds, tool traffic, watcher churn, and semantic collection across N roots viaAFT_STORM_SCALE, and it spawns its own ephemeral daemon so nothing production-facing is touched. One known weakness: it runs the module in-process, somalloc_infomeasures the whole test binary. I'm handling that with start/end deltas plus a zero-workload control arm to measure the scaffolding's own contribution, with a dedicated bin target as fallback if that control shows the scaffolding isn't constant.Design in brief — four arms (scaffolding-only, stock,
MALLOC_ARENA_MAX=2as a negative control on the rig, decay-purging allocator),malloc_info+mallinfo2+ smaps census at start and end, predictions registered before running so the outcome can't be interpreted after the fact. Full design doc available if useful; happy to paste it here or open it as a PR against a docs path if you'd prefer it in-repo.What I'm not doing: proposing an allocator change. That would need this evidence first, its own gate cycle, and your agreement that it's a direction worth taking at all.
Context on severity, so this is priced correctly: after #202 landed (inspect threads 88 → 8 on the same daemon) the live instance sits at ~2.9 GB swap after 12 h, against 15.3 GB at 18 h before. It's churn-bound rather than time-linear — regions and
VmDatago flat across idle hours. So this isn't urgent, and I'd rather spend the effort in the direction you'd actually use.