Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions include/xrpl/basics/MallocTrim.h
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
7 changes: 7 additions & 0 deletions include/xrpl/shamap/SHAMap.h
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,13 @@ class SHAMap
void
invariants() const;

/** Log tree structure statistics for debugging/monitoring
@param j Journal to log to
@param mapName Name to identify this map in logs
*/
void
logTreeStats(beast::Journal j, std::string const& mapName) const;

private:
using SharedPtrNodeStack = std::stack<
std::pair<intr_ptr::SharedPtr<SHAMapTreeNode>, SHAMapNodeID>>;
Expand Down
121 changes: 121 additions & 0 deletions src/libxrpl/basics/MallocTrim.cpp
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);

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
68 changes: 68 additions & 0 deletions src/libxrpl/shamap/SHAMap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1240,4 +1240,72 @@ SHAMap::invariants() const
node->invariants(true);
}

void
SHAMap::logTreeStats(beast::Journal j, std::string const& mapName) const
{
struct Stats
{
std::uint32_t totalNodes = 0;
std::uint32_t innerNodes = 0;
std::uint32_t leafNodes = 0;
std::uint32_t maxDepth = 0;
std::array<std::uint32_t, 65> depthCount;
Stats()
{
depthCount.fill(0);
}
} stats;

std::function<void(
intr_ptr::SharedPtr<SHAMapTreeNode> const&, std::uint32_t)>
traverse;
traverse = [&](intr_ptr::SharedPtr<SHAMapTreeNode> const& node,
std::uint32_t depth) {
if (!node)
return;

stats.totalNodes++;
stats.depthCount[depth]++;
stats.maxDepth = std::max(stats.maxDepth, depth);

if (node->isInner())
{
stats.innerNodes++;
auto inner = dynamic_cast<SHAMapInnerNode*>(node.get());
if (inner)
{
for (int i = 0; i < branchFactor; ++i)
{
if (auto child = inner->getChild(i))
{
traverse(child, depth + 1);
}
}
}
}
else
{
stats.leafNodes++;
}
};

traverse(root_, 0);

JLOG(j.info()) << "SHAMap (" << mapName
<< ") stats: total_nodes=" << stats.totalNodes
<< ", inner_nodes=" << stats.innerNodes
<< ", leaf_nodes=" << stats.leafNodes
<< ", max_depth=" << stats.maxDepth;

std::ostringstream hist;
hist << "Depth histogram: { ";
for (std::uint32_t d = 0; d <= stats.maxDepth; ++d)
{
if (stats.depthCount[d] > 0)
hist << d << ": " << stats.depthCount[d] << ", ";
}
hist << "}";
JLOG(j.debug()) << "SHAMap (" << mapName << ") " << hist.str();
}

} // namespace ripple
Loading