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
0 commit comments