Skip to content

Commit 7889ded

Browse files
author
steve.sterling
committed
systemmemory: flush memory profiling
1 parent 5e3dce0 commit 7889ded

File tree

11 files changed

+701
-52
lines changed

11 files changed

+701
-52
lines changed

memory_profiler.cpp

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// Copyright (c) 2025 The Bitcoin Knots developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#if defined(HAVE_CONFIG_H)
6+
#include <config/bitcoin-config.h>
7+
#endif
8+
9+
#ifdef ENABLE_LEVELDB_METRICS
10+
11+
#include <node/memory_profiler.h>
12+
13+
#include <dbwrapper.h>
14+
#include <logging.h>
15+
#include <util/sysmemory.h>
16+
17+
#include <algorithm>
18+
#include <numeric>
19+
#include <cmath>
20+
21+
std::unique_ptr<MemoryProfiler> g_memory_profiler;
22+
23+
MemoryProfiler::MemoryProfiler()
24+
{
25+
m_profiler_thread = std::make_unique<std::thread>(&MemoryProfiler::ProfilerThread, this);
26+
}
27+
28+
MemoryProfiler::~MemoryProfiler()
29+
{
30+
m_shutdown = true;
31+
{
32+
std::lock_guard<std::mutex> cv_lock(m_cv_mutex);
33+
m_cv.notify_all();
34+
}
35+
if (m_profiler_thread && m_profiler_thread->joinable()) {
36+
m_profiler_thread->join();
37+
}
38+
}
39+
40+
void MemoryProfiler::StartFlushProfiling()
41+
{
42+
{
43+
LOCK(m_mutex);
44+
if (m_profiling) {
45+
LogPrint(BCLog::DBCACHE, "MemoryProfiler: Already profiling, stopping previous profile\n");
46+
// Need to unlock before calling StopFlushProfiling
47+
}
48+
}
49+
50+
if (m_profiling) {
51+
StopFlushProfiling();
52+
}
53+
54+
LOCK(m_mutex);
55+
56+
m_current_profile = std::make_unique<FlushProfile>();
57+
m_current_profile->start_time = std::chrono::steady_clock::now();
58+
59+
// Capture initial memory state (lock already held)
60+
auto initial_sample = CaptureMemorySampleLocked("pre_flush", nullptr, true);
61+
m_current_profile->pre_flush_memory_mb = initial_sample.process_rss_mb;
62+
m_current_profile->peak_memory_mb = initial_sample.process_rss_mb;
63+
m_current_profile->samples.push_back(initial_sample);
64+
65+
m_profiling = true;
66+
67+
// Notify profiler thread
68+
{
69+
std::lock_guard<std::mutex> cv_lock(m_cv_mutex);
70+
m_cv.notify_one();
71+
}
72+
73+
LogPrint(BCLog::DBCACHE, "MemoryProfiler: Started profiling flush operation\n");
74+
}
75+
76+
std::unique_ptr<MemoryProfiler::FlushProfile> MemoryProfiler::StopFlushProfiling()
77+
{
78+
LOCK(m_mutex);
79+
80+
if (!m_current_profile) {
81+
return nullptr;
82+
}
83+
84+
m_profiling = false;
85+
86+
// Capture final memory state (lock already held)
87+
auto final_sample = CaptureMemorySampleLocked("post_flush", nullptr, true);
88+
m_current_profile->samples.push_back(final_sample);
89+
m_current_profile->post_flush_memory_mb = final_sample.process_rss_mb;
90+
m_current_profile->end_time = std::chrono::steady_clock::now();
91+
92+
// Calculate peak from all samples
93+
for (const auto& sample : m_current_profile->samples) {
94+
m_current_profile->peak_memory_mb = std::max(m_current_profile->peak_memory_mb, sample.process_rss_mb);
95+
}
96+
97+
// Store in history
98+
m_flush_history.push_back(*m_current_profile);
99+
if (m_flush_history.size() > MAX_HISTORY_SIZE) {
100+
m_flush_history.pop_front();
101+
}
102+
103+
LogPrint(BCLog::DBCACHE, "MemoryProfiler: Flush profile complete - peak overhead: %zu MB\n",
104+
m_current_profile->peak_memory_mb - m_current_profile->pre_flush_memory_mb);
105+
106+
return std::move(m_current_profile);
107+
}
108+
109+
110+
std::vector<MemoryProfiler::FlushProfile> MemoryProfiler::GetRecentProfiles(size_t count) const
111+
{
112+
LOCK(m_mutex);
113+
114+
std::vector<FlushProfile> result;
115+
size_t start_idx = m_flush_history.size() > count ? m_flush_history.size() - count : 0;
116+
117+
for (size_t i = start_idx; i < m_flush_history.size(); ++i) {
118+
result.push_back(m_flush_history[i]);
119+
}
120+
121+
return result;
122+
}
123+
124+
void MemoryProfiler::ProfilerThread()
125+
{
126+
while (!m_shutdown) {
127+
// Use cv_mutex for condition variable
128+
std::unique_lock<std::mutex> cv_lock(m_cv_mutex);
129+
130+
// Wait for either profiling to start or shutdown
131+
m_cv.wait_for(cv_lock, std::chrono::milliseconds(100), [this] {
132+
return m_profiling.load() || m_shutdown.load();
133+
});
134+
135+
cv_lock.unlock();
136+
137+
if (m_shutdown) break;
138+
139+
if (m_profiling) {
140+
LOCK(m_mutex);
141+
if (m_current_profile) {
142+
// Need to capture sample without holding m_mutex to avoid deadlock
143+
auto profile_copy = *m_current_profile; // Copy current profile state
144+
LEAVE_CRITICAL_SECTION(m_mutex);
145+
146+
auto sample = CaptureMemorySample("profiling", nullptr);
147+
148+
ENTER_CRITICAL_SECTION(m_mutex);
149+
// Double-check we still have a profile after reacquiring lock
150+
if (m_current_profile && m_profiling) {
151+
m_current_profile->samples.push_back(sample);
152+
m_current_profile->peak_memory_mb = std::max(m_current_profile->peak_memory_mb, sample.process_rss_mb);
153+
}
154+
}
155+
}
156+
}
157+
}
158+
159+
MemoryProfiler::MemorySample MemoryProfiler::CaptureMemorySample(const std::string& phase, const CDBWrapper* db) const
160+
{
161+
return CaptureMemorySampleLocked(phase, db, false);
162+
}
163+
164+
MemoryProfiler::MemorySample MemoryProfiler::CaptureMemorySampleLocked(const std::string& phase, const CDBWrapper* db, bool lock_already_held) const
165+
{
166+
MemorySample sample;
167+
sample.timestamp = std::chrono::steady_clock::now();
168+
sample.phase = phase;
169+
170+
// Just get RSS - nothing else
171+
size_t rss_bytes = GetProcessRSS();
172+
sample.process_rss_mb = rss_bytes > 0 ? rss_bytes / (1 << 20) : 0;
173+
174+
return sample;
175+
}
176+
177+
#endif // ENABLE_LEVELDB_METRICS

