Sponsored Content
Skip to content

Explicitly trim the heap after cache sweeps - #6022

Merged
ximinez merged 63 commits into
developfrom
vlntb/malloc-trim
Feb 24, 2026
Merged

Explicitly trim the heap after cache sweeps#6022
ximinez merged 63 commits into
developfrom
vlntb/malloc-trim

Conversation

@vlntb

@vlntb vlntb commented Nov 11, 2025

Copy link
Copy Markdown
Contributor

High Level Overview of Change

  • Introduces a MallocTrim helper in libxrpl to centralize calls to ::malloc_trim(0) on Linux/glibc, and (optionally) record RSS before/after for debugging and reporting.
  • Wires mallocTrim into the sweep path only:
    • Application::doSweep: call immediately after sweeps, when the application has just freed a meaningful amount of heap and we have the best chance of returning unused pages back to the OS.
    • On non-Linux or non-glibc builds, the helper reports supported = false and becomes an effective no-op.

Context of Change

  • Long-running nodes on Linux/glibc showed steady Resident growth under heavier ledger-state scenarios.
  • 24h baseline without malloc_trim:
    • Resident: ~14.0 GB → ~ 32.9 GB (+18.6 GB, ~0.76 GB/h).
    • Referenced: low teens → ~ 32.7 GB (~+29 GB, ~1.2 GB/h).
  • 24h run with malloc_trim from Application::doSweep + online delete:
    • Resident: ~18.3 GB → ~19.0 GB (+0.7 GB, ~0.03 GB/h).
    • Referenced: ~18.0 GB → ~18.8 GB (+0.8 GB, ~0.03 GB/h).
  • Net effect over 24h:
    • ~42% lower Resident/Referenced at the end of the run (~32.9 GB → ~19.0 GB).
    • Growth rate drops from ~0.76 GB/h → ~0.03 GB/h (Resident) and ~1.2 GB/h → ~0.03 GB/h (Referenced), i.e. ~25–30× slower accumulation.
  • Over a longer ~40h trimmed run, we see ~147 GB returned to the OS (~3.7–3.8 GB/h) while steady-state Resident lives in an 18–20 GB band and the Resident–Referenced gap stays small (~0.2 GB). This suggests:
    • malloc_trim is doing real work against fragmentation and short-lived churn.
    • The remaining slow drift is driven by genuine long-lived working set, which will need separate follow-up (caches, data-structure sizing, protocol state), but the production hooks in this PR already give a clear and measurable win.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (non-breaking change that only restructures code)
  • Performance (increase or change in throughput and/or latency)
  • Tests (you added tests for code that already exists, or your new feature included in this PR)
  • Documentation update
  • Chore (no impact to binary, e.g. .gitignore, formatting, dropping support for older tooling)
  • Release

API Impact

  • Public API: New feature (new methods and/or new fields)
  • Public API: Breaking change (in general, breaking changes should only impact the next api_version)
  • libxrpl change (any change that may affect libxrpl or dependents of libxrpl)
  • Peer protocol change (must be backward compatible or bump the peer protocol version)

@vlntb
vlntb marked this pull request as ready for review November 13, 2025 14:12
@vlntb
vlntb requested a review from a team November 13, 2025 14:12
@codecov

codecov Bot commented Nov 13, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.23529% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.8%. Comparing base (24cbaf7) to head (7676de0).
⚠️ Report is 4 commits behind head on develop.

Files with missing lines Patch % Lines
src/libxrpl/basics/MallocTrim.cpp 91.7% 1 Missing ⚠️
src/xrpld/app/main/Application.cpp 0.0% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##           develop   #6022     +/-   ##
=========================================
- Coverage     79.8%   79.8%   -0.0%     
=========================================
  Files          846     848      +2     
  Lines        67746   67763     +17     
  Branches      7560    7558      -2     
=========================================
+ Hits         54067   54072      +5     
- Misses       13679   13691     +12     
Files with missing lines Coverage Δ
include/xrpl/basics/MallocTrim.h 100.0% <100.0%> (ø)
src/libxrpl/basics/MallocTrim.cpp 91.7% <91.7%> (ø)
src/xrpld/app/main/Application.cpp 70.0% <0.0%> (-0.1%) ⬇️

... and 4 files with indirect coverage changes

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread src/libxrpl/basics/MallocTrim.cpp Outdated
auto const statusBefore = readFile(statusPath);
report.rssBeforeKB = detail::parseVmRSSkB(statusBefore);

report.trimResult = ::malloc_trim(0);

