GroveEngine/tests/modules/LeakTestModule.cpp
StillHammer 360f39325b feat: Add Memory Leak Hunter test & fix critical ModuleLoader leaks
**Test Suite Completion - Scenario 5**

Add comprehensive memory leak detection test for hot-reload system with 200 reload cycles.

**New Test: test_05_memory_leak**
- 200 hot-reload cycles without recompilation
- Memory monitoring every 5 seconds (RSS, temp files, .so handles)
- Multi-threaded: Engine (60 FPS) + ReloadScheduler + MemoryMonitor
- Strict validation: <10 MB growth, <50 KB/reload, ≤2 temp files

**New Module: LeakTestModule**
- Controlled memory allocations (1 MB work buffer)
- Large state serialization (100 KB blob)
- Simulates real-world module behavior

**Critical Fix: ModuleLoader Memory Leaks** (src/ModuleLoader.cpp:34-39)
- Auto-unload previous library before loading new one
- Prevents library handle leaks (+200 .so mappings eliminated)
- Prevents temp file accumulation (778 files → 1-2 files)
- Memory leak reduced by 97%: 36.5 MB → 1.9 MB

**Test Results - Before Fix:**
- Memory growth: 36.5 MB 
- Per reload: 187.1 KB 
- Temp files: 778 
- Mapped .so: +200 

**Test Results - After Fix:**
- Memory growth: 1.9 MB 
- Per reload: 9.7 KB 
- Temp files: 1-2 
- Mapped .so: stable 
- 200/200 reloads successful (100%)

**Enhanced SystemUtils helpers:**
- countTempFiles(): Count temp module files
- getMappedLibraryCount(): Track .so handle leaks via /proc/self/maps

**Test Lifecycle Improvements:**
- test_04 & test_05: Destroy old module before reload to prevent use-after-free
- Proper state/config preservation across reload boundary

**Files Modified:**
- src/ModuleLoader.cpp: Auto-unload on load()
- tests/integration/test_05_memory_leak.cpp: NEW - 200 cycle leak detector
- tests/modules/LeakTestModule.cpp: NEW - Test module with allocations
- tests/helpers/SystemUtils.{h,cpp}: Memory monitoring functions
- tests/integration/test_04_race_condition.cpp: Fixed module lifecycle
- tests/CMakeLists.txt: Added test_05 and LeakTestModule

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-16 10:06:18 +08:00

137 lines
4.1 KiB
C++

// ============================================================================
// LeakTestModule.cpp - Module for memory leak detection testing
// ============================================================================
#include "grove/IModule.h"
#include "grove/IDataNode.h"
#include "grove/JsonDataNode.h"
#include "grove/JsonDataValue.h"
#include <vector>
#include <string>
#include <algorithm>
#include <memory>
#include <nlohmann/json.hpp>
namespace grove {
class LeakTestModule : public IModule {
private:
std::vector<uint8_t> workBuffer;
int processCount = 0;
int lastChecksum = 0;
IIO* io = nullptr;
ITaskScheduler* scheduler = nullptr;
std::unique_ptr<IDataNode> config;
public:
LeakTestModule() = default;
~LeakTestModule() override = default;
void process(const IDataNode& input) override {
processCount++;
// Simulate real workload with allocations (1 MB working buffer)
workBuffer.resize(1024 * 1024);
std::fill(workBuffer.begin(), workBuffer.end(),
static_cast<uint8_t>(processCount % 256));
// Small temporary allocations (simulate logging/processing)
std::vector<std::string> logs;
logs.reserve(100);
for (int i = 0; i < 100; i++) {
logs.push_back("Process iteration " + std::to_string(processCount));
}
// Simulate some data processing
int sum = 0;
for (size_t i = 0; i < workBuffer.size(); i += 1024) {
sum += workBuffer[i];
}
lastChecksum = sum;
}
void setConfiguration(const IDataNode& configNode, IIO* ioPtr, ITaskScheduler* schedulerPtr) override {
this->io = ioPtr;
this->scheduler = schedulerPtr;
config = std::make_unique<JsonDataNode>("config", nlohmann::json::object());
}
const IDataNode& getConfiguration() override {
if (!config) {
config = std::make_unique<JsonDataNode>("config", nlohmann::json::object());
}
return *config;
}
std::unique_ptr<IDataNode> getHealthStatus() override {
nlohmann::json health = {
{"status", "healthy"},
{"processCount", processCount},
{"lastChecksum", lastChecksum},
{"bufferSize", workBuffer.size()}
};
return std::make_unique<JsonDataNode>("health", health);
}
void shutdown() override {
// Clean up
workBuffer.clear();
workBuffer.shrink_to_fit();
}
std::unique_ptr<IDataNode> getState() override {
nlohmann::json state = {
{"processCount", processCount},
{"lastChecksum", lastChecksum}
};
// Simulate storing large state data (100 KB blob as base64)
std::vector<uint8_t> stateBlob(100 * 1024);
std::fill(stateBlob.begin(), stateBlob.end(), 0xAB);
// Store as array of ints in JSON (simpler than base64 for test purposes)
std::vector<int> blobData;
blobData.reserve(stateBlob.size());
for (auto byte : stateBlob) {
blobData.push_back(byte);
}
state["stateBlob"] = blobData;
return std::make_unique<JsonDataNode>("state", state);
}
void setState(const IDataNode& state) override {
processCount = state.getInt("processCount", 0);
lastChecksum = state.getInt("lastChecksum", 0);
// Note: We don't need to restore stateBlob, it's just for testing memory
// during serialization
}
std::string getType() const override {
return "LeakTestModule";
}
bool isIdle() const override {
return true;
}
};
} // namespace grove
// ============================================================================
// Module Factory
// ============================================================================
extern "C" {
grove::IModule* createModule() {
return new grove::LeakTestModule();
}
void destroyModule(grove::IModule* module) {
delete module;
}
} // extern "C"