diff --git a/src/common/system.cpp b/src/common/system.cpp index 7af792db44c..cf032adfced 100644 --- a/src/common/system.cpp +++ b/src/common/system.cpp @@ -11,19 +11,25 @@ #include #include -#ifndef WIN32 -#include -#else -#include +#ifdef WIN32 #include +#include +#include +#else +#include +#include #endif #ifdef HAVE_MALLOPT_ARENA_MAX #include #endif +#include +#include +#include #include #include +#include #include #include #include @@ -105,6 +111,17 @@ int GetNumCores() return std::thread::hardware_concurrency(); } +std::optional GetTotalRAM() +{ + auto clamp{[](uint64_t v) { return size_t(std::min(v, uint64_t{std::numeric_limits::max()})); }}; +#ifdef WIN32 + if (MEMORYSTATUSEX m{}; (m.dwLength = sizeof(m), GlobalMemoryStatusEx(&m))) return clamp(m.ullTotalPhys); +#elif defined(__linux__) || defined(__APPLE__) + if (long p{sysconf(_SC_PHYS_PAGES)}, s{sysconf(_SC_PAGESIZE)}; p > 0 && s > 0) return clamp(1ULL * p * s); +#endif + return std::nullopt; +} + // Obtain the application startup time (used for uptime calculation) int64_t GetStartupTime() { diff --git a/src/common/system.h b/src/common/system.h index a4b56be9ac9..2184f1d4c9e 100644 --- a/src/common/system.h +++ b/src/common/system.h @@ -9,6 +9,7 @@ #include // IWYU pragma: keep #include +#include #include // Application startup time (used for uptime calculation) @@ -29,4 +30,9 @@ void runCommand(const std::string& strCommand); */ int GetNumCores(); +/** + * Return the total RAM available on the current system, if detectable. + */ +std::optional GetTotalRAM(); + #endif // BITCOIN_COMMON_SYSTEM_H diff --git a/src/init.cpp b/src/init.cpp index 4ff57a34171..2f518eba14c 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1767,6 +1767,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) // ********************************************************* Step 7: load block chain // cache size calculations + node::LogOversizedDbCache(args); const auto [index_cache_sizes, kernel_cache_sizes] = CalculateCacheSizes(args, g_enabled_filter_types.size()); LogInfo("Cache configuration:"); diff --git a/src/node/caches.cpp b/src/node/caches.cpp index d5d69fc2044..e4a5e6d17e3 100644 --- a/src/node/caches.cpp +++ b/src/node/caches.cpp @@ -5,9 +5,12 @@ #include #include +#include #include #include #include +#include +#include #include #include @@ -23,16 +26,20 @@ static constexpr size_t MAX_FILTER_INDEX_CACHE{1024_MiB}; static constexpr size_t MAX_32BIT_DBCACHE{1024_MiB}; namespace node { +size_t CalculateDbCacheBytes(const ArgsManager& args) +{ + if (auto db_cache{args.GetIntArg("-dbcache")}) { + if (*db_cache < 0) db_cache = 0; + const uint64_t db_cache_bytes{SaturatingLeftShift(*db_cache, 20)}; + constexpr auto max_db_cache{sizeof(void*) == 4 ? MAX_32BIT_DBCACHE : std::numeric_limits::max()}; + return std::max(MIN_DB_CACHE, std::min(db_cache_bytes, max_db_cache)); + } + return DEFAULT_DB_CACHE; +} + CacheSizes CalculateCacheSizes(const ArgsManager& args, size_t n_indexes) { - // Convert -dbcache from MiB units to bytes. The total cache is floored by MIN_DB_CACHE and capped by max size_t value. - size_t total_cache{DEFAULT_DB_CACHE}; - if (std::optional db_cache = args.GetIntArg("-dbcache")) { - if (*db_cache < 0) db_cache = 0; - uint64_t db_cache_bytes = SaturatingLeftShift(*db_cache, 20); - constexpr auto max_db_cache{sizeof(void*) == 4 ? MAX_32BIT_DBCACHE : std::numeric_limits::max()}; - total_cache = std::max(MIN_DB_CACHE, std::min(db_cache_bytes, max_db_cache)); - } + size_t total_cache{CalculateDbCacheBytes(args)}; IndexCacheSizes index_sizes; index_sizes.tx_index = std::min(total_cache / 8, args.GetBoolArg("-txindex", DEFAULT_TXINDEX) ? MAX_TX_INDEX_CACHE : 0); @@ -44,4 +51,15 @@ CacheSizes CalculateCacheSizes(const ArgsManager& args, size_t n_indexes) } return {index_sizes, kernel::CacheSizes{total_cache}}; } + +void LogOversizedDbCache(const ArgsManager& args) noexcept +{ + if (const auto total_ram{GetTotalRAM()}) { + const size_t db_cache{CalculateDbCacheBytes(args)}; + if (ShouldWarnOversizedDbCache(db_cache, *total_ram)) { + InitWarning(bilingual_str{tfm::format(_("A %zu MiB dbcache may be too large for a system memory of only %zu MiB."), + db_cache >> 20, *total_ram >> 20)}); + } + } +} } // namespace node diff --git a/src/node/caches.h b/src/node/caches.h index f24e9cc9103..c0ef41ec696 100644 --- a/src/node/caches.h +++ b/src/node/caches.h @@ -27,6 +27,13 @@ struct CacheSizes { kernel::CacheSizes kernel; }; CacheSizes CalculateCacheSizes(const ArgsManager& args, size_t n_indexes = 0); +constexpr bool ShouldWarnOversizedDbCache(size_t dbcache, size_t total_ram) noexcept +{ + const size_t cap{(total_ram < 2048_MiB) ? DEFAULT_DB_CACHE : (total_ram / 100) * 75}; + return dbcache > cap; +} + +void LogOversizedDbCache(const ArgsManager& args) noexcept; } // namespace node #endif // BITCOIN_NODE_CACHES_H diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index a818dba7205..c963afff351 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -25,6 +25,7 @@ add_executable(test_bitcoin blockmanager_tests.cpp bloom_tests.cpp bswap_tests.cpp + caches_tests.cpp chainstate_write_tests.cpp checkqueue_tests.cpp cluster_linearize_tests.cpp diff --git a/src/test/caches_tests.cpp b/src/test/caches_tests.cpp new file mode 100644 index 00000000000..3086118fc7e --- /dev/null +++ b/src/test/caches_tests.cpp @@ -0,0 +1,40 @@ +#include +#include + +#include + +using namespace node; + +BOOST_AUTO_TEST_SUITE(caches_tests) + +BOOST_AUTO_TEST_CASE(oversized_dbcache_warning) +{ + // memory restricted setup - cap is DEFAULT_DB_CACHE (450 MiB) + BOOST_CHECK(!ShouldWarnOversizedDbCache(/*dbcache=*/4_MiB, /*total_ram=*/1024_MiB)); // Under cap + BOOST_CHECK( ShouldWarnOversizedDbCache(/*dbcache=*/512_MiB, /*total_ram=*/1024_MiB)); // At cap + BOOST_CHECK( ShouldWarnOversizedDbCache(/*dbcache=*/1500_MiB, /*total_ram=*/1024_MiB)); // Over cap + + // 2 GiB RAM - cap is 75% + BOOST_CHECK(!ShouldWarnOversizedDbCache(/*dbcache=*/1500_MiB, /*total_ram=*/2048_MiB)); // Under cap + BOOST_CHECK( ShouldWarnOversizedDbCache(/*dbcache=*/1600_MiB, /*total_ram=*/2048_MiB)); // Over cap + + if constexpr (SIZE_MAX == UINT64_MAX) { + // 4 GiB RAM - cap is 75% + BOOST_CHECK(!ShouldWarnOversizedDbCache(/*dbcache=*/2500_MiB, /*total_ram=*/4096_MiB)); // Under cap + BOOST_CHECK( ShouldWarnOversizedDbCache(/*dbcache=*/3500_MiB, /*total_ram=*/4096_MiB)); // Over cap + + // 8 GiB RAM - cap is 75% + BOOST_CHECK(!ShouldWarnOversizedDbCache(/*dbcache=*/6000_MiB, /*total_ram=*/8192_MiB)); // Under cap + BOOST_CHECK( ShouldWarnOversizedDbCache(/*dbcache=*/7000_MiB, /*total_ram=*/8192_MiB)); // Over cap + + // 16 GiB RAM - cap is 75% + BOOST_CHECK(!ShouldWarnOversizedDbCache(/*dbcache=*/10'000_MiB, /*total_ram=*/16384_MiB)); // Under cap + BOOST_CHECK( ShouldWarnOversizedDbCache(/*dbcache=*/15'000_MiB, /*total_ram=*/16384_MiB)); // Over cap + + // 32 GiB RAM - cap is 75% + BOOST_CHECK(!ShouldWarnOversizedDbCache(/*dbcache=*/20'000_MiB, /*total_ram=*/32768_MiB)); // Under cap + BOOST_CHECK( ShouldWarnOversizedDbCache(/*dbcache=*/30'000_MiB, /*total_ram=*/32768_MiB)); // Over cap + } +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/system_tests.cpp b/src/test/system_tests.cpp index dec4d418290..53db32007ee 100644 --- a/src/test/system_tests.cpp +++ b/src/test/system_tests.cpp @@ -8,6 +8,8 @@ #include #include +#include + #ifdef ENABLE_EXTERNAL_SIGNER #include #endif // ENABLE_EXTERNAL_SIGNER @@ -16,6 +18,17 @@ BOOST_FIXTURE_TEST_SUITE(system_tests, BasicTestingSetup) +BOOST_AUTO_TEST_CASE(total_ram) +{ + BOOST_CHECK_GE(GetTotalRAM(), 1000_MiB); + + if constexpr (SIZE_MAX == UINT64_MAX) { + // Upper bound check only on 64-bit: 32-bit systems can reasonably have max memory, + // but extremely large values on 64-bit likely indicate detection errors + BOOST_CHECK_LT(GetTotalRAM(), 10'000'000_MiB); // >10 TiB memory is unlikely + } +} + #ifdef ENABLE_EXTERNAL_SIGNER BOOST_AUTO_TEST_CASE(run_command)