From b4d28c8e49cc40c27ec8813c03c7c77ecd3a9c84 Mon Sep 17 00:00:00 2001 From: zekageri Date: Tue, 10 Mar 2026 13:11:37 +0100 Subject: [PATCH] chore: align formatter baseline with esptoolkit-template --- .clang-format | 11 + .editorconfig | 11 + .gitignore | 1 - .vscode/bin/clang-format | 19 + .vscode/extensions.json | 9 + .vscode/settings.json | 30 + .vscode/tasks.json | 12 + README.md | 7 + examples/basic_monitor/basic_monitor.ino | 209 +- examples/manual_sampling/manual_sampling.ino | 189 +- examples/panic_hook/panic_hook.ino | 163 +- .../scopes_and_leakcheck.ino | 174 +- scripts/format_cpp.sh | 24 + src/ESPMemoryMonitor.h | 1 - src/esp_memory_monitor/memory_monitor.cpp | 2007 +++++++++-------- src/esp_memory_monitor/memory_monitor.h | 661 +++--- .../memory_monitor_allocator.h | 122 +- .../test_memory_monitor_lifecycle.cpp | 122 +- 18 files changed, 2066 insertions(+), 1706 deletions(-) create mode 100644 .clang-format create mode 100644 .editorconfig create mode 100755 .vscode/bin/clang-format create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100755 scripts/format_cpp.sh diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..8450693 --- /dev/null +++ b/.clang-format @@ -0,0 +1,11 @@ +BasedOnStyle: LLVM +ColumnLimit: 100 +BinPackArguments: false +BinPackParameters: false +AllowAllArgumentsOnNextLine: false +AlignAfterOpenBracket: BlockIndent +UseTab: ForIndentation +IndentWidth: 4 +TabWidth: 4 +ContinuationIndentWidth: 4 +AllowShortFunctionsOnASingleLine: None diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d89c76d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +[*.{c,cc,cpp,h,hpp,ino}] +indent_style = tab +indent_size = tab +tab_width = 4 diff --git a/.gitignore b/.gitignore index 78f49b6..6346d5c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ .venv build/ build_prev_runner/ -.vscode \ No newline at end of file diff --git a/.vscode/bin/clang-format b/.vscode/bin/clang-format new file mode 100755 index 0000000..0df371f --- /dev/null +++ b/.vscode/bin/clang-format @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if command -v clang-format >/dev/null 2>&1; then + exec clang-format "$@" +fi + +_home_dir="${HOME:-}" +if [ -n "$_home_dir" ]; then + _candidate="$(ls -1d "$_home_dir"/.vscode/extensions/ms-vscode.cpptools-*-linux-x64/LLVM/bin/clang-format 2>/dev/null | tail -n 1 || true)" + if [ -n "$_candidate" ] && [ -x "$_candidate" ]; then + exec "$_candidate" "$@" + fi +fi + +echo "clang-format executable not found." >&2 +echo "Install clang-format system-wide or install/update ms-vscode.cpptools." >&2 +exit 127 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..f814711 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "pioarduino.pioarduino-ide", + "xaver.clang-format" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..24368c8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,30 @@ +{ + "files.associations": { + "*.ino": "cpp" + }, + "editor.defaultFormatter": "xaver.clang-format", + "C_Cpp.formatting": "Disabled", + "clang-format.style": "file", + "clang-format.executable": "${workspaceRoot}/.vscode/bin/clang-format", + "[cpp]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + }, + "[c]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + }, + "[arduino]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..20e66d5 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,12 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Format Firmware Sources", + "type": "shell", + "command": "bash ${workspaceFolder}/scripts/format_cpp.sh", + "group": "build", + "problemMatcher": [] + } + ] +} diff --git a/README.md b/README.md index 42a3f45..96d2051 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,13 @@ serializeJson(doc, Serial); - 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. +## Formatting Baseline + +This repository follows the firmware formatting baseline from `esptoolkit-template`: +- `.clang-format` is the source of truth for C/C++/INO layout. +- `.editorconfig` enforces tabs (`tab_width = 4`), LF endings, and final newline. +- Format all tracked firmware sources with `bash scripts/format_cpp.sh`. + ## License MIT — see [LICENSE.md](LICENSE.md). diff --git a/examples/basic_monitor/basic_monitor.ino b/examples/basic_monitor/basic_monitor.ino index 04b564e..7f6e799 100644 --- a/examples/basic_monitor/basic_monitor.ino +++ b/examples/basic_monitor/basic_monitor.ino @@ -6,97 +6,146 @@ ESPMemoryMonitor monitor; MemoryTag httpTag; void setup() { - Serial.begin(115200); + Serial.begin(115200); - MemoryMonitorConfig cfg; - cfg.sampleIntervalMs = 1000; - cfg.historySize = 30; - cfg.internal = {48 * 1024, 32 * 1024}; - cfg.psram = {256 * 1024, 160 * 1024}; - cfg.enablePerTaskStacks = true; - cfg.enableFailedAllocEvents = true; - cfg.enableScopes = true; - cfg.maxScopesInHistory = 16; - cfg.windowStatsSize = 8; - cfg.enableTaskTracking = true; - monitor.init(cfg); + MemoryMonitorConfig cfg; + cfg.sampleIntervalMs = 1000; + cfg.historySize = 30; + cfg.internal = {48 * 1024, 32 * 1024}; + cfg.psram = {256 * 1024, 160 * 1024}; + cfg.enablePerTaskStacks = true; + cfg.enableFailedAllocEvents = true; + cfg.enableScopes = true; + cfg.maxScopesInHistory = 16; + cfg.windowStatsSize = 8; + cfg.enableTaskTracking = true; + monitor.init(cfg); - httpTag = monitor.registerTag("http_server"); - monitor.setTagBudget(httpTag, {60 * 1024, 80 * 1024}); + httpTag = monitor.registerTag("http_server"); + monitor.setTagBudget(httpTag, {60 * 1024, 80 * 1024}); - monitor.onThreshold([](const ThresholdEvent &evt) { - const char *region = evt.region == MemoryRegion::Psram ? "PSRAM" : "DRAM"; - if (evt.state == ThresholdState::Critical) { - ESP_LOGE("MEM", "%s critical free=%u", region, static_cast(evt.stats.freeBytes)); - } else if (evt.state == ThresholdState::Warn) { - ESP_LOGW("MEM", "%s warning free=%u", region, static_cast(evt.stats.freeBytes)); - } else { - ESP_LOGI("MEM", "%s recovered free=%u", region, static_cast(evt.stats.freeBytes)); - } - }); + monitor.onThreshold([](const ThresholdEvent &evt) { + const char *region = evt.region == MemoryRegion::Psram ? "PSRAM" : "DRAM"; + if (evt.state == ThresholdState::Critical) { + ESP_LOGE( + "MEM", + "%s critical free=%u", + region, + static_cast(evt.stats.freeBytes) + ); + } else if (evt.state == ThresholdState::Warn) { + ESP_LOGW( + "MEM", + "%s warning free=%u", + region, + static_cast(evt.stats.freeBytes) + ); + } else { + ESP_LOGI( + "MEM", + "%s recovered free=%u", + region, + static_cast(evt.stats.freeBytes) + ); + } + }); - monitor.onSample([](const MemorySnapshot &snapshot) { - for (const auto ®ion : snapshot.regions) { - const char *regionName = region.region == MemoryRegion::Psram ? "PSRAM" : "DRAM"; - ESP_LOGI("MEM", "%s free=%u min=%u frag=%.02f", regionName, - static_cast(region.freeBytes), - static_cast(region.minimumFreeBytes), - region.fragmentation); - } - for (const auto &stack : snapshot.stacks) { - ESP_LOGD("STACK", "%s water=%uB priority=%u state=%d", stack.name.c_str(), - static_cast(stack.freeHighWaterBytes), stack.priority, static_cast(stack.state)); - } - }); + monitor.onSample([](const MemorySnapshot &snapshot) { + for (const auto ®ion : snapshot.regions) { + const char *regionName = region.region == MemoryRegion::Psram ? "PSRAM" : "DRAM"; + ESP_LOGI( + "MEM", + "%s free=%u min=%u frag=%.02f", + regionName, + static_cast(region.freeBytes), + static_cast(region.minimumFreeBytes), + region.fragmentation + ); + } + for (const auto &stack : snapshot.stacks) { + ESP_LOGD( + "STACK", + "%s water=%uB priority=%u state=%d", + stack.name.c_str(), + static_cast(stack.freeHighWaterBytes), + stack.priority, + static_cast(stack.state) + ); + } + }); - monitor.onFailedAlloc([](const FailedAllocEvent &evt) { - ESP_LOGE("MEM", "alloc failed size=%u caps=0x%08x from %s", static_cast(evt.requestedBytes), evt.caps, evt.functionName); - }); + monitor.onFailedAlloc([](const FailedAllocEvent &evt) { + ESP_LOGE( + "MEM", + "alloc failed size=%u caps=0x%08x from %s", + static_cast(evt.requestedBytes), + evt.caps, + evt.functionName + ); + }); - monitor.onScope([](const ScopeStats &s) { - ESP_LOGI("SCOPE", "%s used %+d DRAM %+d PSRAM in %llu us", - s.name.c_str(), - static_cast(s.deltaInternalBytes), - static_cast(s.deltaPsramBytes), - static_cast(s.durationUs)); - }); + monitor.onScope([](const ScopeStats &s) { + ESP_LOGI( + "SCOPE", + "%s used %+d DRAM %+d PSRAM in %llu us", + s.name.c_str(), + static_cast(s.deltaInternalBytes), + static_cast(s.deltaPsramBytes), + static_cast(s.durationUs) + ); + }); - monitor.onTagThreshold([](const TagThresholdEvent &evt) { - ESP_LOGW("TAG", "%s now %s at %u bytes", - evt.usage.name.c_str(), - evt.usage.state == ThresholdState::Critical ? "CRITICAL" : - evt.usage.state == ThresholdState::Warn ? "WARN" : "OK", - static_cast(evt.usage.totalInternalBytes + evt.usage.totalPsramBytes)); - }); + monitor.onTagThreshold([](const TagThresholdEvent &evt) { + ESP_LOGW( + "TAG", + "%s now %s at %u bytes", + evt.usage.name.c_str(), + evt.usage.state == ThresholdState::Critical ? "CRITICAL" + : evt.usage.state == ThresholdState::Warn ? "WARN" + : "OK", + static_cast(evt.usage.totalInternalBytes + evt.usage.totalPsramBytes) + ); + }); - monitor.onTaskStackThreshold([](const TaskStackEvent &evt) { - if (evt.appeared) { - ESP_LOGI("TASK", "task created: %s", evt.usage.name.c_str()); - } else if (evt.disappeared) { - ESP_LOGW("TASK", "task disappeared: %s", evt.usage.name.c_str()); - } else { - ESP_LOGW("TASK", "task %s stack %s (%uB headroom)", evt.usage.name.c_str(), - evt.state == StackState::Critical ? "CRITICAL" : - evt.state == StackState::Warn ? "WARN" : "SAFE", - static_cast(evt.usage.freeHighWaterBytes)); - } - }); + monitor.onTaskStackThreshold([](const TaskStackEvent &evt) { + if (evt.appeared) { + ESP_LOGI("TASK", "task created: %s", evt.usage.name.c_str()); + } else if (evt.disappeared) { + ESP_LOGW("TASK", "task disappeared: %s", evt.usage.name.c_str()); + } else { + ESP_LOGW( + "TASK", + "task %s stack %s (%uB headroom)", + evt.usage.name.c_str(), + evt.state == StackState::Critical ? "CRITICAL" + : evt.state == StackState::Warn ? "WARN" + : "SAFE", + static_cast(evt.usage.freeHighWaterBytes) + ); + } + }); - monitor.onLeakCheck([](const LeakCheckResult &res) { - for (const auto &d : res.deltas) { - const char *regionName = d.region == MemoryRegion::Psram ? "PSRAM" : "DRAM"; - ESP_LOGI("LEAK", "%s drift %+0.1fB frag %+0.2f", regionName, d.deltaFreeBytes, d.deltaFragmentation); - } - }); + monitor.onLeakCheck([](const LeakCheckResult &res) { + for (const auto &d : res.deltas) { + const char *regionName = d.region == MemoryRegion::Psram ? "PSRAM" : "DRAM"; + ESP_LOGI( + "LEAK", + "%s drift %+0.1fB frag %+0.2f", + regionName, + d.deltaFreeBytes, + d.deltaFragmentation + ); + } + }); } void loop() { - auto scope = monitor.beginScope("http_req", httpTag); - delay(1000); - scope.end(); + auto scope = monitor.beginScope("http_req", httpTag); + delay(1000); + scope.end(); - static uint32_t counter = 0; - if (++counter % 60 == 0) { - monitor.markLeakCheckPoint("steady_state"); - } + static uint32_t counter = 0; + if (++counter % 60 == 0) { + monitor.markLeakCheckPoint("steady_state"); + } } diff --git a/examples/manual_sampling/manual_sampling.ino b/examples/manual_sampling/manual_sampling.ino index ff6bf2e..45933a6 100644 --- a/examples/manual_sampling/manual_sampling.ino +++ b/examples/manual_sampling/manual_sampling.ino @@ -10,105 +10,116 @@ static uint32_t lastSampleMs = 0; static constexpr uint32_t kSamplePeriodMs = 1500; static void logSnapshot(const MemorySnapshot &snapshot) { - for (const auto ®ion : snapshot.regions) { - const char *name = region.region == MemoryRegion::Psram ? "PSRAM" : "DRAM"; - ESP_LOGI("MEM", "%s free=%uB min=%uB frag=%.02f slope=%.01fB/s", - name, - static_cast(region.freeBytes), - static_cast(region.minimumFreeBytes), - region.fragmentation, - region.freeBytesSlope); - } + for (const auto ®ion : snapshot.regions) { + const char *name = region.region == MemoryRegion::Psram ? "PSRAM" : "DRAM"; + ESP_LOGI( + "MEM", + "%s free=%uB min=%uB frag=%.02f slope=%.01fB/s", + name, + static_cast(region.freeBytes), + static_cast(region.minimumFreeBytes), + region.fragmentation, + region.freeBytesSlope + ); + } } static MemoryMonitorConfig buildConfig() { - MemoryMonitorConfig cfg; - cfg.enableSamplerTask = false; - cfg.historySize = 8; - cfg.windowStatsSize = 5; - cfg.internal = {48 * 1024, 32 * 1024}; - cfg.psram = {256 * 1024, 160 * 1024}; - cfg.enableScopes = true; - cfg.maxScopesInHistory = 12; - return cfg; + MemoryMonitorConfig cfg; + cfg.enableSamplerTask = false; + cfg.historySize = 8; + cfg.windowStatsSize = 5; + cfg.internal = {48 * 1024, 32 * 1024}; + cfg.psram = {256 * 1024, 160 * 1024}; + cfg.enableScopes = true; + cfg.maxScopesInHistory = 12; + 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}); - - monitor.onThreshold([](const ThresholdEvent &evt) { - const char *region = evt.region == MemoryRegion::Psram ? "PSRAM" : "DRAM"; - ESP_LOGW("THRESH", "%s now %s (%u free bytes)", - region, - evt.state == ThresholdState::Critical ? "CRITICAL" : - evt.state == ThresholdState::Warn ? "WARN" : "OK", - static_cast(evt.stats.freeBytes)); - }); - - monitor.onSample(logSnapshot); - - monitor.onTagThreshold([](const TagThresholdEvent &evt) { - ESP_LOGW("TAG", "%s %s at %u bytes", - evt.usage.name.c_str(), - evt.usage.state == ThresholdState::Critical ? "CRITICAL" : - evt.usage.state == ThresholdState::Warn ? "WARN" : "OK", - static_cast(evt.usage.totalInternalBytes + evt.usage.totalPsramBytes)); - }); - - // Kick off an initial measurement so history() has data immediately. - lastSampleMs = millis(); - monitor.sampleNow(); - return true; + if (!monitor.init(buildConfig())) { + ESP_LOGE("MEM", "monitor.init() failed"); + return false; + } + + networkTag = monitor.registerTag("network"); + monitor.setTagBudget(networkTag, {48 * 1024, 64 * 1024}); + + monitor.onThreshold([](const ThresholdEvent &evt) { + const char *region = evt.region == MemoryRegion::Psram ? "PSRAM" : "DRAM"; + ESP_LOGW( + "THRESH", + "%s now %s (%u free bytes)", + region, + evt.state == ThresholdState::Critical ? "CRITICAL" + : evt.state == ThresholdState::Warn ? "WARN" + : "OK", + static_cast(evt.stats.freeBytes) + ); + }); + + monitor.onSample(logSnapshot); + + monitor.onTagThreshold([](const TagThresholdEvent &evt) { + ESP_LOGW( + "TAG", + "%s %s at %u bytes", + evt.usage.name.c_str(), + evt.usage.state == ThresholdState::Critical ? "CRITICAL" + : evt.usage.state == ThresholdState::Warn ? "WARN" + : "OK", + static_cast(evt.usage.totalInternalBytes + evt.usage.totalPsramBytes) + ); + }); + + // Kick off an initial measurement so history() has data immediately. + lastSampleMs = millis(); + monitor.sampleNow(); + return true; } void setup() { - Serial.begin(115200); - initMonitor(); + 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); - delay(40); - scope.end(); - - // Manual sampling cadence (no background task running). - const uint32_t now = millis(); - if (now - lastSampleMs >= kSamplePeriodMs) { - monitor.sampleNow(); - lastSampleMs = now; - } - - // Periodically inspect the history buffer. - static uint32_t historyTick = 0; - if (++historyTick % 50 == 0) { - const auto hist = monitor.history(); - ESP_LOGI("HIST", "stored snapshots: %u", static_cast(hist.size())); - } - - delay(60); + 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); + delay(40); + scope.end(); + + // Manual sampling cadence (no background task running). + const uint32_t now = millis(); + if (now - lastSampleMs >= kSamplePeriodMs) { + monitor.sampleNow(); + lastSampleMs = now; + } + + // Periodically inspect the history buffer. + static uint32_t historyTick = 0; + if (++historyTick % 50 == 0) { + const auto hist = monitor.history(); + ESP_LOGI("HIST", "stored snapshots: %u", static_cast(hist.size())); + } + + delay(60); } diff --git a/examples/panic_hook/panic_hook.ino b/examples/panic_hook/panic_hook.ino index 5919fca..0c00e0b 100644 --- a/examples/panic_hook/panic_hook.ino +++ b/examples/panic_hook/panic_hook.ino @@ -16,98 +16,111 @@ ESPMemoryMonitor monitor; static uint32_t lastSampleMs = 0; -static void emitSnapshot(const MemorySnapshot& snap) { +static void emitSnapshot(const MemorySnapshot &snap) { #if ESPMM_HAS_ARDUINOJSON && ESPMM_EXAMPLE_HAS_JSON - JsonDocument doc; - toJson(snap, doc); - serializeJson(doc, Serial); - Serial.println(); + JsonDocument doc; + toJson(snap, doc); + serializeJson(doc, Serial); + Serial.println(); #else - for (const auto& region : snap.regions) { - const char* name = region.region == MemoryRegion::Psram ? "PSRAM" : "DRAM"; - ESP_LOGI("MEM", "%s free=%uB min=%uB", name, - static_cast(region.freeBytes), - static_cast(region.minimumFreeBytes)); - } + for (const auto ®ion : snap.regions) { + const char *name = region.region == MemoryRegion::Psram ? "PSRAM" : "DRAM"; + ESP_LOGI( + "MEM", + "%s free=%uB min=%uB", + name, + static_cast(region.freeBytes), + static_cast(region.minimumFreeBytes) + ); + } #endif } void setup() { - Serial.begin(115200); - ESP_LOGI("PANIC", "Send 'p' over serial to simulate a crash and dump a panic snapshot"); + Serial.begin(115200); + ESP_LOGI("PANIC", "Send 'p' over serial to simulate a crash and dump a panic snapshot"); - MemoryMonitorConfig cfg; - cfg.sampleIntervalMs = 1200; - cfg.historySize = 16; - cfg.windowStatsSize = 6; - cfg.internal = {48 * 1024, 32 * 1024}; - cfg.psram = {256 * 1024, 160 * 1024}; - cfg.enableScopes = true; - cfg.maxScopesInHistory = 20; - cfg.enablePerTaskStacks = true; - cfg.enableTaskTracking = true; - cfg.enableFailedAllocEvents = true; - monitor.init(cfg); + MemoryMonitorConfig cfg; + cfg.sampleIntervalMs = 1200; + cfg.historySize = 16; + cfg.windowStatsSize = 6; + cfg.internal = {48 * 1024, 32 * 1024}; + cfg.psram = {256 * 1024, 160 * 1024}; + cfg.enableScopes = true; + cfg.maxScopesInHistory = 20; + cfg.enablePerTaskStacks = true; + cfg.enableTaskTracking = true; + cfg.enableFailedAllocEvents = true; + monitor.init(cfg); - monitor.setTaskStackThreshold("loopTask", {2048, 1024}); + monitor.setTaskStackThreshold("loopTask", {2048, 1024}); - monitor.onSample(emitSnapshot); + monitor.onSample(emitSnapshot); - monitor.onFailedAlloc([](const FailedAllocEvent& evt) { - ESP_LOGE("ALLOC", "failed alloc size=%u caps=0x%08x from %s", - static_cast(evt.requestedBytes), evt.caps, evt.functionName); - }); + monitor.onFailedAlloc([](const FailedAllocEvent &evt) { + ESP_LOGE( + "ALLOC", + "failed alloc size=%u caps=0x%08x from %s", + static_cast(evt.requestedBytes), + evt.caps, + evt.functionName + ); + }); - monitor.onTaskStackThreshold([](const TaskStackEvent& evt) { - if (evt.appeared) { - ESP_LOGI("TASK", "task appeared: %s", evt.usage.name.c_str()); - return; - } - if (evt.disappeared) { - ESP_LOGW("TASK", "task disappeared: %s", evt.usage.name.c_str()); - return; - } - ESP_LOGW("TASK", "%s stack %s (%uB headroom)", - evt.usage.name.c_str(), - evt.state == StackState::Critical ? "CRITICAL" : evt.state == StackState::Warn ? "WARN" - : "SAFE", - static_cast(evt.usage.freeHighWaterBytes)); - }); + monitor.onTaskStackThreshold([](const TaskStackEvent &evt) { + if (evt.appeared) { + ESP_LOGI("TASK", "task appeared: %s", evt.usage.name.c_str()); + return; + } + if (evt.disappeared) { + ESP_LOGW("TASK", "task disappeared: %s", evt.usage.name.c_str()); + return; + } + ESP_LOGW( + "TASK", + "%s stack %s (%uB headroom)", + evt.usage.name.c_str(), + evt.state == StackState::Critical ? "CRITICAL" + : evt.state == StackState::Warn ? "WARN" + : "SAFE", + static_cast(evt.usage.freeHighWaterBytes) + ); + }); - if (!monitor.installPanicHook([](const MemorySnapshot& snap) { - ESP_LOGE("PANIC", "captured panic snapshot"); - emitSnapshot(snap); - })) { - ESP_LOGE("PANIC", "failed to register panic hook"); - } + if (!monitor.installPanicHook([](const MemorySnapshot &snap) { + ESP_LOGE("PANIC", "captured panic snapshot"); + emitSnapshot(snap); + })) { + ESP_LOGE("PANIC", "failed to register panic hook"); + } - lastSampleMs = millis(); + lastSampleMs = millis(); } void loop() { - // Create some heap churn; in real code this could be your payload buffers. - { - auto scope = monitor.beginScope("stream_chunk"); - std::vector frame(12 * 1024, 0xA5); - delay(40); - (void)frame; - } + // Create some heap churn; in real code this could be your payload buffers. + { + auto scope = monitor.beginScope("stream_chunk"); + std::vector frame(12 * 1024, 0xA5); + delay(40); + (void)frame; + } - // Periodic sampling from the main loop. - const uint32_t now = millis(); - if (now - lastSampleMs >= 1200) { - monitor.sampleNow(); - lastSampleMs = now; - } + // Periodic sampling from the main loop. + const uint32_t now = millis(); + if (now - lastSampleMs >= 1200) { + monitor.sampleNow(); + lastSampleMs = now; + } - // Send 'p' over serial to force a panic and see the hook in action. - if (Serial.available()) { - const int c = Serial.read(); - if (c == 'p' || c == 'P') { - ESP_LOGE("PANIC", "simulating panic..."); - abort(); - } - } + // Send 'p' over serial to force a panic and see the hook in action. + if (Serial.available()) { + const int c = Serial.read(); + if (c == 'p' || c == 'P') { + ESP_LOGE("PANIC", "simulating panic..."); + abort(); + } + } - delay(80); + delay(80); } diff --git a/examples/scopes_and_leakcheck/scopes_and_leakcheck.ino b/examples/scopes_and_leakcheck/scopes_and_leakcheck.ino index 471f017..39f6099 100644 --- a/examples/scopes_and_leakcheck/scopes_and_leakcheck.ino +++ b/examples/scopes_and_leakcheck/scopes_and_leakcheck.ino @@ -16,103 +16,125 @@ static MemoryTag httpTag; static MemoryTag otaTag; void setup() { - Serial.begin(115200); + Serial.begin(115200); - MemoryMonitorConfig cfg; - cfg.sampleIntervalMs = 1000; - cfg.historySize = 40; - cfg.internal = {48 * 1024, 32 * 1024}; - cfg.psram = {256 * 1024, 160 * 1024}; - cfg.enableScopes = true; - cfg.maxScopesInHistory = 24; - cfg.windowStatsSize = 8; - cfg.enablePerTaskStacks = true; - cfg.enableTaskTracking = true; - monitor.init(cfg); + MemoryMonitorConfig cfg; + cfg.sampleIntervalMs = 1000; + cfg.historySize = 40; + cfg.internal = {48 * 1024, 32 * 1024}; + cfg.psram = {256 * 1024, 160 * 1024}; + cfg.enableScopes = true; + cfg.maxScopesInHistory = 24; + cfg.windowStatsSize = 8; + cfg.enablePerTaskStacks = true; + cfg.enableTaskTracking = true; + monitor.init(cfg); - httpTag = monitor.registerTag("http_server"); - otaTag = monitor.registerTag("ota"); - monitor.setTagBudget(httpTag, {60 * 1024, 80 * 1024}); - monitor.setTagBudget(otaTag, {40 * 1024, 64 * 1024}); + httpTag = monitor.registerTag("http_server"); + otaTag = monitor.registerTag("ota"); + monitor.setTagBudget(httpTag, {60 * 1024, 80 * 1024}); + monitor.setTagBudget(otaTag, {40 * 1024, 64 * 1024}); - monitor.onScope([](const ScopeStats& s) { - ESP_LOGI("SCOPE", "%s used %+d DRAM %+d PSRAM in %llu us", - s.name.c_str(), - static_cast(s.deltaInternalBytes), - static_cast(s.deltaPsramBytes), - static_cast(s.durationUs)); - }); + monitor.onScope([](const ScopeStats &s) { + ESP_LOGI( + "SCOPE", + "%s used %+d DRAM %+d PSRAM in %llu us", + s.name.c_str(), + static_cast(s.deltaInternalBytes), + static_cast(s.deltaPsramBytes), + static_cast(s.durationUs) + ); + }); - monitor.onTagThreshold([](const TagThresholdEvent& evt) { - ESP_LOGW("TAG", "%s now %s at %u bytes", - evt.usage.name.c_str(), - evt.usage.state == ThresholdState::Critical ? "CRITICAL" : evt.usage.state == ThresholdState::Warn ? "WARN" - : "OK", - static_cast(evt.usage.totalInternalBytes + evt.usage.totalPsramBytes)); - }); + monitor.onTagThreshold([](const TagThresholdEvent &evt) { + ESP_LOGW( + "TAG", + "%s now %s at %u bytes", + evt.usage.name.c_str(), + evt.usage.state == ThresholdState::Critical ? "CRITICAL" + : evt.usage.state == ThresholdState::Warn ? "WARN" + : "OK", + static_cast(evt.usage.totalInternalBytes + evt.usage.totalPsramBytes) + ); + }); - monitor.onLeakCheck([](const LeakCheckResult& res) { - for (const auto& d : res.deltas) { - const char* regionName = d.region == MemoryRegion::Psram ? "PSRAM" : "DRAM"; - ESP_LOGI("LEAK", "%s drift %+0.1fB frag %+0.2f", regionName, d.deltaFreeBytes, d.deltaFragmentation); - } - if (res.leakSuspected) { - ESP_LOGW("LEAK", "possible leak between %s -> %s", res.fromLabel.c_str(), res.toLabel.c_str()); - } - }); + monitor.onLeakCheck([](const LeakCheckResult &res) { + for (const auto &d : res.deltas) { + const char *regionName = d.region == MemoryRegion::Psram ? "PSRAM" : "DRAM"; + ESP_LOGI( + "LEAK", + "%s drift %+0.1fB frag %+0.2f", + regionName, + d.deltaFreeBytes, + d.deltaFragmentation + ); + } + if (res.leakSuspected) { + ESP_LOGW( + "LEAK", + "possible leak between %s -> %s", + res.fromLabel.c_str(), + res.toLabel.c_str() + ); + } + }); - monitor.onTaskStackThreshold([](const TaskStackEvent& evt) { - if (evt.appeared) { - ESP_LOGI("TASK", "task created: %s", evt.usage.name.c_str()); - return; - } - if (evt.disappeared) { - ESP_LOGW("TASK", "task disappeared: %s", evt.usage.name.c_str()); - return; - } - ESP_LOGW("TASK", "%s stack %s (%uB headroom)", - evt.usage.name.c_str(), - evt.state == StackState::Critical ? "CRITICAL" : evt.state == StackState::Warn ? "WARN" - : "SAFE", - static_cast(evt.usage.freeHighWaterBytes)); - }); + monitor.onTaskStackThreshold([](const TaskStackEvent &evt) { + if (evt.appeared) { + ESP_LOGI("TASK", "task created: %s", evt.usage.name.c_str()); + return; + } + if (evt.disappeared) { + ESP_LOGW("TASK", "task disappeared: %s", evt.usage.name.c_str()); + return; + } + ESP_LOGW( + "TASK", + "%s stack %s (%uB headroom)", + evt.usage.name.c_str(), + evt.state == StackState::Critical ? "CRITICAL" + : evt.state == StackState::Warn ? "WARN" + : "SAFE", + static_cast(evt.usage.freeHighWaterBytes) + ); + }); } static void simulateHttpRequest() { - auto scope = monitor.beginScope("http_req", httpTag); - std::string payload(8 * 1024, 'x'); // simulate buffer - delay(50); - scope.end(); + auto scope = monitor.beginScope("http_req", httpTag); + std::string payload(8 * 1024, 'x'); // simulate buffer + delay(50); + scope.end(); } static void simulateOtaChunk() { - auto scope = monitor.beginScope("ota_chunk", otaTag); - std::vector chunk(16 * 1024, 0xAA); - delay(20); - scope.end(); + auto scope = monitor.beginScope("ota_chunk", otaTag); + std::vector chunk(16 * 1024, 0xAA); + delay(20); + scope.end(); } static void exportSnapshotJson() { #if ESPMM_HAS_ARDUINOJSON && ESPMM_EXAMPLE_HAS_JSON - JsonDocument doc; - const MemorySnapshot snap = monitor.sampleNow(); - toJson(snap, doc); - serializeJson(doc, Serial); - Serial.println(); + JsonDocument doc; + const MemorySnapshot snap = monitor.sampleNow(); + toJson(snap, doc); + serializeJson(doc, Serial); + Serial.println(); #else - (void)monitor; + (void)monitor; #endif } void loop() { - simulateHttpRequest(); - simulateOtaChunk(); + simulateHttpRequest(); + simulateOtaChunk(); - static uint32_t counter = 0; - if (++counter % 30 == 0) { - monitor.markLeakCheckPoint("steady_state"); - exportSnapshotJson(); - } + static uint32_t counter = 0; + if (++counter % 30 == 0) { + monitor.markLeakCheckPoint("steady_state"); + exportSnapshotJson(); + } - delay(500); + delay(500); } diff --git a/scripts/format_cpp.sh b/scripts/format_cpp.sh new file mode 100755 index 0000000..7d17b04 --- /dev/null +++ b/scripts/format_cpp.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -euo pipefail + +_repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +_clang_format="${_repo_root}/.vscode/bin/clang-format" + +if [ ! -x "${_clang_format}" ]; then + echo "clang-format wrapper not found: ${_clang_format}" >&2 + exit 1 +fi + +mapfile -d '' _format_files < <( + git -C "${_repo_root}" ls-files -z -- '*.c' '*.cc' '*.cpp' '*.h' '*.hpp' '*.ino' +) + +if [ "${#_format_files[@]}" -eq 0 ]; then + echo "No tracked C/C++/INO files found to format." + exit 0 +fi + +"${_clang_format}" -i --style=file "${_format_files[@]}" + +echo "Formatted ${#_format_files[@]} files." diff --git a/src/ESPMemoryMonitor.h b/src/ESPMemoryMonitor.h index 7a62113..9ac37a9 100644 --- a/src/ESPMemoryMonitor.h +++ b/src/ESPMemoryMonitor.h @@ -1,4 +1,3 @@ #pragma once #include "esp_memory_monitor/memory_monitor.h" - diff --git a/src/esp_memory_monitor/memory_monitor.cpp b/src/esp_memory_monitor/memory_monitor.cpp index b6bb79f..779fdfa 100644 --- a/src/esp_memory_monitor/memory_monitor.cpp +++ b/src/esp_memory_monitor/memory_monitor.cpp @@ -9,1127 +9,1244 @@ #include namespace { -constexpr const char* kSamplerTaskName = "ESPMemoryMon"; -constexpr const char* kLogTag = "ESPMemoryMon"; +constexpr const char *kSamplerTaskName = "ESPMemoryMon"; +constexpr const char *kLogTag = "ESPMemoryMon"; constexpr float kFragmentationEpsilon = 0.05f; inline TickType_t delayTicks(uint32_t intervalMs) { - const TickType_t ticks = pdMS_TO_TICKS(intervalMs); - return ticks == 0 ? 1 : ticks; + const TickType_t ticks = pdMS_TO_TICKS(intervalMs); + return ticks == 0 ? 1 : ticks; } inline size_t regionIndex(MemoryRegion region) { - return region == MemoryRegion::Psram ? 1 : 0; + return region == MemoryRegion::Psram ? 1 : 0; } inline size_t saturatingSubtract(size_t value, size_t delta) { - return value > delta ? value - delta : 0; + return value > delta ? value - delta : 0; } -} // namespace +} // namespace -ESPMemoryMonitor* gPanicInstance = nullptr; -ESPMemoryMonitor* ESPMemoryMonitor::_failedAllocInstance = nullptr; +ESPMemoryMonitor *gPanicInstance = nullptr; +ESPMemoryMonitor *ESPMemoryMonitor::_failedAllocInstance = nullptr; ESPMemoryMonitor::ESPMemoryMonitor() : _history(MemoryMonitorAllocator(false)), _scopeHistory(MemoryMonitorAllocator(false)), _tagUsage(MemoryMonitorAllocator(false)), _tagBudgets(MemoryMonitorAllocator(false)), - _taskThresholds(0, std::hash{}, std::equal_to{}, MemoryMonitorAllocator>(false)), - _knownTasks(0, std::hash{}, std::equal_to{}, MemoryMonitorAllocator>(false)), + _taskThresholds( + 0, + std::hash{}, + std::equal_to{}, + MemoryMonitorAllocator>(false) + ), + _knownTasks( + 0, + std::hash{}, + std::equal_to{}, + MemoryMonitorAllocator>(false) + ), _leakHistory(MemoryMonitorAllocator(false)), - _leakCheckpoints(MemoryMonitorAllocator(false)) {} + _leakCheckpoints(MemoryMonitorAllocator(false)) { +} -MemoryScope::MemoryScope(ESPMemoryMonitor* monitor, std::string name, MemoryTag tag, size_t startInternal, size_t startPsram, uint64_t startUs) - : _monitor(monitor), _name(std::move(name)), _tag(tag), _startInternal(startInternal), _startPsram(startPsram), _startUs(startUs) {} +MemoryScope::MemoryScope( + ESPMemoryMonitor *monitor, + std::string name, + MemoryTag tag, + size_t startInternal, + size_t startPsram, + uint64_t startUs +) + : _monitor(monitor), _name(std::move(name)), _tag(tag), _startInternal(startInternal), + _startPsram(startPsram), _startUs(startUs) { +} MemoryScope::~MemoryScope() { - if (active()) { - end(); - } + if (active()) { + end(); + } } -MemoryScope::MemoryScope(MemoryScope&& other) noexcept { - *this = std::move(other); +MemoryScope::MemoryScope(MemoryScope &&other) noexcept { + *this = std::move(other); } -MemoryScope& MemoryScope::operator=(MemoryScope&& other) noexcept { - if (this != &other) { - if (active()) { - end(); - } - _monitor = other._monitor; - _name = std::move(other._name); - _tag = other._tag; - _startInternal = other._startInternal; - _startPsram = other._startPsram; - _startUs = other._startUs; - _ended = other._ended; - - other._monitor = nullptr; - other._ended = true; - } - return *this; +MemoryScope &MemoryScope::operator=(MemoryScope &&other) noexcept { + if (this != &other) { + if (active()) { + end(); + } + _monitor = other._monitor; + _name = std::move(other._name); + _tag = other._tag; + _startInternal = other._startInternal; + _startPsram = other._startPsram; + _startUs = other._startUs; + _ended = other._ended; + + other._monitor = nullptr; + other._ended = true; + } + return *this; } ScopeStats MemoryScope::end() { - if (!active()) { - return {}; - } - - _ended = true; - ScopeStats stats = _monitor->finalizeScope(*this); - _monitor = nullptr; - return stats; + if (!active()) { + return {}; + } + + _ended = true; + ScopeStats stats = _monitor->finalizeScope(*this); + _monitor = nullptr; + return stats; } ESPMemoryMonitor::~ESPMemoryMonitor() { - deinit(); + deinit(); } -bool ESPMemoryMonitor::init(const MemoryMonitorConfig& config) { - if (_initialized) { - deinit(); - } - - _config = config; - _usePSRAMBuffers = _config.usePSRAMBuffers; - resetOwnedContainers(); - - _mutex = xSemaphoreCreateMutex(); - if (_mutex == nullptr) { - return false; - } - - if (_config.enableFailedAllocEvents) { - registerFailedAllocCallback(); - } - - _running = _config.enableSamplerTask && _config.sampleIntervalMs > 0; - - if (_running) { - const BaseType_t created = xTaskCreatePinnedToCore( - &ESPMemoryMonitor::samplerTaskThunk, - kSamplerTaskName, - _config.stackSize, - this, - _config.priority, - &_samplerTask, - _config.coreId); - - if (created != pdPASS) { - _running = false; - unregisterFailedAllocCallback(); - vSemaphoreDelete(_mutex); - _mutex = nullptr; - return false; - } - } - - _initialized = true; - return true; +bool ESPMemoryMonitor::init(const MemoryMonitorConfig &config) { + if (_initialized) { + deinit(); + } + + _config = config; + _usePSRAMBuffers = _config.usePSRAMBuffers; + resetOwnedContainers(); + + _mutex = xSemaphoreCreateMutex(); + if (_mutex == nullptr) { + return false; + } + + if (_config.enableFailedAllocEvents) { + registerFailedAllocCallback(); + } + + _running = _config.enableSamplerTask && _config.sampleIntervalMs > 0; + + if (_running) { + const BaseType_t created = xTaskCreatePinnedToCore( + &ESPMemoryMonitor::samplerTaskThunk, + kSamplerTaskName, + _config.stackSize, + this, + _config.priority, + &_samplerTask, + _config.coreId + ); + + if (created != pdPASS) { + _running = false; + unregisterFailedAllocCallback(); + vSemaphoreDelete(_mutex); + _mutex = nullptr; + return false; + } + } + + _initialized = true; + return true; } void ESPMemoryMonitor::deinit() { - if (!_initialized) { - return; - } - - _running = false; - - if (_samplerTask != nullptr) { - TickType_t start = xTaskGetTickCount(); - while (_samplerTask != nullptr && (xTaskGetTickCount() - start) <= pdMS_TO_TICKS(200)) { - vTaskDelay(pdMS_TO_TICKS(10)); - } - if (_samplerTask != nullptr) { - vTaskDelete(_samplerTask); - _samplerTask = nullptr; - } - } - - unregisterFailedAllocCallback(); - unregisterPanicHandler(); - - { - LockGuard guard(_mutex); - // Recreate monitor-owned containers so reserved capacity is released. - resetOwnedContainers(); - _thresholdStates = {ThresholdState::Normal, ThresholdState::Normal}; - _sampleCallback = nullptr; - _thresholdCallback = nullptr; - _allocCallback = nullptr; - _scopeCallback = nullptr; - _tagThresholdCallback = nullptr; - _taskStackCallback = nullptr; - _leakCallback = nullptr; - _panicCallback = nullptr; - } - - if (_mutex != nullptr) { - vSemaphoreDelete(_mutex); - _mutex = nullptr; - } - _initialized = false; - _panicHookInstalled = false; - _running = false; - _config = MemoryMonitorConfig{}; - _usePSRAMBuffers = false; + if (!_initialized) { + return; + } + + _running = false; + + if (_samplerTask != nullptr) { + TickType_t start = xTaskGetTickCount(); + while (_samplerTask != nullptr && (xTaskGetTickCount() - start) <= pdMS_TO_TICKS(200)) { + vTaskDelay(pdMS_TO_TICKS(10)); + } + if (_samplerTask != nullptr) { + vTaskDelete(_samplerTask); + _samplerTask = nullptr; + } + } + + unregisterFailedAllocCallback(); + unregisterPanicHandler(); + + { + LockGuard guard(_mutex); + // Recreate monitor-owned containers so reserved capacity is released. + resetOwnedContainers(); + _thresholdStates = {ThresholdState::Normal, ThresholdState::Normal}; + _sampleCallback = nullptr; + _thresholdCallback = nullptr; + _allocCallback = nullptr; + _scopeCallback = nullptr; + _tagThresholdCallback = nullptr; + _taskStackCallback = nullptr; + _leakCallback = nullptr; + _panicCallback = nullptr; + } + + if (_mutex != nullptr) { + vSemaphoreDelete(_mutex); + _mutex = nullptr; + } + _initialized = false; + _panicHookInstalled = false; + _running = false; + _config = MemoryMonitorConfig{}; + _usePSRAMBuffers = false; } MemorySnapshot ESPMemoryMonitor::sampleNow() { - if (!_initialized) { - return {}; - } - - InternalMemorySnapshot snapshot = captureSnapshot(); - MemorySnapshot publicSnapshot; - SampleCallback sampleCb; - ThresholdCallback thresholdCb; - TaskStackThresholdCallback stackCb; - MemoryMonitorVector events{MemoryMonitorAllocator(_usePSRAMBuffers)}; - MemoryMonitorVector stackEvents{MemoryMonitorAllocator(_usePSRAMBuffers)}; - - { - LockGuard guard(_mutex); - enrichSnapshotLocked(snapshot); - appendHistoryLocked(snapshot); - events = evaluateThresholdsLocked(snapshot); - trackTasksLocked(snapshot, stackEvents); - sampleCb = _sampleCallback; - thresholdCb = _thresholdCallback; - stackCb = _taskStackCallback; - publicSnapshot = toPublicSnapshot(snapshot); - } - - for (const auto& evt : events) { - if (thresholdCb) { - thresholdCb(evt); - } - } - - for (const auto& evt : stackEvents) { - if (stackCb) { - stackCb(evt); - } - } - - if (sampleCb) { - sampleCb(publicSnapshot); - } - - return publicSnapshot; + if (!_initialized) { + return {}; + } + + InternalMemorySnapshot snapshot = captureSnapshot(); + MemorySnapshot publicSnapshot; + SampleCallback sampleCb; + ThresholdCallback thresholdCb; + TaskStackThresholdCallback stackCb; + MemoryMonitorVector events{ + MemoryMonitorAllocator(_usePSRAMBuffers) + }; + MemoryMonitorVector stackEvents{ + MemoryMonitorAllocator(_usePSRAMBuffers) + }; + + { + LockGuard guard(_mutex); + enrichSnapshotLocked(snapshot); + appendHistoryLocked(snapshot); + events = evaluateThresholdsLocked(snapshot); + trackTasksLocked(snapshot, stackEvents); + sampleCb = _sampleCallback; + thresholdCb = _thresholdCallback; + stackCb = _taskStackCallback; + publicSnapshot = toPublicSnapshot(snapshot); + } + + for (const auto &evt : events) { + if (thresholdCb) { + thresholdCb(evt); + } + } + + for (const auto &evt : stackEvents) { + if (stackCb) { + stackCb(evt); + } + } + + if (sampleCb) { + sampleCb(publicSnapshot); + } + + return publicSnapshot; } std::vector ESPMemoryMonitor::history() const { - LockGuard guard(_mutex); - std::vector out; - out.reserve(_history.size()); - for (const auto& snap : _history) { - out.push_back(toPublicSnapshot(snap)); - } - return out; + LockGuard guard(_mutex); + std::vector out; + out.reserve(_history.size()); + for (const auto &snap : _history) { + out.push_back(toPublicSnapshot(snap)); + } + return out; } MemoryMonitorConfig ESPMemoryMonitor::currentConfig() const { - LockGuard guard(_mutex); - return _config; + LockGuard guard(_mutex); + return _config; } void ESPMemoryMonitor::onSample(SampleCallback callback) { - LockGuard guard(_mutex); - _sampleCallback = std::move(callback); + LockGuard guard(_mutex); + _sampleCallback = std::move(callback); } void ESPMemoryMonitor::onThreshold(ThresholdCallback callback) { - LockGuard guard(_mutex); - _thresholdCallback = std::move(callback); + LockGuard guard(_mutex); + _thresholdCallback = std::move(callback); } void ESPMemoryMonitor::onFailedAlloc(FailedAllocCallback callback) { - LockGuard guard(_mutex); - _allocCallback = std::move(callback); + LockGuard guard(_mutex); + _allocCallback = std::move(callback); } void ESPMemoryMonitor::onScope(ScopeCallback callback) { - LockGuard guard(_mutex); - _scopeCallback = std::move(callback); + LockGuard guard(_mutex); + _scopeCallback = std::move(callback); } void ESPMemoryMonitor::onTagThreshold(TagThresholdCallback callback) { - LockGuard guard(_mutex); - _tagThresholdCallback = std::move(callback); + LockGuard guard(_mutex); + _tagThresholdCallback = std::move(callback); } void ESPMemoryMonitor::onTaskStackThreshold(TaskStackThresholdCallback callback) { - LockGuard guard(_mutex); - _taskStackCallback = std::move(callback); + LockGuard guard(_mutex); + _taskStackCallback = std::move(callback); } void ESPMemoryMonitor::onLeakCheck(LeakCheckCallback callback) { - LockGuard guard(_mutex); - _leakCallback = std::move(callback); + LockGuard guard(_mutex); + _leakCallback = std::move(callback); } -MemoryScope ESPMemoryMonitor::beginScope(const std::string& name, MemoryTag tag) { - if (!_initialized || !_config.enableScopes) { - return {}; - } +MemoryScope ESPMemoryMonitor::beginScope(const std::string &name, MemoryTag tag) { + if (!_initialized || !_config.enableScopes) { + return {}; + } - const uint64_t startUs = esp_timer_get_time(); - const RegionStats internal = captureRegion(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT, MemoryRegion::Internal); - const RegionStats psram = captureRegion(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MemoryRegion::Psram); + const uint64_t startUs = esp_timer_get_time(); + const RegionStats internal = + captureRegion(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT, MemoryRegion::Internal); + const RegionStats psram = + captureRegion(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MemoryRegion::Psram); - return MemoryScope(this, name, tag, internal.freeBytes, psram.freeBytes, startUs); + return MemoryScope(this, name, tag, internal.freeBytes, psram.freeBytes, startUs); } -MemoryTag ESPMemoryMonitor::registerTag(const std::string& name) { - LockGuard guard(_mutex); - const MemoryTag tag = static_cast(_tagUsage.size() + 1); - InternalTagUsage usage(_usePSRAMBuffers); - usage.tag = tag; - usage.name.assign(name.c_str(), name.size()); - _tagUsage.push_back(std::move(usage)); - _tagBudgets.push_back({}); - return tag; +MemoryTag ESPMemoryMonitor::registerTag(const std::string &name) { + LockGuard guard(_mutex); + const MemoryTag tag = static_cast(_tagUsage.size() + 1); + InternalTagUsage usage(_usePSRAMBuffers); + usage.tag = tag; + usage.name.assign(name.c_str(), name.size()); + _tagUsage.push_back(std::move(usage)); + _tagBudgets.push_back({}); + return tag; } -bool ESPMemoryMonitor::setTagBudget(MemoryTag tag, const TagBudget& budget) { - LockGuard guard(_mutex); - if (tag == kInvalidMemoryTag || tag == 0 || static_cast(tag) > _tagBudgets.size()) { - return false; - } +bool ESPMemoryMonitor::setTagBudget(MemoryTag tag, const TagBudget &budget) { + LockGuard guard(_mutex); + if (tag == kInvalidMemoryTag || tag == 0 || static_cast(tag) > _tagBudgets.size()) { + return false; + } - _tagBudgets[tag - 1] = budget; - return true; + _tagBudgets[tag - 1] = budget; + return true; } std::vector ESPMemoryMonitor::scopeHistory() const { - LockGuard guard(_mutex); - std::vector out; - out.reserve(_scopeHistory.size()); - for (const auto& scope : _scopeHistory) { - out.push_back(toPublicScopeStats(scope)); - } - return out; + LockGuard guard(_mutex); + std::vector out; + out.reserve(_scopeHistory.size()); + for (const auto &scope : _scopeHistory) { + out.push_back(toPublicScopeStats(scope)); + } + return out; } std::vector ESPMemoryMonitor::tagUsage() const { - LockGuard guard(_mutex); - std::vector out; - out.reserve(_tagUsage.size()); - for (const auto& usage : _tagUsage) { - out.push_back(toPublicTagUsage(usage)); - } - return out; + LockGuard guard(_mutex); + std::vector out; + out.reserve(_tagUsage.size()); + for (const auto &usage : _tagUsage) { + out.push_back(toPublicTagUsage(usage)); + } + return out; } -LeakCheckResult ESPMemoryMonitor::markLeakCheckPoint(const std::string& label) { - if (!_initialized) { - return {}; - } +LeakCheckResult ESPMemoryMonitor::markLeakCheckPoint(const std::string &label) { + if (!_initialized) { + return {}; + } - sampleNow(); + sampleNow(); - InternalLeakCheckResult internal(_usePSRAMBuffers); - LeakCheckCallback cb; - { - LockGuard guard(_mutex); - internal = buildLeakCheckLocked(label); - cb = _leakCallback; - } - LeakCheckResult result = toPublicLeakCheckResult(internal); + InternalLeakCheckResult internal(_usePSRAMBuffers); + LeakCheckCallback cb; + { + LockGuard guard(_mutex); + internal = buildLeakCheckLocked(label); + cb = _leakCallback; + } + LeakCheckResult result = toPublicLeakCheckResult(internal); - if (cb && !result.deltas.empty()) { - cb(result); - } + if (cb && !result.deltas.empty()) { + cb(result); + } - return result; + return result; } -bool ESPMemoryMonitor::setTaskStackThreshold(const std::string& taskName, const TaskStackThreshold& threshold) { - LockGuard guard(_mutex); - MemoryMonitorString internalName(taskName.c_str(), MemoryMonitorAllocator(_usePSRAMBuffers)); - _taskThresholds[std::move(internalName)] = threshold; - return true; +bool ESPMemoryMonitor::setTaskStackThreshold( + const std::string &taskName, const TaskStackThreshold &threshold +) { + LockGuard guard(_mutex); + MemoryMonitorString internalName( + taskName.c_str(), + MemoryMonitorAllocator(_usePSRAMBuffers) + ); + _taskThresholds[std::move(internalName)] = threshold; + return true; } bool ESPMemoryMonitor::installPanicHook(PanicCallback callback) { - if (!_initialized) { - return false; - } - - LockGuard guard(_mutex); - _panicCallback = std::move(callback); - if (_panicHookInstalled) { - return true; - } - - if (!registerPanicHandler()) { - return false; - } - - _panicHookInstalled = true; - return true; + if (!_initialized) { + return false; + } + + LockGuard guard(_mutex); + _panicCallback = std::move(callback); + if (_panicHookInstalled) { + return true; + } + + if (!registerPanicHandler()) { + return false; + } + + _panicHookInstalled = true; + return true; } void ESPMemoryMonitor::uninstallPanicHook() { - LockGuard guard(_mutex); - _panicCallback = nullptr; - unregisterPanicHandler(); - _panicHookInstalled = false; + LockGuard guard(_mutex); + _panicCallback = nullptr; + unregisterPanicHandler(); + _panicHookInstalled = false; } -void ESPMemoryMonitor::samplerTaskThunk(void* arg) { - auto* self = static_cast(arg); - if (self == nullptr) { - return; - } +void ESPMemoryMonitor::samplerTaskThunk(void *arg) { + auto *self = static_cast(arg); + if (self == nullptr) { + return; + } - self->samplerTaskLoop(); + self->samplerTaskLoop(); } void ESPMemoryMonitor::samplerTaskLoop() { - while (_running) { - sampleNow(); - vTaskDelay(delayTicks(_config.sampleIntervalMs)); - } - _samplerTask = nullptr; - vTaskDelete(nullptr); + while (_running) { + sampleNow(); + vTaskDelay(delayTicks(_config.sampleIntervalMs)); + } + _samplerTask = nullptr; + vTaskDelete(nullptr); } ESPMemoryMonitor::InternalMemorySnapshot ESPMemoryMonitor::captureSnapshot() const { - InternalMemorySnapshot snapshot(_usePSRAMBuffers); - snapshot.timestampUs = esp_timer_get_time(); + InternalMemorySnapshot snapshot(_usePSRAMBuffers); + snapshot.timestampUs = esp_timer_get_time(); - snapshot.regions.push_back(captureRegion(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT, MemoryRegion::Internal)); - snapshot.regions.push_back(captureRegion(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MemoryRegion::Psram)); + snapshot.regions.push_back( + captureRegion(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT, MemoryRegion::Internal) + ); + snapshot.regions.push_back( + captureRegion(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MemoryRegion::Psram) + ); - if (_config.enablePerTaskStacks) { - snapshot.stacks = captureStacks(); - } + if (_config.enablePerTaskStacks) { + snapshot.stacks = captureStacks(); + } - return snapshot; + return snapshot; } RegionStats ESPMemoryMonitor::captureRegion(uint32_t caps, MemoryRegion region) const { - RegionStats stats{}; - stats.region = region; - - multi_heap_info_t info{}; - heap_caps_get_info(&info, caps); - - stats.freeBytes = info.total_free_bytes; - stats.largestFreeBlock = info.largest_free_block; - if (_config.enableMinEverFree) { - stats.minimumFreeBytes = heap_caps_get_minimum_free_size(caps); - } - if (_config.enableFragmentation && stats.freeBytes > 0 && stats.largestFreeBlock <= stats.freeBytes) { - stats.fragmentation = 1.0f - static_cast(stats.largestFreeBlock) / static_cast(stats.freeBytes); - } else { - stats.fragmentation = 0.0f; - } - - return stats; + RegionStats stats{}; + stats.region = region; + + multi_heap_info_t info{}; + heap_caps_get_info(&info, caps); + + stats.freeBytes = info.total_free_bytes; + stats.largestFreeBlock = info.largest_free_block; + if (_config.enableMinEverFree) { + stats.minimumFreeBytes = heap_caps_get_minimum_free_size(caps); + } + if (_config.enableFragmentation && stats.freeBytes > 0 && + stats.largestFreeBlock <= stats.freeBytes) { + stats.fragmentation = + 1.0f - static_cast(stats.largestFreeBlock) / static_cast(stats.freeBytes); + } else { + stats.fragmentation = 0.0f; + } + + return stats; } -MemoryMonitorVector ESPMemoryMonitor::captureStacks() const { +MemoryMonitorVector +ESPMemoryMonitor::captureStacks() const { #if defined(configUSE_TRACE_FACILITY) && (configUSE_TRACE_FACILITY == 1) - const UBaseType_t taskCount = uxTaskGetNumberOfTasks(); - if (taskCount == 0) { - return {}; - } - - MemoryMonitorVector statuses{MemoryMonitorAllocator(_usePSRAMBuffers)}; - statuses.resize(taskCount); - uint32_t totalRuntime = 0; - const UBaseType_t written = uxTaskGetSystemState(statuses.data(), statuses.size(), &totalRuntime); - statuses.resize(written); - - MemoryMonitorVector usages{MemoryMonitorAllocator(_usePSRAMBuffers)}; - usages.reserve(statuses.size()); - for (const auto& status : statuses) { - InternalTaskStackUsage usage(_usePSRAMBuffers); - usage.name = status.pcTaskName != nullptr ? status.pcTaskName : "unknown"; - usage.freeHighWaterBytes = static_cast(status.usStackHighWaterMark) * sizeof(StackType_t); - usage.state = status.eCurrentState; - usage.priority = status.uxCurrentPriority; - usage.handle = status.xHandle; - usages.push_back(std::move(usage)); - } - - return usages; + const UBaseType_t taskCount = uxTaskGetNumberOfTasks(); + if (taskCount == 0) { + return {}; + } + + MemoryMonitorVector statuses{ + MemoryMonitorAllocator(_usePSRAMBuffers) + }; + statuses.resize(taskCount); + uint32_t totalRuntime = 0; + const UBaseType_t written = + uxTaskGetSystemState(statuses.data(), statuses.size(), &totalRuntime); + statuses.resize(written); + + MemoryMonitorVector usages{ + MemoryMonitorAllocator(_usePSRAMBuffers) + }; + usages.reserve(statuses.size()); + for (const auto &status : statuses) { + InternalTaskStackUsage usage(_usePSRAMBuffers); + usage.name = status.pcTaskName != nullptr ? status.pcTaskName : "unknown"; + usage.freeHighWaterBytes = + static_cast(status.usStackHighWaterMark) * sizeof(StackType_t); + usage.state = status.eCurrentState; + usage.priority = status.uxCurrentPriority; + usage.handle = status.xHandle; + usages.push_back(std::move(usage)); + } + + return usages; #else - return {}; + return {}; #endif } -ScopeStats ESPMemoryMonitor::finalizeScope(const MemoryScope& scope) { - InternalScopeStats stats(_usePSRAMBuffers); - stats.name.assign(scope._name.c_str(), scope._name.size()); - stats.tag = scope._tag; - stats.startInternalFreeBytes = scope._startInternal; - stats.startPsramFreeBytes = scope._startPsram; - - const uint64_t endUs = esp_timer_get_time(); - const RegionStats internal = captureRegion(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT, MemoryRegion::Internal); - const RegionStats psram = captureRegion(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MemoryRegion::Psram); - - stats.endInternalFreeBytes = internal.freeBytes; - stats.endPsramFreeBytes = psram.freeBytes; - stats.deltaInternalBytes = static_cast(scope._startInternal) - static_cast(internal.freeBytes); - stats.deltaPsramBytes = static_cast(scope._startPsram) - static_cast(psram.freeBytes); - stats.startedUs = scope._startUs; - stats.endedUs = endUs; - stats.durationUs = endUs - scope._startUs; - - ScopeCallback scopeCb; - TagThresholdCallback tagCb; - MemoryMonitorVector tagEvents{MemoryMonitorAllocator(_usePSRAMBuffers)}; - { - LockGuard guard(_mutex); - appendScopeLocked(stats); - tagEvents = evaluateTagThresholdsLocked(stats); - scopeCb = _scopeCallback; - tagCb = _tagThresholdCallback; - } - - if (scopeCb) { - scopeCb(toPublicScopeStats(stats)); - } - - if (tagCb) { - for (const auto& evt : tagEvents) { - tagCb(evt); - } - } - - return toPublicScopeStats(stats); +ScopeStats ESPMemoryMonitor::finalizeScope(const MemoryScope &scope) { + InternalScopeStats stats(_usePSRAMBuffers); + stats.name.assign(scope._name.c_str(), scope._name.size()); + stats.tag = scope._tag; + stats.startInternalFreeBytes = scope._startInternal; + stats.startPsramFreeBytes = scope._startPsram; + + const uint64_t endUs = esp_timer_get_time(); + const RegionStats internal = + captureRegion(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT, MemoryRegion::Internal); + const RegionStats psram = + captureRegion(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MemoryRegion::Psram); + + stats.endInternalFreeBytes = internal.freeBytes; + stats.endPsramFreeBytes = psram.freeBytes; + stats.deltaInternalBytes = + static_cast(scope._startInternal) - static_cast(internal.freeBytes); + stats.deltaPsramBytes = + static_cast(scope._startPsram) - static_cast(psram.freeBytes); + stats.startedUs = scope._startUs; + stats.endedUs = endUs; + stats.durationUs = endUs - scope._startUs; + + ScopeCallback scopeCb; + TagThresholdCallback tagCb; + MemoryMonitorVector tagEvents{ + MemoryMonitorAllocator(_usePSRAMBuffers) + }; + { + LockGuard guard(_mutex); + appendScopeLocked(stats); + tagEvents = evaluateTagThresholdsLocked(stats); + scopeCb = _scopeCallback; + tagCb = _tagThresholdCallback; + } + + if (scopeCb) { + scopeCb(toPublicScopeStats(stats)); + } + + if (tagCb) { + for (const auto &evt : tagEvents) { + tagCb(evt); + } + } + + return toPublicScopeStats(stats); } -ScopeStats ESPMemoryMonitor::toPublicScopeStats(const InternalScopeStats& stats) const { - ScopeStats out{}; - out.name = stats.name.c_str(); - out.tag = stats.tag; - out.startInternalFreeBytes = stats.startInternalFreeBytes; - out.startPsramFreeBytes = stats.startPsramFreeBytes; - out.endInternalFreeBytes = stats.endInternalFreeBytes; - out.endPsramFreeBytes = stats.endPsramFreeBytes; - out.deltaInternalBytes = stats.deltaInternalBytes; - out.deltaPsramBytes = stats.deltaPsramBytes; - out.durationUs = stats.durationUs; - out.startedUs = stats.startedUs; - out.endedUs = stats.endedUs; - return out; +ScopeStats ESPMemoryMonitor::toPublicScopeStats(const InternalScopeStats &stats) const { + ScopeStats out{}; + out.name = stats.name.c_str(); + out.tag = stats.tag; + out.startInternalFreeBytes = stats.startInternalFreeBytes; + out.startPsramFreeBytes = stats.startPsramFreeBytes; + out.endInternalFreeBytes = stats.endInternalFreeBytes; + out.endPsramFreeBytes = stats.endPsramFreeBytes; + out.deltaInternalBytes = stats.deltaInternalBytes; + out.deltaPsramBytes = stats.deltaPsramBytes; + out.durationUs = stats.durationUs; + out.startedUs = stats.startedUs; + out.endedUs = stats.endedUs; + return out; } -TagUsage ESPMemoryMonitor::toPublicTagUsage(const InternalTagUsage& usage) const { - TagUsage out{}; - out.tag = usage.tag; - out.name = usage.name.c_str(); - out.totalInternalBytes = usage.totalInternalBytes; - out.totalPsramBytes = usage.totalPsramBytes; - out.state = usage.state; - return out; +TagUsage ESPMemoryMonitor::toPublicTagUsage(const InternalTagUsage &usage) const { + TagUsage out{}; + out.tag = usage.tag; + out.name = usage.name.c_str(); + out.totalInternalBytes = usage.totalInternalBytes; + out.totalPsramBytes = usage.totalPsramBytes; + out.state = usage.state; + return out; } -TaskStackUsage ESPMemoryMonitor::toPublicTaskStackUsage(const InternalTaskStackUsage& usage) const { - TaskStackUsage out{}; - out.name = usage.name.c_str(); - out.freeHighWaterBytes = usage.freeHighWaterBytes; - out.state = usage.state; - out.priority = usage.priority; - out.handle = usage.handle; - return out; +TaskStackUsage ESPMemoryMonitor::toPublicTaskStackUsage(const InternalTaskStackUsage &usage) const { + TaskStackUsage out{}; + out.name = usage.name.c_str(); + out.freeHighWaterBytes = usage.freeHighWaterBytes; + out.state = usage.state; + out.priority = usage.priority; + out.handle = usage.handle; + return out; } -MemorySnapshot ESPMemoryMonitor::toPublicSnapshot(const InternalMemorySnapshot& snapshot) const { - MemorySnapshot out{}; - out.timestampUs = snapshot.timestampUs; - out.regions.assign(snapshot.regions.begin(), snapshot.regions.end()); - out.stacks.reserve(snapshot.stacks.size()); - for (const auto& stack : snapshot.stacks) { - out.stacks.push_back(toPublicTaskStackUsage(stack)); - } - return out; +MemorySnapshot ESPMemoryMonitor::toPublicSnapshot(const InternalMemorySnapshot &snapshot) const { + MemorySnapshot out{}; + out.timestampUs = snapshot.timestampUs; + out.regions.assign(snapshot.regions.begin(), snapshot.regions.end()); + out.stacks.reserve(snapshot.stacks.size()); + for (const auto &stack : snapshot.stacks) { + out.stacks.push_back(toPublicTaskStackUsage(stack)); + } + return out; } -LeakCheckResult ESPMemoryMonitor::toPublicLeakCheckResult(const InternalLeakCheckResult& result) const { - LeakCheckResult out{}; - out.fromLabel = result.fromLabel.c_str(); - out.toLabel = result.toLabel.c_str(); - out.deltas.assign(result.deltas.begin(), result.deltas.end()); - out.leakSuspected = result.leakSuspected; - return out; +LeakCheckResult +ESPMemoryMonitor::toPublicLeakCheckResult(const InternalLeakCheckResult &result) const { + LeakCheckResult out{}; + out.fromLabel = result.fromLabel.c_str(); + out.toLabel = result.toLabel.c_str(); + out.deltas.assign(result.deltas.begin(), result.deltas.end()); + out.leakSuspected = result.leakSuspected; + return out; } -void ESPMemoryMonitor::appendHistoryLocked(const InternalMemorySnapshot& snapshot) { - if (_config.historySize == 0) { - return; - } +void ESPMemoryMonitor::appendHistoryLocked(const InternalMemorySnapshot &snapshot) { + if (_config.historySize == 0) { + return; + } - _history.push_back(snapshot); - while (_history.size() > _config.historySize) { - _history.pop_front(); - } + _history.push_back(snapshot); + while (_history.size() > _config.historySize) { + _history.pop_front(); + } } -MemoryMonitorVector ESPMemoryMonitor::evaluateThresholdsLocked(const InternalMemorySnapshot& snapshot) { - MemoryMonitorVector events{MemoryMonitorAllocator(_usePSRAMBuffers)}; - events.reserve(snapshot.regions.size()); - - for (const auto& regionStats : snapshot.regions) { - const size_t idx = regionIndex(regionStats.region); - const ThresholdState current = _thresholdStates[idx]; - const RegionThreshold& limits = regionStats.region == MemoryRegion::Psram ? _config.psram : _config.internal; - - if (limits.warnBytes == 0 && limits.criticalBytes == 0) { - continue; - } - - const ThresholdState next = evaluateState(current, limits, regionStats.freeBytes); - if (next != current) { - _thresholdStates[idx] = next; - events.push_back({regionStats.region, next, regionStats}); - } - } - - return events; +MemoryMonitorVector +ESPMemoryMonitor::evaluateThresholdsLocked(const InternalMemorySnapshot &snapshot) { + MemoryMonitorVector events{ + MemoryMonitorAllocator(_usePSRAMBuffers) + }; + events.reserve(snapshot.regions.size()); + + for (const auto ®ionStats : snapshot.regions) { + const size_t idx = regionIndex(regionStats.region); + const ThresholdState current = _thresholdStates[idx]; + const RegionThreshold &limits = + regionStats.region == MemoryRegion::Psram ? _config.psram : _config.internal; + + if (limits.warnBytes == 0 && limits.criticalBytes == 0) { + continue; + } + + const ThresholdState next = evaluateState(current, limits, regionStats.freeBytes); + if (next != current) { + _thresholdStates[idx] = next; + events.push_back({regionStats.region, next, regionStats}); + } + } + + return events; } -ThresholdState ESPMemoryMonitor::evaluateState(ThresholdState current, const RegionThreshold& threshold, size_t freeBytes) const { - const size_t warnRecover = threshold.warnBytes + _config.thresholdHysteresisBytes; - const size_t criticalRecover = threshold.criticalBytes + _config.thresholdHysteresisBytes; - - switch (current) { - case ThresholdState::Normal: - if (threshold.criticalBytes > 0 && freeBytes <= threshold.criticalBytes) { - return ThresholdState::Critical; - } - if (threshold.warnBytes > 0 && freeBytes <= threshold.warnBytes) { - return ThresholdState::Warn; - } - return ThresholdState::Normal; - - case ThresholdState::Warn: - if (threshold.criticalBytes > 0 && freeBytes <= threshold.criticalBytes) { - return ThresholdState::Critical; - } - if (threshold.warnBytes == 0 || freeBytes > warnRecover) { - return ThresholdState::Normal; - } - return ThresholdState::Warn; - - case ThresholdState::Critical: - if (threshold.criticalBytes == 0) { - return threshold.warnBytes > 0 && freeBytes <= threshold.warnBytes ? ThresholdState::Warn : ThresholdState::Normal; - } - if (freeBytes <= threshold.criticalBytes || freeBytes <= criticalRecover) { - return ThresholdState::Critical; - } - if (threshold.warnBytes > 0 && freeBytes <= warnRecover) { - return ThresholdState::Warn; - } - return ThresholdState::Normal; - } - - return ThresholdState::Normal; +ThresholdState ESPMemoryMonitor::evaluateState( + ThresholdState current, const RegionThreshold &threshold, size_t freeBytes +) const { + const size_t warnRecover = threshold.warnBytes + _config.thresholdHysteresisBytes; + const size_t criticalRecover = threshold.criticalBytes + _config.thresholdHysteresisBytes; + + switch (current) { + case ThresholdState::Normal: + if (threshold.criticalBytes > 0 && freeBytes <= threshold.criticalBytes) { + return ThresholdState::Critical; + } + if (threshold.warnBytes > 0 && freeBytes <= threshold.warnBytes) { + return ThresholdState::Warn; + } + return ThresholdState::Normal; + + case ThresholdState::Warn: + if (threshold.criticalBytes > 0 && freeBytes <= threshold.criticalBytes) { + return ThresholdState::Critical; + } + if (threshold.warnBytes == 0 || freeBytes > warnRecover) { + return ThresholdState::Normal; + } + return ThresholdState::Warn; + + case ThresholdState::Critical: + if (threshold.criticalBytes == 0) { + return threshold.warnBytes > 0 && freeBytes <= threshold.warnBytes + ? ThresholdState::Warn + : ThresholdState::Normal; + } + if (freeBytes <= threshold.criticalBytes || freeBytes <= criticalRecover) { + return ThresholdState::Critical; + } + if (threshold.warnBytes > 0 && freeBytes <= warnRecover) { + return ThresholdState::Warn; + } + return ThresholdState::Normal; + } + + return ThresholdState::Normal; } -void ESPMemoryMonitor::handleAllocEvent(size_t requestedBytes, uint32_t caps, const char* functionName) { - FailedAllocCallback cb; - { - LockGuard guard(_mutex); - cb = _allocCallback; - } - - if (!cb) { - return; - } - - FailedAllocEvent event{}; - event.requestedBytes = requestedBytes; - event.caps = caps; - event.functionName = functionName; - event.timestampUs = esp_timer_get_time(); - - cb(event); +void ESPMemoryMonitor::handleAllocEvent( + size_t requestedBytes, uint32_t caps, const char *functionName +) { + FailedAllocCallback cb; + { + LockGuard guard(_mutex); + cb = _allocCallback; + } + + if (!cb) { + return; + } + + FailedAllocEvent event{}; + event.requestedBytes = requestedBytes; + event.caps = caps; + event.functionName = functionName; + event.timestampUs = esp_timer_get_time(); + + cb(event); } -void ESPMemoryMonitor::allocFailedHook(size_t requestedBytes, uint32_t caps, const char* functionName) { - if (_failedAllocInstance != nullptr) { - _failedAllocInstance->handleAllocEvent(requestedBytes, caps, functionName); - } +void ESPMemoryMonitor::allocFailedHook( + size_t requestedBytes, uint32_t caps, const char *functionName +) { + if (_failedAllocInstance != nullptr) { + _failedAllocInstance->handleAllocEvent(requestedBytes, caps, functionName); + } } bool ESPMemoryMonitor::registerFailedAllocCallback() { - _failedAllocInstance = this; - - esp_err_t err = heap_caps_register_failed_alloc_callback(&ESPMemoryMonitor::allocFailedHook); - if (err != ESP_OK) { - _failedAllocInstance = nullptr; - return false; - } - return true; + _failedAllocInstance = this; + + esp_err_t err = heap_caps_register_failed_alloc_callback(&ESPMemoryMonitor::allocFailedHook); + if (err != ESP_OK) { + _failedAllocInstance = nullptr; + return false; + } + return true; } void ESPMemoryMonitor::unregisterFailedAllocCallback() { - heap_caps_register_failed_alloc_callback(nullptr); - if (_failedAllocInstance == this) { - _failedAllocInstance = nullptr; - } + heap_caps_register_failed_alloc_callback(nullptr); + if (_failedAllocInstance == this) { + _failedAllocInstance = nullptr; + } } -void ESPMemoryMonitor::enrichSnapshotLocked(InternalMemorySnapshot& snapshot) const { - const size_t windowSize = _config.windowStatsSize; - if (windowSize == 0) { - return; - } - - for (auto& region : snapshot.regions) { - const size_t idx = regionIndex(region.region); - - MemoryMonitorVector window{MemoryMonitorAllocator(_usePSRAMBuffers)}; - MemoryMonitorVector timestamps{MemoryMonitorAllocator(_usePSRAMBuffers)}; - const size_t targetWindow = std::min(windowSize, _history.size() + 1); - window.reserve(targetWindow); - timestamps.reserve(targetWindow); - - if (targetWindow > 1) { - size_t collected = 0; - for (auto it = _history.rbegin(); it != _history.rend() && collected < targetWindow - 1; ++it, ++collected) { - if (idx < it->regions.size()) { - window.push_back(&it->regions[idx]); - timestamps.push_back(it->timestampUs); - } - } - } - - window.push_back(®ion); - timestamps.push_back(snapshot.timestampUs); - - std::reverse(window.begin(), window.end()); - std::reverse(timestamps.begin(), timestamps.end()); - - size_t minFree = window.front()->freeBytes; - size_t maxFree = window.front()->freeBytes; - uint64_t sumFree = 0; - float sumFrag = 0.0f; - for (const auto* rs : window) { - minFree = std::min(minFree, rs->freeBytes); - maxFree = std::max(maxFree, rs->freeBytes); - sumFree += rs->freeBytes; - sumFrag += rs->fragmentation; - } - - const float count = static_cast(window.size()); - region.window.minFree = minFree; - region.window.maxFree = maxFree; - region.window.avgFree = static_cast(sumFree / static_cast(window.size())); - region.window.avgFragmentation = count > 0.0f ? sumFrag / count : 0.0f; - - region.freeBytesSlope = 0.0f; - region.secondsToWarn = 0; - region.secondsToCritical = 0; - - if (window.size() >= 2) { - const float deltaFree = static_cast(window.back()->freeBytes) - static_cast(window.front()->freeBytes); - const float deltaSeconds = static_cast(timestamps.back() - timestamps.front()) / 1'000'000.0f; - if (deltaSeconds > 0.0f) { - region.freeBytesSlope = deltaFree / deltaSeconds; - } - - if (region.freeBytesSlope < 0.0f) { - const RegionThreshold& limits = region.region == MemoryRegion::Psram ? _config.psram : _config.internal; - if (limits.warnBytes > 0 && region.freeBytes > limits.warnBytes) { - const float seconds = (static_cast(region.freeBytes) - static_cast(limits.warnBytes)) / -region.freeBytesSlope; - region.secondsToWarn = static_cast(seconds); - } - if (limits.criticalBytes > 0 && region.freeBytes > limits.criticalBytes) { - const float seconds = (static_cast(region.freeBytes) - static_cast(limits.criticalBytes)) / -region.freeBytesSlope; - region.secondsToCritical = static_cast(seconds); - } - } - } - } +void ESPMemoryMonitor::enrichSnapshotLocked(InternalMemorySnapshot &snapshot) const { + const size_t windowSize = _config.windowStatsSize; + if (windowSize == 0) { + return; + } + + for (auto ®ion : snapshot.regions) { + const size_t idx = regionIndex(region.region); + + MemoryMonitorVector window{ + MemoryMonitorAllocator(_usePSRAMBuffers) + }; + MemoryMonitorVector timestamps{ + MemoryMonitorAllocator(_usePSRAMBuffers) + }; + const size_t targetWindow = std::min(windowSize, _history.size() + 1); + window.reserve(targetWindow); + timestamps.reserve(targetWindow); + + if (targetWindow > 1) { + size_t collected = 0; + for (auto it = _history.rbegin(); it != _history.rend() && collected < targetWindow - 1; + ++it, ++collected) { + if (idx < it->regions.size()) { + window.push_back(&it->regions[idx]); + timestamps.push_back(it->timestampUs); + } + } + } + + window.push_back(®ion); + timestamps.push_back(snapshot.timestampUs); + + std::reverse(window.begin(), window.end()); + std::reverse(timestamps.begin(), timestamps.end()); + + size_t minFree = window.front()->freeBytes; + size_t maxFree = window.front()->freeBytes; + uint64_t sumFree = 0; + float sumFrag = 0.0f; + for (const auto *rs : window) { + minFree = std::min(minFree, rs->freeBytes); + maxFree = std::max(maxFree, rs->freeBytes); + sumFree += rs->freeBytes; + sumFrag += rs->fragmentation; + } + + const float count = static_cast(window.size()); + region.window.minFree = minFree; + region.window.maxFree = maxFree; + region.window.avgFree = static_cast(sumFree / static_cast(window.size())); + region.window.avgFragmentation = count > 0.0f ? sumFrag / count : 0.0f; + + region.freeBytesSlope = 0.0f; + region.secondsToWarn = 0; + region.secondsToCritical = 0; + + if (window.size() >= 2) { + const float deltaFree = static_cast(window.back()->freeBytes) - + static_cast(window.front()->freeBytes); + const float deltaSeconds = + static_cast(timestamps.back() - timestamps.front()) / 1'000'000.0f; + if (deltaSeconds > 0.0f) { + region.freeBytesSlope = deltaFree / deltaSeconds; + } + + if (region.freeBytesSlope < 0.0f) { + const RegionThreshold &limits = + region.region == MemoryRegion::Psram ? _config.psram : _config.internal; + if (limits.warnBytes > 0 && region.freeBytes > limits.warnBytes) { + const float seconds = (static_cast(region.freeBytes) - + static_cast(limits.warnBytes)) / + -region.freeBytesSlope; + region.secondsToWarn = static_cast(seconds); + } + if (limits.criticalBytes > 0 && region.freeBytes > limits.criticalBytes) { + const float seconds = (static_cast(region.freeBytes) - + static_cast(limits.criticalBytes)) / + -region.freeBytesSlope; + region.secondsToCritical = static_cast(seconds); + } + } + } + } } -void ESPMemoryMonitor::appendScopeLocked(const InternalScopeStats& stats) { - if (!_config.enableScopes) { - return; - } - - auto adjustUsage = [&](const InternalScopeStats& s, int direction) { - if (s.tag == kInvalidMemoryTag || s.tag == 0 || static_cast(s.tag) > _tagUsage.size()) { - return; - } - - InternalTagUsage& usage = _tagUsage[s.tag - 1]; - auto apply = [&](size_t& target, int64_t delta) { - int64_t next = static_cast(target) + direction * delta; - if (next < 0) { - next = 0; - } - target = static_cast(next); - }; - - apply(usage.totalInternalBytes, s.deltaInternalBytes); - apply(usage.totalPsramBytes, s.deltaPsramBytes); - }; - - adjustUsage(stats, 1); - - if (_config.maxScopesInHistory == 0) { - return; - } - - _scopeHistory.push_back(stats); - while (_scopeHistory.size() > _config.maxScopesInHistory) { - const InternalScopeStats& dropped = _scopeHistory.front(); - adjustUsage(dropped, -1); - _scopeHistory.pop_front(); - } +void ESPMemoryMonitor::appendScopeLocked(const InternalScopeStats &stats) { + if (!_config.enableScopes) { + return; + } + + auto adjustUsage = [&](const InternalScopeStats &s, int direction) { + if (s.tag == kInvalidMemoryTag || s.tag == 0 || + static_cast(s.tag) > _tagUsage.size()) { + return; + } + + InternalTagUsage &usage = _tagUsage[s.tag - 1]; + auto apply = [&](size_t &target, int64_t delta) { + int64_t next = static_cast(target) + direction * delta; + if (next < 0) { + next = 0; + } + target = static_cast(next); + }; + + apply(usage.totalInternalBytes, s.deltaInternalBytes); + apply(usage.totalPsramBytes, s.deltaPsramBytes); + }; + + adjustUsage(stats, 1); + + if (_config.maxScopesInHistory == 0) { + return; + } + + _scopeHistory.push_back(stats); + while (_scopeHistory.size() > _config.maxScopesInHistory) { + const InternalScopeStats &dropped = _scopeHistory.front(); + adjustUsage(dropped, -1); + _scopeHistory.pop_front(); + } } -MemoryMonitorVector ESPMemoryMonitor::evaluateTagThresholdsLocked(const InternalScopeStats& stats) { - MemoryMonitorVector events{MemoryMonitorAllocator(_usePSRAMBuffers)}; - if (!_config.enableScopes || _tagUsage.empty()) { - return events; - } - - const size_t count = _tagUsage.size(); - events.reserve(count); - for (size_t i = 0; i < count; ++i) { - InternalTagUsage& usage = _tagUsage[i]; - const TagBudget& budget = _tagBudgets[i]; - if (budget.warnBytes == 0 && budget.criticalBytes == 0) { - continue; - } - - const size_t totalBytes = usage.totalInternalBytes + usage.totalPsramBytes; - const ThresholdState next = evaluateRisingState(usage.state, budget, totalBytes); - if (next != usage.state) { - usage.state = next; - TagThresholdEvent evt{}; - evt.usage = toPublicTagUsage(usage); - evt.budget = budget; - if (usage.tag == stats.tag) { - evt.lastScope = toPublicScopeStats(stats); - } - events.push_back(std::move(evt)); - } - } - - return events; +MemoryMonitorVector +ESPMemoryMonitor::evaluateTagThresholdsLocked(const InternalScopeStats &stats) { + MemoryMonitorVector events{ + MemoryMonitorAllocator(_usePSRAMBuffers) + }; + if (!_config.enableScopes || _tagUsage.empty()) { + return events; + } + + const size_t count = _tagUsage.size(); + events.reserve(count); + for (size_t i = 0; i < count; ++i) { + InternalTagUsage &usage = _tagUsage[i]; + const TagBudget &budget = _tagBudgets[i]; + if (budget.warnBytes == 0 && budget.criticalBytes == 0) { + continue; + } + + const size_t totalBytes = usage.totalInternalBytes + usage.totalPsramBytes; + const ThresholdState next = evaluateRisingState(usage.state, budget, totalBytes); + if (next != usage.state) { + usage.state = next; + TagThresholdEvent evt{}; + evt.usage = toPublicTagUsage(usage); + evt.budget = budget; + if (usage.tag == stats.tag) { + evt.lastScope = toPublicScopeStats(stats); + } + events.push_back(std::move(evt)); + } + } + + return events; } -ThresholdState ESPMemoryMonitor::evaluateRisingState(ThresholdState current, const TagBudget& budget, size_t usageBytes) const { - const size_t warnRecover = saturatingSubtract(budget.warnBytes, _config.thresholdHysteresisBytes); - const size_t criticalRecover = saturatingSubtract(budget.criticalBytes, _config.thresholdHysteresisBytes); - - switch (current) { - case ThresholdState::Normal: - if (budget.criticalBytes > 0 && usageBytes >= budget.criticalBytes) { - return ThresholdState::Critical; - } - if (budget.warnBytes > 0 && usageBytes >= budget.warnBytes) { - return ThresholdState::Warn; - } - return ThresholdState::Normal; - - case ThresholdState::Warn: - if (budget.criticalBytes > 0 && usageBytes >= budget.criticalBytes) { - return ThresholdState::Critical; - } - if (budget.warnBytes == 0 || usageBytes <= warnRecover) { - return ThresholdState::Normal; - } - return ThresholdState::Warn; - - case ThresholdState::Critical: - if (budget.criticalBytes == 0) { - if (budget.warnBytes > 0 && usageBytes >= budget.warnBytes) { - return ThresholdState::Warn; - } - return ThresholdState::Normal; - } - if (usageBytes >= criticalRecover) { - return ThresholdState::Critical; - } - if (budget.warnBytes > 0 && usageBytes >= warnRecover) { - return ThresholdState::Warn; - } - return ThresholdState::Normal; - } - - return ThresholdState::Normal; +ThresholdState ESPMemoryMonitor::evaluateRisingState( + ThresholdState current, const TagBudget &budget, size_t usageBytes +) const { + const size_t warnRecover = + saturatingSubtract(budget.warnBytes, _config.thresholdHysteresisBytes); + const size_t criticalRecover = + saturatingSubtract(budget.criticalBytes, _config.thresholdHysteresisBytes); + + switch (current) { + case ThresholdState::Normal: + if (budget.criticalBytes > 0 && usageBytes >= budget.criticalBytes) { + return ThresholdState::Critical; + } + if (budget.warnBytes > 0 && usageBytes >= budget.warnBytes) { + return ThresholdState::Warn; + } + return ThresholdState::Normal; + + case ThresholdState::Warn: + if (budget.criticalBytes > 0 && usageBytes >= budget.criticalBytes) { + return ThresholdState::Critical; + } + if (budget.warnBytes == 0 || usageBytes <= warnRecover) { + return ThresholdState::Normal; + } + return ThresholdState::Warn; + + case ThresholdState::Critical: + if (budget.criticalBytes == 0) { + if (budget.warnBytes > 0 && usageBytes >= budget.warnBytes) { + return ThresholdState::Warn; + } + return ThresholdState::Normal; + } + if (usageBytes >= criticalRecover) { + return ThresholdState::Critical; + } + if (budget.warnBytes > 0 && usageBytes >= warnRecover) { + return ThresholdState::Warn; + } + return ThresholdState::Normal; + } + + return ThresholdState::Normal; } -StackState ESPMemoryMonitor::computeStackState(const InternalTaskStackUsage& usage) const { - TaskStackThreshold threshold{}; - auto it = _taskThresholds.find(usage.name); - if (it != _taskThresholds.end()) { - threshold = it->second; - } else { - threshold.warnBytes = static_cast(_config.defaultTaskStackBytes * _config.stackWarnFraction); - threshold.criticalBytes = static_cast(_config.defaultTaskStackBytes * _config.stackCriticalFraction); - } - - if (threshold.criticalBytes > 0 && usage.freeHighWaterBytes <= threshold.criticalBytes) { - return StackState::Critical; - } - if (threshold.warnBytes > 0 && usage.freeHighWaterBytes <= threshold.warnBytes) { - return StackState::Warn; - } - return StackState::Safe; +StackState ESPMemoryMonitor::computeStackState(const InternalTaskStackUsage &usage) const { + TaskStackThreshold threshold{}; + auto it = _taskThresholds.find(usage.name); + if (it != _taskThresholds.end()) { + threshold = it->second; + } else { + threshold.warnBytes = + static_cast(_config.defaultTaskStackBytes * _config.stackWarnFraction); + threshold.criticalBytes = + static_cast(_config.defaultTaskStackBytes * _config.stackCriticalFraction); + } + + if (threshold.criticalBytes > 0 && usage.freeHighWaterBytes <= threshold.criticalBytes) { + return StackState::Critical; + } + if (threshold.warnBytes > 0 && usage.freeHighWaterBytes <= threshold.warnBytes) { + return StackState::Warn; + } + return StackState::Safe; } -void ESPMemoryMonitor::trackTasksLocked(const InternalMemorySnapshot& snapshot, MemoryMonitorVector& events) { - if (!_config.enablePerTaskStacks || !_config.enableTaskTracking) { - return; - } - - MemoryMonitorUnorderedMap current( - 0, - std::hash{}, - std::equal_to{}, - MemoryMonitorAllocator>(_usePSRAMBuffers)); - current.reserve(snapshot.stacks.size()); - - for (const auto& usage : snapshot.stacks) { - if (usage.handle == nullptr) { - continue; - } - - current[usage.handle] = usage; - const StackState state = computeStackState(usage); - - auto it = _knownTasks.find(usage.handle); - if (it == _knownTasks.end()) { - events.push_back({toPublicTaskStackUsage(usage), state, true, false}); - } else { - const StackState previousState = computeStackState(it->second); - if (previousState != state) { - events.push_back({toPublicTaskStackUsage(usage), state, false, false}); - } - } - } - - for (const auto& [handle, prior] : _knownTasks) { - if (current.find(handle) == current.end()) { - events.push_back({toPublicTaskStackUsage(prior), computeStackState(prior), false, true}); - } - } - - _knownTasks.swap(current); +void ESPMemoryMonitor::trackTasksLocked( + const InternalMemorySnapshot &snapshot, MemoryMonitorVector &events +) { + if (!_config.enablePerTaskStacks || !_config.enableTaskTracking) { + return; + } + + MemoryMonitorUnorderedMap current( + 0, + std::hash{}, + std::equal_to{}, + MemoryMonitorAllocator>( + _usePSRAMBuffers + ) + ); + current.reserve(snapshot.stacks.size()); + + for (const auto &usage : snapshot.stacks) { + if (usage.handle == nullptr) { + continue; + } + + current[usage.handle] = usage; + const StackState state = computeStackState(usage); + + auto it = _knownTasks.find(usage.handle); + if (it == _knownTasks.end()) { + events.push_back({toPublicTaskStackUsage(usage), state, true, false}); + } else { + const StackState previousState = computeStackState(it->second); + if (previousState != state) { + events.push_back({toPublicTaskStackUsage(usage), state, false, false}); + } + } + } + + for (const auto &[handle, prior] : _knownTasks) { + if (current.find(handle) == current.end()) { + events.push_back( + {toPublicTaskStackUsage(prior), computeStackState(prior), false, true} + ); + } + } + + _knownTasks.swap(current); } void ESPMemoryMonitor::resetOwnedContainers() { - _history = MemoryMonitorDeque(MemoryMonitorAllocator(_usePSRAMBuffers)); - _scopeHistory = MemoryMonitorDeque(MemoryMonitorAllocator(_usePSRAMBuffers)); - _tagUsage = MemoryMonitorVector(MemoryMonitorAllocator(_usePSRAMBuffers)); - _tagBudgets = MemoryMonitorVector(MemoryMonitorAllocator(_usePSRAMBuffers)); - _taskThresholds = MemoryMonitorUnorderedMap( - 0, - std::hash{}, - std::equal_to{}, - MemoryMonitorAllocator>(_usePSRAMBuffers)); - _knownTasks = MemoryMonitorUnorderedMap( - 0, - std::hash{}, - std::equal_to{}, - MemoryMonitorAllocator>(_usePSRAMBuffers)); - _leakHistory = MemoryMonitorDeque(MemoryMonitorAllocator(_usePSRAMBuffers)); - _leakCheckpoints = MemoryMonitorDeque(MemoryMonitorAllocator(_usePSRAMBuffers)); + _history = MemoryMonitorDeque( + MemoryMonitorAllocator(_usePSRAMBuffers) + ); + _scopeHistory = MemoryMonitorDeque( + MemoryMonitorAllocator(_usePSRAMBuffers) + ); + _tagUsage = MemoryMonitorVector( + MemoryMonitorAllocator(_usePSRAMBuffers) + ); + _tagBudgets = + MemoryMonitorVector(MemoryMonitorAllocator(_usePSRAMBuffers)); + _taskThresholds = MemoryMonitorUnorderedMap( + 0, + std::hash{}, + std::equal_to{}, + MemoryMonitorAllocator>( + _usePSRAMBuffers + ) + ); + _knownTasks = MemoryMonitorUnorderedMap( + 0, + std::hash{}, + std::equal_to{}, + MemoryMonitorAllocator>( + _usePSRAMBuffers + ) + ); + _leakHistory = MemoryMonitorDeque( + MemoryMonitorAllocator(_usePSRAMBuffers) + ); + _leakCheckpoints = + MemoryMonitorDeque(MemoryMonitorAllocator(_usePSRAMBuffers)); } -ESPMemoryMonitor::InternalLeakCheckResult ESPMemoryMonitor::buildLeakCheckLocked(const std::string& label) { - InternalLeakCheckResult result(_usePSRAMBuffers); - if (_history.empty()) { - return result; - } - - const size_t leakHistoryLimit = std::max(1, _config.maxLeakChecksInHistory); - const uint64_t latestTs = _history.back().timestampUs; - const uint64_t startTs = _leakCheckpoints.empty() ? _history.front().timestampUs : _leakCheckpoints.back(); - _leakCheckpoints.push_back(latestTs); - while (_leakCheckpoints.size() > leakHistoryLimit) { - _leakCheckpoints.pop_front(); - } - - auto computeAverages = [&](uint64_t from, uint64_t to) { - struct RegionAvg { - MemoryRegion region; - float freeBytes = 0.0f; - float fragmentation = 0.0f; - size_t samples = 0; - }; - - std::array avgs{{{MemoryRegion::Internal, 0.0f, 0.0f, 0}, {MemoryRegion::Psram, 0.0f, 0.0f, 0}}}; - - for (const auto& snap : _history) { - if (snap.timestampUs < from || snap.timestampUs > to) { - continue; - } - for (const auto& region : snap.regions) { - const size_t idx = regionIndex(region.region); - avgs[idx].freeBytes += static_cast(region.freeBytes); - avgs[idx].fragmentation += region.fragmentation; - avgs[idx].samples++; - } - } - - for (auto& avg : avgs) { - if (avg.samples == 0) { - continue; - } - avg.freeBytes /= static_cast(avg.samples); - avg.fragmentation /= static_cast(avg.samples); - } - - return avgs; - }; - - const auto averages = computeAverages(startTs, latestTs); - - if (_leakHistory.empty()) { - result.fromLabel.assign(label.c_str(), label.size()); - result.toLabel.assign(label.c_str(), label.size()); - for (const auto& avg : averages) { - LeakCheckDelta delta{}; - delta.region = avg.region; - delta.averageStartFreeBytes = avg.freeBytes; - delta.averageEndFreeBytes = avg.freeBytes; - delta.averageStartFragmentation = avg.fragmentation; - delta.averageEndFragmentation = avg.fragmentation; - delta.deltaFreeBytes = 0.0f; - delta.deltaFragmentation = 0.0f; - result.deltas.push_back(delta); - } - _leakHistory.push_back(result); - while (_leakHistory.size() > leakHistoryLimit) { - _leakHistory.pop_front(); - } - return result; - } - - const auto& previous = _leakHistory.back(); - if (previous.toLabel.empty()) { - result.fromLabel = previous.fromLabel; - } else { - result.fromLabel = previous.toLabel; - } - result.toLabel.assign(label.c_str(), label.size()); - - for (const auto& avg : averages) { - LeakCheckDelta delta{}; - delta.region = avg.region; - delta.averageEndFreeBytes = avg.freeBytes; - delta.averageEndFragmentation = avg.fragmentation; - - for (const auto& prevDelta : previous.deltas) { - if (prevDelta.region == avg.region) { - delta.averageStartFreeBytes = prevDelta.averageEndFreeBytes; - delta.averageStartFragmentation = prevDelta.averageEndFragmentation; - break; - } - } - - delta.deltaFreeBytes = delta.averageEndFreeBytes - delta.averageStartFreeBytes; - delta.deltaFragmentation = delta.averageEndFragmentation - delta.averageStartFragmentation; - if (delta.deltaFreeBytes < -static_cast(_config.leakNoiseBytes) || delta.deltaFragmentation > kFragmentationEpsilon) { - result.leakSuspected = true; - } - - result.deltas.push_back(delta); - } - - _leakHistory.push_back(result); - while (_leakHistory.size() > leakHistoryLimit) { - _leakHistory.pop_front(); - } - return result; +ESPMemoryMonitor::InternalLeakCheckResult +ESPMemoryMonitor::buildLeakCheckLocked(const std::string &label) { + InternalLeakCheckResult result(_usePSRAMBuffers); + if (_history.empty()) { + return result; + } + + const size_t leakHistoryLimit = std::max(1, _config.maxLeakChecksInHistory); + const uint64_t latestTs = _history.back().timestampUs; + const uint64_t startTs = + _leakCheckpoints.empty() ? _history.front().timestampUs : _leakCheckpoints.back(); + _leakCheckpoints.push_back(latestTs); + while (_leakCheckpoints.size() > leakHistoryLimit) { + _leakCheckpoints.pop_front(); + } + + auto computeAverages = [&](uint64_t from, uint64_t to) { + struct RegionAvg { + MemoryRegion region; + float freeBytes = 0.0f; + float fragmentation = 0.0f; + size_t samples = 0; + }; + + std::array avgs{ + {{MemoryRegion::Internal, 0.0f, 0.0f, 0}, {MemoryRegion::Psram, 0.0f, 0.0f, 0}} + }; + + for (const auto &snap : _history) { + if (snap.timestampUs < from || snap.timestampUs > to) { + continue; + } + for (const auto ®ion : snap.regions) { + const size_t idx = regionIndex(region.region); + avgs[idx].freeBytes += static_cast(region.freeBytes); + avgs[idx].fragmentation += region.fragmentation; + avgs[idx].samples++; + } + } + + for (auto &avg : avgs) { + if (avg.samples == 0) { + continue; + } + avg.freeBytes /= static_cast(avg.samples); + avg.fragmentation /= static_cast(avg.samples); + } + + return avgs; + }; + + const auto averages = computeAverages(startTs, latestTs); + + if (_leakHistory.empty()) { + result.fromLabel.assign(label.c_str(), label.size()); + result.toLabel.assign(label.c_str(), label.size()); + for (const auto &avg : averages) { + LeakCheckDelta delta{}; + delta.region = avg.region; + delta.averageStartFreeBytes = avg.freeBytes; + delta.averageEndFreeBytes = avg.freeBytes; + delta.averageStartFragmentation = avg.fragmentation; + delta.averageEndFragmentation = avg.fragmentation; + delta.deltaFreeBytes = 0.0f; + delta.deltaFragmentation = 0.0f; + result.deltas.push_back(delta); + } + _leakHistory.push_back(result); + while (_leakHistory.size() > leakHistoryLimit) { + _leakHistory.pop_front(); + } + return result; + } + + const auto &previous = _leakHistory.back(); + if (previous.toLabel.empty()) { + result.fromLabel = previous.fromLabel; + } else { + result.fromLabel = previous.toLabel; + } + result.toLabel.assign(label.c_str(), label.size()); + + for (const auto &avg : averages) { + LeakCheckDelta delta{}; + delta.region = avg.region; + delta.averageEndFreeBytes = avg.freeBytes; + delta.averageEndFragmentation = avg.fragmentation; + + for (const auto &prevDelta : previous.deltas) { + if (prevDelta.region == avg.region) { + delta.averageStartFreeBytes = prevDelta.averageEndFreeBytes; + delta.averageStartFragmentation = prevDelta.averageEndFragmentation; + break; + } + } + + delta.deltaFreeBytes = delta.averageEndFreeBytes - delta.averageStartFreeBytes; + delta.deltaFragmentation = delta.averageEndFragmentation - delta.averageStartFragmentation; + if (delta.deltaFreeBytes < -static_cast(_config.leakNoiseBytes) || + delta.deltaFragmentation > kFragmentationEpsilon) { + result.leakSuspected = true; + } + + result.deltas.push_back(delta); + } + + _leakHistory.push_back(result); + while (_leakHistory.size() > leakHistoryLimit) { + _leakHistory.pop_front(); + } + return result; } void ESPMemoryMonitor::runPanicHook() { - InternalMemorySnapshot internal = captureSnapshot(); - MemorySnapshot snapshot = toPublicSnapshot(internal); - PanicCallback cb; - { - LockGuard guard(_mutex); - cb = _panicCallback; - } - - for (const auto& region : snapshot.regions) { - const char* name = region.region == MemoryRegion::Psram ? "PSRAM" : "DRAM"; - ESP_EARLY_LOGE(kLogTag, "panic snapshot %s free=%uB min=%uB largest=%uB frag=%.03f", - name, - static_cast(region.freeBytes), - static_cast(region.minimumFreeBytes), - static_cast(region.largestFreeBlock), - region.fragmentation); - } - - if (cb) { - cb(snapshot); - } + InternalMemorySnapshot internal = captureSnapshot(); + MemorySnapshot snapshot = toPublicSnapshot(internal); + PanicCallback cb; + { + LockGuard guard(_mutex); + cb = _panicCallback; + } + + for (const auto ®ion : snapshot.regions) { + const char *name = region.region == MemoryRegion::Psram ? "PSRAM" : "DRAM"; + ESP_EARLY_LOGE( + kLogTag, + "panic snapshot %s free=%uB min=%uB largest=%uB frag=%.03f", + name, + static_cast(region.freeBytes), + static_cast(region.minimumFreeBytes), + static_cast(region.largestFreeBlock), + region.fragmentation + ); + } + + if (cb) { + cb(snapshot); + } } bool ESPMemoryMonitor::registerPanicHandler() { - gPanicInstance = this; - const esp_err_t err = esp_register_shutdown_handler(&ESPMemoryMonitor::panicShutdownThunk); - if (err != ESP_OK) { - gPanicInstance = nullptr; - return false; - } - return true; + gPanicInstance = this; + const esp_err_t err = esp_register_shutdown_handler(&ESPMemoryMonitor::panicShutdownThunk); + if (err != ESP_OK) { + gPanicInstance = nullptr; + return false; + } + return true; } void ESPMemoryMonitor::unregisterPanicHandler() { - esp_unregister_shutdown_handler(&ESPMemoryMonitor::panicShutdownThunk); - if (gPanicInstance == this) { - gPanicInstance = nullptr; - } + esp_unregister_shutdown_handler(&ESPMemoryMonitor::panicShutdownThunk); + if (gPanicInstance == this) { + gPanicInstance = nullptr; + } } void ESPMemoryMonitor::panicShutdownThunk() { - if (gPanicInstance != nullptr) { - gPanicInstance->runPanicHook(); - } + if (gPanicInstance != nullptr) { + gPanicInstance->runPanicHook(); + } } #if ESPMM_HAS_ARDUINOJSON -void toJson(const MemorySnapshot& snap, JsonDocument& doc) { - JsonObject root = doc.to(); - root["timestampUs"] = snap.timestampUs; - - JsonArray regions = root["regions"].to(); - for (const auto& region : snap.regions) { - JsonObject obj = regions.add(); - obj["region"] = region.region == MemoryRegion::Psram ? "psram" : "internal"; - obj["freeBytes"] = region.freeBytes; - obj["minimumFreeBytes"] = region.minimumFreeBytes; - obj["largestFreeBlock"] = region.largestFreeBlock; - obj["fragmentation"] = region.fragmentation; - obj["freeBytesSlope"] = region.freeBytesSlope; - obj["secondsToWarn"] = region.secondsToWarn; - obj["secondsToCritical"] = region.secondsToCritical; - - JsonObject window = obj["window"].to(); - window["minFree"] = region.window.minFree; - window["maxFree"] = region.window.maxFree; - window["avgFree"] = region.window.avgFree; - window["avgFragmentation"] = region.window.avgFragmentation; - } - - JsonArray stacks = root["stacks"].to(); - for (const auto& stack : snap.stacks) { - JsonObject obj = stacks.add(); - obj["name"] = stack.name; - obj["freeHighWaterBytes"] = stack.freeHighWaterBytes; - obj["state"] = static_cast(stack.state); - obj["priority"] = static_cast(stack.priority); - } +void toJson(const MemorySnapshot &snap, JsonDocument &doc) { + JsonObject root = doc.to(); + root["timestampUs"] = snap.timestampUs; + + JsonArray regions = root["regions"].to(); + for (const auto ®ion : snap.regions) { + JsonObject obj = regions.add(); + obj["region"] = region.region == MemoryRegion::Psram ? "psram" : "internal"; + obj["freeBytes"] = region.freeBytes; + obj["minimumFreeBytes"] = region.minimumFreeBytes; + obj["largestFreeBlock"] = region.largestFreeBlock; + obj["fragmentation"] = region.fragmentation; + obj["freeBytesSlope"] = region.freeBytesSlope; + obj["secondsToWarn"] = region.secondsToWarn; + obj["secondsToCritical"] = region.secondsToCritical; + + JsonObject window = obj["window"].to(); + window["minFree"] = region.window.minFree; + window["maxFree"] = region.window.maxFree; + window["avgFree"] = region.window.avgFree; + window["avgFragmentation"] = region.window.avgFragmentation; + } + + JsonArray stacks = root["stacks"].to(); + for (const auto &stack : snap.stacks) { + JsonObject obj = stacks.add(); + obj["name"] = stack.name; + obj["freeHighWaterBytes"] = stack.freeHighWaterBytes; + obj["state"] = static_cast(stack.state); + obj["priority"] = static_cast(stack.priority); + } } #endif diff --git a/src/esp_memory_monitor/memory_monitor.h b/src/esp_memory_monitor/memory_monitor.h index efb5f6a..265acb1 100644 --- a/src/esp_memory_monitor/memory_monitor.h +++ b/src/esp_memory_monitor/memory_monitor.h @@ -35,392 +35,417 @@ extern "C" { #include "memory_monitor_allocator.h" enum class MemoryRegion { - Internal = 0, - Psram = 1, + Internal = 0, + Psram = 1, }; enum class ThresholdState { - Normal = 0, - Warn, - Critical, + Normal = 0, + Warn, + Critical, }; struct RegionThreshold { - size_t warnBytes = 40 * 1024; - size_t criticalBytes = 20 * 1024; + size_t warnBytes = 40 * 1024; + size_t criticalBytes = 20 * 1024; }; struct TagBudget { - size_t warnBytes = 0; - size_t criticalBytes = 0; + size_t warnBytes = 0; + size_t criticalBytes = 0; }; struct MemoryMonitorConfig { - static constexpr BaseType_t any = tskNO_AFFINITY; - - uint32_t sampleIntervalMs = 1000; - size_t historySize = 60; - uint32_t stackSize = 4096 * sizeof(StackType_t); - BaseType_t coreId = any; - UBaseType_t priority = 1; - uint32_t thresholdHysteresisBytes = 4 * 1024; - RegionThreshold internal{}; - RegionThreshold psram{}; - bool enableSamplerTask = true; - bool enableFragmentation = true; - bool enableMinEverFree = true; - bool enablePerTaskStacks = false; - bool enableFailedAllocEvents = false; - bool enableScopes = false; - size_t maxScopesInHistory = 32; - size_t windowStatsSize = 0; - bool enableTaskTracking = false; - size_t defaultTaskStackBytes = 4096; - float stackWarnFraction = 0.25f; - float stackCriticalFraction = 0.10f; - size_t leakNoiseBytes = 1024; - size_t maxLeakChecksInHistory = 16; - bool usePSRAMBuffers = false; + static constexpr BaseType_t any = tskNO_AFFINITY; + + uint32_t sampleIntervalMs = 1000; + size_t historySize = 60; + uint32_t stackSize = 4096 * sizeof(StackType_t); + BaseType_t coreId = any; + UBaseType_t priority = 1; + uint32_t thresholdHysteresisBytes = 4 * 1024; + RegionThreshold internal{}; + RegionThreshold psram{}; + bool enableSamplerTask = true; + bool enableFragmentation = true; + bool enableMinEverFree = true; + bool enablePerTaskStacks = false; + bool enableFailedAllocEvents = false; + bool enableScopes = false; + size_t maxScopesInHistory = 32; + size_t windowStatsSize = 0; + bool enableTaskTracking = false; + size_t defaultTaskStackBytes = 4096; + float stackWarnFraction = 0.25f; + float stackCriticalFraction = 0.10f; + size_t leakNoiseBytes = 1024; + size_t maxLeakChecksInHistory = 16; + bool usePSRAMBuffers = false; }; struct WindowStats { - size_t minFree = 0; - size_t maxFree = 0; - size_t avgFree = 0; - float avgFragmentation = 0.0f; + size_t minFree = 0; + size_t maxFree = 0; + size_t avgFree = 0; + float avgFragmentation = 0.0f; }; struct RegionStats { - MemoryRegion region{MemoryRegion::Internal}; - size_t freeBytes = 0; - size_t minimumFreeBytes = 0; - size_t largestFreeBlock = 0; - float fragmentation = 0.0f; - float freeBytesSlope = 0.0f; - uint32_t secondsToWarn = 0; - uint32_t secondsToCritical = 0; - WindowStats window{}; + MemoryRegion region{MemoryRegion::Internal}; + size_t freeBytes = 0; + size_t minimumFreeBytes = 0; + size_t largestFreeBlock = 0; + float fragmentation = 0.0f; + float freeBytesSlope = 0.0f; + uint32_t secondsToWarn = 0; + uint32_t secondsToCritical = 0; + WindowStats window{}; }; struct ThresholdEvent { - MemoryRegion region{MemoryRegion::Internal}; - ThresholdState state{ThresholdState::Normal}; - RegionStats stats{}; + MemoryRegion region{MemoryRegion::Internal}; + ThresholdState state{ThresholdState::Normal}; + RegionStats stats{}; }; struct TaskStackUsage { - std::string name; - size_t freeHighWaterBytes = 0; - eTaskState state = eInvalid; - UBaseType_t priority = 0; - TaskHandle_t handle = nullptr; + std::string name; + size_t freeHighWaterBytes = 0; + eTaskState state = eInvalid; + UBaseType_t priority = 0; + TaskHandle_t handle = nullptr; }; struct FailedAllocEvent { - size_t requestedBytes = 0; - uint32_t caps = 0; - const char* functionName = nullptr; - uint64_t timestampUs = 0; + size_t requestedBytes = 0; + uint32_t caps = 0; + const char *functionName = nullptr; + uint64_t timestampUs = 0; }; struct MemorySnapshot { - uint64_t timestampUs = 0; - std::vector regions; - std::vector stacks; + uint64_t timestampUs = 0; + std::vector regions; + std::vector stacks; }; using MemoryTag = uint16_t; constexpr MemoryTag kInvalidMemoryTag = 0; struct ScopeStats { - std::string name; - MemoryTag tag = kInvalidMemoryTag; - size_t startInternalFreeBytes = 0; - size_t startPsramFreeBytes = 0; - size_t endInternalFreeBytes = 0; - size_t endPsramFreeBytes = 0; - int64_t deltaInternalBytes = 0; - int64_t deltaPsramBytes = 0; - uint64_t durationUs = 0; - uint64_t startedUs = 0; - uint64_t endedUs = 0; + std::string name; + MemoryTag tag = kInvalidMemoryTag; + size_t startInternalFreeBytes = 0; + size_t startPsramFreeBytes = 0; + size_t endInternalFreeBytes = 0; + size_t endPsramFreeBytes = 0; + int64_t deltaInternalBytes = 0; + int64_t deltaPsramBytes = 0; + uint64_t durationUs = 0; + uint64_t startedUs = 0; + uint64_t endedUs = 0; }; struct TagUsage { - MemoryTag tag = kInvalidMemoryTag; - std::string name; - size_t totalInternalBytes = 0; - size_t totalPsramBytes = 0; - ThresholdState state{ThresholdState::Normal}; + MemoryTag tag = kInvalidMemoryTag; + std::string name; + size_t totalInternalBytes = 0; + size_t totalPsramBytes = 0; + ThresholdState state{ThresholdState::Normal}; }; struct TagThresholdEvent { - TagUsage usage; - TagBudget budget; - ScopeStats lastScope; + TagUsage usage; + TagBudget budget; + ScopeStats lastScope; }; enum class StackState { - Safe = 0, - Warn, - Critical, + Safe = 0, + Warn, + Critical, }; struct TaskStackThreshold { - size_t warnBytes = 0; - size_t criticalBytes = 0; + size_t warnBytes = 0; + size_t criticalBytes = 0; }; struct TaskStackEvent { - TaskStackUsage usage; - StackState state{StackState::Safe}; - bool appeared = false; - bool disappeared = false; + TaskStackUsage usage; + StackState state{StackState::Safe}; + bool appeared = false; + bool disappeared = false; }; struct LeakCheckDelta { - MemoryRegion region{MemoryRegion::Internal}; - float averageStartFreeBytes = 0.0f; - float averageEndFreeBytes = 0.0f; - float averageStartFragmentation = 0.0f; - float averageEndFragmentation = 0.0f; - float deltaFreeBytes = 0.0f; - float deltaFragmentation = 0.0f; + MemoryRegion region{MemoryRegion::Internal}; + float averageStartFreeBytes = 0.0f; + float averageEndFreeBytes = 0.0f; + float averageStartFragmentation = 0.0f; + float averageEndFragmentation = 0.0f; + float deltaFreeBytes = 0.0f; + float deltaFragmentation = 0.0f; }; struct LeakCheckResult { - std::string fromLabel; - std::string toLabel; - std::vector deltas; - bool leakSuspected = false; + std::string fromLabel; + std::string toLabel; + std::vector deltas; + bool leakSuspected = false; }; -using ThresholdCallback = std::function; -using SampleCallback = std::function; -using FailedAllocCallback = std::function; -using ScopeCallback = std::function; -using TagThresholdCallback = std::function; -using TaskStackThresholdCallback = std::function; -using LeakCheckCallback = std::function; -using PanicCallback = std::function; +using ThresholdCallback = std::function; +using SampleCallback = std::function; +using FailedAllocCallback = std::function; +using ScopeCallback = std::function; +using TagThresholdCallback = std::function; +using TaskStackThresholdCallback = std::function; +using LeakCheckCallback = std::function; +using PanicCallback = std::function; class MemoryScope; class ESPMemoryMonitor; class MemoryScope { - public: - MemoryScope() = default; - ~MemoryScope(); - - MemoryScope(const MemoryScope&) = delete; - MemoryScope& operator=(const MemoryScope&) = delete; - MemoryScope(MemoryScope&& other) noexcept; - MemoryScope& operator=(MemoryScope&& other) noexcept; - - ScopeStats end(); - bool active() const { return _monitor != nullptr && !_ended; } - - private: - friend class ESPMemoryMonitor; - MemoryScope(ESPMemoryMonitor* monitor, std::string name, MemoryTag tag, size_t startInternal, size_t startPsram, uint64_t startUs); - - ESPMemoryMonitor* _monitor = nullptr; - std::string _name; - MemoryTag _tag = kInvalidMemoryTag; - size_t _startInternal = 0; - size_t _startPsram = 0; - uint64_t _startUs = 0; - bool _ended = false; + public: + MemoryScope() = default; + ~MemoryScope(); + + MemoryScope(const MemoryScope &) = delete; + MemoryScope &operator=(const MemoryScope &) = delete; + MemoryScope(MemoryScope &&other) noexcept; + MemoryScope &operator=(MemoryScope &&other) noexcept; + + ScopeStats end(); + bool active() const { + return _monitor != nullptr && !_ended; + } + + private: + friend class ESPMemoryMonitor; + MemoryScope( + ESPMemoryMonitor *monitor, + std::string name, + MemoryTag tag, + size_t startInternal, + size_t startPsram, + uint64_t startUs + ); + + ESPMemoryMonitor *_monitor = nullptr; + std::string _name; + MemoryTag _tag = kInvalidMemoryTag; + size_t _startInternal = 0; + size_t _startPsram = 0; + uint64_t _startUs = 0; + bool _ended = false; }; class ESPMemoryMonitor { - public: - friend class MemoryScope; - ESPMemoryMonitor(); - ~ESPMemoryMonitor(); - - bool init(const MemoryMonitorConfig& config = MemoryMonitorConfig{}); - void deinit(); - bool isInitialized() const { return _initialized; } - - MemorySnapshot sampleNow(); - std::vector history() const; - MemoryMonitorConfig currentConfig() const; - - void onSample(SampleCallback callback); - void onThreshold(ThresholdCallback callback); - void onFailedAlloc(FailedAllocCallback callback); - void onScope(ScopeCallback callback); - void onTagThreshold(TagThresholdCallback callback); - void onTaskStackThreshold(TaskStackThresholdCallback callback); - void onLeakCheck(LeakCheckCallback callback); - - MemoryScope beginScope(const std::string& name, MemoryTag tag = kInvalidMemoryTag); - MemoryTag registerTag(const std::string& name); - bool setTagBudget(MemoryTag tag, const TagBudget& budget); - std::vector scopeHistory() const; - std::vector tagUsage() const; - - LeakCheckResult markLeakCheckPoint(const std::string& label); - - bool setTaskStackThreshold(const std::string& taskName, const TaskStackThreshold& threshold); - - bool installPanicHook(PanicCallback callback = nullptr); - void uninstallPanicHook(); - - private: - enum class AllocHookType { - None = 0, - WithArg, - NoArg, - }; - - struct LockGuard { - explicit LockGuard(SemaphoreHandle_t handle) : _handle(handle) { - if (_handle != nullptr) { - xSemaphoreTake(_handle, portMAX_DELAY); - } - } - - ~LockGuard() { - if (_handle != nullptr) { - xSemaphoreGive(_handle); - } - } - - LockGuard(const LockGuard&) = delete; - LockGuard& operator=(const LockGuard&) = delete; - - private: - SemaphoreHandle_t _handle; - }; - - static void samplerTaskThunk(void* arg); - void samplerTaskLoop(); - - static void allocFailedHook(size_t requestedBytes, uint32_t caps, const char* functionName); - static ESPMemoryMonitor* _failedAllocInstance; - - struct InternalTaskStackUsage { - InternalTaskStackUsage() = default; - explicit InternalTaskStackUsage(bool usePSRAMBuffers) : name(MemoryMonitorAllocator(usePSRAMBuffers)) {} - - MemoryMonitorString name; - size_t freeHighWaterBytes = 0; - eTaskState state = eInvalid; - UBaseType_t priority = 0; - TaskHandle_t handle = nullptr; - }; - - struct InternalMemorySnapshot { - InternalMemorySnapshot() = default; - explicit InternalMemorySnapshot(bool usePSRAMBuffers) - : regions(MemoryMonitorAllocator(usePSRAMBuffers)), - stacks(MemoryMonitorAllocator(usePSRAMBuffers)) {} - - uint64_t timestampUs = 0; - MemoryMonitorVector regions; - MemoryMonitorVector stacks; - }; - - struct InternalScopeStats { - InternalScopeStats() = default; - explicit InternalScopeStats(bool usePSRAMBuffers) : name(MemoryMonitorAllocator(usePSRAMBuffers)) {} - - MemoryMonitorString name; - MemoryTag tag = kInvalidMemoryTag; - size_t startInternalFreeBytes = 0; - size_t startPsramFreeBytes = 0; - size_t endInternalFreeBytes = 0; - size_t endPsramFreeBytes = 0; - int64_t deltaInternalBytes = 0; - int64_t deltaPsramBytes = 0; - uint64_t durationUs = 0; - uint64_t startedUs = 0; - uint64_t endedUs = 0; - }; - - struct InternalTagUsage { - InternalTagUsage() = default; - explicit InternalTagUsage(bool usePSRAMBuffers) : name(MemoryMonitorAllocator(usePSRAMBuffers)) {} - - MemoryTag tag = kInvalidMemoryTag; - MemoryMonitorString name; - size_t totalInternalBytes = 0; - size_t totalPsramBytes = 0; - ThresholdState state{ThresholdState::Normal}; - }; - - struct InternalLeakCheckResult { - InternalLeakCheckResult() = default; - explicit InternalLeakCheckResult(bool usePSRAMBuffers) - : fromLabel(MemoryMonitorAllocator(usePSRAMBuffers)), - toLabel(MemoryMonitorAllocator(usePSRAMBuffers)), - deltas(MemoryMonitorAllocator(usePSRAMBuffers)) {} - - MemoryMonitorString fromLabel; - MemoryMonitorString toLabel; - MemoryMonitorVector deltas; - bool leakSuspected = false; - }; - - InternalMemorySnapshot captureSnapshot() const; - RegionStats captureRegion(uint32_t caps, MemoryRegion region) const; - MemoryMonitorVector captureStacks() const; - ScopeStats finalizeScope(const MemoryScope& scope); - ScopeStats toPublicScopeStats(const InternalScopeStats& stats) const; - TagUsage toPublicTagUsage(const InternalTagUsage& usage) const; - TaskStackUsage toPublicTaskStackUsage(const InternalTaskStackUsage& usage) const; - MemorySnapshot toPublicSnapshot(const InternalMemorySnapshot& snapshot) const; - LeakCheckResult toPublicLeakCheckResult(const InternalLeakCheckResult& result) const; - - void appendHistoryLocked(const InternalMemorySnapshot& snapshot); - MemoryMonitorVector evaluateThresholdsLocked(const InternalMemorySnapshot& snapshot); - ThresholdState evaluateState(ThresholdState current, const RegionThreshold& threshold, size_t freeBytes) const; - void handleAllocEvent(size_t requestedBytes, uint32_t caps, const char* functionName); - - void enrichSnapshotLocked(InternalMemorySnapshot& snapshot) const; - void appendScopeLocked(const InternalScopeStats& stats); - MemoryMonitorVector evaluateTagThresholdsLocked(const InternalScopeStats& stats); - ThresholdState evaluateRisingState(ThresholdState current, const TagBudget& budget, size_t usageBytes) const; - StackState computeStackState(const InternalTaskStackUsage& usage) const; - void trackTasksLocked(const InternalMemorySnapshot& snapshot, MemoryMonitorVector& events); - InternalLeakCheckResult buildLeakCheckLocked(const std::string& label); - void runPanicHook(); - static void panicShutdownThunk(); - - bool registerFailedAllocCallback(); - void unregisterFailedAllocCallback(); - bool registerPanicHandler(); - void unregisterPanicHandler(); - void resetOwnedContainers(); - - MemoryMonitorConfig _config{}; - bool _initialized = false; - bool _running = false; - bool _usePSRAMBuffers = false; - SemaphoreHandle_t _mutex = nullptr; - TaskHandle_t _samplerTask = nullptr; - MemoryMonitorDeque _history; - std::array _thresholdStates{ThresholdState::Normal, ThresholdState::Normal}; - SampleCallback _sampleCallback; - ThresholdCallback _thresholdCallback; - FailedAllocCallback _allocCallback; - ScopeCallback _scopeCallback; - TagThresholdCallback _tagThresholdCallback; - TaskStackThresholdCallback _taskStackCallback; - LeakCheckCallback _leakCallback; - PanicCallback _panicCallback; - MemoryMonitorDeque _scopeHistory; - MemoryMonitorVector _tagUsage; - MemoryMonitorVector _tagBudgets; - MemoryMonitorUnorderedMap _taskThresholds; - MemoryMonitorUnorderedMap _knownTasks; - MemoryMonitorDeque _leakHistory; - MemoryMonitorDeque _leakCheckpoints; - bool _panicHookInstalled = false; + public: + friend class MemoryScope; + ESPMemoryMonitor(); + ~ESPMemoryMonitor(); + + bool init(const MemoryMonitorConfig &config = MemoryMonitorConfig{}); + void deinit(); + bool isInitialized() const { + return _initialized; + } + + MemorySnapshot sampleNow(); + std::vector history() const; + MemoryMonitorConfig currentConfig() const; + + void onSample(SampleCallback callback); + void onThreshold(ThresholdCallback callback); + void onFailedAlloc(FailedAllocCallback callback); + void onScope(ScopeCallback callback); + void onTagThreshold(TagThresholdCallback callback); + void onTaskStackThreshold(TaskStackThresholdCallback callback); + void onLeakCheck(LeakCheckCallback callback); + + MemoryScope beginScope(const std::string &name, MemoryTag tag = kInvalidMemoryTag); + MemoryTag registerTag(const std::string &name); + bool setTagBudget(MemoryTag tag, const TagBudget &budget); + std::vector scopeHistory() const; + std::vector tagUsage() const; + + LeakCheckResult markLeakCheckPoint(const std::string &label); + + bool setTaskStackThreshold(const std::string &taskName, const TaskStackThreshold &threshold); + + bool installPanicHook(PanicCallback callback = nullptr); + void uninstallPanicHook(); + + private: + enum class AllocHookType { + None = 0, + WithArg, + NoArg, + }; + + struct LockGuard { + explicit LockGuard(SemaphoreHandle_t handle) : _handle(handle) { + if (_handle != nullptr) { + xSemaphoreTake(_handle, portMAX_DELAY); + } + } + + ~LockGuard() { + if (_handle != nullptr) { + xSemaphoreGive(_handle); + } + } + + LockGuard(const LockGuard &) = delete; + LockGuard &operator=(const LockGuard &) = delete; + + private: + SemaphoreHandle_t _handle; + }; + + static void samplerTaskThunk(void *arg); + void samplerTaskLoop(); + + static void allocFailedHook(size_t requestedBytes, uint32_t caps, const char *functionName); + static ESPMemoryMonitor *_failedAllocInstance; + + struct InternalTaskStackUsage { + InternalTaskStackUsage() = default; + explicit InternalTaskStackUsage(bool usePSRAMBuffers) + : name(MemoryMonitorAllocator(usePSRAMBuffers)) { + } + + MemoryMonitorString name; + size_t freeHighWaterBytes = 0; + eTaskState state = eInvalid; + UBaseType_t priority = 0; + TaskHandle_t handle = nullptr; + }; + + struct InternalMemorySnapshot { + InternalMemorySnapshot() = default; + explicit InternalMemorySnapshot(bool usePSRAMBuffers) + : regions(MemoryMonitorAllocator(usePSRAMBuffers)), + stacks(MemoryMonitorAllocator(usePSRAMBuffers)) { + } + + uint64_t timestampUs = 0; + MemoryMonitorVector regions; + MemoryMonitorVector stacks; + }; + + struct InternalScopeStats { + InternalScopeStats() = default; + explicit InternalScopeStats(bool usePSRAMBuffers) + : name(MemoryMonitorAllocator(usePSRAMBuffers)) { + } + + MemoryMonitorString name; + MemoryTag tag = kInvalidMemoryTag; + size_t startInternalFreeBytes = 0; + size_t startPsramFreeBytes = 0; + size_t endInternalFreeBytes = 0; + size_t endPsramFreeBytes = 0; + int64_t deltaInternalBytes = 0; + int64_t deltaPsramBytes = 0; + uint64_t durationUs = 0; + uint64_t startedUs = 0; + uint64_t endedUs = 0; + }; + + struct InternalTagUsage { + InternalTagUsage() = default; + explicit InternalTagUsage(bool usePSRAMBuffers) + : name(MemoryMonitorAllocator(usePSRAMBuffers)) { + } + + MemoryTag tag = kInvalidMemoryTag; + MemoryMonitorString name; + size_t totalInternalBytes = 0; + size_t totalPsramBytes = 0; + ThresholdState state{ThresholdState::Normal}; + }; + + struct InternalLeakCheckResult { + InternalLeakCheckResult() = default; + explicit InternalLeakCheckResult(bool usePSRAMBuffers) + : fromLabel(MemoryMonitorAllocator(usePSRAMBuffers)), + toLabel(MemoryMonitorAllocator(usePSRAMBuffers)), + deltas(MemoryMonitorAllocator(usePSRAMBuffers)) { + } + + MemoryMonitorString fromLabel; + MemoryMonitorString toLabel; + MemoryMonitorVector deltas; + bool leakSuspected = false; + }; + + InternalMemorySnapshot captureSnapshot() const; + RegionStats captureRegion(uint32_t caps, MemoryRegion region) const; + MemoryMonitorVector captureStacks() const; + ScopeStats finalizeScope(const MemoryScope &scope); + ScopeStats toPublicScopeStats(const InternalScopeStats &stats) const; + TagUsage toPublicTagUsage(const InternalTagUsage &usage) const; + TaskStackUsage toPublicTaskStackUsage(const InternalTaskStackUsage &usage) const; + MemorySnapshot toPublicSnapshot(const InternalMemorySnapshot &snapshot) const; + LeakCheckResult toPublicLeakCheckResult(const InternalLeakCheckResult &result) const; + + void appendHistoryLocked(const InternalMemorySnapshot &snapshot); + MemoryMonitorVector + evaluateThresholdsLocked(const InternalMemorySnapshot &snapshot); + ThresholdState + evaluateState(ThresholdState current, const RegionThreshold &threshold, size_t freeBytes) const; + void handleAllocEvent(size_t requestedBytes, uint32_t caps, const char *functionName); + + void enrichSnapshotLocked(InternalMemorySnapshot &snapshot) const; + void appendScopeLocked(const InternalScopeStats &stats); + MemoryMonitorVector + evaluateTagThresholdsLocked(const InternalScopeStats &stats); + ThresholdState + evaluateRisingState(ThresholdState current, const TagBudget &budget, size_t usageBytes) const; + StackState computeStackState(const InternalTaskStackUsage &usage) const; + void trackTasksLocked( + const InternalMemorySnapshot &snapshot, MemoryMonitorVector &events + ); + InternalLeakCheckResult buildLeakCheckLocked(const std::string &label); + void runPanicHook(); + static void panicShutdownThunk(); + + bool registerFailedAllocCallback(); + void unregisterFailedAllocCallback(); + bool registerPanicHandler(); + void unregisterPanicHandler(); + void resetOwnedContainers(); + + MemoryMonitorConfig _config{}; + bool _initialized = false; + bool _running = false; + bool _usePSRAMBuffers = false; + SemaphoreHandle_t _mutex = nullptr; + TaskHandle_t _samplerTask = nullptr; + MemoryMonitorDeque _history; + std::array _thresholdStates{ThresholdState::Normal, ThresholdState::Normal}; + SampleCallback _sampleCallback; + ThresholdCallback _thresholdCallback; + FailedAllocCallback _allocCallback; + ScopeCallback _scopeCallback; + TagThresholdCallback _tagThresholdCallback; + TaskStackThresholdCallback _taskStackCallback; + LeakCheckCallback _leakCallback; + PanicCallback _panicCallback; + MemoryMonitorDeque _scopeHistory; + MemoryMonitorVector _tagUsage; + MemoryMonitorVector _tagBudgets; + MemoryMonitorUnorderedMap _taskThresholds; + MemoryMonitorUnorderedMap _knownTasks; + MemoryMonitorDeque _leakHistory; + MemoryMonitorDeque _leakCheckpoints; + bool _panicHookInstalled = false; }; #if ESPMM_HAS_ARDUINOJSON -void toJson(const MemorySnapshot& snap, JsonDocument& doc); +void toJson(const MemorySnapshot &snap, JsonDocument &doc); #endif diff --git a/src/esp_memory_monitor/memory_monitor_allocator.h b/src/esp_memory_monitor/memory_monitor_allocator.h index 8eae61b..198adc6 100644 --- a/src/esp_memory_monitor/memory_monitor_allocator.h +++ b/src/esp_memory_monitor/memory_monitor_allocator.h @@ -23,93 +23,93 @@ namespace memory_monitor_allocator_detail { inline void *allocate(std::size_t bytes, bool usePSRAMBuffers) noexcept { #if ESP_MEMORY_MONITOR_HAS_BUFFER_MANAGER - return ESPBufferManager::allocate(bytes, usePSRAMBuffers); + return ESPBufferManager::allocate(bytes, usePSRAMBuffers); #else - (void)usePSRAMBuffers; - return std::malloc(bytes); + (void)usePSRAMBuffers; + return std::malloc(bytes); #endif } inline void deallocate(void *ptr) noexcept { #if ESP_MEMORY_MONITOR_HAS_BUFFER_MANAGER - ESPBufferManager::deallocate(ptr); + ESPBufferManager::deallocate(ptr); #else - std::free(ptr); + std::free(ptr); #endif } -} // namespace memory_monitor_allocator_detail - -template -class MemoryMonitorAllocator { - public: - using value_type = T; - using propagate_on_container_move_assignment = std::true_type; - using propagate_on_container_copy_assignment = std::true_type; - using propagate_on_container_swap = std::true_type; - using is_always_equal = std::false_type; - - MemoryMonitorAllocator() noexcept = default; - explicit MemoryMonitorAllocator(bool usePSRAMBuffers) noexcept : _usePSRAMBuffers(usePSRAMBuffers) {} - - template - MemoryMonitorAllocator(const MemoryMonitorAllocator &other) noexcept : _usePSRAMBuffers(other.usePSRAMBuffers()) {} - - T *allocate(std::size_t n) { - if (n == 0) { - return nullptr; - } - if (n > (std::numeric_limits::max() / sizeof(T))) { +} // namespace memory_monitor_allocator_detail + +template class MemoryMonitorAllocator { + public: + using value_type = T; + using propagate_on_container_move_assignment = std::true_type; + using propagate_on_container_copy_assignment = std::true_type; + using propagate_on_container_swap = std::true_type; + using is_always_equal = std::false_type; + + MemoryMonitorAllocator() noexcept = default; + explicit MemoryMonitorAllocator(bool usePSRAMBuffers) noexcept + : _usePSRAMBuffers(usePSRAMBuffers) { + } + + template + MemoryMonitorAllocator(const MemoryMonitorAllocator &other) noexcept + : _usePSRAMBuffers(other.usePSRAMBuffers()) { + } + + T *allocate(std::size_t n) { + if (n == 0) { + return nullptr; + } + if (n > (std::numeric_limits::max() / sizeof(T))) { #if defined(__cpp_exceptions) - throw std::bad_alloc(); + throw std::bad_alloc(); #else - std::abort(); + std::abort(); #endif - } + } - void *memory = memory_monitor_allocator_detail::allocate(n * sizeof(T), _usePSRAMBuffers); - if (memory == nullptr) { + void *memory = memory_monitor_allocator_detail::allocate(n * sizeof(T), _usePSRAMBuffers); + if (memory == nullptr) { #if defined(__cpp_exceptions) - throw std::bad_alloc(); + throw std::bad_alloc(); #else - std::abort(); + std::abort(); #endif - } + } - return static_cast(memory); - } + return static_cast(memory); + } - void deallocate(T *ptr, std::size_t) noexcept { - memory_monitor_allocator_detail::deallocate(ptr); - } + void deallocate(T *ptr, std::size_t) noexcept { + memory_monitor_allocator_detail::deallocate(ptr); + } - bool usePSRAMBuffers() const noexcept { - return _usePSRAMBuffers; - } + bool usePSRAMBuffers() const noexcept { + return _usePSRAMBuffers; + } - template - bool operator==(const MemoryMonitorAllocator &other) const noexcept { - return _usePSRAMBuffers == other.usePSRAMBuffers(); - } + template bool operator==(const MemoryMonitorAllocator &other) const noexcept { + return _usePSRAMBuffers == other.usePSRAMBuffers(); + } - template - bool operator!=(const MemoryMonitorAllocator &other) const noexcept { - return !(*this == other); - } + template bool operator!=(const MemoryMonitorAllocator &other) const noexcept { + return !(*this == other); + } - private: - template - friend class MemoryMonitorAllocator; + private: + template friend class MemoryMonitorAllocator; - bool _usePSRAMBuffers = false; + bool _usePSRAMBuffers = false; }; -template -using MemoryMonitorVector = std::vector>; +template using MemoryMonitorVector = std::vector>; -template -using MemoryMonitorDeque = std::deque>; +template using MemoryMonitorDeque = std::deque>; template , typename Eq = std::equal_to> -using MemoryMonitorUnorderedMap = std::unordered_map>>; +using MemoryMonitorUnorderedMap = + std::unordered_map>>; -using MemoryMonitorString = std::basic_string, MemoryMonitorAllocator>; +using MemoryMonitorString = + std::basic_string, MemoryMonitorAllocator>; diff --git a/test/test_memory_monitor_lifecycle/test_memory_monitor_lifecycle.cpp b/test/test_memory_monitor_lifecycle/test_memory_monitor_lifecycle.cpp index 98fe30d..9007898 100644 --- a/test/test_memory_monitor_lifecycle/test_memory_monitor_lifecycle.cpp +++ b/test/test_memory_monitor_lifecycle/test_memory_monitor_lifecycle.cpp @@ -5,94 +5,96 @@ 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; + 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()); + ESPMemoryMonitor monitor; + TEST_ASSERT_FALSE(monitor.isInitialized()); - monitor.deinit(); - 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()); + 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()); - 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()); + 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()); - TEST_ASSERT_TRUE(monitor.init(testConfig())); - TEST_ASSERT_TRUE(monitor.isInitialized()); - monitor.deinit(); + 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())); + ESPMemoryMonitor monitor; + TEST_ASSERT_TRUE(monitor.init(testConfig())); - (void)monitor.sampleNow(); - (void)monitor.sampleNow(); - TEST_ASSERT_TRUE(monitor.history().size() > 0); + (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())); + 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(); + { + 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 +} // namespace -void setUp() {} -void tearDown() {} +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(); + 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); + delay(1000); }