memory_profiler.h

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright (c) 2025 The Bitcoin Knots developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#ifndef BITCOIN_NODE_MEMORY_PROFILER_H
6+
#define BITCOIN_NODE_MEMORY_PROFILER_H
7+
8+
#if defined(HAVE_CONFIG_H)
9+
#include <config/bitcoin-config.h>
10+
#endif
11+
12+
#ifdef ENABLE_LEVELDB_METRICS
13+
14+
#include <sync.h>
15+
#include <util/time.h>
16+
17+
#include <atomic>
18+
#include <chrono>
19+
#include <condition_variable>
20+
#include <deque>
21+
#include <memory>
22+
#include <mutex>
23+
#include <thread>
24+
25+
class CDBWrapper;
26+
27+
/**
28+
* Memory profiler for tracking memory usage during database operations
29+
* Particularly focused on understanding memory overhead during LevelDB flushes
30+
*/
31+
class MemoryProfiler
32+
{
33+
public:
34+
struct MemorySample {
35+
std::chrono::steady_clock::time_point timestamp;
36+
size_t process_rss_mb; // Process resident set size
37+
std::string phase; // Current operation phase
38+
};
39+
40+
struct FlushProfile {
41+
std::chrono::steady_clock::time_point start_time;
42+
std::chrono::steady_clock::time_point end_time;
43+
size_t pre_flush_memory_mb;
44+
size_t peak_memory_mb;
45+
size_t post_flush_memory_mb;
46+
std::vector<MemorySample> samples;
47+
48+
int64_t GetDurationMs() const {
49+
return std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count();
50+
}
51+
};
52+
53+
MemoryProfiler();
54+
~MemoryProfiler();
55+
56+
// Start profiling a flush operation
57+
void StartFlushProfiling() EXCLUSIVE_LOCKS_REQUIRED(!m_mutex);
58+
59+
// Stop profiling and return the profile
60+
std::unique_ptr<FlushProfile> StopFlushProfiling() EXCLUSIVE_LOCKS_REQUIRED(!m_mutex);
61+
62+
// Get historical flush profiles
63+
std::vector<FlushProfile> GetRecentProfiles(size_t count = 10) const EXCLUSIVE_LOCKS_REQUIRED(!m_mutex);
64+
65+
66+
private:
67+
void ProfilerThread() NO_THREAD_SAFETY_ANALYSIS;
68+
MemorySample CaptureMemorySample(const std::string& phase, const CDBWrapper* db) const NO_THREAD_SAFETY_ANALYSIS;
69+
MemorySample CaptureMemorySampleLocked(const std::string& phase, const CDBWrapper* db, bool lock_already_held) const NO_THREAD_SAFETY_ANALYSIS;
70+
71+
mutable Mutex m_mutex;
72+
73+
// Current profiling state
74+
std::atomic<bool> m_profiling{false};
75+
std::unique_ptr<FlushProfile> m_current_profile GUARDED_BY(m_mutex);
76+
77+
// Historical data
78+
std::deque<FlushProfile> m_flush_history GUARDED_BY(m_mutex);
79+
static constexpr size_t MAX_HISTORY_SIZE = 100;
80+
81+
// Profiler thread
82+
std::unique_ptr<std::thread> m_profiler_thread;
83+
std::atomic<bool> m_shutdown{false};
84+
mutable std::mutex m_cv_mutex;
85+
mutable std::condition_variable m_cv;
86+
};
87+
88+
// Global instance (initialized in init.cpp if profiling is enabled)
89+
extern std::unique_ptr<MemoryProfiler> g_memory_profiler;
90+
91+
#endif // ENABLE_LEVELDB_METRICS
92+
93+
#endif // BITCOIN_NODE_MEMORY_PROFILER_H

src/init.cpp

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
#include <util/check.h>
8282
#include <util/fs.h>
8383
#include <util/fs_helpers.h>
84-
#include <util/mempressure.h>
84+
#include <util/systemmemory.h>
8585
#include <util/moneystr.h>
8686
#include <util/result.h>
8787
#include <util/signalinterrupt.h>
@@ -403,6 +403,7 @@ void Shutdown(NodeContext& node)
403403
node.chainman.reset();
404404
node.validation_signals.reset();
405405
node.scheduler.reset();
406+
g_memory_profiler.reset();
406407
node.ecc_context.reset();
407408
node.kernel.reset();
408409

@@ -1605,11 +1606,15 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
16051606
}
16061607
}, std::chrono::minutes{5});
16071608

1608-
// Check system memory pressure every 10 seconds.
1609+
// Initialize memory profiler for tracking flush overhead
1610+
assert(!g_memory_profiler);
1611+
g_memory_profiler = std::make_unique<MemoryProfiler>();
1612+
1613+
// Check system memory pressure every 5 seconds.
16091614
// Updates g_system_needs_memory_released flag used by FlushStateToDisk.
16101615
scheduler.scheduleEvery([]{
16111616
CheckMemoryPressure();
1612-
}, std::chrono::seconds{10});
1617+
}, std::chrono::seconds{5});
16131618

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

src/test/validation_chainstate_tests.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
#include <test/util/setup_common.h>
1515
#include <uint256.h>
1616
#include <util/check.h>
17-
#include <util/mempressure.h>
17+
#include <util/systemmemory.h>
1818
#include <validation.h>
1919

2020
#include <vector>

src/util/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ add_library(bitcoin_util STATIC EXCLUDE_FROM_ALL
1515
fs_helpers.cpp
1616
hasher.cpp
1717
ioprio.cpp
18-
mempressure.cpp
18+
systemmemory.cpp
19+
memory_profiler.cpp
1920
moneystr.cpp
2021
rbf.cpp
2122
readwritefile.cpp

0 commit comments

Comments
 (0)