Skip to content

Commit 2095f32

Browse files
committed
ENH: Add MemoryBudgetManager to simplnx core
Adds a unified cache budget manager to simplnx. Tracks allocations registered by cache subsystems (ChunkCache, stride cache, etc.) and evicts globally-oldest entries via callbacks when memory pressure exceeds the configured budget. Singleton; thread-safe; the public API matches the previous OocMemoryBudgetManager but lives in simplnx core so non-OOC builds and visualization code can use it without depending on the SimplnxOoc plugin. Test moved from simplnx-ooc/test/OocMemoryBudgetManagerTest.cpp.
1 parent 6fbfc8d commit 2095f32

5 files changed

Lines changed: 446 additions & 0 deletions

File tree

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,7 @@ set(SIMPLNX_HDRS
561561
${SIMPLNX_SOURCE_DIR}/Utilities/HistogramUtilities.hpp
562562
${SIMPLNX_SOURCE_DIR}/Utilities/MaskCompareUtilities.hpp
563563
${SIMPLNX_SOURCE_DIR}/Utilities/MemoryUtilities.hpp
564+
${SIMPLNX_SOURCE_DIR}/Utilities/MemoryBudgetManager.hpp
564565
${SIMPLNX_SOURCE_DIR}/Utilities/MessageHelper.hpp
565566
${SIMPLNX_SOURCE_DIR}/Utilities/StringUtilities.hpp
566567
${SIMPLNX_SOURCE_DIR}/Utilities/StringInterpretationUtilities.hpp
@@ -767,6 +768,7 @@ set(SIMPLNX_SRCS
767768
${SIMPLNX_SOURCE_DIR}/Utilities/DataStoreUtilities.cpp
768769
${SIMPLNX_SOURCE_DIR}/Utilities/MaskCompareUtilities.cpp
769770
${SIMPLNX_SOURCE_DIR}/Utilities/MemoryUtilities.cpp
771+
${SIMPLNX_SOURCE_DIR}/Utilities/MemoryBudgetManager.cpp
770772
${SIMPLNX_SOURCE_DIR}/Utilities/MessageHelper.cpp
771773
${SIMPLNX_SOURCE_DIR}/Utilities/IParallelAlgorithm.cpp
772774
${SIMPLNX_SOURCE_DIR}/Utilities/ParallelDataAlgorithm.cpp
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
#include "simplnx/Utilities/MemoryBudgetManager.hpp"
2+
3+
#include <algorithm>
4+
#include <limits>
5+
#include <stdexcept>
6+
7+
#ifdef __APPLE__
8+
#include <sys/sysctl.h>
9+
#include <sys/types.h>
10+
#elif defined(__linux__)
11+
#include <fstream>
12+
#include <string>
13+
#elif defined(_WIN32)
14+
#define NOMINMAX
15+
#include <windows.h>
16+
#endif
17+
18+
namespace nx::core
19+
{
20+
21+
MemoryBudgetManager::MemoryBudgetManager()
22+
: m_BudgetBytes(defaultBudgetBytes())
23+
{
24+
}
25+
26+
MemoryBudgetManager& MemoryBudgetManager::instance()
27+
{
28+
static MemoryBudgetManager s_Instance;
29+
return s_Instance;
30+
}
31+
32+
uint64 MemoryBudgetManager::defaultBudgetBytes()
33+
{
34+
static constexpr uint64 k_MinBudget = uint64{1} * 1024 * 1024 * 1024; // 1 GB
35+
36+
uint64 totalRam = 0;
37+
38+
#ifdef __APPLE__
39+
int mib[2] = {CTL_HW, HW_MEMSIZE};
40+
uint64 memsize = 0;
41+
size_t len = sizeof(memsize);
42+
if(sysctl(mib, 2, &memsize, &len, nullptr, 0) == 0)
43+
{
44+
totalRam = memsize;
45+
}
46+
#elif defined(__linux__)
47+
std::ifstream meminfo("/proc/meminfo");
48+
std::string line;
49+
while(std::getline(meminfo, line))
50+
{
51+
if(line.find("MemTotal:") == 0)
52+
{
53+
// Format: "MemTotal: 12345678 kB"
54+
uint64 kb = 0;
55+
// Skip "MemTotal:" prefix and parse the number
56+
auto pos = line.find_first_of("0123456789");
57+
if(pos != std::string::npos)
58+
{
59+
try
60+
{
61+
kb = std::stoull(line.substr(pos));
62+
} catch(const std::exception&)
63+
{
64+
return k_MinBudget;
65+
}
66+
}
67+
totalRam = kb * 1024;
68+
break;
69+
}
70+
}
71+
#elif defined(_WIN32)
72+
MEMORYSTATUSEX memStatus;
73+
memStatus.dwLength = sizeof(memStatus);
74+
if(GlobalMemoryStatusEx(&memStatus))
75+
{
76+
totalRam = memStatus.ullTotalPhys;
77+
}
78+
#endif
79+
80+
if(totalRam == 0)
81+
{
82+
return k_MinBudget;
83+
}
84+
85+
uint64 halfRam = totalRam / 2;
86+
return std::max(halfRam, k_MinBudget);
87+
}
88+
89+
std::pair<MemoryBudgetManager::AllocationHandle, std::vector<MemoryBudgetManager::AllocationHandle>> MemoryBudgetManager::allocate(const std::string& subsystem, const std::string& key,
90+
uint64 sizeBytes, EvictionCallback onEvict)
91+
{
92+
std::lock_guard<std::mutex> lock(m_Mutex);
93+
94+
std::vector<AllocationHandle> evicted = makeRoom(sizeBytes);
95+
96+
AllocationHandle handle = m_NextHandle++;
97+
Entry entry;
98+
entry.subsystem = subsystem;
99+
entry.key = key;
100+
entry.sizeBytes = sizeBytes;
101+
entry.lastAccessed = std::chrono::steady_clock::now();
102+
entry.onEvict = std::move(onEvict);
103+
104+
m_Entries.emplace(handle, std::move(entry));
105+
m_UsedBytes += sizeBytes;
106+
107+
return {handle, std::move(evicted)};
108+
}
109+
110+
void MemoryBudgetManager::touch(AllocationHandle handle)
111+
{
112+
std::lock_guard<std::mutex> lock(m_Mutex);
113+
auto it = m_Entries.find(handle);
114+
if(it != m_Entries.end())
115+
{
116+
it->second.lastAccessed = std::chrono::steady_clock::now();
117+
}
118+
}
119+
120+
void MemoryBudgetManager::release(AllocationHandle handle)
121+
{
122+
std::lock_guard<std::mutex> lock(m_Mutex);
123+
auto it = m_Entries.find(handle);
124+
if(it != m_Entries.end())
125+
{
126+
m_UsedBytes -= it->second.sizeBytes;
127+
m_Entries.erase(it);
128+
}
129+
}
130+
131+
void MemoryBudgetManager::setBudgetBytes(uint64 bytes)
132+
{
133+
std::lock_guard<std::mutex> lock(m_Mutex);
134+
m_BudgetBytes = bytes;
135+
}
136+
137+
uint64 MemoryBudgetManager::budgetBytes() const
138+
{
139+
std::lock_guard<std::mutex> lock(m_Mutex);
140+
return m_BudgetBytes;
141+
}
142+
143+
uint64 MemoryBudgetManager::usedBytes() const
144+
{
145+
std::lock_guard<std::mutex> lock(m_Mutex);
146+
return m_UsedBytes;
147+
}
148+
149+
void MemoryBudgetManager::clear()
150+
{
151+
std::lock_guard<std::mutex> lock(m_Mutex);
152+
m_Entries.clear();
153+
m_UsedBytes = 0;
154+
}
155+
156+
std::vector<MemoryBudgetManager::AllocationHandle> MemoryBudgetManager::makeRoom(uint64 needed)
157+
{
158+
std::vector<AllocationHandle> evicted;
159+
160+
while(!m_Entries.empty() && m_UsedBytes + needed > m_BudgetBytes)
161+
{
162+
// Find entry with oldest lastAccessed using a direct iterator
163+
auto oldest = m_Entries.end();
164+
for(auto it = m_Entries.begin(); it != m_Entries.end(); ++it)
165+
{
166+
if(oldest == m_Entries.end() || it->second.lastAccessed < oldest->second.lastAccessed)
167+
{
168+
oldest = it;
169+
}
170+
}
171+
172+
if(oldest == m_Entries.end())
173+
{
174+
break;
175+
}
176+
177+
// Invoke eviction callback under mutex (must be non-blocking)
178+
if(oldest->second.onEvict)
179+
{
180+
oldest->second.onEvict();
181+
}
182+
183+
m_UsedBytes -= oldest->second.sizeBytes;
184+
evicted.push_back(oldest->first);
185+
m_Entries.erase(oldest);
186+
}
187+
188+
return evicted;
189+
}
190+
191+
} // namespace nx::core
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
#pragma once
2+
3+
#include "simplnx/simplnx_export.hpp"
4+
5+
#include "simplnx/Common/Types.hpp"
6+
7+
#include <chrono>
8+
#include <functional>
9+
#include <mutex>
10+
#include <string>
11+
#include <unordered_map>
12+
#include <utility>
13+
#include <vector>
14+
15+
namespace nx::core
16+
{
17+
18+
/**
19+
* @brief Unified memory budget manager for cache subsystems across simplnx and visualization code.
20+
*
21+
* All cache subsystems (ChunkCache, stride cache, partition cache) register their
22+
* allocations with this singleton. When memory pressure exceeds the budget, the
23+
* manager evicts the globally-oldest entry regardless of which subsystem owns it.
24+
*
25+
* Thread-safe: allocate/touch/release can be called from any thread.
26+
*
27+
* Eviction callbacks are invoked under the manager's mutex and MUST be non-blocking
28+
* (mark data for removal, don't do I/O or VTK operations). Callers receive the list
29+
* of evicted handles after the mutex is released for post-eviction cleanup.
30+
*/
31+
class SIMPLNX_EXPORT MemoryBudgetManager
32+
{
33+
public:
34+
using AllocationHandle = uint64;
35+
36+
/**
37+
* @brief Callback invoked when an entry is evicted.
38+
*
39+
* The callback is invoked under the manager's internal mutex.
40+
* Callbacks MUST NOT call allocate/touch/release on this manager -- that
41+
* would deadlock (the mutex is not recursive). Only mark state for removal;
42+
* do no I/O.
43+
*/
44+
using EvictionCallback = std::function<void()>;
45+
46+
/**
47+
* @brief Returns the singleton instance.
48+
*/
49+
static MemoryBudgetManager& instance();
50+
51+
/**
52+
* @brief Returns a default budget of 50% of system RAM, clamped to a minimum of 1 GB.
53+
*/
54+
static uint64 defaultBudgetBytes();
55+
56+
/**
57+
* @brief Allocates a tracked entry. Evicts oldest entries if needed to stay within budget.
58+
* @param subsystem Name of the owning subsystem (e.g. "chunk", "stride", "partition")
59+
* @param key Subsystem-specific key for identification
60+
* @param sizeBytes Size of the allocation in bytes
61+
* @param onEvict Callback invoked when this entry is evicted (must be non-blocking)
62+
* @return Pair of (new handle, list of evicted handles for post-eviction cleanup)
63+
*/
64+
std::pair<AllocationHandle, std::vector<AllocationHandle>> allocate(const std::string& subsystem, const std::string& key, uint64 sizeBytes, EvictionCallback onEvict);
65+
66+
/**
67+
* @brief Updates the last-accessed timestamp of an allocation.
68+
* @param handle The allocation handle to touch
69+
*/
70+
void touch(AllocationHandle handle);
71+
72+
/**
73+
* @brief Voluntarily releases an allocation.
74+
* @param handle The allocation handle to release
75+
*/
76+
void release(AllocationHandle handle);
77+
78+
/**
79+
* @brief Sets the memory budget in bytes.
80+
*/
81+
void setBudgetBytes(uint64 bytes);
82+
83+
/**
84+
* @brief Returns the current memory budget in bytes.
85+
*/
86+
uint64 budgetBytes() const;
87+
88+
/**
89+
* @brief Returns the current total memory usage in bytes.
90+
*/
91+
uint64 usedBytes() const;
92+
93+
/**
94+
* @brief Clears all tracked entries and resets used bytes to zero.
95+
*
96+
* Intended for tests that share the mutable singleton -- call at the start
97+
* of each test case so leaked entries from a prior failure do not pollute
98+
* budget accounting.
99+
*/
100+
void clear();
101+
102+
private:
103+
MemoryBudgetManager();
104+
~MemoryBudgetManager() = default;
105+
106+
MemoryBudgetManager(const MemoryBudgetManager&) = delete;
107+
MemoryBudgetManager& operator=(const MemoryBudgetManager&) = delete;
108+
109+
struct Entry
110+
{
111+
std::string subsystem;
112+
std::string key;
113+
uint64 sizeBytes = 0;
114+
std::chrono::steady_clock::time_point lastAccessed;
115+
/// Eviction callback. MUST NOT call allocate/touch/release on this
116+
/// manager -- that would deadlock. Only mark state for removal; do no I/O.
117+
EvictionCallback onEvict;
118+
};
119+
120+
/**
121+
* @brief Evicts the oldest entries until m_UsedBytes + needed <= m_BudgetBytes.
122+
* Must be called with m_Mutex held.
123+
* @param needed Number of bytes needed for a new allocation
124+
* @return List of evicted handles
125+
*/
126+
std::vector<AllocationHandle> makeRoom(uint64 needed);
127+
128+
mutable std::mutex m_Mutex;
129+
std::unordered_map<AllocationHandle, Entry> m_Entries;
130+
uint64 m_BudgetBytes = 0;
131+
uint64 m_UsedBytes = 0;
132+
AllocationHandle m_NextHandle = 1;
133+
};
134+
135+
} // namespace nx::core

test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ add_executable(simplnx_test
4141
IntersectionUtilitiesTest.cpp
4242
IOFormat.cpp
4343
IParallelAlgorithmTest.cpp
44+
MemoryBudgetManagerTest.cpp
4445
MontageTest.cpp
4546
NeighborUtilitiesTest.cpp
4647
PluginTest.cpp

0 commit comments

Comments
 (0)