Repository navigation
Long-term RSS growth in Scrypted plugins is glibc malloc retention, not actual memory leak #2027
Ruuuuu-Bu
started this conversation in
Show and tell
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Long-term RSS growth in Scrypted plugins is glibc malloc retention, not actual memory leak
TL;DR
After investigating apparent memory leaks in three Scrypted plugins (
homekit,prebuffer-mixin,snapshot), I found the root cause is glibc malloc retention, not native memory leaks. A singlemalloc_trim(0)call viagdbreclaimed 596 MB across the three plugins (homekit alone freed 350 MB). V8 heap was constant throughout. The "leak" disappears completely after trim.This affects anyone running Scrypted on resource-constrained hosts (e.g., 2 GB LXC, small VMs). On hosts with 8+ GB RAM the symptom takes longer to surface but still accumulates.
Setup
.localfork bundled by Scrypted)MALLOC_ARENA_MAXtuning)Symptom
After every
systemctl restart scrypted, LXCusedmemory starts at ~580 MB and grows linearly:Per-plugin RSS at hour 22 (post-restart):
V8 heap (
process.memoryUsage().heapUsed) was stable at 80 MB / 14 MB / 14 MB respectively across the same period — the growth is entirely outside the JS heap.Investigation steps
Heap snapshots (
v8.writeHeapSnapshot()via Scrypted plugin REPL TCP socket): essentially flat. homekit went from 49.3 MB → 49.8 MB in 10 hours. Confirmed leak is not in JS objects./proc/PID/smapsanalysis: for homekit, 354 MB of the 512 MB RSS sat in the single[heap]mapping. The remaining 105 MB was distributed across ~350 small[anon]mappings (256 KB each — the standard glibc mmap chunk size for >128 KB allocations).smaps diff over 46 minutes (during low-activity period):
[heap]grew 0 KB,[anon]cycled with high churn but net +0.6 MB. Growth is bursty, correlated with HAP activity (motion events from UniFi Protect, HomeKit hub polling).malloc_trim(0)via gdb:Result:
The fact that
malloc_trim(0)reclaimed this much memory proves the native code is callingfree()correctly — glibc is just holding the freed memory in its arena free lists for reuse instead of returning it viamadvise(MADV_DONTNEED)ormunmap().Why this happens
This is documented glibc behavior. ptmalloc2 (glibc's allocator) creates per-thread arenas (default cap:
8 × num_cores, so 16 on a 2-core box). Each arena keeps freed chunks in size-class bins. Memory is only returned to the OS when:M_TRIM_THRESHOLD(default 128 KB), ORmalloc_trim()is explicitly calledFor a Node.js process under high allocation churn (every HAP event, every motion notification, every HKSV frame, every TLS handshake → many small mallocs and frees), fragmentation and bin retention prevent the natural top-chunk trim from triggering. The arena keeps freed memory indefinitely.
References:
Why it's particularly visible in Scrypted
On a 16 GB host it would still leak; users just don't notice until much later.
Existing related issues that I believe trace back to this
None of these mention glibc retention or
malloc_trim. They were closed without root cause.Suggested fixes
In order of how invasive they are:
1. Periodic
malloc_trim(0)via cron (workaround, no code change)Daily cron, 1 second per process:
Pros: trivial to deploy, no Scrypted change.
Cons: external scheduling, gdb dependency.
2.
MALLOC_ARENA_MAX=2env var (built-in glibc tuning)In the systemd unit:
Reduces per-thread arena count, concentrates allocations, glibc has a better chance of returning whole arenas. Requires Scrypted restart. No documented downside for this workload.
3. Switch to jemalloc via
LD_PRELOAD(more invasive)jemalloc's default
dirty_decay_ms/muzzy_decay_msactively returns freed memory to the OS. Many large projects (Redis, MariaDB, Firefox) ship jemalloc for exactly this reason.4. In-process periodic
malloc_trim(cleanest)Add a Scrypted-side periodic call:
This makes the fix portable across deploy methods.
Reproduction (minimal)
process.memoryUsage().rssvsheapUsedand the size of[heap]in/proc/<plugin-pid>/smaps.malloc_trim(0)call — observe RSS drop while plugins continue functioning normally.Closing thought
Posting this not as a bug report — glibc retention isn't a Scrypted bug — but to give other users a name for what they're seeing, and to invite a discussion about whether Scrypted should ship with one of the fixes above by default. Happy to provide more data or run targeted experiments.
All reactions