mirror of
https://github.com/arendst/Tasmota.git
synced 2026-10-26 22:11:22 +00:00
Heap Map optional feature fur detailed memory usage at boot (#25186)
This commit is contained in:
1 parent
4df6fa1d4b
commit
0ad5879abe
14 files changed
+417
-10
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user