@pratikmankawde pratikmankawde Nov 21, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest, instead of calling malloc_trim(0), which would try to trim up to the main heap's top boundary (leaving only a minimum amount, 1 page, which is usually 4 or 8 KB, link for other readers), we should instead call malloc_trim(m);, where m is the minimum amount of memory we expect we will need soon enough. It could range from few hundred MBs to few GBs. The description of this PR mentions a rate of about ~1.2 GB/h (on the higher side). So, I would suggest we keep m=~2.4GB for starters. The reports we are accumulating in this class can then help us fine tune it.

Why?
Memory allocation is an expensive operation in itself. In this case(after calling malloc_trim(0)) any new allocations would also require heap extension. This will be exceptionally expensive(by the order of 1000 times, involving user to kernel space context switch and then allocation of new pages by OS). We can avoid that by keeping few GBs in reserve.

The trim after NetworkOps -> SyncComplete could be one of the suboptimal calls, if we do any heavy operations after SyncComplete, requiring memory allocations exceeding 1 page.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This note in the manual suggests that attempting to use the trim padding is wasted effort:

Only the main heap (using sbrk(2)) honors the pad argument; thread heaps do not.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A malloc_trim(m) call will check all the arenas (main and thread) to see if chunks of memory can be released. It will use the padding arg to decide(keep upto) the chunk that can be freed from main heap top.

For thread heaps(sub-heaps), malloc_trim anyway can't free the sub-heap or part of it(hence neglect padding), if there's even a small block in use. If a sub-heap region becomes completely empty after the last call to free(), allocator will anyway return the memory to OS. So padding doesn't make much sense for thread heaps. Thread heaps are anyway self-managed. So, we don't need to optimise for them. If there's fragmentation in the sub-heaps, that will remain until an entire sub-heap is empty(which will then be released).

@vlntb vlntb Feb 11, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trim after NetworkOps -> SyncComplete could be one of the suboptimal calls, if we do any heavy operations after SyncComplete, requiring memory allocations exceeding 1 page.

I revisited the malloc_trim triggers. The sweep-based trigger accounts for the largest observed memory reclamation (as shown in the candle charts). Piggybacking on sweep provides regular reclamation from a background job context, keeping trim overhead away from consensus/RPC hot paths. As a result, I left only the sweep-based trigger.

rss_candles_clearPrior rss_candles_clearCaches rss_candles_doSweep

@vlntb vlntb Feb 11, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I did research exploring padding and ran several tests.

1) Memory reclaim

Padding Mean RSS after 1h (MB) P95 RSS after 1h (MB) End RSS (MB) Notes
0 16013.7 16611.0 16579.5 Best sustained/typical RSS
256KB 16288.9 16900.6 16729.5 Slightly higher RSS than 0
1MB 16397.9 17052.5 15877.5 Lowest final RSS, but higher average/variance
16MB 16151.4 17120.1 17360.6 Highest end RSS; retains most memory late-run

Interpretation

  • If we care about consistent memory footprint, 0 padding looks best (lowest mean and p95 RSS after warm-up).
  • 1MB appears to allow memory to build up more during the run, then occasionally drops sharply (hence low end RSS but higher average).
  • 16MB keeps a larger “reserve” in the allocator, and the run ends with the highest RSS.

2) Latency impact

Padding Calls Avg (ms) P95 (ms) P99 (ms) Max (ms)
0 670 162.9 392.5 673.6 904.4
256KB 706 171.0 372.6 702.2 837.1
1MB 645 178.6 415.8 794.5 989.0
16MB 621 166.2 372.1 673.4 929.3

Interpretation

  • 1MB is the clear latency outlier (highest avg / p95 / p99 / max).
  • 256KB shows slightly better p95 and max than 0, but slightly worse average.
  • 16MB doesn’t materially improve tail latency vs 256KB, and still has a high max.

Based on the 12‑hour Mainnet runs, there isn’t a strong, consistent benefit to moving away from the default malloc_trim behavior (padding = 0). While small non‑zero paddings (e.g., 256KB) can marginally shift some tail-latency metrics, the improvements are not decisive and come with trade-offs (slightly higher RSS or no clear sustained memory advantage). Larger paddings (1MB and 16MB) do not provide a compelling overall benefit: 1MB showed worse trim-latency characteristics, and 16MB tended to retain more memory without meaningful latency wins.

We should keep padding = 0 (default) as the production setting. It is the simplest, most predictable choice and provides a solid balance of memory reclamation and trim overhead without introducing additional tuning surface area or risk of unexpected behavior under varying Mainnet load.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, as per our discussion today, I understand that the memory we are claiming in each call to malloc_trim is in MBs. So, using a padding of few hundred MBs won't give us much trimming. Also, there's some latency introduced by each reclaim call. So, we don't want to wait too long, and claim a bigger chunk, since it will take some time and might introduce some slowness in the app.
So, the current implementation looks fine to me. I would be interested in seeing how it performs on prod. and then we can fine tune the frequency and the padding params further.

