diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fa3553..4bded43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,10 @@ All notable changes to this project will be documented in this file. - Routed additional hot-path transient monitor scratch containers (threshold/task events, task-status capture, scope/tag threshold staging, and window analytics scratch vectors) through the same `ESPBufferManager` policy. - Added internal/public model separation for history/scope/tag/task/leak state: monitor internals now use PSRAM-policy-aware storage models and convert to existing public API structs at return/callback boundaries. - Switched sampler task lifecycle to native FreeRTOS task handling (`xTaskCreatePinnedToCore`/`vTaskDelete`). +- Added lifecycle teardown tests in `test/test_memory_monitor_lifecycle` covering pre-init `deinit()`, idempotent teardown, re-init, and destructor behavior. ### Fixed - Mark the failed-allocation hook instance pointer as static so Arduino builds compile the callback correctly. +- `deinit()` now releases monitor-owned container capacity (not just entries), fully clears runtime state, and resets config/runtime flags for deterministic re-init. ## [1.0.0] - 2025-12-02 ### Added diff --git a/README.md b/README.md index 68a96c6..c6be3c7 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,13 @@ void loop() { } ``` +When a service is shutting down (or before a controlled reboot), explicitly tear down monitor resources: +```cpp +if (monitor.isInitialized()) { + monitor.deinit(); +} +``` + When you need richer stack info or failed-allocation events, flip `enablePerTaskStacks` and `enableFailedAllocEvents` in the config. The latest snapshot and the full ring buffer are always available via `sampleNow()`/`history()`. ### Example sketches @@ -122,7 +129,7 @@ When you need richer stack info or failed-allocation events, flip `enablePerTask - `examples/panic_hook`: per-task stack thresholds, failed-allocation hook, optional JSON export, and a panic hook that dumps a final snapshot (send `p` over serial to try it). ## API Reference -- `bool init(const MemoryMonitorConfig &cfg = {})` / `void deinit()` – start/stop the monitor. When `enableSamplerTask` is `false`, call `sampleNow()` manually. +- `bool init(const MemoryMonitorConfig &cfg = {})` / `void deinit()` / `bool isInitialized() const` – start/stop and inspect runtime monitor state. When `enableSamplerTask` is `false`, call `sampleNow()` manually. - `MemorySnapshot sampleNow()` – collect a snapshot immediately; triggers callbacks and updates the ring buffer. - `std::vector history()` – copy the stored snapshots (size capped by `historySize`). - `MemoryMonitorConfig currentConfig() const` – inspect live settings. @@ -182,7 +189,8 @@ serializeJson(doc, Serial); - No dynamic allocation inside the sampler path beyond what the STL containers already hold. ## Tests -A dedicated host test suite is not shipped yet. Exercise the library through `examples/basic_monitor` on hardware and wire it into your CI if you extend the feature set. +- Lifecycle teardown tests are available in `test/test_memory_monitor_lifecycle` (pre-init `deinit()`, idempotent `deinit()`, re-init, and destructor teardown behavior). +- Host-side tests are disabled because this library depends on ESP-IDF/FreeRTOS runtime APIs; run the lifecycle suite on device with PlatformIO/Arduino. ## License MIT — see [LICENSE.md](LICENSE.md). diff --git a/examples/manual_sampling/manual_sampling.ino b/examples/manual_sampling/manual_sampling.ino index c32c04f..ff6bf2e 100644 --- a/examples/manual_sampling/manual_sampling.ino +++ b/examples/manual_sampling/manual_sampling.ino @@ -21,10 +21,7 @@ static void logSnapshot(const MemorySnapshot &snapshot) { } } -void setup() { - Serial.begin(115200); - - // Disable the background sampler; we will call sampleNow() manually. +static MemoryMonitorConfig buildConfig() { MemoryMonitorConfig cfg; cfg.enableSamplerTask = false; cfg.historySize = 8; @@ -33,7 +30,14 @@ void setup() { cfg.psram = {256 * 1024, 160 * 1024}; cfg.enableScopes = true; cfg.maxScopesInHistory = 12; - monitor.init(cfg); + return cfg; +} + +static bool initMonitor() { + if (!monitor.init(buildConfig())) { + ESP_LOGE("MEM", "monitor.init() failed"); + return false; + } networkTag = monitor.registerTag("network"); monitor.setTagBudget(networkTag, {48 * 1024, 64 * 1024}); @@ -60,9 +64,32 @@ void setup() { // Kick off an initial measurement so history() has data immediately. lastSampleMs = millis(); monitor.sampleNow(); + return true; +} + +void setup() { + Serial.begin(115200); + initMonitor(); } void loop() { + if (Serial.available()) { + const int c = Serial.read(); + if ((c == 'x' || c == 'X') && monitor.isInitialized()) { + monitor.deinit(); + ESP_LOGI("MEM", "monitor deinitialized (send 'i' to init again)"); + } else if ((c == 'i' || c == 'I') && !monitor.isInitialized()) { + if (initMonitor()) { + ESP_LOGI("MEM", "monitor initialized"); + } + } + } + + if (!monitor.isInitialized()) { + delay(60); + return; + } + // Simulate a request that consumes heap; attribute it to a tag. auto scope = monitor.beginScope("net_req", networkTag); std::vector payload(6 * 1024, 0xCD); diff --git a/src/esp_memory_monitor/memory_monitor.cpp b/src/esp_memory_monitor/memory_monitor.cpp index 5ec06dd..4d11a62 100644 --- a/src/esp_memory_monitor/memory_monitor.cpp +++ b/src/esp_memory_monitor/memory_monitor.cpp @@ -154,14 +154,8 @@ void ESPMemoryMonitor::deinit() { { LockGuard guard(_mutex); - _history.clear(); - _scopeHistory.clear(); - _tagUsage.clear(); - _tagBudgets.clear(); - _taskThresholds.clear(); - _knownTasks.clear(); - _leakHistory.clear(); - _leakCheckpoints.clear(); + // Recreate monitor-owned containers so reserved capacity is released. + resetOwnedContainers(); _thresholdStates = {ThresholdState::Normal, ThresholdState::Normal}; _sampleCallback = nullptr; _thresholdCallback = nullptr; @@ -179,6 +173,9 @@ void ESPMemoryMonitor::deinit() { } _initialized = false; _panicHookInstalled = false; + _running = false; + _config = MemoryMonitorConfig{}; + _usePSRAMBuffers = false; } MemorySnapshot ESPMemoryMonitor::sampleNow() { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..82a1384 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,4 @@ +# Host-side tests are not enabled because ESPMemoryMonitor depends on ESP-IDF +# heap/FreeRTOS primitives that are unavailable in the default host toolchain. +# Run on-device tests in test/test_memory_monitor_lifecycle with PlatformIO/Arduino. +message(STATUS "ESPMemoryMonitor: host tests are disabled.") diff --git a/test/test_memory_monitor_lifecycle/test_memory_monitor_lifecycle.cpp b/test/test_memory_monitor_lifecycle/test_memory_monitor_lifecycle.cpp new file mode 100644 index 0000000..98fe30d --- /dev/null +++ b/test/test_memory_monitor_lifecycle/test_memory_monitor_lifecycle.cpp @@ -0,0 +1,98 @@ +#include +#include +#include + +namespace { + +MemoryMonitorConfig testConfig() { + MemoryMonitorConfig cfg{}; + cfg.enableSamplerTask = false; + cfg.sampleIntervalMs = 0; + cfg.historySize = 4; + cfg.windowStatsSize = 0; + cfg.enableScopes = false; + cfg.enablePerTaskStacks = false; + cfg.enableTaskTracking = false; + cfg.enableFailedAllocEvents = false; + return cfg; +} + +void test_deinit_is_safe_before_init() { + ESPMemoryMonitor monitor; + TEST_ASSERT_FALSE(monitor.isInitialized()); + + monitor.deinit(); + TEST_ASSERT_FALSE(monitor.isInitialized()); +} + +void test_deinit_is_idempotent() { + ESPMemoryMonitor monitor; + TEST_ASSERT_TRUE(monitor.init(testConfig())); + TEST_ASSERT_TRUE(monitor.isInitialized()); + + monitor.deinit(); + TEST_ASSERT_FALSE(monitor.isInitialized()); + + monitor.deinit(); + TEST_ASSERT_FALSE(monitor.isInitialized()); +} + +void test_reinit_after_deinit() { + ESPMemoryMonitor monitor; + TEST_ASSERT_TRUE(monitor.init(testConfig())); + TEST_ASSERT_TRUE(monitor.isInitialized()); + + monitor.deinit(); + TEST_ASSERT_FALSE(monitor.isInitialized()); + + TEST_ASSERT_TRUE(monitor.init(testConfig())); + TEST_ASSERT_TRUE(monitor.isInitialized()); + monitor.deinit(); +} + +void test_deinit_releases_runtime_state() { + ESPMemoryMonitor monitor; + TEST_ASSERT_TRUE(monitor.init(testConfig())); + + (void)monitor.sampleNow(); + (void)monitor.sampleNow(); + TEST_ASSERT_TRUE(monitor.history().size() > 0); + + monitor.deinit(); + TEST_ASSERT_FALSE(monitor.isInitialized()); + TEST_ASSERT_EQUAL_UINT32(0, static_cast(monitor.history().size())); +} + +void test_destructor_deinits_active_instance() { + { + ESPMemoryMonitor scoped; + TEST_ASSERT_TRUE(scoped.init(testConfig())); + TEST_ASSERT_TRUE(scoped.isInitialized()); + (void)scoped.sampleNow(); + } + + ESPMemoryMonitor second; + TEST_ASSERT_TRUE(second.init(testConfig())); + TEST_ASSERT_TRUE(second.isInitialized()); + second.deinit(); +} + +} // namespace + +void setUp() {} +void tearDown() {} + +void setup() { + delay(2000); + UNITY_BEGIN(); + RUN_TEST(test_deinit_is_safe_before_init); + RUN_TEST(test_deinit_is_idempotent); + RUN_TEST(test_reinit_after_deinit); + RUN_TEST(test_deinit_releases_runtime_state); + RUN_TEST(test_destructor_deinits_active_instance); + UNITY_END(); +} + +void loop() { + delay(1000); +}