Continues #550 (closed by #551, which shipped the instrument) with the bridge-side composition posted on #551 and the CIRISStatus#69 A/B. Filed so the analysis lives on an issue rather than a merged PR.
What is known
Sister node (ciris-status v0.3.61, same substrate, same host): MALLOC_ARENA_MAX=2 cut committed 1458 → 611 MB (−58%), held flat 205 min, uordblks unchanged 44.3 → 44.5 MB. The entire 847 MB was allocator free-list, accumulated while processing zero traces — the churn is the loops, not data volume. keepcost 104 KB afterwards.
This node (ciris-server 0.5.198 on the canonical, 18.8 h, 2 vCPU, 2 GB limit), from smaps: committed anonymous ≈ 2.47 GB, 1.5 GB of it in host swap.
arena-class (64 MB mmap) : n=15 committed= 836 MB ← the glibc cap: 8 × nproc(2)
large non-arena : n=7 committed= 1371 MB ← of which [heap] (brk) 706 MB, 91% paged out
everything else : n=242 committed= 259 MB
So it is the same disease with a different shape: the arena cap plausibly addresses ~836 MB of 2466, and the 706 MB brk heap — reserved, touched, then cold for hours under memory pressure — is the part a cap cannot consolidate.
What 0.5.199's instrument settles first
GET /v1/node/diagnostics/memory (--diagnostics / CIRIS_DIAGNOSTICS=1, loopback-only) reports mallinfo2 from inside the process. The cold 641 MB of [heap] is one of two things and live_fraction says which:
fordblks — freed, not returned: churn/fragmentation, allocation-side fix (fewer, reused buffers in the loops) and/or a release policy;
uordblks — live but cold: something retains ~0.6 GB it stopped reading hours ago, a retention defect, and the more interesting one.
Recommended rollout to make that reading clean: at the 0.5.199 restart of the canonical's ciris-server container, set both CIRIS_DIAGNOSTICS=1 and MALLOC_ARENA_MAX=2. With one spare arena at most, mallinfo2.arena is essentially the brk heap plus 64 MB, so fordblks vs uordblks reads directly as the brk heap's composition — which the 15-arena default would blur. The cap's own effect is already known from status; what we do not know is the brk heap, and this measures it in one restart instead of two.
Candidate cures, in the order the reading will pick them
- Arena cap in-process, not by deploy config.
mallopt(M_ARENA_MAX, 2) at the top of run / py_main on target_env = "gnu", honouring an explicit MALLOC_ARENA_MAX if the environment set one. Every glibc node gets the −58% class of win without an operator knowing the knob exists; Android (scudo), iOS, macOS and Windows are untouched because the call does not exist there. Ready to write; waiting for the canonical reading only so the change lands with a number.
- A trim trial, not a trim policy. If
fordblks dominates: malloc_trim(0) since glibc 2.8 does NOT only cut the top chunk — it walks every arena and MADV_DONTNEEDs whole free pages inside free chunks, so keepcost (top only) understates what it would release from a fragmented brk heap. The honest test is a loopback-only POST /v1/node/diagnostics/memory/trim under the same switch, called by hand once, with mallinfo2 + RssAnon/VmSwap read before and after. Status's diag.rs deliberately does not trim; a trial door under the operator switch keeps that separation.
- The churn source. The loops that allocate on a cadence with no traffic: per-peer replication coordinators paging rows every anti-entropy round, config reconcile (30 s), mesh-config fold, the scorer (60 s, window 500), retention (3600 s), equivocation (900 s, up to 4096 rows), health probes. Status's finding that free lists grew with zero traces says the same is likely here; finding which loop needs an allocation profile on the canonical (or the same node under the mesh harness), after 1 and 2 have taken the easy 1.3 GB off the table.
Not for 0.5.199; the instrument is in it and the first reading decides the order.
Continues #550 (closed by #551, which shipped the instrument) with the bridge-side composition posted on #551 and the CIRISStatus#69 A/B. Filed so the analysis lives on an issue rather than a merged PR.
What is known
Sister node (ciris-status v0.3.61, same substrate, same host):
MALLOC_ARENA_MAX=2cut committed 1458 → 611 MB (−58%), held flat 205 min,uordblksunchanged 44.3 → 44.5 MB. The entire 847 MB was allocator free-list, accumulated while processing zero traces — the churn is the loops, not data volume.keepcost104 KB afterwards.This node (ciris-server 0.5.198 on the canonical, 18.8 h, 2 vCPU, 2 GB limit), from
smaps: committed anonymous ≈ 2.47 GB, 1.5 GB of it in host swap.So it is the same disease with a different shape: the arena cap plausibly addresses ~836 MB of 2466, and the 706 MB
brkheap — reserved, touched, then cold for hours under memory pressure — is the part a cap cannot consolidate.What 0.5.199's instrument settles first
GET /v1/node/diagnostics/memory(--diagnostics/CIRIS_DIAGNOSTICS=1, loopback-only) reportsmallinfo2from inside the process. The cold 641 MB of[heap]is one of two things andlive_fractionsays which:fordblks— freed, not returned: churn/fragmentation, allocation-side fix (fewer, reused buffers in the loops) and/or a release policy;uordblks— live but cold: something retains ~0.6 GB it stopped reading hours ago, a retention defect, and the more interesting one.Recommended rollout to make that reading clean: at the 0.5.199 restart of the canonical's
ciris-servercontainer, set bothCIRIS_DIAGNOSTICS=1andMALLOC_ARENA_MAX=2. With one spare arena at most,mallinfo2.arenais essentially the brk heap plus 64 MB, sofordblksvsuordblksreads directly as the brk heap's composition — which the 15-arena default would blur. The cap's own effect is already known from status; what we do not know is the brk heap, and this measures it in one restart instead of two.Candidate cures, in the order the reading will pick them
mallopt(M_ARENA_MAX, 2)at the top ofrun/py_mainontarget_env = "gnu", honouring an explicitMALLOC_ARENA_MAXif the environment set one. Every glibc node gets the −58% class of win without an operator knowing the knob exists; Android (scudo), iOS, macOS and Windows are untouched because the call does not exist there. Ready to write; waiting for the canonical reading only so the change lands with a number.fordblksdominates:malloc_trim(0)since glibc 2.8 does NOT only cut the top chunk — it walks every arena andMADV_DONTNEEDs whole free pages inside free chunks, sokeepcost(top only) understates what it would release from a fragmented brk heap. The honest test is a loopback-onlyPOST /v1/node/diagnostics/memory/trimunder the same switch, called by hand once, withmallinfo2+RssAnon/VmSwapread before and after. Status'sdiag.rsdeliberately does not trim; a trial door under the operator switch keeps that separation.Not for 0.5.199; the instrument is in it and the first reading decides the order.