Heap Map optional feature fur detailed memory usage at boot (#25186)

This commit is contained in:
s-hadinger authored and GitHub committed 2026-10-24 12:31:28 +02:00
1 parent 4df6fa1d4b
commit 0ad5879abe
14 files changed
+417 -10

No files matched your search

+1
View File
@@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file.
- LVGL add logging before crash when heap allocation fails (#25170)
- BLE timeout and eQ3 TRV enhancements (#25173)
- TuyaMCU pace outgoing frames so the MCU is never written to back-to-back (#25151)
- Heap Map optional feature fur detailed memory usage at boot
### Breaking Changed
- BTHome un-reverse MAC address usage for command `MI32Keys` to make it consistent
@@ -29,6 +29,7 @@ extern int l_time_dump(bvm *vm);
extern int l_strftime(bvm *vm);
extern int l_strptime(bvm *vm);
extern int l_memory(bvm *vm);
extern int l_heapmap(bvm *vm);
extern int l_wifi(bvm *vm);
extern int l_eth(bvm *vm);
extern int l_hostname(bvm *vm);
@@ -121,6 +122,7 @@ class be_class_tasmota (scope: global, name: Tasmota) {
strftime, func(l_strftime)
strptime, func(l_strptime)
memory, static_func(l_memory)
heapmap, static_func(l_heapmap), USE_HEAP_MAP
wifi, static_func(l_wifi)
eth, static_func(l_eth)
hostname, func(l_hostname)
+34
View File
@@ -571,6 +571,40 @@ bool first_device_group_is_local = true;
#define SHOW_FREE_MEM(WHERE)
#endif
#if defined(ESP32) && defined(USE_HEAP_MAP) // support_esp32.ino
#ifndef HEAP_MAP_PERIOD
#define HEAP_MAP_PERIOD 30 // seconds between periodic heap map rows after boot, 0 = off
#endif
#ifndef HEAP_MAP_CALL_MIN
#define HEAP_MAP_CALL_MIN 100 // minimum internal heap change in bytes to log a driver call, 0 = off
#endif
#ifndef HEAP_MAP_EVENTS_BOOT_ONLY
#define HEAP_MAP_EVENTS_BOOT_ONLY 1 // stop logging stages and driver calls once "boot" is logged, keep only the periodic rows
#endif
#define HEAP_MAP(STAGE) HeapMapEvent(STAGE);
#else
#define HEAP_MAP(STAGE)
#endif // ESP32 and USE_HEAP_MAP
// Wrap a driver call: logs the internal heap change of the call if at least HEAP_MAP_CALL_MIN bytes,
// only for the events listed in support_heap_map.ino (kHeapMapFuncCode and kHeapMapFuncDisplay)
// TYPE is "xdrv", "xsns", "xlgt", "xnrg" or "xdsp", FUNCTION the FUNC_xxx code
// ID (read after the call) is the driver number, or HEAP_MAP_ID_POS | position in the driver list if the
// number is unknown, or HEAP_MAP_ID_DSP | position to ask the display driver with FUNC_DISPLAY_MODEL
#if defined(ESP32) && defined(USE_HEAP_MAP) && (HEAP_MAP_CALL_MIN > 0)
#define HEAP_MAP_ID_POS 0x10000
#define HEAP_MAP_ID_DSP 0x20000
#define HEAP_MAP_CALL(TYPE, ID, FUNCTION, CALL) ({ \
bool _hm_track = HeapMapTracked(((ID) & HEAP_MAP_ID_DSP) != 0, (FUNCTION)); \
int32_t _hm_nested = 0; \
uint32_t _hm_free = _hm_track ? HeapMapCallStart(&_hm_nested) : 0; \
bool _hm_result = (CALL); \
if (_hm_track) { HeapMapCallEnd(PSTR(TYPE), (ID), (FUNCTION), _hm_free, _hm_nested); } \
_hm_result; })
#else
#define HEAP_MAP_CALL(TYPE, ID, FUNCTION, CALL) (CALL)
#endif // ESP32 and USE_HEAP_MAP and HEAP_MAP_CALL_MIN
#ifndef USE_PROFILING
#undef USE_PROFILE_DRIVER
#undef USE_PROFILE_FUNCTION
+4
View File
@@ -1395,6 +1395,10 @@
//#define DEBUG_TASMOTA_SENSOR // Enable sensor debug messages
//#define USE_DEBUG_DRIVER // Use xdrv_99_debug.ino providing commands CpuChk, CfgXor, CfgDump, CfgPeek and CfgPoke
//#define USE_ESP8266_DEBUG_HEAP // Add heap fragmentation display support. Needs build_flags -DUMM_STATS_FULL and -DUMM_INLINE_METRICS too (+2k5 code)
//#define USE_HEAP_MAP // (ESP32) Log heap summary (used/free/largest free/min free) at boot stages and periodically, heap change of each driver call and Berry tasmota.heapmap("label")
//#define HEAP_MAP_PERIOD 30 // (ESP32) With USE_HEAP_MAP, seconds between heap map rows after boot (default 30, 0 = off)
//#define HEAP_MAP_CALL_MIN 100 // (ESP32) With USE_HEAP_MAP, minimum internal heap change in bytes to log a driver call (default 100, 0 = off)
//#define HEAP_MAP_EVENTS_BOOT_ONLY 1 // (ESP32) With USE_HEAP_MAP, stop logging stages and driver calls once "boot" is logged, only periodic rows remain (default 1, 0 = keep logging)
/*********************************************************************************************\
* Profiling features
+5
View File
@@ -575,6 +575,7 @@ void setup(void) {
#endif // ESP32
// Ready for AddLog use
HEAP_MAP(PSTR("setup"))
// AddLog(LOG_LEVEL_INFO, PSTR("ADR: Settings %p, Log %p"), Settings, TasmotaGlobal.log_buffer);
#ifdef ESP32
@@ -597,6 +598,7 @@ void setup(void) {
SettingsLoad();
SettingsDelta();
SettingsMinimum(); // Set life-saving parameters if out-of-range due to reconfig Settings Area
HEAP_MAP(PSTR("settings"))
OsWatchInit();
@@ -737,9 +739,11 @@ void setup(void) {
if (!TasmotaGlobal.no_autoexec) {
BerryInit(); // Load preinit.be
}
HEAP_MAP(PSTR("berry"))
#endif // USE_BERRY
XdrvXsnsCall(FUNC_PRE_INIT); // FUNC_PRE_INIT
HEAP_MAP(PSTR("preinit"))
TasmotaGlobal.init_state = INIT_GPIOS;
@@ -759,6 +763,7 @@ void setup(void) {
#endif // ESP8266
XdrvXsnsCall(FUNC_INIT); // FUNC_INIT
HEAP_MAP(PSTR("init"))
#ifdef USE_SCRIPT
if (bitRead(Settings->rule_enabled, 0)) Run_Scripter(">BS",3,0);
#endif // USE_SCRIPT
@@ -0,0 +1,306 @@
/*
support_heap_map.ino - ESP32 heap map (debug) support for Tasmota
Copyright (C) 2026 Stephan Hadinger
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#if defined(ESP32) && defined(USE_HEAP_MAP)
/*********************************************************************************************\
* Heap map (debug)
*
* USE_HEAP_MAP log one table row per heap at boot stages (macro HEAP_MAP):
* largest free block, minimum free since boot, used blocks with delta against the previous row,
* used bytes with delta against the line above, free, and a used bar
* After boot, a row "boot" at end of boot (uptime 10s), then a row every HEAP_MAP_PERIOD
* seconds (default 30, 0 = off). The table header is repeated every HEAP_MAP_HEADER_ROWS rows.
* Berry: tasmota.heapmap("label") logs a row with this label.
* Each driver call that changes the internal heap by HEAP_MAP_CALL_MIN bytes is logged (see below).
* With HEAP_MAP_EVENTS_BOOT_ONLY (default 1) stage rows and driver call lines stop after the "boot" row,
* only the periodic rows (and Berry tasmota.heapmap()) remain.
* Only these driver events are logged: PRE_INIT, INIT, MODULE_INIT, MQTT_INIT, WEB_ADD_HANDLER,
* NETWORK_UP and the display DISPLAY_INIT_DRIVER, DISPLAY_INIT (see kHeapMapFuncCode).
*
* The internal row walks MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT heaps only (all caps must match):
* - ESP32 IRAM-only heaps (EXEC | 32BIT, used by Berry with USE_BERRY_IRAM) have no 8BIT and are excluded
* - PSRAM has no INTERNAL and is excluded (not reported at all)
* - RTC fast memory is included where the IDF adds it as general heap (e.g. C3/S3, 8 KB)
* On chips with unified D/IRAM (C3, S3, ...), 32-bit allocations come from the same memory
* as everything else and are counted, as they do reduce general heap.
*
* heap_caps_walk() calls the walker with the heap lock held (critical section), so the
* the walker only fills a local structure. Logging is done after the walk.
\*********************************************************************************************/
#define HEAP_MAP_BAR_WIDTH 12
#define HEAP_MAP_HEADER_ROWS 20 // repeat the table header after this many rows
typedef struct {
uint32_t used_blocks;
uint32_t largest_free;
} HeapMapSummary_t;
// Heap map state, zero at boot
struct {
// Internal heap, for deltas
uint32_t last_free; // free bytes after the last HMP line (table row or driver call line), deltas are since the line above
uint32_t last_blocks; // used blocks at the last table row (driver call lines have no block count)
bool has_last; // last_free and last_blocks are set
uint32_t rows; // table rows since the last header, 0 = no header yet
bool network_up; // network rows logged for the current network up (support_tasmota.ino)
bool boot_done; // row "boot" logged (support_tasmota.ino), with HEAP_MAP_EVENTS_BOOT_ONLY stages and driver calls stop
#if HEAP_MAP_CALL_MIN > 0
// Driver call tracking, internal heap only (see HeapMapCallEnd)
int32_t call_nested; // heap reported by lines printed inside the current call
uint32_t call_depth; // nesting of driver calls, 0 = not in a driver call
bool call_logging; // avoid logging from calls made by the logger
int32_t other; // heap used outside logged driver calls, not reported yet
uint32_t mark; // free at the end of the last top level call or table row
bool mark_valid; // mark is set
#endif // HEAP_MAP_CALL_MIN > 0
} HeapMapGlobal;
// Bytes as KB with one decimal, e.g. "35.9K"
static void HeapMapKB(char *buf, size_t size, uint32_t bytes) {
uint32_t k10 = (uint32_t)(((uint64_t)bytes * 10 + 512) / 1024);
snprintf_P(buf, size, PSTR("%u.%uK"), (unsigned int)(k10 / 10), (unsigned int)(k10 % 10));
}
// Signed delta in KB with one decimal (e.g. "+42.5K", "+0.0K" up to 51 bytes), "0" only if exactly zero
static void HeapMapDeltaKB(char *buf, size_t size, int32_t delta) {
if (0 == delta) {
snprintf_P(buf, size, PSTR("0"));
return;
}
uint32_t abs_delta = (delta < 0) ? (uint32_t)(-(int64_t)delta) : (uint32_t)delta;
uint32_t k10 = (uint32_t)(((uint64_t)abs_delta * 10 + 512) / 1024);
snprintf_P(buf, size, PSTR("%c%u.%uK"), (delta < 0) ? '-' : '+', (unsigned int)(k10 / 10), (unsigned int)(k10 % 10));
}
// Bar and percentage of the heap that is not free, e.g. "[####........] 32%"
// (not free = used blocks and block headers, the same for table rows and driver call lines)
static void HeapMapBar(char *buf, size_t size, uint32_t free_bytes, uint32_t total) {
uint32_t not_free = (total > free_bytes) ? total - free_bytes : 0;
uint32_t pct = total ? (uint32_t)(((uint64_t)not_free * 100 + total / 2) / total) : 0;
uint32_t fill = total ? (uint32_t)(((uint64_t)not_free * HEAP_MAP_BAR_WIDTH + total / 2) / total) : 0;
char bar[HEAP_MAP_BAR_WIDTH + 1];
for (uint32_t i = 0; i < HEAP_MAP_BAR_WIDTH; i++) { bar[i] = (i < fill) ? '#' : '.'; }
bar[HEAP_MAP_BAR_WIDTH] = 0;
snprintf_P(buf, size, PSTR("[%s] %3u%%"), bar, (unsigned int)pct);
}
// Row layout: the less useful columns come first, so that the longer label of a driver call line
// (driver and function, up to 43 chars) covers them, and its used, delta and free align with the table:
// HMP: stage largest min blks delta used delta free heap used
// HMP: network_connect 112.9K 187.7K 400 +243 88.6K +40.2K 189.7K [####........] 32%
// HMP: xdrv_52 WEB_ADD_HANDLER 89.9K +1.2K 188.5K [####........] 33%
// HMP: network_started 112.9K 187.7K 411 +11 90.2K +0.3K 188.1K [####........] 33%
// used = heap size - free (used blocks and block headers), free is the heap free counter, for all lines
static void HeapMapHeader(void) {
AddLog(LOG_LEVEL_DEBUG, PSTR("HMP: %-15s %7s %7s %5s %5s %7s %7s %7s heap used"),
PSTR("stage"), PSTR("largest"), PSTR("min"), PSTR("blks"), PSTR("delta"), PSTR("used"), PSTR("delta"), PSTR("free"));
HeapMapGlobal.rows = 0;
}
static void HeapMapSummary(const char *stage) {
const uint32_t caps = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
HeapMapSummary_t s;
memset(&s, 0, sizeof(s));
heap_caps_walk(caps, [](walker_heap_into_t heap_info, walker_block_info_t block_info, void *user_data) -> bool {
HeapMapSummary_t *sum = (HeapMapSummary_t*)user_data;
if (block_info.used) {
sum->used_blocks++;
} else if (block_info.size > sum->largest_free) {
sum->largest_free = block_info.size;
}
return true;
}, &s);
uint32_t free_bytes = heap_caps_get_free_size(caps); // same counter as driver call lines
uint32_t total = heap_caps_get_total_size(caps);
uint32_t used_bytes = (total > free_bytes) ? total - free_bytes : 0;
char used[12], free_str[12], largest[12], min_str[12];
HeapMapKB(used, sizeof(used), used_bytes);
HeapMapKB(free_str, sizeof(free_str), free_bytes);
HeapMapKB(largest, sizeof(largest), s.largest_free);
HeapMapKB(min_str, sizeof(min_str), heap_caps_get_minimum_free_size(caps));
char d_used[14] = ""; // empty on the first row
char d_blocks[8] = "";
if (HeapMapGlobal.has_last) {
HeapMapDeltaKB(d_used, sizeof(d_used), (int32_t)(HeapMapGlobal.last_free - free_bytes));
int32_t db = (int32_t)(s.used_blocks - HeapMapGlobal.last_blocks);
snprintf_P(d_blocks, sizeof(d_blocks), db ? PSTR("%+d") : PSTR("0"), (int)db);
}
char bar[HEAP_MAP_BAR_WIDTH + 8];
HeapMapBar(bar, sizeof(bar), free_bytes, total);
AddLog(LOG_LEVEL_DEBUG, PSTR("HMP: %-15.15s %7s %7s %5u %5s %7s %7s %7s %s"),
stage, largest, min_str, (unsigned int)s.used_blocks, d_blocks, used, d_used, free_str, bar);
uint32_t free_logged = heap_caps_get_free_size(caps); // after the log line, so its cost is not in the next delta
#if HEAP_MAP_CALL_MIN > 0
// this row reports everything since the line above, including "other"
if (HeapMapGlobal.call_depth > 0) { // row printed inside a driver call (e.g. Berry): don't report it twice
if (HeapMapGlobal.has_last) {
HeapMapGlobal.call_nested += (int32_t)(HeapMapGlobal.last_free - free_logged) - HeapMapGlobal.other;
}
} else {
HeapMapGlobal.mark = free_logged;
HeapMapGlobal.mark_valid = true;
}
HeapMapGlobal.other = 0;
#endif // HEAP_MAP_CALL_MIN > 0
HeapMapGlobal.last_free = free_logged;
HeapMapGlobal.last_blocks = s.used_blocks;
HeapMapGlobal.has_last = true;
}
// Stage row (macro HEAP_MAP), not logged after boot if HEAP_MAP_EVENTS_BOOT_ONLY
void HeapMapEvent(const char *stage) {
#if HEAP_MAP_EVENTS_BOOT_ONLY
if (HeapMapGlobal.boot_done) { return; }
#endif // HEAP_MAP_EVENTS_BOOT_ONLY
HeapMap(stage, false);
}
// header: force the table header (Berry); otherwise it is printed every HEAP_MAP_HEADER_ROWS rows
void HeapMap(const char *stage, bool header) {
if (header || (0 == HeapMapGlobal.rows) || (HeapMapGlobal.rows >= HEAP_MAP_HEADER_ROWS)) { HeapMapHeader(); }
HeapMapGlobal.rows++;
HeapMapSummary(stage);
}
#if HEAP_MAP_CALL_MIN > 0
/*********************************************************************************************\
* Heap change per driver call (macro HEAP_MAP_CALL in the xdrv/xsns/xlgt/xnrg/xdsp dispatchers)
*
* Logs `HMP: xdrv_52 EVERY_50_MSECOND 50.1K +1.2K 45.6K [######......] 50%` (used, delta, free, bar) when a single driver call changes the
* internal heap (MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT, PSRAM excluded) by at least HEAP_MAP_CALL_MIN bytes.
* + means the driver now holds more memory, - means it released memory.
*
* Nested driver calls (e.g. a rule or Berry running a command) are logged on their own and
* excluded from the caller, as is the cost of the log line itself.
* Heap used outside logged calls (core code, other tasks, calls below the threshold) is summed and shown
* as an `other` line before the next driver call line (if at least HEAP_MAP_CALL_MIN), or included in the
* delta of the next table row. So the deltas add up from line to line.
* The driver number is the one the driver reports: TasmotaGlobal.light_driver / energy_driver once
* claimed, Settings->display_model for the display driver that answers FUNC_DISPLAY_MODEL. Drivers that did
* not claim the device are shown by position in the driver list, e.g. `xlgt#3`.
* The heap free counter is global: allocations by other tasks (WiFi, LwIP, ...) during the call
* are attributed to the driver. A + and - pair of the same size on unrelated calls is usually such noise.
\*********************************************************************************************/
// Tracked events, all other driver calls (LOOP, EVERY_xx_MSECOND, JSON_APPEND, COMMAND, ...) are not logged.
// Codes and names are in the same order
const uint8_t kHeapMapFuncCode[] PROGMEM = {
FUNC_PRE_INIT, FUNC_INIT, FUNC_MODULE_INIT, FUNC_MQTT_INIT, FUNC_WEB_ADD_HANDLER, FUNC_NETWORK_UP };
const char kHeapMapFuncName[] PROGMEM =
"PRE_INIT|INIT|MODULE_INIT|MQTT_INIT|WEB_ADD_HANDLER|NETWORK_UP";
// Display events are the first of enum XdspFunctions (xdrv_13_display.ino): FUNC_DISPLAY_INIT_DRIVER, FUNC_DISPLAY_INIT
const char kHeapMapFuncDisplay[] PROGMEM =
"DISPLAY_INIT_DRIVER|DISPLAY_INIT";
#define HEAP_MAP_DISPLAY_TRACKED 2
// Index of a tracked event in kHeapMapFuncName (or kHeapMapFuncDisplay for the display driver), -1 if not tracked
static int32_t HeapMapFuncIndex(bool display, uint32_t function) {
if (display) { return (function < HEAP_MAP_DISPLAY_TRACKED) ? (int32_t)function : -1; }
for (uint32_t i = 0; i < sizeof(kHeapMapFuncCode); i++) {
if (pgm_read_byte(kHeapMapFuncCode + i) == function) { return i; }
}
return -1;
}
// Called by macro HEAP_MAP_CALL before each driver call: true if the call is tracked
bool HeapMapTracked(bool display, uint32_t function) {
#if HEAP_MAP_EVENTS_BOOT_ONLY
if (HeapMapGlobal.boot_done) { return false; }
#endif // HEAP_MAP_EVENTS_BOOT_ONLY
return HeapMapFuncIndex(display, function) >= 0;
}
// Line for a driver call or "other": label covers the first table columns, then used, delta, free and bar
static void HeapMapCallLine(const char *label, int32_t delta, uint32_t free_bytes) {
uint32_t total = heap_caps_get_total_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
char used_str[12], delta_str[14], free_str[12];
HeapMapKB(used_str, sizeof(used_str), (total > free_bytes) ? total - free_bytes : 0);
HeapMapDeltaKB(delta_str, sizeof(delta_str), delta);
HeapMapKB(free_str, sizeof(free_str), free_bytes);
char bar[HEAP_MAP_BAR_WIDTH + 8];
HeapMapBar(bar, sizeof(bar), free_bytes, total);
AddLog(LOG_LEVEL_DEBUG, PSTR("HMP: %-43s %7s %7s %7s %s"), label, used_str, delta_str, free_str, bar);
}
uint32_t HeapMapCallStart(int32_t *nested) {
uint32_t free_start = heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
if ((0 == HeapMapGlobal.call_depth) && HeapMapGlobal.mark_valid) {
HeapMapGlobal.other += (int32_t)(HeapMapGlobal.mark - free_start); // used since the last top level call or table row
}
HeapMapGlobal.call_depth++;
*nested = HeapMapGlobal.call_nested; // save the caller's sum
HeapMapGlobal.call_nested = 0;
return free_start;
}
void HeapMapCallEnd(const char *type, uint32_t id, uint32_t function, uint32_t free_start, int32_t nested) {
uint32_t free_end = heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
int32_t used = (int32_t)(free_start - free_end); // heap used by this call including nested calls
int32_t self = used - HeapMapGlobal.call_nested; // minus what lines inside this call already reported
int32_t reported = HeapMapGlobal.call_nested; // what the caller must exclude
HeapMapGlobal.call_depth--;
if (!HeapMapGlobal.call_logging && ((self >= HEAP_MAP_CALL_MIN) || (self <= -HEAP_MAP_CALL_MIN))) {
HeapMapGlobal.call_logging = true;
if ((HeapMapGlobal.other >= HEAP_MAP_CALL_MIN) || (HeapMapGlobal.other <= -HEAP_MAP_CALL_MIN)) {
uint32_t free_other = HeapMapGlobal.has_last ? HeapMapGlobal.last_free - HeapMapGlobal.other : free_start;
HeapMapCallLine(PSTR("other"), HeapMapGlobal.other, free_other);
}
HeapMapGlobal.other = 0; // printed, or too small and covered by the used column
char fname[20];
bool display = (id & HEAP_MAP_ID_DSP);
GetTextIndexed(fname, sizeof(fname), HeapMapFuncIndex(display, function), display ? kHeapMapFuncDisplay : kHeapMapFuncName);
uint32_t pos = id & 0xFFFF; // position in the driver list if the number is unknown
#ifdef USE_DISPLAY
if (display) { // ask the display driver if it is the active one
id = XdspHeapMapId(pos);
if (0 == id) { id = HEAP_MAP_ID_POS | pos; }
}
#endif // USE_DISPLAY
char driver[12]; // e.g. "xdrv_52", or "xlgt#3" by position, padded below so columns align
if (id & (HEAP_MAP_ID_POS | HEAP_MAP_ID_DSP)) {
snprintf_P(driver, sizeof(driver), PSTR("%s#%u"), type, (unsigned int)pos);
} else {
snprintf_P(driver, sizeof(driver), PSTR("%s_%02u"), type, (unsigned int)id);
}
char label[44]; // driver and function, as wide as the first 5 table columns
snprintf_P(label, sizeof(label), PSTR("%-8s %s"), driver, fname);
HeapMapCallLine(label, self, free_end);
uint32_t free_logged = heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
reported = used + (int32_t)(free_end - free_logged); // the whole call and the log lines
free_end = free_logged;
HeapMapGlobal.last_free = free_logged; // next delta starts from here
HeapMapGlobal.has_last = true;
HeapMapGlobal.call_logging = false;
} else if (0 == HeapMapGlobal.call_depth) {
HeapMapGlobal.other += self; // top level call not logged, report it later as "other"
}
HeapMapGlobal.call_nested = nested + reported; // restore the caller's sum, adding what was reported here
if (0 == HeapMapGlobal.call_depth) {
HeapMapGlobal.mark = free_end;
HeapMapGlobal.mark_valid = true;
}
}
#endif // HEAP_MAP_CALL_MIN > 0
#endif // ESP32 and USE_HEAP_MAP
@@ -1153,6 +1153,17 @@ void PerformEverySecond(void)
#endif
}
#if defined(ESP32) && defined(USE_HEAP_MAP)
if (BOOT_LOOP_TIME == TasmotaGlobal.uptime) {
HEAP_MAP(PSTR("boot")) // end of boot
HeapMapGlobal.boot_done = true; // with HEAP_MAP_EVENTS_BOOT_ONLY stop logging stages and driver calls
} else if (HEAP_MAP_PERIOD && (TasmotaGlobal.uptime > BOOT_LOOP_TIME) && (0 == (TasmotaGlobal.uptime % HEAP_MAP_PERIOD))) {
char stage[16];
snprintf_P(stage, sizeof(stage), PSTR("up %us"), (unsigned int)TasmotaGlobal.uptime);
HeapMap(stage, false); // periodic row
}
#endif // ESP32 and USE_HEAP_MAP
if (TasmotaGlobal.power_on_delay) {
if (1 == Settings->param[P_POWER_ON_DELAY2]) { // SetOption47 1
// Allow relay power on once network is available
@@ -1653,6 +1664,11 @@ void Every250mSeconds(void)
case 3:
{
if (!TasmotaGlobal.global_state.network_down) {
#if defined(ESP32) && defined(USE_HEAP_MAP)
if (!HeapMapGlobal.network_up) { // network just connected, before network services start
HEAP_MAP(PSTR("network_connect"))
}
#endif // USE_HEAP_MAP
#ifdef FIRMWARE_MINIMAL
//#ifdef CONFIG_IDF_TARGET_ESP32C3
#ifdef ESP32
@@ -1700,6 +1716,12 @@ void Every250mSeconds(void)
// send FUNC_NETWORK_UP to all modules
// AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("WIF: Sending FUNC_NETWORK_UP"));
XdrvXsnsCall(FUNC_NETWORK_UP);
#if defined(ESP32) && defined(USE_HEAP_MAP)
if (!HeapMapGlobal.network_up) { // FUNC_NETWORK_UP is sent every second, log only on transition to up
HeapMapGlobal.network_up = true;
HEAP_MAP(PSTR("network_started")) // network services (webserver, mDNS, ...) started
}
#endif // USE_HEAP_MAP
MqttCheck();
} else {
@@ -1711,6 +1733,9 @@ void Every250mSeconds(void)
// send FUNC_NETWORK_DOWN to all modules
// AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("WIF: Sending FUNC_NETWORK_DOWN"));
XdrvXsnsCall(FUNC_NETWORK_DOWN);
#if defined(ESP32) && defined(USE_HEAP_MAP)
HeapMapGlobal.network_up = false;
#endif // USE_HEAP_MAP
} // Every x.75 second
}
break;
@@ -245,6 +245,20 @@ extern "C" {
be_return(vm);
}
#ifdef USE_HEAP_MAP
// Berry: tasmota.heapmap([label:string [, header:bool]]) -> nil
//
// Log a heap map row with the given label (default "berry"), header forces the table header
int32_t l_heapmap(struct bvm *vm);
int32_t l_heapmap(struct bvm *vm) {
int32_t top = be_top(vm); // Get the number of arguments
const char *label = (top >= 1 && be_isstring(vm, 1)) ? be_tostring(vm, 1) : "berry";
bool header = (top >= 2) ? be_tobool(vm, 2) : false;
HeapMap(label, header);
be_return_nil(vm);
}
#endif // USE_HEAP_MAP
// Berry: tasmota.wifi() -> map
//
int32_t l_wifi(struct bvm *vm);
@@ -837,6 +837,7 @@ bool Xdrv52(uint32_t function)
BrLoad("autoexec.be"); // run autoexec.be at first tick, so we know all modules are initialized
callBerryString(berry.vm, be_post_autoxec_code);
berry.autoexec_done = true;
HEAP_MAP(PSTR("autoexec"))
#ifdef USE_WEBSERVER
// check if `web_add_handler` was missed, for example because of Berry VM restart
@@ -1129,7 +1129,7 @@ bool XdrvCallDriver(uint32_t driver, uint32_t function) {
uint32_t listed = kXdrvList[x];
#endif
if (driver == listed) {
return xdrv_func_ptr[x](function);
return HEAP_MAP_CALL("xdrv", driver, function, xdrv_func_ptr[x](function));
}
}
return false;
@@ -1149,7 +1149,7 @@ bool XdrvCallNextJsonAppend(void) {
return false;
}
} while (!XdrvActive(xdrv_index));
xdrv_func_ptr[xdrv_index](FUNC_JSON_APPEND);
HEAP_MAP_CALL("xdrv", pgm_read_byte(kXdrvList + xdrv_index), FUNC_JSON_APPEND, xdrv_func_ptr[xdrv_index](FUNC_JSON_APPEND));
return true;
}
@@ -1188,7 +1188,7 @@ bool XdrvCall(uint32_t function) {
}
#endif // DEBUG_COMMAND_HANG
result = xdrv_func_ptr[x](function);
result = HEAP_MAP_CALL("xdrv", pgm_read_byte(kXdrvList + x), function, xdrv_func_ptr[x](function));
#ifdef USE_WEBSERVER
if (FUNC_WEB_SENSOR == function) {
+16 -1
View File
@@ -181,13 +181,28 @@ uint8_t XdspPresent(void) {
return xdsp_present;
}
#if defined(ESP32) && defined(USE_HEAP_MAP)
// Heap map: number of the display driver at position index if it is the active one, else 0
uint32_t XdspHeapMapId(uint32_t index) {
if (index >= xdsp_present) { return 0; }
#ifdef XDSP_18
// Berry display has its own number and answers FUNC_DISPLAY_MODEL by running Berry code, don't ask it
if (&Xdsp18 == xdsp_func_ptr[index]) { return XDSP_18; }
#endif // XDSP_18
if (xdsp_func_ptr[index](FUNC_DISPLAY_MODEL)) {
return Settings->display_model;
}
return 0;
}
#endif // ESP32 and USE_HEAP_MAP
bool XdspCall(uint32_t function) {
bool result = false;
DEBUG_TRACE_LOG(PSTR("DSP: %d"), function);
for (uint32_t x = 0; x < xdsp_present; x++) {
result = xdsp_func_ptr[x](function);
result = HEAP_MAP_CALL("xdsp", (HEAP_MAP_ID_DSP | x), function, xdsp_func_ptr[x](function));
if (result && (FUNC_DISPLAY_MODEL == function)) {
break;
@@ -99,7 +99,7 @@ bool XlgtCall(uint32_t function) {
if (FUNC_MODULE_INIT == function) {
for (uint32_t x = 0; x < xlgt_present; x++) {
xlgt_func_ptr[x](function);
HEAP_MAP_CALL("xlgt", (TasmotaGlobal.light_driver) ? TasmotaGlobal.light_driver : (HEAP_MAP_ID_POS | x), function, xlgt_func_ptr[x](function));
if (TasmotaGlobal.light_driver) {
xlgt_active = x;
return true; // Stop further driver investigation
@@ -107,7 +107,7 @@ bool XlgtCall(uint32_t function) {
}
}
else if (TasmotaGlobal.light_driver) {
return xlgt_func_ptr[xlgt_active](function);
return HEAP_MAP_CALL("xlgt", TasmotaGlobal.light_driver, function, xlgt_func_ptr[xlgt_active](function));
}
return false;
}
@@ -167,7 +167,7 @@ bool XnrgCall(uint32_t function) {
if (FUNC_PRE_INIT == function) {
for (uint32_t x = 0; x < xnrg_present; x++) {
xnrg_func_ptr[x](function);
HEAP_MAP_CALL("xnrg", (TasmotaGlobal.energy_driver) ? TasmotaGlobal.energy_driver : (HEAP_MAP_ID_POS | x), function, xnrg_func_ptr[x](function));
if (TasmotaGlobal.energy_driver) {
xnrg_active = x;
return true; // Stop further driver investigation
@@ -175,7 +175,7 @@ bool XnrgCall(uint32_t function) {
}
}
else if (TasmotaGlobal.energy_driver) {
return xnrg_func_ptr[xnrg_active](function);
return HEAP_MAP_CALL("xnrg", TasmotaGlobal.energy_driver, function, xnrg_func_ptr[xnrg_active](function));
}
return false;
}
@@ -1104,7 +1104,7 @@ bool XsnsCallNextJsonAppend(void) {
return false;
}
} while (!XsnsEnabled(0, xsns_index));
xsns_func_ptr[xsns_index](FUNC_JSON_APPEND);
HEAP_MAP_CALL("xsns", pgm_read_byte(kXsnsList + xsns_index), FUNC_JSON_APPEND, xsns_func_ptr[xsns_index](FUNC_JSON_APPEND));
return true;
}
@@ -1145,7 +1145,7 @@ bool XsnsCall(uint32_t function) {
}
#endif // DEBUG_COMMAND_HANG
result = xsns_func_ptr[x](function);
result = HEAP_MAP_CALL("xsns", pgm_read_byte(kXsnsList + x), function, xsns_func_ptr[x](function));
#ifdef USE_WEBSERVER
if (FUNC_WEB_SENSOR == function) { WSContentSeparator(1); } // Show separator if needed