-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Explicit heap trimming #6022
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Explicit heap trimming #6022
Changes from 16 commits
ac5554e
3fdd42a
a1ed175
72b34e6
52c8368
2d41bfe
334382f
d85f707
50d6065
ff8b435
2b2b361
efe7177
6a8a1b7
e77bd4e
265ea4b
645fdda
8973ec1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| #ifndef XRPL_BASICS_MALLOCTRIM_H_INCLUDED | ||
| #define XRPL_BASICS_MALLOCTRIM_H_INCLUDED | ||
|
|
||
| #include <xrpl/beast/utility/Journal.h> | ||
|
|
||
| #include <optional> | ||
| #include <string> | ||
|
|
||
| namespace ripple { | ||
|
|
||
| // ----------------------------------------------------------------------------- | ||
| // Allocator interaction note: | ||
| // - This facility invokes glibc's malloc_trim(0) on Linux/glibc to request that | ||
| // ptmalloc return free heap pages to the OS. | ||
| // - If an alternative allocator (e.g. jemalloc or tcmalloc) is linked or | ||
| // preloaded (LD_PRELOAD), calling glibc's malloc_trim typically has no effect | ||
| // on the *active* heap. The call is harmless but may not reclaim memory | ||
| // because those allocators manage their own arenas. | ||
| // - Only glibc sbrk/arena space is eligible for trimming; large mmap-backed | ||
| // allocations are usually returned to the OS on free regardless of trimming. | ||
| // - Call at known reclamation points (e.g., after cache sweeps / online delete) | ||
| // and consider rate limiting to avoid churn. | ||
| // ----------------------------------------------------------------------------- | ||
|
|
||
| struct MallocTrimReport | ||
| { | ||
| bool supported{false}; | ||
| int trimResult{-1}; | ||
| long rssBeforeKB{-1}; | ||
| long rssAfterKB{-1}; | ||
|
|
||
| [[nodiscard]] long | ||
| deltaKB() const noexcept | ||
| { | ||
| if (rssBeforeKB < 0 || rssAfterKB < 0) | ||
| return 0; | ||
| return rssAfterKB - rssBeforeKB; | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * @brief Attempt to return freed memory to the operating system. | ||
| * | ||
| * On Linux with glibc malloc, this issues ::malloc_trim(0), which may release | ||
| * free space from ptmalloc arenas back to the kernel. On other platforms, or if | ||
| * a different allocator is in use, this function is a no-op and the report will | ||
| * indicate that trimming is unsupported or had no effect. | ||
| * | ||
| * @param tag Optional identifier for logging/debugging purposes. | ||
| * @param journal Journal for diagnostic logging. | ||
| * @return Report containing before/after metrics and the trim result. | ||
| * | ||
| * @note If an alternative allocator (jemalloc/tcmalloc) is linked or preloaded, | ||
| * calling glibc's malloc_trim may have no effect on the active heap. The | ||
| * call is harmless but typically does not reclaim memory under those | ||
| * allocators. | ||
| * | ||
| * @note Only memory served from glibc's sbrk/arena heaps is eligible for trim. | ||
| * Large allocations satisfied via mmap are usually returned on free | ||
| * independently of trimming. | ||
| * | ||
| * @note Intended for use after operations that free significant memory (e.g., | ||
| * cache sweeps, ledger cleanup, online delete). Consider rate limiting. | ||
| */ | ||
| MallocTrimReport | ||
| mallocTrim(std::optional<std::string> const& tag, beast::Journal journal); | ||
|
|
||
| } // namespace ripple | ||
|
|
||
| #endif |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| #include <xrpl/basics/Log.h> | ||
| #include <xrpl/basics/MallocTrim.h> | ||
|
|
||
| #include <boost/predef.h> | ||
|
|
||
| #include <cstdio> | ||
| #include <fstream> | ||
|
|
||
| #if defined(__GLIBC__) && BOOST_OS_LINUX | ||
| #include <malloc.h> | ||
| #include <unistd.h> | ||
|
|
||
| namespace { | ||
| pid_t const cachedPid = ::getpid(); | ||
| } // namespace | ||
| #endif | ||
|
|
||
| namespace ripple { | ||
|
|
||
| namespace detail { | ||
|
|
||
| #if defined(__GLIBC__) && BOOST_OS_LINUX | ||
|
|
||
| long | ||
| parseVmRSSkB(std::string const& status) | ||
| { | ||
| std::istringstream iss(status); | ||
| std::string line; | ||
|
|
||
| while (std::getline(iss, line)) | ||
| { | ||
| // Allow leading spaces/tabs before the key. | ||
| auto const firstNonWs = line.find_first_not_of(" \t"); | ||
| if (firstNonWs == std::string::npos) | ||
| continue; | ||
|
|
||
| constexpr char key[] = "VmRSS:"; | ||
| constexpr auto keyLen = sizeof(key) - 1; | ||
|
|
||
| // Require the line (after leading whitespace) to start with "VmRSS:". | ||
| // Check if we have enough characters and the substring matches. | ||
| if (firstNonWs + keyLen > line.size() || | ||
| line.substr(firstNonWs, keyLen) != key) | ||
| continue; | ||
|
|
||
| // Move past "VmRSS:" and any following whitespace. | ||
| auto pos = firstNonWs + keyLen; | ||
| while (pos < line.size() && | ||
| std::isspace(static_cast<unsigned char>(line[pos]))) | ||
| { | ||
| ++pos; | ||
| } | ||
|
|
||
| long value = -1; | ||
| if (std::sscanf(line.c_str() + pos, "%ld", &value) == 1) | ||
| return value; | ||
|
|
||
| // Found the key but couldn't parse a number. | ||
| return -1; | ||
| } | ||
|
|
||
| // No VmRSS line found. | ||
| return -1; | ||
| } | ||
|
|
||
| #endif // __GLIBC__ && BOOST_OS_LINUX | ||
|
|
||
| } // namespace detail | ||
|
|
||
| MallocTrimReport | ||
| mallocTrim( | ||
| [[maybe_unused]] std::optional<std::string> const& tag, | ||
| beast::Journal journal) | ||
| { | ||
| MallocTrimReport report; | ||
|
|
||
| #if !(defined(__GLIBC__) && BOOST_OS_LINUX) | ||
| JLOG(journal.debug()) << "malloc_trim not supported on this platform"; | ||
| #else | ||
|
|
||
| report.supported = true; | ||
|
|
||
| if (journal.debug()) | ||
| { | ||
| auto readFile = [](std::string const& path) -> std::string { | ||
| std::ifstream ifs(path); | ||
| if (!ifs.is_open()) | ||
| return {}; | ||
| return std::string( | ||
| std::istreambuf_iterator<char>(ifs), | ||
| std::istreambuf_iterator<char>()); | ||
| }; | ||
|
|
||
| std::string const tagStr = tag.value_or("default"); | ||
| std::string const statusPath = | ||
| "/proc/" + std::to_string(cachedPid) + "/status"; | ||
|
|
||
| auto const statusBefore = readFile(statusPath); | ||
| report.rssBeforeKB = detail::parseVmRSSkB(statusBefore); | ||
|
|
||
| report.trimResult = ::malloc_trim(0); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would suggest, instead of calling Why? The trim after
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A For thread heaps(sub-heaps), |
||
|
|
||
| auto const statusAfter = readFile(statusPath); | ||
| report.rssAfterKB = detail::parseVmRSSkB(statusAfter); | ||
|
|
||
| JLOG(journal.debug()) | ||
| << "malloc_trim tag=" << tagStr << " result=" << report.trimResult | ||
| << " rss_before=" << report.rssBeforeKB << "kB" | ||
| << " rss_after=" << report.rssAfterKB << "kB" | ||
| << " delta=" << report.deltaKB() << "kB"; | ||
| } | ||
| else | ||
| { | ||
| report.trimResult = ::malloc_trim(0); | ||
| } | ||
| #endif | ||
|
|
||
| return report; | ||
| } | ||
|
|
||
| } // namespace ripple | ||
There was a problem hiding this comment.
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/statminstead of/proc/self/status