Skip to content

Commit b000f56

Browse files
committed
mempressure: Implement two-stage memory release for memory pressure events
Implements Luke's two-stage approach for responding to memory pressure: - Stage 1: Drop clean cache entries - Stage 2: If still under pressure, Sync() dirty entries then drop those Changes: - Add CCoinsViewCache::UncacheCleanEntries() to remove all clean entries - Add CCoinsViewCache::HandleMemoryPressure() encapsulating two-stage logic - Track memory pressure separately from cache size limits in FlushStateToDisk - Apply two-stage approach only to system memory pressure events Additional improvements: - Simplified scheduler to fixed 10-second interval (avoid self-referential lambda) - Added exception handling for std::stoull when parsing /proc/meminfo Addresses #216
1 parent d50a191 commit b000f56

File tree

5 files changed

+95
-35
lines changed

5 files changed

+95
-35
lines changed

src/coins.cpp

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <consensus/consensus.h>
88
#include <logging.h>
99
#include <random.h>
10+
#include <util/mempressure.h>
1011
#include <util/trace.h>
1112

1213
TRACEPOINT_SEMAPHORE(utxocache, add);
@@ -286,6 +287,48 @@ void CCoinsViewCache::Uncache(const COutPoint& hash)
286287
}
287288
}
288289

290+
size_t CCoinsViewCache::UncacheCleanEntries()
291+
{
292+
size_t freed = 0;
293+
for (auto it = cacheCoins.begin(); it != cacheCoins.end(); ) {
294+
// Only remove clean entries (not dirty, not fresh)
295+
if (!it->second.IsDirty() && !it->second.IsFresh()) {
296+
size_t coin_usage = it->second.coin.DynamicMemoryUsage();
297+
freed += coin_usage;
298+
cachedCoinsUsage -= coin_usage;
299+
it = cacheCoins.erase(it);
300+
} else {
301+
++it;
302+
}
303+
}
304+
return freed;
305+
}
306+
307+
bool CCoinsViewCache::HandleMemoryPressure()
308+
{
309+
// Stage 1: Drop clean entries (fast, no disk I/O)
310+
size_t freed = UncacheCleanEntries();
311+
LogPrintLevel(BCLog::COINDB, BCLog::Level::Info, "Memory pressure: freed %d bytes from clean cache entries\n", freed);
312+
313+
// Recheck memory pressure after freeing clean entries
314+
CheckMemoryPressure();
315+
316+
// Stage 2: If still under pressure, sync dirty entries and drop those
317+
if (SystemNeedsMemoryReleased()) {
318+
LogPrintLevel(BCLog::COINDB, BCLog::Level::Info, "Memory pressure: still critical after freeing clean entries, syncing dirty entries\n");
319+
if (!Sync()) {
320+
return false;
321+
}
322+
// Drop the newly-cleaned entries
323+
size_t freed_stage2 = UncacheCleanEntries();
324+
LogPrintLevel(BCLog::COINDB, BCLog::Level::Info, "Memory pressure: freed additional %d bytes after sync, total freed %d bytes\n", freed_stage2, freed + freed_stage2);
325+
326+
// Final recheck
327+
CheckMemoryPressure();
328+
}
329+
return true;
330+
}
331+
289332
unsigned int CCoinsViewCache::GetCacheSize() const {
290333
return cacheCoins.size();
291334
}

src/coins.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,21 @@ class CCoinsViewCache : public CCoinsViewBacked
460460
*/
461461
void Uncache(const COutPoint &outpoint);
462462

463+
/**
464+
* Removes all clean (not dirty, not fresh) entries from the cache.
465+
* Used during memory pressure to free memory without disk I/O.
466+
* Returns the amount of memory freed in bytes.
467+
*/
468+
size_t UncacheCleanEntries();
469+
470+
/**
471+
* Two-stage memory pressure response.
472+
* Stage 1: Drop clean entries (no disk I/O).
473+
* Stage 2: If still pressure, Sync() dirty entries then drop those.
474+
* Returns false if Sync() fails, true otherwise.
475+
*/
476+
bool HandleMemoryPressure();
477+
463478
//! Calculate the size of the cache (in number of transaction outputs)
464479
unsigned int GetCacheSize() const;
465480

src/init.cpp

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1605,28 +1605,11 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
16051605
}
16061606
}, std::chrono::minutes{5});
16071607

1608-
// Check system memory pressure periodically (dynamic interval based on IBD status).
1609-
// During IBD: check every 10 seconds for faster response to memory pressure.
1610-
// During normal operation: check every 10 minutes (between block arrivals).
1611-
// FlushStateToDisk calls SystemNeedsMemoryReleased() which uses the cached flag.
1612-
std::function<void()> check_memory_pressure;
1613-
check_memory_pressure = [&node, &scheduler, &check_memory_pressure]() {
1614-
// Check memory pressure and update the flag
1608+
// Check system memory pressure every 10 seconds.
1609+
// Updates g_system_needs_memory_released flag used by FlushStateToDisk.
1610+
scheduler.scheduleEvery([]{
16151611
CheckMemoryPressure();
1616-
1617-
// Determine next check interval based on IBD status
1618-
std::chrono::milliseconds next_interval;
1619-
if (node.chainman && node.chainman->IsInitialBlockDownload()) {
1620-
next_interval = std::chrono::seconds{10}; // Fast checks during IBD
1621-
} else {
1622-
next_interval = std::chrono::minutes{10}; // Slower checks during normal operation
1623-
}
1624-
1625-
// Reschedule ourselves with the appropriate interval
1626-
scheduler.scheduleFromNow(check_memory_pressure, next_interval);
1627-
};
1628-
// Initial schedule - start with fast interval
1629-
scheduler.scheduleFromNow(check_memory_pressure, std::chrono::seconds{10});
1612+
}, std::chrono::seconds{10});
16301613

