ESP8266: fix heap metric functions and add OOM diagnostics (#24777)

* ESP8266: fix ESP_getMaxAllocHeap() and ESP_getHeapFragmentation()

Both functions returned wrong values: ESP_getMaxAllocHeap() returned
ESP.getFreeHeap() (total free, not largest contiguous block);
ESP_getHeapFragmentation() read ummHeapInfo.maxFreeContiguousBlocks,
which is only valid immediately after a umm_info() heap walk.

Fix: cache the result of umm_max_block_size() in ESP_UpdateHeapMetrics().
The cache is refreshed once per second when SetOption130 is active, and
on demand in CmndStatus() before Status 4 is output. Both getter functions
read the cached value; the call site overhead is a single integer read.

umm_max_block_size() is unconditionally available (UMM_INFO is hardcoded
in the Arduino ESP8266 umm_malloc_cfg.h), so no build flags are required.

Status 4 (StatusMEM) gains two ESP8266-specific fields: MaxFreeBlock (KB)
and Frag (%).

* ESP8266: add heap OOM diagnostics, Status 4 fields, Status 44 dump

OOM event monitoring (requires UMM_INLINE_METRICS or UMM_STATS_FULL):
- ESP_HeapOomCheck(): called once per second; logs the OOM counter delta
  when the counter changes ("OOM: count N (+M)")
- ESP_HeapOomTest(): logs current OOM count on demand

Status 4 (StatusMEM) gains additional ESP8266-specific fields when the
corresponding build flags are active:
- OomCount: cumulative out-of-memory events (UMM_INLINE_METRICS or
  UMM_STATS_FULL)
- HeapLwm (KB): heap low-watermark since boot (UMM_STATS_FULL)
- MaxAllocSz (bytes): peak single allocation size (UMM_STATS_FULL)

Status 44 (ESP8266-only diagnostic command):
- Triggers umm_info(nullptr, true) to print a full heap block map to
  the serial console
- Calls ESP_HeapOomTest() to log the current OOM count
- Returns {"Status44":{"HeapDump":"serial"}}
- Status 44 is accepted regardless of MAX_STATUS

Build flags UMM_STATS_FULL and UMM_INLINE_METRICS can be enabled via
platformio_override.ini build_flags; documented in
platformio_override_sample.ini.
This commit is contained in:
joluxer
2026-05-25 17:25:04 +02:00
committed by GitHub
parent 166bb946bd
commit ba88358ed6
5 changed files with 85 additions and 8 deletions
+1 -6
View File
@@ -2804,17 +2804,12 @@ void AddLogData(uint32_t loglevel, const char* log_data, const char* log_data_pa
TasAutoMutex mutex((SemaphoreHandle_t *)&TasmotaGlobal.log_buffer_mutex);
#endif // ESP32
char mxtime[21]; // "13:45:21.999-123/12 "
char mxtime[25]; // "13:45:21.999-123/12 " or with fragmentation suffix
snprintf_P(mxtime, sizeof(mxtime), PSTR("%02d" D_HOUR_MINUTE_SEPARATOR "%02d" D_MINUTE_SECOND_SEPARATOR "%02d.%03d"),
RtcTime.hour, RtcTime.minute, RtcTime.second, RtcMillis());
if (Settings->flag5.show_heap_with_timestamp) {
#ifdef ESP8266
snprintf_P(mxtime, sizeof(mxtime), PSTR("%s-%03d"),
mxtime, ESP_getFreeHeap1024());
#else
snprintf_P(mxtime, sizeof(mxtime), PSTR("%s-%03d/%02d"),
mxtime, ESP_getFreeHeap1024(), ESP_getHeapFragmentation());
#endif
}
strcat(mxtime, " ");
@@ -17,6 +17,10 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifdef ESP8266
#include <umm_malloc/umm_malloc_cfg.h>
#endif
const char kTasmotaCommands[] PROGMEM = "|" // No prefix
// SetOptions synonyms
D_SO_WIFINOSLEEP "|"
@@ -901,7 +905,11 @@ void CmndStatus(void)
if (0 == XdrvMailbox.index) { payload = 0; } // All status messages in one MQTT message (status0)
#ifdef ESP8266
if (payload > MAX_STATUS && 44 != payload) { return; } // {"Command":"Error"}
#else
if (payload > MAX_STATUS) { return; } // {"Command":"Error"}
#endif
if (!Settings->flag.mqtt_enabled && (6 == payload)) { return; } // SetOption3 - Enable MQTT
if (!TasmotaGlobal.energy_driver && (9 == payload)) { return; }
#ifndef FIRMWARE_MINIMAL
@@ -1036,6 +1044,20 @@ void CmndStatus(void)
#endif // ESP8266
, ESP_getFlashChipId()
, ESP_getFlashChipSpeed()/1000000);
#ifdef ESP8266
ESP_UpdateHeapMetrics();
ResponseAppend_P(PSTR(",\"MaxFreeBlock\":%d,\"Frag\":%d"),
(uint32_t)ESP_getMaxAllocHeap()/1024,
(int32_t)ESP_getHeapFragmentation());
#if defined(UMM_INLINE_METRICS) || defined(UMM_STATS_FULL)
ResponseAppend_P(PSTR(",\"OomCount\":%d"), (uint32_t)umm_get_oom_count());
#endif
#ifdef UMM_STATS_FULL
ResponseAppend_P(PSTR(",\"HeapLwm\":%d,\"MaxAllocSz\":%d"),
(uint32_t)umm_free_heap_size_lw_min()/1024,
(uint32_t)umm_get_max_alloc_size());
#endif // UMM_STATS_FULL
#endif // ESP8266
ResponseAppendFeatures();
XsnsDriverState();
ResponseAppend_P(PSTR(",\"Sensors\":"));
@@ -1184,6 +1206,19 @@ void CmndStatus(void)
}
#endif
#ifdef ESP8266
// Status 44 - trigger umm heap dump to serial + OOM test
// ESP8266-only heap diagnostic. Chosen above the standard range
// (0-MAX_STATUS = 0-13) and below the reserved value 99 (full status dump).
if (44 == payload) {
umm_info(nullptr, true);
ESP_HeapOomTest();
Response_P(PSTR("{\"" D_CMND_STATUS "44\":{\"HeapDump\":\"serial\"}}"));
CmndStatusResponse(44);
return;
}
#endif // ESP8266
CmndStatusResponse(99);
ResponseClear();
+39 -2
View File
@@ -11,6 +11,8 @@
* ESP8266 and ESP8285 Support
\*********************************************************************************************/
#include <umm_malloc/umm_malloc_cfg.h>
const static char kWifiPhyMode[] PROGMEM = "low rate|11b|11g|11n"; // Wi-Fi Modes
extern "C" {
@@ -104,8 +106,43 @@ float ESP_getFreeHeap1024(void) {
}
*/
// umm_max_block_size() requires a heap walk (ICACHE_FLASH_ATTR, IRQs disabled).
// Cache the result; refresh once per second via ESP_UpdateHeapMetrics().
// umm_max_block_size() and umm_free_heap_size_lw() are available unconditionally
// (UMM_INFO and UMM_STATS are hardcoded active in umm_malloc_cfg.h).
static uint32_t s_umm_max_free_block = 0;
void ESP_UpdateHeapMetrics(void) {
s_umm_max_free_block = umm_max_block_size();
}
uint32_t ESP_getMaxAllocHeap(void) {
return ESP.getFreeHeap();
return s_umm_max_free_block ? s_umm_max_free_block : ESP.getFreeHeap();
}
int32_t ESP_getHeapFragmentation(void) {
// Same formula as ESP32: 100 - (largestFreeBlock * 100 / totalFree)
uint32_t free_heap = umm_free_heap_size_lw(); // ISR-safe, no heap walk
if (free_heap == 0 || s_umm_max_free_block == 0) { return 0; }
int32_t frag = 100 - (int32_t)(s_umm_max_free_block * 100 / free_heap);
return (frag < 0) ? 0 : frag;
}
void ESP_HeapOomCheck(void) {
#if defined(UMM_INLINE_METRICS) || defined(UMM_STATS_FULL)
static size_t oom_prev = 0;
size_t oom = UMM_OOM_COUNT;
if (oom != oom_prev) {
AddLog(LOG_LEVEL_INFO, PSTR("OOM: count %u (+%u)"), oom, oom - oom_prev);
oom_prev = oom;
}
#endif
}
void ESP_HeapOomTest(void) {
#if defined(UMM_INLINE_METRICS) || defined(UMM_STATS_FULL)
AddLog(LOG_LEVEL_INFO, PSTR("OOM: count %u (test-trigger)"), (uint32_t)umm_get_oom_count());
#endif
}
uint32_t ESP_getFlashChipId(void) {
@@ -312,4 +349,4 @@ extern "C" void __wrap_sntp_stop(void) {
// Prevent lwIP SNTP client from stopping on WiFi disconnects and causing timeout list corruption
}
#endif // ESP8266
#endif // ESP8266
@@ -1127,6 +1127,13 @@ void PerformEverySecond(void)
{
TasmotaGlobal.uptime++;
#ifdef ESP8266
if (Settings->flag5.show_heap_with_timestamp) {
ESP_UpdateHeapMetrics();
}
ESP_HeapOomCheck();
#endif
if (POWER_CYCLE_TIME == TasmotaGlobal.uptime) {
UpdateQuickPowerCycle(false);
}