Skip to content

Commit 8310027

Browse files
committed
Mempressure: fix mempressure detection on linux, add memory pressure detection for macOS
- Linux: Use /proc/meminfo MemAvailable (accounts for reclaimable memory), with /proc/self/meminfo preferred for container environments - Windows: Use GlobalMemoryStatusEx with Job Object support for container memory limit detection - macOS: Use kern.memorystatus_vm_pressure_level sysctl to detect kernel memory pressure levels Split SystemNeedsMemoryReleased into two functions: - CheckMemoryPressure(): updates atomic flag - SystemNeedsMemoryReleased(): read of cached flag Schedule CheckMemoryPressure to run every 10 seconds, and recheck after FlushStateToDisk to prevent repeated flushes on the same pressure event.
1 parent eeb9cc1 commit 8310027

File tree

5 files changed

+168
-21
lines changed

5 files changed

+168
-21
lines changed

src/init.cpp

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

1608+
// Check system memory pressure every 10 seconds.
1609+
// Updates g_system_needs_memory_released flag used by FlushStateToDisk.
1610+
scheduler.scheduleEvery([]{
1611+
CheckMemoryPressure();
1612+
}, std::chrono::seconds{10});
1613+
16081614
if (args.GetBoolArg("-logratelimit", BCLog::DEFAULT_LOGRATELIMIT)) {
16091615
LogInstance().SetRateLimiting(BCLog::LogRateLimiter::Create(
16101616
[&scheduler](auto func, auto window) { scheduler.scheduleEvery(std::move(func), window); },

src/util/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,5 @@ target_link_libraries(bitcoin_util
4646
bitcoin_crypto
4747
$<$<PLATFORM_ID:Windows>:ws2_32>
4848
$<$<PLATFORM_ID:Windows>:iphlpapi>
49+
$<$<PLATFORM_ID:Windows>:psapi>
4950
)

src/util/mempressure.cpp

Lines changed: 141 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,48 +9,168 @@
99
#include <logging.h>
1010
#include <util/byte_units.h>
1111

12-
#ifdef HAVE_LINUX_SYSINFO
13-
#include <sys/sysinfo.h>
14-
#endif
1512
#ifdef WIN32
1613
#include <windows.h>
14+
#include <psapi.h>
15+
#endif
16+
17+
#ifdef __APPLE__
18+
#include <sys/sysctl.h>
1719
#endif
1820

21+
#include <algorithm>
22+
#include <atomic>
1923
#include <cstddef>
2024
#include <cstdint>
25+
#include <exception>
26+
#include <fstream>
27+
#include <string>
2128

2229
size_t g_low_memory_threshold{64_MiB};
30+
std::atomic<bool> g_system_needs_memory_released{false};
2331

24-
bool SystemNeedsMemoryReleased()
32+
#ifdef WIN32
33+
namespace {
34+
// Query Job Object memory limit for Windows container support.
35+
// Docker, etc on Windows use Job Objects to enforce memory limits.
36+
size_t GetJobObjectMemoryLimit()
37+
{
38+
// Query information about the job object the current process belongs to
39+
JOBOBJECT_EXTENDED_LIMIT_INFORMATION job_info = {};
40+
41+
if (QueryInformationJobObject(nullptr, JobObjectExtendedLimitInformation,
42+
&job_info, sizeof(job_info), nullptr)) {
43+
// Check if job memory limit is set
44+
if (job_info.BasicLimitInformation.LimitFlags & JOB_OBJECT_LIMIT_JOB_MEMORY) {
45+
SIZE_T job_memory_limit = job_info.JobMemoryLimit;
46+
47+
// JobMemoryLimit can be in bytes or pages depending on flags
48+
// For modern Windows containers, it's typically in bytes
49+
if (job_memory_limit > 0 && job_memory_limit < SIZE_MAX) {
50+
LogPrintf("GetJobObjectMemoryLimit: Detected Job Object memory limit: %zu bytes\n",
51+
job_memory_limit);
52+
return static_cast<size_t>(job_memory_limit);
53+
}
54+
}
55+
}
56+
57+
// Not in a job object with memory limits (bare metal Windows)
58+
return 0;
59+
}
60+
61+
}
62+
#endif
63+
64+
void CheckMemoryPressure()
2565
{
2666
if (g_low_memory_threshold <= 0) {
2767
// Intentionally bypass other metrics when disabled entirely
28-
return false;
68+
g_system_needs_memory_released.store(false, std::memory_order_relaxed);
69+
return;
2970
}
71+
72+
bool needs_release = false;
73+
3074
#ifdef WIN32
3175
MEMORYSTATUSEX mem_status;
3276
mem_status.dwLength = sizeof(mem_status);
3377
if (GlobalMemoryStatusEx(&mem_status)) {
34-
if (mem_status.dwMemoryLoad >= 99 ||
35-
mem_status.ullAvailPhys < g_low_memory_threshold ||
36-
mem_status.ullAvailVirtual < g_low_memory_threshold) {
37-
LogPrintf("%s: YES: %s%% memory load; %s available physical memory; %s available virtual memory\n", __func__, int(mem_status.dwMemoryLoad), size_t(mem_status.ullAvailPhys), size_t(mem_status.ullAvailVirtual));
38-
return true;
78+
// Use the lesser of available physical or virtual memory
79+
size_t available_memory = std::min(static_cast<size_t>(mem_status.ullAvailPhys),
80+
static_cast<size_t>(mem_status.ullAvailVirtual));
81+
82+
// Check if running in a Windows container with memory limits
83+
size_t container_memory_limit = GetJobObjectMemoryLimit();
84+
if (container_memory_limit > 0) {
85+
// In a container - try to get container-aware available memory
86+
PROCESS_MEMORY_COUNTERS_EX pmc;
87+
if (GetProcessMemoryInfo(GetCurrentProcess(), (PROCESS_MEMORY_COUNTERS*)&pmc, sizeof(pmc))) {
88+
// Success - use container available memory calculation
89+
size_t current_usage = pmc.PrivateUsage;
90+
available_memory = (current_usage < container_memory_limit)
91+
? (container_memory_limit - current_usage)
92+
: 0;
93+
}
94+
// If GetProcessMemoryInfo failed, keep using system available_memory as fallback
95+
}
96+
97+
if (mem_status.dwMemoryLoad >= 99 || available_memory < g_low_memory_threshold) {
98+
LogPrintf("%s: YES: %d%% memory load; %zu available memory%s\n",
99+
__func__, int(mem_status.dwMemoryLoad), available_memory,
100+
container_memory_limit > 0 ? " (in container)" : "");
101+
needs_release = true;
102+
}
103+
}
104+
#endif
105+
#ifdef __linux__
106+
// use /proc/meminfo MemAvailable
107+
// - Prefer /proc/self/meminfo for containerized environments
108+
// - Fallback to /proc/meminfo otherwise
109+
// - /proc/self/meminfo should return same info if available and not in a container
110+
std::ifstream meminfo("/proc/self/meminfo");
111+
if (!meminfo.is_open()) {
112+
meminfo.open("/proc/meminfo");
113+
}
114+
115+
if (meminfo.is_open()) {
116+
std::string line;
117+
while (std::getline(meminfo, line)) {
118+
if (line.compare(0, 13, "MemAvailable:") == 0) {
119+
// Format: "MemAvailable: 1234567 kB"
120+
size_t pos = line.find_first_of("0123456789");
121+
if (pos != std::string::npos) {
122+
try {
123+
uint64_t mem_kb = std::stoull(line.substr(pos));
124+
125+
// Check for overflow before multiplication
126+
if (mem_kb > UINT64_MAX / 1024) {
127+
// huge value is "plenty of memory"
128+
break;
129+
}
130+
131+
uint64_t available_memory = mem_kb * 1024;
132+
133+
if (available_memory < g_low_memory_threshold) {
134+
LogPrintf("%s: YES: %s available memory\n", __func__, available_memory);
135+
needs_release = true;
136+
}
137+
} catch (const std::exception& e) {
138+
// Invalid format in /proc/meminfo - ignore and leave needs_release as false
139+
LogPrintf("%s: Failed to parse MemAvailable: %s\n", __func__, e.what());
140+
}
141+
}
142+
break;
143+
}
39144
}
40145
}
41146
#endif
42-
#ifdef HAVE_LINUX_SYSINFO
43-
struct sysinfo sys_info;
44-
if (!sysinfo(&sys_info)) {
45-
// Explicitly 64-bit in case of 32-bit userspace on 64-bit kernel
46-
const uint64_t free_ram = uint64_t(sys_info.freeram) * sys_info.mem_unit;
47-
const uint64_t buffer_ram = uint64_t(sys_info.bufferram) * sys_info.mem_unit;
48-
if (free_ram + buffer_ram < g_low_memory_threshold) {
49-
LogPrintf("%s: YES: %s free RAM + %s buffer RAM\n", __func__, free_ram, buffer_ram);
50-
return true;
147+
#ifdef __APPLE__
148+
// On macOS, use kern.memorystatus_vm_pressure_level sysctl.
149+
// TODO: native macOS containers may need more complex handling.
150+
// - NORMAL (1): No pressure
151+
// - WARN (2): Kernel has begun mitigations (compression) - trigger reclamation
152+
// - CRITICAL (4): System actively paging to disk - aggressive reclamation needed
153+
int pressure_level = 0;
154+
size_t size = sizeof(pressure_level);
155+
156+
if (sysctlbyname("kern.memorystatus_vm_pressure_level", &pressure_level, &size, nullptr, 0) == 0) {
157+
// Trigger memory release at WARN level or higher
158+
if (pressure_level >= 2) {
159+
LogPrintf("%s: YES: macOS memory pressure level %d (WARN or CRITICAL)\n", __func__, pressure_level);
160+
needs_release = true;
51161
}
52162
}
53163
#endif
54-
// NOTE: sysconf(_SC_AVPHYS_PAGES) doesn't account for caches on at least Linux, so not safe to use here
55-
return false;
164+
165+
g_system_needs_memory_released.store(needs_release, std::memory_order_relaxed);
166+
}
167+
168+
bool SystemNeedsMemoryReleased()
169+
{
170+
if (g_low_memory_threshold <= 0) {
171+
// Intentionally bypass if disabled (lowmem=0)
172+
return false;
173+
}
174+
175+
return g_system_needs_memory_released.load(std::memory_order_relaxed);
56176
}

src/util/mempressure.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,24 @@
55
#ifndef BITCOIN_UTIL_MEMPRESSURE_H
66
#define BITCOIN_UTIL_MEMPRESSURE_H
77

8+
#include <atomic>
89
#include <cstddef>
10+
#include <cstdint>
911

1012
extern size_t g_low_memory_threshold;
13+
extern std::atomic<bool> g_system_needs_memory_released;
1114

15+
/** Checks system memory pressure and updates g_system_needs_memory_released flag.
16+
* On Linux, reads /proc/meminfo MemAvailable or /proc/self/meminfo if in a cgroup (container).
17+
* On Windows, uses GlobalMemoryStatusEx and/or Job Object limits if in a container.
18+
* On macOS, uses kern.memorystatus_vm_pressure_level sysctl.
19+
* This function is called periodically by the scheduler, and also after memory release.
20+
*/
21+
void CheckMemoryPressure();
22+
23+
/** Fast check if system needs memory released.
24+
* Returns the cached g_system_needs_memory_released flag.
25+
*/
1226
bool SystemNeedsMemoryReleased();
1327

1428
#endif // BITCOIN_UTIL_MEMPRESSURE_H

src/validation.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3215,6 +3215,12 @@ bool Chainstate::FlushStateToDisk(
32153215
(uint64_t)coins_mem_usage,
32163216
(bool)fFlushForPrune);
32173217

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+
32183224
}
32193225
}
32203226

0 commit comments

Comments
 (0)