16311614
if (args.GetBoolArg("-logratelimit", BCLog::DEFAULT_LOGRATELIMIT)) {
16321615
LogInstance().SetRateLimiting(BCLog::LogRateLimiter::Create(

src/util/mempressure.cpp

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include <atomic>
1717
#include <cstddef>
1818
#include <cstdint>
19+
#include <exception>
1920
#include <fstream>
2021
#include <string>
2122

@@ -54,12 +55,24 @@ void CheckMemoryPressure()
5455
// Format: "MemAvailable: 1234567 kB"
5556
size_t pos = line.find_first_of("0123456789");
5657
if (pos != std::string::npos) {
57-
uint64_t mem_kb = std::stoull(line.substr(pos));
58-
uint64_t available_memory = mem_kb * 1024; // Convert kB to bytes
58+
try {
59+
uint64_t mem_kb = std::stoull(line.substr(pos));
5960

60-
if (available_memory < g_low_memory_threshold) {
61-
LogPrintf("%s: YES: %s available memory\n", __func__, available_memory);
62-
needs_release = true;
61+
// Check for overflow before multiplication
62+
if (mem_kb > UINT64_MAX / 1024) {
63+
// Treat huge values as "plenty of memory"
64+
break;
65+
}
66+
67+
uint64_t available_memory = mem_kb * 1024; // Convert kB to bytes
68+
69+
if (available_memory < g_low_memory_threshold) {
70+
LogPrintf("%s: YES: %s available memory\n", __func__, available_memory);
71+
needs_release = true;
72+
}
73+
} catch (const std::exception& e) {
74+
// Invalid format in /proc/meminfo - ignore and leave needs_release as false
75+
LogPrintf("%s: Failed to parse MemAvailable from /proc/meminfo: %s\n", __func__, e.what());
6376
}
6477
}
6578
break;

src/validation.cpp

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3144,11 +3144,14 @@ bool Chainstate::FlushStateToDisk(
31443144
// The cache is large and we're within 10% and 10 MiB of the limit, but we have time now (not in the middle of a block processing).
31453145
bool fCacheLarge = mode == FlushStateMode::PERIODIC && cache_state >= CoinsCacheSizeState::LARGE;
31463146
bool fCacheCritical = false;
3147+
bool fMemoryPressure = false;
31473148
if (mode == FlushStateMode::IF_NEEDED) {
31483149
if (cache_state >= CoinsCacheSizeState::CRITICAL) {
31493150
// The cache is over the limit, we have to write now.
31503151
fCacheCritical = true;
31513152
} else if (SystemNeedsMemoryReleased()) {
3153+
// System memory pressure - use two-stage approach
3154+
fMemoryPressure = true;
31523155
fCacheCritical = true;
31533156
}
31543157
}
@@ -3203,24 +3206,27 @@ bool Chainstate::FlushStateToDisk(
32033206
return FatalError(m_chainman.GetNotifications(), state, _("Disk space is too low!"));
32043207
}
32053208
// Flush the chainstate (which may refer to block index entries).
3206-
const auto empty_cache{(mode == FlushStateMode::ALWAYS) || fCacheLarge || fCacheCritical};
3207-
if (empty_cache ? !CoinsTip().Flush() : !CoinsTip().Sync()) {
3208-
return FatalError(m_chainman.GetNotifications(), state, _("Failed to write to coin database."));
3209+
if (fMemoryPressure) {
3210+
// Two-stage memory pressure response (only for system memory pressure)
3211+
if (!CoinsTip().HandleMemoryPressure()) {
3212+
return FatalError(m_chainman.GetNotifications(), state, _("Failed to write to coin database."));
3213+
}
3214+
} else {
3215+
// Normal flush/sync (cache size limits, periodic writes, etc.)
3216+
const auto empty_cache{(mode == FlushStateMode::ALWAYS) || fCacheLarge || fCacheCritical};
3217+
if (empty_cache ? !CoinsTip().Flush() : !CoinsTip().Sync()) {
3218+
return FatalError(m_chainman.GetNotifications(), state, _("Failed to write to coin database."));
3219+
}
32093220
}
32103221
full_flush_completed = true;
3222+
32113223
TRACEPOINT(utxocache, flush,
32123224
int64_t{Ticks<std::chrono::microseconds>(NodeClock::now() - nNow)},
32133225
(uint32_t)mode,
32143226
(uint64_t)coins_count,
32153227
(uint64_t)coins_mem_usage,
32163228
(bool)fFlushForPrune);
32173229

3218-
// After flushing, immediately recheck memory pressure to update the flag
3219-
// This prevents repeated flushes on the same memory pressure event
3220-
if (SystemNeedsMemoryReleased()) {
3221-
CheckMemoryPressure();
3222-
}
3223-
32243230
}
32253231
}
32263232

0 commit comments

Comments
 (0)