mirror of
https://github.com/kvcache-ai/AgentENV.git
synced 2026-11-05 01:17:36 +00:00
[PR #179] [MERGED] Fix: bound uvm-ublk-daemon memory growth under pause/resume churn #232
Labels
No labels
bug
documentation
enhancement
enhancement
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
kvcache-ai/AgentENV#232
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/kvcache-ai/AgentENV/pull/179
Author: @YXalix
Created: 8/17/2026
Status: ✅ Merged
Merged: 8/18/2026
Merged by: @yingdi-shan
Base:
main← Head:fix_leak📝 Commits (2)
6a7800cfix(overlaybd): offload premerged index prune scan and trigger it by cache growthc4c6dfdfix(ublk-daemon): adopt jemalloc📊 Changes
5 files changed (+216 additions, -24 deletions)
View changed files
📝
Cargo.lock(+1 -0)📝
storage/overlaybd/src/lsmt/file/helper.rs(+156 -24)📝
storage/overlaybd/src/lsmt/file/tests.rs(+54 -0)📝
storage/ublk-daemon/Cargo.toml(+1 -0)📝
storage/ublk-daemon/src/main.rs(+4 -0)📄 Description
What
Bound
uvm-ublk-daemonmemory growth under pause/resume churn, via two commits:8939f2bfix(overlaybd): offload premerged index prune scan and trigger it by cache growth — the prune scan runs as onespawn_blockingtask overstd::fs(one blocking op per scan, not per directory entry), triggers by cache growth (one scan permax_dir_bytes / 8of new writes instead of one queued scan per write), and no longer pins the merged index while queued.355692bfix(ublk-daemon): adopt jemalloc — globaltikv-jemallocatorwith thebackground_threadsfeature (compile-timebackground_thread:true), mirroringsrc/bin/server.rs, so freed pages return to the OS instead of lingering in glibc arenas. No run-timemalloc_confexport is kept; the default 10s dirty/muzzy decay is what the validation numbers below actually ran with.Stress replay of the issue's workload — 600 sandboxes, 100 running concurrently,
continuous pause/resume (16,128 timing samples per run)
8939f2bonly (prune)Why
Issue #171: a 600-VM pause/resume stress cycle grows daemon anonymous memory at 20–40 MB/s up to tens of GB.
memleak-bpfccpoints at the premerged-index write path. Two compounding mechanisms:tokio::fs::ReadDir(one blocking-pool round-trip per entry — the 101kread_dirallocations in the trace; a large cache dir becomes tens of thousands of queued blocking ops). Each queued task also pinned itsArc<ReadOnlyIndex>(can be hundreds of MB) and a manifestVec<(PathBuf, u64, SystemTime)>of the whole directory (the ~27.5 MBgrow_onestack in the trace).On this workload the baseline reproduces the reported behavior (28.39 GB daemon anon memory); the two commits together bring it to 0.72 GB (−97%) — see Validation.
Related issue
Closes #171
Scope and non-goals
Included: only the two commits above —
storage/overlaybd/src/lsmt/file/{helper,tests}.rs(prune path) andstorage/ublk-daemon/{Cargo.toml,src/main.rs}(allocator).Non-goals: no redesign of the cache layout (e.g. incremental artifact registry or generation directories — considered, deferred until scan cost is shown to matter beyond this fix); no changes to the premerged artifact format; no unrelated refactoring.
Design and behavior changes
spawn_blockingtask usingstd::fs; entries deleted concurrently with the scan are skipped instead of aborting the prune.cache_dir, like the other premerged helpers) charges written artifact bytes; once growth since the last scan reachesmax_dir_bytes / 8(floored at one byte, so tiny budgets cannot degenerate into a scan per write), a guarded swap elects exactly one scanner. At most one scan per dir runs at a time, and bytes charged during a running scan are voided when it ends — scans neither overlap nor run back to back, and their frequency is bounded by write rate, not dir size. Bursts coalesce into one scan; idle or slow caches never scan. Failed writes charge nothing (tmp file is removed, no net growth).drop(merged)before the lock-release/prune tail, so queued tasks stop pinning the merged index.src/bin/server.rsexactly —tikv-jemallocatorwith thebackground_threadsfeature, which compiles jemalloc with a build-timebackground_thread:trueso purging runs off the allocation path; dirty/muzzy decay keeps jemalloc's 10s defaults. A run-timemalloc_confexport is deliberately not kept (see Risks and reviewer notes).max_dir_bytes / 8of new bytes per cache dir (≥ 8 MiB given the 64 MiB minimum budget, so it cannot degenerate).Compatibility and operations
PMIDX001v1 format; only eviction scheduling changes.tikv-jemallocatortouvm-ublk-daemon(the same crate and feature the server already uses; bundled jemalloc, no system package needed); slightly larger daemon binary; no new ports or permissions.Validation
make fmtmake clippymake test-unitmake -C services test(required whenservices/changes)maketargetSkipped checks and reasons:
Risks and reviewer notes
fetch_adds its bytes, and the crossing writer wins an in-flight flag swap — exactly one scan fires per threshold crossing per dir. Losing writers keep their bytes accumulated until the elected scan voids them at its end. Relaxed ordering suffices because the counter guards no other memory.dirty_decay_ms:1000,muzzy_decay_ms:1000,background_thread:truevia amalloc_confsymbol, which the prefixed jemalloc build (--with-jemalloc-prefix=_rjem_) silently never reads — the validated 0.72 GB therefore reflects the default decay this PR now ships verbatim. The run-time export was dropped rather than kept as a silent-failure footgun.storage/overlaybd/src/lsmt/file/helper.rs,storage/ublk-daemon/src/main.rs.Checklist
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.