@bthomee
bthomee requested a review from lmaisons November 21, 2025 20:11

@lmaisons lmaisons left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The points where we trim look reasonable. Unless there's some obvious caveat I'm not aware of, I think the RSS reporting should use the self and statm proc features so as to minimize parsing / clerical errors.

Comment thread src/libxrpl/basics/MallocTrim.cpp Outdated

std::string const tagStr = tag.value_or("default");
std::string const statusPath =
"/proc/" + std::to_string(cachedPid) + "/status";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we not just use /proc/self/...? Also, it seems we would need to do fewer parsing contortions if we read from /proc/self/statm instead of /proc/self/status

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I agree, it simplifies this part of the code a lot.

Comment thread src/libxrpl/basics/MallocTrim.cpp Outdated
auto const statusBefore = readFile(statusPath);
report.rssBeforeKB = detail::parseVmRSSkB(statusBefore);

report.trimResult = ::malloc_trim(0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This note in the manual suggests that attempting to use the trim padding is wasted effort:

Only the main heap (using sbrk(2)) honors the pad argument; thread heaps do not.

@bthomee
bthomee requested a review from g-ripple November 24, 2025 18:55
Comment thread src/libxrpl/basics/MallocTrim.cpp Outdated
Comment thread src/libxrpl/basics/MallocTrim.cpp Outdated
@vlntb

vlntb commented Feb 23, 2026

Copy link
Copy Markdown
Contributor Author

Looks OK to me. I echo @a1q123456's recommendation to avoid implementation defined integer sizes, though with the caveat that any interactions with the native API should use the API specified types, and if we think we should be coercing them, to do it in a different expression / statement.

My thoughts exactly. See my reply to @a1q123456's comment above

@vlntb
vlntb requested a review from a1q123456 February 23, 2026 16:52

@a1q123456 a1q123456 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@vlntb vlntb added the Ready to merge *PR author* thinks it's ready to merge. Has passed code review. Perf sign-off may still be required. label Feb 23, 2026
@ximinez
ximinez requested a review from Copilot February 24, 2026 01:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces explicit heap memory trimming functionality to address memory growth issues in long-running rippled nodes on Linux/glibc systems. The implementation adds a new MallocTrim utility in libxrpl that wraps malloc_trim(0) and integrates it into the application's periodic sweep operations.

Changes:

  • Adds MallocTrim helper with RSS tracking and diagnostic reporting
  • Integrates mallocTrim() call into Application::doSweep() after cache sweeps
  • Provides comprehensive test coverage for the new functionality

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
include/xrpl/basics/MallocTrim.h Defines MallocTrimReport struct and mallocTrim() function with platform-specific documentation
src/libxrpl/basics/MallocTrim.cpp Implements malloc_trim wrapper with RSS measurement via /proc/self/statm and rusage tracking
src/tests/libxrpl/basics/MallocTrim.cpp Comprehensive unit tests covering report structure, parsing, and trim functionality across platforms
src/xrpld/app/main/Application.cpp Integrates mallocTrim call at end of doSweep() after all cache cleanup operations

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/xrpld/app/main/Application.cpp Outdated
Comment thread src/tests/libxrpl/basics/MallocTrim.cpp Outdated
Comment thread src/tests/libxrpl/basics/MallocTrim.cpp Outdated
Comment thread src/tests/libxrpl/basics/MallocTrim.cpp Outdated
Comment thread src/tests/libxrpl/basics/MallocTrim.cpp Outdated
@vlntb vlntb removed the Ready to merge *PR author* thinks it's ready to merge. Has passed code review. Perf sign-off may still be required. label Feb 24, 2026
@ximinez
ximinez enabled auto-merge (squash) February 24, 2026 21:05
@ximinez
ximinez disabled auto-merge February 24, 2026 21:05
@ximinez
ximinez enabled auto-merge (squash) February 24, 2026 21:06
@ximinez
ximinez merged commit bdd106d into develop Feb 24, 2026
1 check passed
@ximinez
ximinez deleted the vlntb/malloc-trim branch February 24, 2026 21:33
@ximinez ximinez changed the title Explicit heap trimming Explicitly trim the heap after cache sweeps Feb 25, 2026
@bthomee bthomee added the QE test required RippleX QE Team must look at this PR. label Feb 26, 2026
@mvadari mvadari added this to the 3.2.0 milestone May 20, 2026
beartec-jpg pushed a commit to beartec-jpg/FalconLedger that referenced this pull request Jun 1, 2026
pratikmankawde added a commit that referenced this pull request Jun 17, 2026
Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com>
mathbunnyru pushed a commit to mathbunnyru/rippled that referenced this pull request Jun 17, 2026
@bthomee bthomee modified the milestones: 3.2.0, 3.3.0 Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

QE test required RippleX QE Team must look at this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants