Skip to content

Commit 83d51c7

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

File tree

9 files changed

+431
-52
lines changed

9 files changed

+431
-52
lines changed

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

src/util/memory_profiler.cpp

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
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+
#include <bitcoin-build-config.h> // IWYU pragma: keep
6+
7+
#include <util/memory_profiler.h>
8+
9+
#include <algorithm>
10+
11+
#ifdef WIN32
12+
#include <windows.h>
13+
#include <psapi.h>
14+
#elif defined(__linux__)
15+
#include <fstream>
16+
#include <sstream>
17+
#include <string>
18+
#elif defined(__APPLE__)
19+
#include <sys/resource.h>
20+
#include <mach/mach.h>
21+
#endif
22+
23+
std::unique_ptr<MemoryProfiler> g_memory_profiler;
24+
25+
MemoryProfiler::MemoryProfiler()
26+
{
27+
m_profiler_thread = std::make_unique<std::thread>(&MemoryProfiler::ProfilerThread, this);
28+
}
29+
30+
MemoryProfiler::~MemoryProfiler()
31+
{
32+
m_shutdown = true;
33+
{
34+
std::lock_guard<std::mutex> cv_lock(m_cv_mutex);
35+
m_cv.notify_all();
36+
}
37+
if (m_profiler_thread && m_profiler_thread->joinable()) {
38+
m_profiler_thread->join();
39+
}
40+
}
41+
42+
size_t MemoryProfiler::GetProcessRSS()
43+
{
44+
#ifdef WIN32
45+
PROCESS_MEMORY_COUNTERS_EX pmc;
46+
if (GetProcessMemoryInfo(GetCurrentProcess(), (PROCESS_MEMORY_COUNTERS*)&pmc, sizeof(pmc))) {
47+
return static_cast<size_t>(pmc.PrivateUsage); // Current commit charge
48+
}
49+
#elif defined(__linux__)
50+
std::ifstream status("/proc/self/status");
51+
std::string line;
52+
while (std::getline(status, line)) {
53+
if (line.compare(0, 6, "VmRSS:") == 0) {
54+
size_t pos = line.find_first_of("0123456789");
55+
if (pos != std::string::npos) {
56+
size_t kb = std::stoull(line.substr(pos));
57+
return kb * 1024;
58+
}
59+
}
60+
}
61+
#elif defined(__APPLE__)
62+
struct mach_task_basic_info info;
63+
mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT;
64+
if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &count) == KERN_SUCCESS) {
65+
return static_cast<size_t>(info.resident_size);
66+
}
67+
#endif
68+
return 0;
69+
}
70+
71+
void MemoryProfiler::StartFlushProfiling()
72+
{
73+
// Check if already profiling and stop previous profile if needed
74+
if (m_profiling.load()) {
75+
StopFlushProfiling();
76+
}
77+
78+
LOCK(m_mutex);
79+
80+
m_current_profile = std::make_unique<FlushProfile>();
81+
m_current_profile->start_time = std::chrono::steady_clock::now();
82+
83+
// Capture initial memory state (lock already held)
84+
auto initial_sample = CaptureMemorySampleLocked("pre_flush", true);
85+
m_current_profile->pre_flush_memory_mb = initial_sample.process_rss_mb;
86+
m_current_profile->peak_memory_mb = initial_sample.process_rss_mb;
87+
m_current_profile->samples.push_back(initial_sample);
88+
89+
m_profiling = true;
90+
91+
// Notify profiler thread
92+
{
93+
std::lock_guard<std::mutex> cv_lock(m_cv_mutex);
94+
m_cv.notify_one();
95+
}
96+
}
97+
98+
std::unique_ptr<MemoryProfiler::FlushProfile> MemoryProfiler::StopFlushProfiling()
99+
{
100+
LOCK(m_mutex);
101+
102+
if (!m_current_profile) {
103+
return nullptr;
104+
}
105+
106+
m_profiling = false;
107+
108+
// Capture final memory state (lock already held)
109+
auto final_sample = CaptureMemorySampleLocked("post_flush", true);
110+
m_current_profile->samples.push_back(final_sample);
111+
m_current_profile->post_flush_memory_mb = final_sample.process_rss_mb;
112+
m_current_profile->end_time = std::chrono::steady_clock::now();
113+
114+
// Calculate peak from all samples
115+
for (const auto& sample : m_current_profile->samples) {
116+
m_current_profile->peak_memory_mb = std::max(m_current_profile->peak_memory_mb, sample.process_rss_mb);
117+
}
118+
119+
// Store in history
120+
m_flush_history.push_back(*m_current_profile);
121+
if (m_flush_history.size() > MAX_HISTORY_SIZE) {
122+
m_flush_history.pop_front();
123+
}
124+
125+
return std::move(m_current_profile);
126+
}
127+
128+
std::vector<MemoryProfiler::FlushProfile> MemoryProfiler::GetRecentProfiles(size_t count) const
129+
{
130+
LOCK(m_mutex);
131+
132+
std::vector<FlushProfile> result;
133+
size_t start_idx = m_flush_history.size() > count ? m_flush_history.size() - count : 0;
134+
135+
for (size_t i = start_idx; i < m_flush_history.size(); ++i) {
136+
result.push_back(m_flush_history[i]);
137+
}
138+
139+
return result;
140+
}
141+
142+
void MemoryProfiler::ProfilerThread()
143+
{
144+
while (!m_shutdown) {
145+
// Use cv_mutex for condition variable
146+
std::unique_lock<std::mutex> cv_lock(m_cv_mutex);
147+
148+
// Wait for either profiling to start or shutdown
149+
m_cv.wait_for(cv_lock, std::chrono::milliseconds(100), [this] {
150+
return m_profiling.load() || m_shutdown.load();
151+
});
152+
153+
cv_lock.unlock();
154+
155+
if (m_shutdown) break;
156+
157+
if (m_profiling) {
158+
LOCK(m_mutex);
159+
if (m_current_profile) {
160+
// Need to capture sample without holding m_mutex to avoid deadlock
161+
auto profile_copy = *m_current_profile; // Copy current profile state
162+
LEAVE_CRITICAL_SECTION(m_mutex);
163+
164+
auto sample = CaptureMemorySample("profiling");
165+
166+
ENTER_CRITICAL_SECTION(m_mutex);
167+
// Double-check we still have a profile after reacquiring lock
168+
if (m_current_profile && m_profiling) {
169+
m_current_profile->samples.push_back(sample);
170+
m_current_profile->peak_memory_mb = std::max(m_current_profile->peak_memory_mb, sample.process_rss_mb);
171+
}
172+
}
173+
}
174+
}
175+
}
176+
177+
MemoryProfiler::MemorySample MemoryProfiler::CaptureMemorySample(const std::string& phase) const
178+
{
179+
return CaptureMemorySampleLocked(phase, false);
180+
}
181+
182+
MemoryProfiler::MemorySample MemoryProfiler::CaptureMemorySampleLocked(const std::string& phase, bool lock_already_held) const
183+
{
184+
MemorySample sample;
185+
sample.timestamp = std::chrono::steady_clock::now();
186+
sample.phase = phase;
187+
188+
// Just get RSS - nothing else
189+
size_t rss_bytes = GetProcessRSS();
190+
sample.process_rss_mb = rss_bytes > 0 ? rss_bytes / (1 << 20) : 0;
191+
192+
return sample;
193+
}

src/util/memory_profiler.h

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

0 commit comments

Comments
 (0)