diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index f43275201c..7793d1e3ab 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -14,4 +14,4 @@ jobs: uses: XRPLF/actions/.github/workflows/pre-commit.yml@320be44621ca2a080f05aeb15817c44b84518108 with: runs_on: ubuntu-latest - container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-ab4d1f0" }' + container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-41ec7c1" }' diff --git a/.gitignore b/.gitignore index a1c2f034d1..60e8fef56c 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,9 @@ gmon.out # Locally patched Conan recipes external/conan-center-index/ +# Local conan directory +.conan + # XCode IDE. *.pbxuser !default.pbxuser @@ -72,5 +75,8 @@ DerivedData /.claude /CLAUDE.md +# Direnv's directory +/.direnv + # clangd cache /.cache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9117fe0d3e..6e04c752e9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -57,6 +57,16 @@ repos: - .git/COMMIT_EDITMSG stages: [commit-msg] + - repo: local + hooks: + - id: nix-fmt + name: Format Nix files + entry: nix --extra-experimental-features 'nix-command flakes' fmt + language: system + types: + - nix + pass_filenames: true + exclude: | (?x)^( external/.*| diff --git a/cspell.config.yaml b/cspell.config.yaml index 87258758c4..e2b20ac098 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -173,6 +173,9 @@ words: - nftokens - nftpage - nikb + - nixfmt + - nixos + - nixpkgs - nonxrp - noripple - nudb diff --git a/docs/build/environment.md b/docs/build/environment.md index c6b735ba48..c67877a082 100644 --- a/docs/build/environment.md +++ b/docs/build/environment.md @@ -3,6 +3,8 @@ environment complete with Git, Python, Conan, CMake, and a C++ compiler. This document exists to help readers set one up on any of the Big Three platforms: Linux, macOS, or Windows. +As an alternative to system packages, the Nix development shell can be used to provide a development environment. See [using nix development shell](./nix.md) for more details. + [BUILD.md]: ../../BUILD.md ## Linux diff --git a/docs/build/nix.md b/docs/build/nix.md new file mode 100644 index 0000000000..33bb3711d0 --- /dev/null +++ b/docs/build/nix.md @@ -0,0 +1,95 @@ +# Using Nix Development Shell for xrpld Development + +This guide explains how to use Nix to set up a reproducible development environment for xrpld. Using Nix eliminates the need to manually install utilities and ensures consistent tooling across different machines. + +## Benefits of Using Nix + +- **Reproducible environment**: Everyone gets the same versions of tools and compilers +- **No system pollution**: Dependencies are isolated and don't affect your system packages +- **Multiple compiler versions**: Easily switch between different GCC and Clang versions +- **Quick setup**: Get started with a single command +- **Works on Linux and macOS**: Consistent experience across platforms + +## Install Nix + +Please follow [the official installation instructions of nix package manager](https://nixos.org/download/) for your system. + +## Entering the Development Shell + +### Basic Usage + +From the root of the xrpld repository, enter the default development shell: + +```bash +nix --experimental-features 'nix-command flakes' develop +``` + +This will: + +- Download and set up all required development tools (CMake, Ninja, Conan, etc.) +- Configure the appropriate compiler for your platform: + - **macOS**: Apple Clang (default system compiler) + - **Linux**: GCC 15 + +The first time you run this command, it will take a few minutes to download and build the environment. Subsequent runs will be much faster. + +> [!TIP] +> To avoid typing `--experimental-features 'nix-command flakes'` every time, you can permanently enable flakes by creating `~/.config/nix/nix.conf`: +> +> ```bash +> mkdir -p ~/.config/nix +> echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf +> ``` +> +> After this, you can simply use `nix develop` instead. + +> [!NOTE] +> The examples below assume you've enabled flakes in your config. If you haven't, add `--experimental-features 'nix-command flakes'` after each `nix` command. + +### Choosing a different compiler + +A compiler can be chosen by providing its name with the `.#` prefix, e.g. `nix develop .#gcc15`. +Use `nix flake show` to see all the available development shells. + +Use `nix develop .#no_compiler` to use the compiler from your system. + +### Example Usage + +```bash +# Use GCC 14 +nix develop .#gcc14 + +# Use Clang 19 +nix develop .#clang19 + +# Use default for your platform +nix develop +``` + +### Using a different shell + +`nix develop` opens bash by default. If you want to use another shell this could be done by adding `-c` flag. For example: + +```bash +nix develop -c zsh +``` + +## Building xrpld with Nix + +Once inside the Nix development shell, follow the standard [build instructions](../../BUILD.md#steps). The Nix shell provides all necessary tools (CMake, Ninja, Conan, etc.). + +## Automatic Activation with direnv + +[direnv](https://direnv.net/) or [nix-direnv](https://github.com/nix-community/nix-direnv) can automatically activate the Nix development shell when you enter the repository directory. + +## Conan and Prebuilt Packages + +Please note that there is no guarantee that binaries from conan cache will work when using nix. If you encounter any errors, please use `--build '*'` to force conan to compile everything from source: + +```bash +conan install .. --output-folder . --build '*' --settings build_type=Release +``` + +## Updating `flake.lock` file + +To update `flake.lock` to the latest revision use `nix flake update` command. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000000..fd43f5b683 --- /dev/null +++ b/flake.lock @@ -0,0 +1,26 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1769461804, + "narHash": "sha256-6h5sROT/3CTHvzPy9koKBmoCa2eJKh4fzQK8eYFEgl8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b579d443b37c9c5373044201ea77604e37e748c8", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-unstable", + "type": "indirect" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000000..4c500f1933 --- /dev/null +++ b/flake.nix @@ -0,0 +1,16 @@ +{ + description = "Nix related things for xrpld"; + inputs = { + nixpkgs.url = "nixpkgs/nixos-unstable"; + }; + + outputs = + { nixpkgs, ... }: + let + forEachSystem = (import ./nix/utils.nix { inherit nixpkgs; }).forEachSystem; + in + { + devShells = forEachSystem (import ./nix/devshell.nix); + formatter = forEachSystem ({ pkgs, ... }: pkgs.nixfmt); + }; +} diff --git a/include/xrpl/basics/MallocTrim.h b/include/xrpl/basics/MallocTrim.h new file mode 100644 index 0000000000..2d0cf989ba --- /dev/null +++ b/include/xrpl/basics/MallocTrim.h @@ -0,0 +1,73 @@ +#pragma once + +#include + +#include +#include +#include + +namespace xrpl { + +// cSpell:ignore ptmalloc + +// ----------------------------------------------------------------------------- +// 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}; + std::int64_t rssBeforeKB{-1}; + std::int64_t rssAfterKB{-1}; + std::chrono::microseconds durationUs{-1}; + std::int64_t minfltDelta{-1}; + std::int64_t majfltDelta{-1}; + + [[nodiscard]] std::int64_t + 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 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::string_view tag, beast::Journal journal); + +} // namespace xrpl diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 961bc6e44c..a40d524c70 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -15,9 +15,10 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. + XRPL_FIX (PermissionedDomainInvariant, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (ExpiredNFTokenOfferRemoval, Supported::yes, VoteBehavior::DefaultNo) -XRPL_FIX (BatchInnerSigs, Supported::yes, VoteBehavior::DefaultNo) +XRPL_FIX (BatchInnerSigs, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(LendingProtocol, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionDelegationV1_1, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (DirectoryLimit, Supported::yes, VoteBehavior::DefaultNo) @@ -31,7 +32,7 @@ XRPL_FEATURE(TokenEscrow, Supported::yes, VoteBehavior::DefaultNo XRPL_FIX (EnforceNFTokenTrustlineV2, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (AMMv1_3, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionedDEX, Supported::yes, VoteBehavior::DefaultNo) -XRPL_FEATURE(Batch, Supported::yes, VoteBehavior::DefaultNo) +XRPL_FEATURE(Batch, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(SingleAssetVault, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo) // Check flags in Credential transactions diff --git a/nix/devshell.nix b/nix/devshell.nix new file mode 100644 index 0000000000..1d907f4d87 --- /dev/null +++ b/nix/devshell.nix @@ -0,0 +1,140 @@ +{ pkgs, ... }: +let + commonPackages = with pkgs; [ + ccache + cmake + conan + gcovr + git + gnumake + llvmPackages_21.clang-tools + ninja + perl # needed for openssl + pkg-config + pre-commit + python314 + ]; + + # Supported compiler versions + gccVersion = pkgs.lib.range 13 15; + clangVersions = pkgs.lib.range 18 21; + + defaultCompiler = if pkgs.stdenv.isDarwin then "apple-clang" else "gcc"; + defaultGccVersion = pkgs.lib.last gccVersion; + defaultClangVersion = pkgs.lib.last clangVersions; + + strToCompilerEnv = + compiler: version: + ( + if compiler == "gcc" then + let + gccPkg = pkgs."gcc${toString version}Stdenv" or null; + in + if gccPkg != null && builtins.elem version gccVersion then + gccPkg + else + throw "Invalid GCC version: ${toString version}. Must be one of: ${toString gccVersion}" + else if compiler == "clang" then + let + clangPkg = pkgs."llvmPackages_${toString version}".stdenv or null; + in + if clangPkg != null && builtins.elem version clangVersions then + clangPkg + else + throw "Invalid Clang version: ${toString version}. Must be one of: ${toString clangVersions}" + else if compiler == "apple-clang" || compiler == "none" then + pkgs.stdenvNoCC + else + throw "Invalid compiler: ${compiler}. Must be one of: gcc, clang, apple-clang, none" + ); + + # Helper function to create a shell with a specific compiler + makeShell = + { + compiler ? defaultCompiler, + version ? ( + if compiler == "gcc" then + defaultGccVersion + else if compiler == "clang" then + defaultClangVersion + else + null + ), + }: + let + compilerStdEnv = strToCompilerEnv compiler version; + + compilerName = + if compiler == "apple-clang" then + "clang" + else if compiler == "none" then + null + else + compiler; + + gccOnMacWarning = + if pkgs.stdenv.isDarwin && compiler == "gcc" then + '' + echo "WARNING: Using GCC on macOS with Conan may not work." + echo " Consider using 'nix develop .#clang' or the default shell instead." + echo "" + '' + else + ""; + + compilerVersion = + if compilerName != null then + '' + echo "Compiler: " + ${compilerName} --version + '' + else + '' + echo "No compiler specified - using system compiler" + ''; + + shellAttrs = { + packages = commonPackages; + + shellHook = '' + echo "Welcome to xrpld development shell"; + ${gccOnMacWarning}${compilerVersion} + ''; + }; + in + pkgs.mkShell.override { stdenv = compilerStdEnv; } shellAttrs; + + # Generate shells for each compiler version + gccShells = builtins.listToAttrs ( + map (version: { + name = "gcc${toString version}"; + value = makeShell { + compiler = "gcc"; + version = version; + }; + }) gccVersion + ); + + clangShells = builtins.listToAttrs ( + map (version: { + name = "clang${toString version}"; + value = makeShell { + compiler = "clang"; + version = version; + }; + }) clangVersions + ); + +in +gccShells +// clangShells +// { + # Default shells + default = makeShell { }; + gcc = makeShell { compiler = "gcc"; }; + clang = makeShell { compiler = "clang"; }; + + # No compiler + no-compiler = makeShell { compiler = "none"; }; + apple-clang = makeShell { compiler = "apple-clang"; }; +} diff --git a/nix/utils.nix b/nix/utils.nix new file mode 100644 index 0000000000..821d60a6f6 --- /dev/null +++ b/nix/utils.nix @@ -0,0 +1,19 @@ +{ nixpkgs }: +{ + forEachSystem = + function: + nixpkgs.lib.genAttrs + [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ] + ( + system: + function { + inherit system; + pkgs = import nixpkgs { inherit system; }; + } + ); +} diff --git a/src/libxrpl/basics/MallocTrim.cpp b/src/libxrpl/basics/MallocTrim.cpp new file mode 100644 index 0000000000..1b0932b39d --- /dev/null +++ b/src/libxrpl/basics/MallocTrim.cpp @@ -0,0 +1,157 @@ +#include +#include + +#include + +#include +#include +#include +#include +#include + +#if defined(__GLIBC__) && BOOST_OS_LINUX +#include + +#include +#include + +// Require RUSAGE_THREAD for thread-scoped page fault tracking +#ifndef RUSAGE_THREAD +#error "MallocTrim rusage instrumentation requires RUSAGE_THREAD on Linux/glibc" +#endif + +namespace { + +bool +getRusageThread(struct rusage& ru) +{ + return ::getrusage(RUSAGE_THREAD, &ru) == 0; // LCOV_EXCL_LINE +} + +} // namespace +#endif + +namespace xrpl { + +namespace detail { + +// cSpell:ignore statm + +#if defined(__GLIBC__) && BOOST_OS_LINUX + +inline int +mallocTrimWithPad(std::size_t padBytes) +{ + return ::malloc_trim(padBytes); +} + +long +parseStatmRSSkB(std::string const& statm) +{ + // /proc/self/statm format: size resident shared text lib data dt + // We want the second field (resident) which is in pages + std::istringstream iss(statm); + long size, resident; + if (!(iss >> size >> resident)) + return -1; + + // Convert pages to KB + long const pageSize = ::sysconf(_SC_PAGESIZE); + if (pageSize <= 0) + return -1; + + return (resident * pageSize) / 1024; +} + +#endif // __GLIBC__ && BOOST_OS_LINUX + +} // namespace detail + +MallocTrimReport +mallocTrim(std::string_view tag, beast::Journal journal) +{ + // LCOV_EXCL_START + + MallocTrimReport report; + +#if !(defined(__GLIBC__) && BOOST_OS_LINUX) + JLOG(journal.debug()) << "malloc_trim not supported on this platform (tag=" << tag << ")"; +#else + // Keep glibc malloc_trim padding at 0 (default): 12h Mainnet tests across 0/256KB/1MB/16MB + // showed no clear, consistent benefit from custom padding—0 provided the best overall balance + // of RSS reduction and trim-latency stability without adding a tuning surface. + constexpr std::size_t TRIM_PAD = 0; + + report.supported = true; + + if (journal.debug()) + { + auto readFile = [](std::string const& path) -> std::string { + std::ifstream ifs(path, std::ios::in | std::ios::binary); + if (!ifs.is_open()) + return {}; + + // /proc files are often not seekable; read as a stream. + std::ostringstream oss; + oss << ifs.rdbuf(); + return oss.str(); + }; + + std::string const tagStr{tag}; + std::string const statmPath = "/proc/self/statm"; + + auto const statmBefore = readFile(statmPath); + long const rssBeforeKB = detail::parseStatmRSSkB(statmBefore); + + struct rusage ru0{}; + bool const have_ru0 = getRusageThread(ru0); + + auto const t0 = std::chrono::steady_clock::now(); + + report.trimResult = detail::mallocTrimWithPad(TRIM_PAD); + + auto const t1 = std::chrono::steady_clock::now(); + + struct rusage ru1{}; + bool const have_ru1 = getRusageThread(ru1); + + auto const statmAfter = readFile(statmPath); + long const rssAfterKB = detail::parseStatmRSSkB(statmAfter); + + // Populate report fields + report.rssBeforeKB = rssBeforeKB; + report.rssAfterKB = rssAfterKB; + report.durationUs = std::chrono::duration_cast(t1 - t0); + + if (have_ru0 && have_ru1) + { + report.minfltDelta = ru1.ru_minflt - ru0.ru_minflt; + report.majfltDelta = ru1.ru_majflt - ru0.ru_majflt; + } + + std::int64_t const deltaKB = (rssBeforeKB < 0 || rssAfterKB < 0) + ? 0 + : (static_cast(rssAfterKB) - static_cast(rssBeforeKB)); + + JLOG(journal.debug()) << "malloc_trim tag=" << tagStr << " result=" << report.trimResult + << " pad=" << TRIM_PAD << " bytes" + << " rss_before=" << rssBeforeKB << "kB" + << " rss_after=" << rssAfterKB << "kB" + << " delta=" << deltaKB << "kB" + << " duration_us=" << report.durationUs.count() + << " minflt_delta=" << report.minfltDelta + << " majflt_delta=" << report.majfltDelta; + } + else + { + report.trimResult = detail::mallocTrimWithPad(TRIM_PAD); + } + +#endif + + return report; + + // LCOV_EXCL_STOP +} + +} // namespace xrpl diff --git a/src/tests/libxrpl/basics/MallocTrim.cpp b/src/tests/libxrpl/basics/MallocTrim.cpp new file mode 100644 index 0000000000..f01bd91bbf --- /dev/null +++ b/src/tests/libxrpl/basics/MallocTrim.cpp @@ -0,0 +1,209 @@ +#include + +#include + +#include + +using namespace xrpl; + +// cSpell:ignore statm + +#if defined(__GLIBC__) && BOOST_OS_LINUX +namespace xrpl::detail { +long +parseStatmRSSkB(std::string const& statm); +} // namespace xrpl::detail +#endif + +TEST(MallocTrimReport, structure) +{ + // Test default construction + MallocTrimReport report; + EXPECT_EQ(report.supported, false); + EXPECT_EQ(report.trimResult, -1); + EXPECT_EQ(report.rssBeforeKB, -1); + EXPECT_EQ(report.rssAfterKB, -1); + EXPECT_EQ(report.durationUs, std::chrono::microseconds{-1}); + EXPECT_EQ(report.minfltDelta, -1); + EXPECT_EQ(report.majfltDelta, -1); + EXPECT_EQ(report.deltaKB(), 0); + + // Test deltaKB calculation - memory freed + report.rssBeforeKB = 1000; + report.rssAfterKB = 800; + EXPECT_EQ(report.deltaKB(), -200); + + // Test deltaKB calculation - memory increased + report.rssBeforeKB = 500; + report.rssAfterKB = 600; + EXPECT_EQ(report.deltaKB(), 100); + + // Test deltaKB calculation - no change + report.rssBeforeKB = 1234; + report.rssAfterKB = 1234; + EXPECT_EQ(report.deltaKB(), 0); +} + +#if defined(__GLIBC__) && BOOST_OS_LINUX +TEST(parseStatmRSSkB, standard_format) +{ + using xrpl::detail::parseStatmRSSkB; + + // Test standard format: size resident shared text lib data dt + // Assuming 4KB page size: resident=1000 pages = 4000 KB + { + std::string statm = "25365 1000 2377 0 0 5623 0"; + long result = parseStatmRSSkB(statm); + // Note: actual result depends on system page size + // On most systems it's 4KB, so 1000 pages = 4000 KB + EXPECT_GT(result, 0); + } + + // Test with newline + { + std::string statm = "12345 2000 1234 0 0 3456 0\n"; + long result = parseStatmRSSkB(statm); + EXPECT_GT(result, 0); + } + + // Test with tabs + { + std::string statm = "12345\t2000\t1234\t0\t0\t3456\t0"; + long result = parseStatmRSSkB(statm); + EXPECT_GT(result, 0); + } + + // Test zero resident pages + { + std::string statm = "25365 0 2377 0 0 5623 0"; + long result = parseStatmRSSkB(statm); + EXPECT_EQ(result, 0); + } + + // Test with extra whitespace + { + std::string statm = " 25365 1000 2377 "; + long result = parseStatmRSSkB(statm); + EXPECT_GT(result, 0); + } + + // Test empty string + { + std::string statm = ""; + long result = parseStatmRSSkB(statm); + EXPECT_EQ(result, -1); + } + + // Test malformed data (only one field) + { + std::string statm = "25365"; + long result = parseStatmRSSkB(statm); + EXPECT_EQ(result, -1); + } + + // Test malformed data (non-numeric) + { + std::string statm = "abc def ghi"; + long result = parseStatmRSSkB(statm); + EXPECT_EQ(result, -1); + } + + // Test malformed data (second field non-numeric) + { + std::string statm = "25365 abc 2377"; + long result = parseStatmRSSkB(statm); + EXPECT_EQ(result, -1); + } +} +#endif + +TEST(mallocTrim, without_debug_logging) +{ + beast::Journal journal{beast::Journal::getNullSink()}; + + MallocTrimReport report = mallocTrim("without_debug", journal); + +#if defined(__GLIBC__) && BOOST_OS_LINUX + EXPECT_EQ(report.supported, true); + EXPECT_GE(report.trimResult, 0); + EXPECT_EQ(report.durationUs, std::chrono::microseconds{-1}); + EXPECT_EQ(report.minfltDelta, -1); + EXPECT_EQ(report.majfltDelta, -1); +#else + EXPECT_EQ(report.supported, false); + EXPECT_EQ(report.trimResult, -1); + EXPECT_EQ(report.rssBeforeKB, -1); + EXPECT_EQ(report.rssAfterKB, -1); + EXPECT_EQ(report.durationUs, std::chrono::microseconds{-1}); + EXPECT_EQ(report.minfltDelta, -1); + EXPECT_EQ(report.majfltDelta, -1); +#endif +} + +TEST(mallocTrim, empty_tag) +{ + beast::Journal journal{beast::Journal::getNullSink()}; + MallocTrimReport report = mallocTrim("", journal); + +#if defined(__GLIBC__) && BOOST_OS_LINUX + EXPECT_EQ(report.supported, true); + EXPECT_GE(report.trimResult, 0); +#else + EXPECT_EQ(report.supported, false); +#endif +} + +TEST(mallocTrim, with_debug_logging) +{ + struct DebugSink : public beast::Journal::Sink + { + DebugSink() : Sink(beast::severities::kDebug, false) + { + } + void + write(beast::severities::Severity, std::string const&) override + { + } + void + writeAlways(beast::severities::Severity, std::string const&) override + { + } + }; + + DebugSink sink; + beast::Journal journal{sink}; + + MallocTrimReport report = mallocTrim("debug_test", journal); + +#if defined(__GLIBC__) && BOOST_OS_LINUX + EXPECT_EQ(report.supported, true); + EXPECT_GE(report.trimResult, 0); + EXPECT_GE(report.durationUs.count(), 0); + EXPECT_GE(report.minfltDelta, 0); + EXPECT_GE(report.majfltDelta, 0); +#else + EXPECT_EQ(report.supported, false); + EXPECT_EQ(report.trimResult, -1); + EXPECT_EQ(report.durationUs, std::chrono::microseconds{-1}); + EXPECT_EQ(report.minfltDelta, -1); + EXPECT_EQ(report.majfltDelta, -1); +#endif +} + +TEST(mallocTrim, repeated_calls) +{ + beast::Journal journal{beast::Journal::getNullSink()}; + + // Call malloc_trim multiple times to ensure it's safe + for (int i = 0; i < 5; ++i) + { + MallocTrimReport report = mallocTrim("iteration_" + std::to_string(i), journal); + +#if defined(__GLIBC__) && BOOST_OS_LINUX + EXPECT_EQ(report.supported, true); + EXPECT_GE(report.trimResult, 0); +#else + EXPECT_EQ(report.supported, false); +#endif + } +} diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp index 91cc387d54..1162bc497a 100644 --- a/src/xrpld/app/main/Application.cpp +++ b/src/xrpld/app/main/Application.cpp @@ -31,6 +31,7 @@ #include #include +#include #include #include #include @@ -1053,6 +1054,8 @@ public: << "; size after: " << cachedSLEs_.size(); } + mallocTrim("doSweep", m_journal); + // Set timer to do another sweep later. setSweepTimer(); }