From 42fa6ab2e73cb70509e102d9dcf07c7d3be95f12 Mon Sep 17 00:00:00 2001 From: zekageri Date: Wed, 25 Feb 2026 10:27:17 +0100 Subject: [PATCH] Bound leak-check state with ring retention --- README.md | 3 ++- src/esp_memory_monitor/memory_monitor.cpp | 14 ++++++++++++-- src/esp_memory_monitor/memory_monitor.h | 5 +++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c6be3c7..3c1752a 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ ESPMemoryMonitor is a tiny C++17 helper that wraps ESP-IDF heap/stack inspection - Per-region thresholds with hysteresis; `onThreshold` fires on enter/exit of warn/critical bands so alerts do not spam as memory bounces. - Optional extras: per-task stack high-water (via `uxTaskGetSystemState`), min-ever-free, and IDF failed-allocation callbacks (`heap_caps_register_failed_alloc_callback`). - Scope-based deltas and tag budgets: wrap a code path in `beginScope()` to measure DRAM/PSRAM consumed (or released), attribute it to a tag, and fire `onScope`/`onTagThreshold` callbacks when soft budgets are crossed. -- Leak suspicion helpers: mark checkpoints for steady-state phases; the monitor compares averages and flags downward free-memory drift or rising fragmentation via `onLeakCheck`. +- Leak suspicion helpers: mark checkpoints for steady-state phases; the monitor compares averages and flags downward free-memory drift or rising fragmentation via `onLeakCheck`, with bounded checkpoint/result retention. - Derived insights: windowed min/avg/max, slope-based bytes/second, and time-to-warn/critical estimates per region. - Task visibility: stack state transitions (`Safe/Warn/Critical`), optional new/vanished task detection, and per-task thresholds. - Export/panic helpers: convert snapshots to ArduinoJson for telemetry and install a shutdown/panic hook that captures a final snapshot before abort/restart. @@ -172,6 +172,7 @@ serializeJson(doc, Serial); | `enableTaskTracking` | `false` | Emit stack-state transitions and task create/destroy events (requires `enablePerTaskStacks`). | | `defaultTaskStackBytes` / `stackWarnFraction` / `stackCriticalFraction` | `4096` / `0.25` / `0.10` | Default stack headroom thresholds when per-task overrides are absent. | | `leakNoiseBytes` | `1024` | Ignore free-byte changes smaller than this when flagging leak drift between checkpoints. | +| `maxLeakChecksInHistory` | `16` | Ring-buffer depth for leak checkpoint timestamps/results (minimum effective value is 1). | | `usePSRAMBuffers` | `false` | Best-effort PSRAM preference for monitor-owned dynamic containers and internal storage models (history/scope/tag/task/leak internals plus transient threshold, scope/tag, window, and task-tracking scratch buffers); automatically falls back to normal heap when PSRAM is unavailable. | `MemorySnapshot` holds `timestampUs` plus vectors of `RegionStats` (free bytes, low-water, largest block, fragmentation, slope/time estimates, window stats) and optional `TaskStackUsage` entries (task name, priority, state, free high-water bytes). diff --git a/src/esp_memory_monitor/memory_monitor.cpp b/src/esp_memory_monitor/memory_monitor.cpp index 4d11a62..b6bb79f 100644 --- a/src/esp_memory_monitor/memory_monitor.cpp +++ b/src/esp_memory_monitor/memory_monitor.cpp @@ -940,8 +940,8 @@ void ESPMemoryMonitor::resetOwnedContainers() { std::hash{}, std::equal_to{}, MemoryMonitorAllocator>(_usePSRAMBuffers)); - _leakHistory = MemoryMonitorVector(MemoryMonitorAllocator(_usePSRAMBuffers)); - _leakCheckpoints = MemoryMonitorVector(MemoryMonitorAllocator(_usePSRAMBuffers)); + _leakHistory = MemoryMonitorDeque(MemoryMonitorAllocator(_usePSRAMBuffers)); + _leakCheckpoints = MemoryMonitorDeque(MemoryMonitorAllocator(_usePSRAMBuffers)); } ESPMemoryMonitor::InternalLeakCheckResult ESPMemoryMonitor::buildLeakCheckLocked(const std::string& label) { @@ -950,9 +950,13 @@ ESPMemoryMonitor::InternalLeakCheckResult ESPMemoryMonitor::buildLeakCheckLocked 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 { @@ -1004,6 +1008,9 @@ ESPMemoryMonitor::InternalLeakCheckResult ESPMemoryMonitor::buildLeakCheckLocked result.deltas.push_back(delta); } _leakHistory.push_back(result); + while (_leakHistory.size() > leakHistoryLimit) { + _leakHistory.pop_front(); + } return result; } @@ -1039,6 +1046,9 @@ ESPMemoryMonitor::InternalLeakCheckResult ESPMemoryMonitor::buildLeakCheckLocked } _leakHistory.push_back(result); + while (_leakHistory.size() > leakHistoryLimit) { + _leakHistory.pop_front(); + } return result; } diff --git a/src/esp_memory_monitor/memory_monitor.h b/src/esp_memory_monitor/memory_monitor.h index 514a27d..efb5f6a 100644 --- a/src/esp_memory_monitor/memory_monitor.h +++ b/src/esp_memory_monitor/memory_monitor.h @@ -79,6 +79,7 @@ struct MemoryMonitorConfig { float stackWarnFraction = 0.25f; float stackCriticalFraction = 0.10f; size_t leakNoiseBytes = 1024; + size_t maxLeakChecksInHistory = 16; bool usePSRAMBuffers = false; }; @@ -415,8 +416,8 @@ class ESPMemoryMonitor { MemoryMonitorVector _tagBudgets; MemoryMonitorUnorderedMap _taskThresholds; MemoryMonitorUnorderedMap _knownTasks; - MemoryMonitorVector _leakHistory; - MemoryMonitorVector _leakCheckpoints; + MemoryMonitorDeque _leakHistory; + MemoryMonitorDeque _leakCheckpoints; bool _panicHookInstalled = false; };