Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<MemorySnapshot> history()` – copy the stored snapshots (size capped by `historySize`).
- `MemoryMonitorConfig currentConfig() const` – inspect live settings.
Expand Down Expand Up @@ -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).
Expand Down
37 changes: 32 additions & 5 deletions examples/manual_sampling/manual_sampling.ino
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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});
Expand All @@ -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<uint8_t> payload(6 * 1024, 0xCD);
Expand Down
13 changes: 5 additions & 8 deletions src/esp_memory_monitor/memory_monitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -179,6 +173,9 @@ void ESPMemoryMonitor::deinit() {
}
_initialized = false;
_panicHookInstalled = false;
_running = false;
_config = MemoryMonitorConfig{};
_usePSRAMBuffers = false;
}

MemorySnapshot ESPMemoryMonitor::sampleNow() {
Expand Down
4 changes: 4 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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.")
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#include <Arduino.h>
#include <ESPMemoryMonitor.h>
#include <unity.h>

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<uint32_t>(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);
}