Add MemAudit: per-subsystem heap accounting in the boot log (#10900)

* Add MemAudit: per-subsystem heap accounting in the boot log

The 2.8.0 nRF52840 heap-exhaustion field reports had to be diagnosed by
hand, reconstructing each subsystem's heap footprint from source and
build flags one report at a time. This makes every future report
self-diagnosing from the serial log: a tiny fixed-size registry
(src/memory/MemAudit.*) that big long-lived allocations report into,
printed as one line at the end of setup() and alongside the periodic
"Heap free:" log:

  MemAudit[boot]: tmm=2500 warm=4000 pkthist=5824 nodedb=13440
  msgstore=2200 pktpool(live)=3270 total=31234

Instrumented: NodeDB hot vector (nodedb) + satellite maps (satmaps,
rb-tree overhead estimated), WarmNodeStore (warm), PacketHistory records
and hash index (pkthist), TrafficManagement caches (tmm/tmm_ni),
MessageStore text pool (msgstore), TFT line/repaint buffers (display),
and live in-flight packets (pktpool(live)) via an optional audit tag on
the packet pool allocator - the one hot path, counted with a relaxed
32-bit atomic add (single instructions on Cortex-M, no locks).

Cost: 128 B RAM for the 16-tag table, well under 1 KB flash on rak4631.
MESHTASTIC_MEM_AUDIT=0 compiles it out to inline no-op stubs (call
sites need no ifdefs); STM32WL, the tightest flash target, defaults off.

New native suite test_mem_audit covers add/set/snapshot arithmetic, tag
reuse (pointer and cross-TU strcmp fallback), null/unknown tags, and
table-full behavior; test/native-suite-count bumped to 28.

* native-wasm: add src/memory/ to the curated source filter

The wasm env denies all sources and adds an explicit file list; MemAudit
callers (main, MeshService, NodeDB, PacketHistory) are in that list but
src/memory/MemAudit.cpp was not, so wasm-ld failed on undefined
memaudit:: symbols.
This commit is contained in:
Ben Meadors
2026-07-06 13:18:49 -05:00
committed by GitHub
co-authored by GitHub
parent ed03a69555
commit 6c7ee8afc7
15 changed files with 440 additions and 7 deletions
+3
View File
@@ -6,6 +6,7 @@
#include "SPILock.h" #include "SPILock.h"
#include "SafeFile.h" #include "SafeFile.h"
#include "gps/RTC.h" #include "gps/RTC.h"
#include "memory/MemAudit.h"
#include <cstring> // memcpy #include <cstring> // memcpy
#ifndef MESSAGE_TEXT_POOL_SIZE #ifndef MESSAGE_TEXT_POOL_SIZE
@@ -28,8 +29,10 @@ static inline void resetMessagePool()
g_messagePool = static_cast<char *>(malloc(MESSAGE_TEXT_POOL_SIZE)); g_messagePool = static_cast<char *>(malloc(MESSAGE_TEXT_POOL_SIZE));
if (!g_messagePool) { if (!g_messagePool) {
LOG_ERROR("MessageStore: Failed to allocate %d bytes for message pool", MESSAGE_TEXT_POOL_SIZE); LOG_ERROR("MessageStore: Failed to allocate %d bytes for message pool", MESSAGE_TEXT_POOL_SIZE);
memaudit::set("msgstore", 0);
return; return;
} }
memaudit::set("msgstore", MESSAGE_TEXT_POOL_SIZE);
} }
g_poolWritePos = 0; g_poolWritePos = 0;
memset(g_messagePool, 0, MESSAGE_TEXT_POOL_SIZE); memset(g_messagePool, 0, MESSAGE_TEXT_POOL_SIZE);
+4
View File
@@ -1,5 +1,6 @@
#include "configuration.h" #include "configuration.h"
#include "main.h" #include "main.h"
#include "memory/MemAudit.h"
#if USE_TFTDISPLAY #if USE_TFTDISPLAY
#if ARCH_PORTDUINO #if ARCH_PORTDUINO
@@ -1228,6 +1229,7 @@ TFTDisplay::~TFTDisplay()
free(repaintChunkBuffer); free(repaintChunkBuffer);
repaintChunkBuffer = nullptr; repaintChunkBuffer = nullptr;
} }
memaudit::set("display", 0);
} }
// Write the buffer to the display memory // Write the buffer to the display memory
@@ -1654,6 +1656,7 @@ bool TFTDisplay::connect()
LOG_ERROR("Not enough memory to create TFT line buffer\n"); LOG_ERROR("Not enough memory to create TFT line buffer\n");
return false; return false;
} }
memaudit::add("display", sizeof(uint16_t) * displayWidth);
} }
if (this->repaintChunkBuffer == NULL) { if (this->repaintChunkBuffer == NULL) {
this->repaintChunkBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth * kFullRepaintChunkRows); this->repaintChunkBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth * kFullRepaintChunkRows);
@@ -1662,6 +1665,7 @@ bool TFTDisplay::connect()
LOG_ERROR("Not enough memory to create TFT repaint chunk buffer\n"); LOG_ERROR("Not enough memory to create TFT repaint chunk buffer\n");
return false; return false;
} }
memaudit::add("display", sizeof(uint16_t) * displayWidth * kFullRepaintChunkRows);
} }
return true; return true;
} }
+4
View File
@@ -38,6 +38,7 @@
#include "detect/einkScan.h" #include "detect/einkScan.h"
#include "graphics/Screen.h" #include "graphics/Screen.h"
#include "main.h" #include "main.h"
#include "memory/MemAudit.h"
#include "mesh/generated/meshtastic/config.pb.h" #include "mesh/generated/meshtastic/config.pb.h"
#include "meshUtils.h" #include "meshUtils.h"
#include "modules/Modules.h" #include "modules/Modules.h"
@@ -1151,6 +1152,9 @@ void setup()
LOG_DEBUG("Free PSRAM : %7d bytes", ESP.getFreePsram()); LOG_DEBUG("Free PSRAM : %7d bytes", ESP.getFreePsram());
#endif #endif
// Log the per-subsystem heap breakdown now that the big allocations are done
memaudit::logBreakdown("boot");
// We manually run this to update the NodeStatus // We manually run this to update the NodeStatus
nodeDB->notifyObservers(true); nodeDB->notifyObservers(true);
} }
+2
View File
@@ -9,6 +9,7 @@
*/ */
#include "memGet.h" #include "memGet.h"
#include "configuration.h" #include "configuration.h"
#include "memory/MemAudit.h"
#if defined(MESHTASTIC_DYNAMIC_SBRK_HEAP) #if defined(MESHTASTIC_DYNAMIC_SBRK_HEAP)
#include <malloc.h> #include <malloc.h>
@@ -118,4 +119,5 @@ void displayPercentHeapFree()
} }
int percent = (int)((freeHeap * 100) / totalHeap); int percent = (int)((freeHeap * 100) / totalHeap);
LOG_INFO("Heap free: %d%% (%u/%u bytes)", percent, freeHeap, totalHeap); LOG_INFO("Heap free: %d%% (%u/%u bytes)", percent, freeHeap, totalHeap);
memaudit::logBreakdown("heap"); // per-subsystem breakdown rides along with the periodic heap log
} }
+116
View File
@@ -0,0 +1,116 @@
#include "MemAudit.h"
#if MESHTASTIC_MEM_AUDIT
#include "DebugConfiguration.h"
#include <atomic>
#include <stdio.h>
#include <string.h>
namespace memaudit
{
namespace
{
struct Entry {
std::atomic<const char *> tag; // registered literal; nullptr = free slot
std::atomic<int32_t> bytes;
};
// Static storage only - the accounting registry must never itself allocate.
// Zero-initialized (BSS), so it is usable from constructors of static objects.
Entry table[kMaxTags];
// Find the slot for a tag, registering it on first use.
// Returns nullptr for a null tag or when the table is full (update dropped).
Entry *findOrRegister(const char *tag)
{
if (!tag)
return nullptr;
// Fast path: same literal, pointer compare only. This is all the hot
// per-packet add() ever executes once the tag is registered.
size_t used = 0;
for (; used < kMaxTags; used++) {
const char *cur = table[used].tag.load(std::memory_order_acquire);
if (!cur)
break; // slots fill in order - first empty slot ends the table
if (cur == tag)
return &table[used];
}
// Slow path: same text from a different literal (duplicated across
// translation units, so not pointer-identical).
for (size_t i = 0; i < used; i++) {
if (strcmp(table[i].tag.load(std::memory_order_relaxed), tag) == 0)
return &table[i];
}
// First use: claim a free slot. compare_exchange keeps a registration race
// from double-claiming; the loser re-checks what the winner wrote.
for (size_t i = used; i < kMaxTags; i++) {
const char *expected = nullptr;
if (table[i].tag.compare_exchange_strong(expected, tag, std::memory_order_acq_rel))
return &table[i];
if (expected == tag || strcmp(expected, tag) == 0)
return &table[i];
}
return nullptr; // table full - bump kMaxTags if this ever happens
}
} // namespace
void add(const char *tag, int32_t delta)
{
Entry *e = findOrRegister(tag);
if (e)
e->bytes.fetch_add(delta, std::memory_order_relaxed);
}
void set(const char *tag, uint32_t bytes)
{
Entry *e = findOrRegister(tag);
if (e)
e->bytes.store((int32_t)bytes, std::memory_order_relaxed);
}
size_t snapshot(Tag *out, size_t max)
{
size_t n = 0;
for (size_t i = 0; i < kMaxTags && n < max; i++) {
const char *tag = table[i].tag.load(std::memory_order_acquire);
if (!tag)
break;
out[n].tag = tag;
out[n].bytes = table[i].bytes.load(std::memory_order_relaxed);
n++;
}
return n;
}
void logBreakdown(const char *when)
{
Tag rows[kMaxTags];
size_t n = snapshot(rows, kMaxTags);
if (n == 0)
return;
// Worst case per row: 16-char tag + '=' + "-2147483648" + ' ' = 29 bytes.
char line[kMaxTags * 30 + 1];
size_t pos = 0;
int32_t total = 0;
for (size_t i = 0; i < n; i++) {
int written = snprintf(line + pos, sizeof(line) - pos, "%s%s=%ld", pos ? " " : "", rows[i].tag, (long)rows[i].bytes);
if (written < 0 || pos + written >= sizeof(line))
break;
pos += written;
total += rows[i].bytes;
}
LOG_INFO("MemAudit[%s]: %s total=%ld", when ? when : "?", line, (long)total);
}
} // namespace memaudit
#endif // MESHTASTIC_MEM_AUDIT
+77
View File
@@ -0,0 +1,77 @@
#pragma once
#include "configuration.h"
#include <stddef.h>
#include <stdint.h>
// MemAudit: tiny per-subsystem heap accounting registry.
//
// Subsystems that own a large long-lived allocation report it here under a short
// tag ("nodedb", "pkthist", ...). logBreakdown() then prints one line, e.g.
// MemAudit[boot]: tmm=2500 warm=4000 pkthist=5824 nodedb=13440 total=25764
// so heap regressions in field reports self-diagnose from the serial log instead
// of needing a hand-built breakdown for every release.
//
// Tags must be string LITERALS (or otherwise immortal strings): the registry
// stores the pointer, compares by pointer first and falls back to strcmp for
// the same text duplicated across translation units.
//
// Concurrency: counters are 32-bit std::atomic accessed with relaxed ordering -
// on ARM Cortex-M aligned 32-bit loads/stores are single instructions and the
// update sites are low-rate, so add() stays a few instructions with no locks
// (the one hot path is the per-packet pool add). Registration claims a table
// slot with a compare-exchange, so first-use racing is safe too. Counts are
// best-effort diagnostics, not exact bookkeeping.
//
// Compiled out (no-op inline stubs, so call sites need no #ifdefs) when
// MESHTASTIC_MEM_AUDIT is 0 - the default on STM32WL, the tightest flash target.
#ifndef MESHTASTIC_MEM_AUDIT
#ifdef ARCH_STM32WL
#define MESHTASTIC_MEM_AUDIT 0
#else
#define MESHTASTIC_MEM_AUDIT 1
#endif
#endif
namespace memaudit
{
// Fixed registry capacity - updates for tags beyond this are dropped (bump if needed).
constexpr size_t kMaxTags = 16;
// One snapshot row, as returned by snapshot().
struct Tag {
const char *tag; // the literal passed to add()/set()
int32_t bytes; // current byte count for that subsystem
};
#if MESHTASTIC_MEM_AUDIT
// Adjust a subsystem's byte count (registers the tag on first use). Safe from
// concurrent threads; this is the form to use on per-object alloc/free paths.
void add(const char *tag, int32_t delta);
// Set a subsystem's byte count outright - for one-shot pool/table allocations
// where the total is known (use 0 on free or allocation failure).
void set(const char *tag, uint32_t bytes);
// Copy up to max registered tags into out; returns the number written.
size_t snapshot(Tag *out, size_t max);
// Log the whole table as a single LOG_INFO line, labeled with `when` ("boot", ...).
void logBreakdown(const char *when);
#else
// No-op stubs so call sites compile away without #ifdefs.
inline void add(const char *, int32_t) {}
inline void set(const char *, uint32_t) {}
inline size_t snapshot(Tag *, size_t)
{
return 0;
}
inline void logBreakdown(const char *) {}
#endif // MESHTASTIC_MEM_AUDIT
} // namespace memaudit
+19 -2
View File
@@ -7,12 +7,15 @@
#include "PointerQueue.h" #include "PointerQueue.h"
#include "configuration.h" // For LOG_WARN, LOG_DEBUG, LOG_HEAP #include "configuration.h" // For LOG_WARN, LOG_DEBUG, LOG_HEAP
#include "memory/MemAudit.h"
template <class T> class Allocator template <class T> class Allocator
{ {
public: public:
Allocator() : deleter([this](T *p) { this->release(p); }) {} /// Optional memaudit tag: when set, live objects from this allocator are
/// reported under it (+/- sizeof(T) per alloc/release).
explicit Allocator(const char *auditTag = nullptr) : deleter([this](T *p) { this->release(p); }), auditTag(auditTag) {}
virtual ~Allocator() {} virtual ~Allocator() {}
/// Return a queable object which has been prefilled with zeros. Return nullptr if no buffer is available /// Return a queable object which has been prefilled with zeros. Return nullptr if no buffer is available
@@ -73,9 +76,17 @@ template <class T> class Allocator
// Alloc some storage // Alloc some storage
virtual T *alloc(TickType_t maxWait) = 0; virtual T *alloc(TickType_t maxWait) = 0;
// Report a live-object delta to memaudit (no-op when untagged)
void auditAdd(int32_t delta)
{
if (auditTag)
memaudit::add(auditTag, delta);
}
private: private:
// std::unique_ptr Deleter function; calls release(). // std::unique_ptr Deleter function; calls release().
const std::function<void(T *)> deleter; const std::function<void(T *)> deleter;
const char *auditTag; // memaudit tag, or nullptr for untracked pools
}; };
/** /**
@@ -84,6 +95,8 @@ template <class T> class Allocator
template <class T> class MemoryDynamic : public Allocator<T> template <class T> class MemoryDynamic : public Allocator<T>
{ {
public: public:
explicit MemoryDynamic(const char *auditTag = nullptr) : Allocator<T>(auditTag) {}
/// Return a buffer for use by others /// Return a buffer for use by others
virtual void release(T *p) override virtual void release(T *p) override
{ {
@@ -92,6 +105,7 @@ template <class T> class MemoryDynamic : public Allocator<T>
LOG_HEAP("Freeing 0x%x", p); LOG_HEAP("Freeing 0x%x", p);
this->auditAdd(-(int32_t)sizeof(T));
free(p); free(p);
} }
@@ -101,6 +115,7 @@ template <class T> class MemoryDynamic : public Allocator<T>
{ {
T *p = (T *)malloc(sizeof(T)); T *p = (T *)malloc(sizeof(T));
assert(p); assert(p);
this->auditAdd((int32_t)sizeof(T));
return p; return p;
} }
}; };
@@ -115,7 +130,7 @@ template <class T, int MaxSize> class MemoryPool : public Allocator<T>
bool used[MaxSize]; bool used[MaxSize];
public: public:
MemoryPool() : pool{}, used{} explicit MemoryPool(const char *auditTag = nullptr) : Allocator<T>(auditTag), pool{}, used{}
{ {
// Arrays are now zero-initialized by member initializer list // Arrays are now zero-initialized by member initializer list
// pool array: all elements are default-constructed (zero for POD types) // pool array: all elements are default-constructed (zero for POD types)
@@ -135,6 +150,7 @@ template <class T, int MaxSize> class MemoryPool : public Allocator<T>
if (index >= 0 && index < MaxSize) { if (index >= 0 && index < MaxSize) {
assert(used[index]); // Should be marked as used assert(used[index]); // Should be marked as used
used[index] = false; used[index] = false;
this->auditAdd(-(int32_t)sizeof(T));
LOG_HEAP("Released static pool item %d at 0x%x", index, p); LOG_HEAP("Released static pool item %d at 0x%x", index, p);
} else { } else {
LOG_WARN("Pointer 0x%x not from our pool!", p); LOG_WARN("Pointer 0x%x not from our pool!", p);
@@ -149,6 +165,7 @@ template <class T, int MaxSize> class MemoryPool : public Allocator<T>
for (int i = 0; i < MaxSize; i++) { for (int i = 0; i < MaxSize; i++) {
if (!used[i]) { if (!used[i]) {
used[i] = true; used[i] = true;
this->auditAdd((int32_t)sizeof(T));
LOG_HEAP("Allocated static pool item %d at 0x%x", i, &pool[i]); LOG_HEAP("Allocated static pool item %d at 0x%x", i, &pool[i]);
return &pool[i]; return &pool[i];
} }
+21
View File
@@ -22,6 +22,7 @@
#include "TypeConversions.h" #include "TypeConversions.h"
#include "error.h" #include "error.h"
#include "main.h" #include "main.h"
#include "memory/MemAudit.h"
#include "mesh-pb-constants.h" #include "mesh-pb-constants.h"
#include "mesh/generated/meshtastic/deviceonly_legacy.pb.h" #include "mesh/generated/meshtastic/deviceonly_legacy.pb.h"
#include "meshUtils.h" #include "meshUtils.h"
@@ -1796,6 +1797,25 @@ bool NodeDB::enforceSatelliteCaps()
#endif #endif
(void)trim; // all four maps may be compiled out (void)trim; // all four maps may be compiled out
// Approximate satellite heap usage: each std::map entry is one rb-tree node,
// value_type plus ~44 B of node overhead (parent/left/right pointers, color,
// allocator rounding on 32-bit targets - an estimate, not exact bookkeeping).
size_t satBytes = 0;
#if !MESHTASTIC_EXCLUDE_POSITIONDB
satBytes += nodePositions.size() * (sizeof(decltype(nodePositions)::value_type) + 44);
#endif
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
satBytes += nodeTelemetry.size() * (sizeof(decltype(nodeTelemetry)::value_type) + 44);
#endif
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
satBytes += nodeEnvironment.size() * (sizeof(decltype(nodeEnvironment)::value_type) + 44);
#endif
#if !MESHTASTIC_EXCLUDE_STATUSDB
satBytes += nodeStatus.size() * (sizeof(decltype(nodeStatus)::value_type) + 44);
#endif
memaudit::set("satmaps", satBytes);
return trimmedAny; return trimmedAny;
} }
@@ -2080,6 +2100,7 @@ void NodeDB::nodeDBSelfCare()
// Normalise the backing store to the hot cap so getOrCreateMeshNode always // Normalise the backing store to the hot cap so getOrCreateMeshNode always
// has spare slots to append into (it indexes meshNodes->at(numMeshNodes++)). // has spare slots to append into (it indexes meshNodes->at(numMeshNodes++)).
meshNodes->resize(MAX_NUM_NODES); meshNodes->resize(MAX_NUM_NODES);
memaudit::set("nodedb", MAX_NUM_NODES * sizeof(meshtastic_NodeInfoLite));
const bool satsTrimmed = enforceSatelliteCaps(); const bool satsTrimmed = enforceSatelliteCaps();
+3
View File
@@ -1,5 +1,6 @@
#include "PacketHistory.h" #include "PacketHistory.h"
#include "configuration.h" #include "configuration.h"
#include "memory/MemAudit.h"
#include "mesh-pb-constants.h" #include "mesh-pb-constants.h"
#include "meshUtils.h" #include "meshUtils.h"
@@ -44,6 +45,7 @@ PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0) // Initia
// Initialize the recent packets array to zero // Initialize the recent packets array to zero
memset(recentPackets.get(), 0, sizeof(PacketRecord) * recentPacketsCapacity); memset(recentPackets.get(), 0, sizeof(PacketRecord) * recentPacketsCapacity);
memaudit::set("pkthist", sizeof(PacketRecord) * recentPacketsCapacity);
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH #if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
// Allocate hash index with load factor <= 0.5 for short probe chains // Allocate hash index with load factor <= 0.5 for short probe chains
@@ -57,6 +59,7 @@ PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0) // Initia
return; return;
} }
memset(hashIndex.get(), 0xFF, sizeof(uint16_t) * hashCapacity); // Fill with HASH_EMPTY (0xFFFF) memset(hashIndex.get(), 0xFF, sizeof(uint16_t) * hashCapacity); // Fill with HASH_EMPTY (0xFFFF)
memaudit::set("pkthist", sizeof(PacketRecord) * recentPacketsCapacity + sizeof(uint16_t) * hashCapacity);
#endif #endif
} }
+6 -3
View File
@@ -41,7 +41,8 @@
(MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \
2) // max number of packets which can be in flight (either queued from reception or queued for sending) 2) // max number of packets which can be in flight (either queued from reception or queued for sending)
static MemoryDynamic<meshtastic_MeshPacket> dynamicPool; // Live in-flight packet bytes are tracked under "pktpool(live)" in the MemAudit breakdown
static MemoryDynamic<meshtastic_MeshPacket> dynamicPool("pktpool(live)");
Allocator<meshtastic_MeshPacket> &packetPool = dynamicPool; Allocator<meshtastic_MeshPacket> &packetPool = dynamicPool;
#elif defined(ARCH_STM32WL) || defined(BOARD_HAS_PSRAM) #elif defined(ARCH_STM32WL) || defined(BOARD_HAS_PSRAM)
// On STM32 and boards with PSRAM, there isn't enough heap left over for the rest of the firmware if we allocate this statically. // On STM32 and boards with PSRAM, there isn't enough heap left over for the rest of the firmware if we allocate this statically.
@@ -50,7 +51,8 @@ Allocator<meshtastic_MeshPacket> &packetPool = dynamicPool;
(MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \
2) // max number of packets which can be in flight (either queued from reception or queued for sending) 2) // max number of packets which can be in flight (either queued from reception or queued for sending)
static MemoryDynamic<meshtastic_MeshPacket> dynamicPool; // Live in-flight packet bytes are tracked under "pktpool(live)" in the MemAudit breakdown
static MemoryDynamic<meshtastic_MeshPacket> dynamicPool("pktpool(live)");
Allocator<meshtastic_MeshPacket> &packetPool = dynamicPool; Allocator<meshtastic_MeshPacket> &packetPool = dynamicPool;
#else #else
// Embedded targets use static memory pools with compile-time constants // Embedded targets use static memory pools with compile-time constants
@@ -58,7 +60,8 @@ Allocator<meshtastic_MeshPacket> &packetPool = dynamicPool;
(MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \
2) // max number of packets which can be in flight (either queued from reception or queued for sending) 2) // max number of packets which can be in flight (either queued from reception or queued for sending)
static MemoryPool<meshtastic_MeshPacket, MAX_PACKETS_STATIC> staticPool; // Static pool RAM is BSS, not heap; "pktpool(live)" still shows in-flight packet bytes
static MemoryPool<meshtastic_MeshPacket, MAX_PACKETS_STATIC> staticPool("pktpool(live)");
Allocator<meshtastic_MeshPacket> &packetPool = staticPool; Allocator<meshtastic_MeshPacket> &packetPool = staticPool;
#endif #endif
+3
View File
@@ -6,6 +6,7 @@
#include "SPILock.h" #include "SPILock.h"
#include "SafeFile.h" #include "SafeFile.h"
#include "configuration.h" #include "configuration.h"
#include "memory/MemAudit.h"
#include "power/PowerHAL.h" #include "power/PowerHAL.h"
#include <ErriezCRC32.h> #include <ErriezCRC32.h>
#include <vector> #include <vector>
@@ -62,6 +63,7 @@ WarmNodeStore::WarmNodeStore()
#else #else
entries = static_cast<WarmNodeEntry *>(calloc(WARM_NODE_COUNT, sizeof(WarmNodeEntry))); entries = static_cast<WarmNodeEntry *>(calloc(WARM_NODE_COUNT, sizeof(WarmNodeEntry)));
#endif #endif
memaudit::set("warm", entries ? WARM_NODE_COUNT * sizeof(WarmNodeEntry) : 0);
#if defined(NRF52840_XXAA) #if defined(NRF52840_XXAA)
memset(pageOf, kNoPage, sizeof(pageOf)); memset(pageOf, kNoPage, sizeof(pageOf));
#endif #endif
@@ -71,6 +73,7 @@ WarmNodeStore::~WarmNodeStore()
{ {
free(entries); // always malloc-family (calloc / ps_calloc) free(entries); // always malloc-family (calloc / ps_calloc)
entries = nullptr; entries = nullptr;
memaudit::set("warm", 0);
} }
WarmNodeEntry *WarmNodeStore::find(NodeNum num) const WarmNodeEntry *WarmNodeStore::find(NodeNum num) const
+5
View File
@@ -12,6 +12,7 @@
#include "airtime.h" #include "airtime.h"
#include "concurrency/LockGuard.h" #include "concurrency/LockGuard.h"
#include "configuration.h" #include "configuration.h"
#include "memory/MemAudit.h"
#include "mesh-pb-constants.h" #include "mesh-pb-constants.h"
#include "meshUtils.h" #include "meshUtils.h"
#include <Arduino.h> #include <Arduino.h>
@@ -163,6 +164,7 @@ TrafficManagementModule::TrafficManagementModule() : MeshModule("TrafficManageme
cache = new UnifiedCacheEntry[allocSize](); cache = new UnifiedCacheEntry[allocSize]();
#endif #endif
memaudit::set("tmm", cache ? allocSize * sizeof(UnifiedCacheEntry) : 0);
#endif // TRAFFIC_MANAGEMENT_CACHE_SIZE > 0 #endif // TRAFFIC_MANAGEMENT_CACHE_SIZE > 0
#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM) #if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM)
@@ -177,6 +179,7 @@ TrafficManagementModule::TrafficManagementModule() : MeshModule("TrafficManageme
} else { } else {
TM_LOG_WARN("NodeInfo PSRAM payload allocation failed; direct responses will fall back to NodeDB"); TM_LOG_WARN("NodeInfo PSRAM payload allocation failed; direct responses will fall back to NodeDB");
} }
memaudit::set("tmm_ni", nodeInfoPayload ? nodeInfoTargetEntries() * sizeof(NodeInfoPayloadEntry) : 0);
#else #else
TM_LOG_DEBUG("NodeInfo PSRAM cache not available on this target"); TM_LOG_DEBUG("NodeInfo PSRAM cache not available on this target");
#endif #endif
@@ -198,6 +201,7 @@ TrafficManagementModule::~TrafficManagementModule()
delete[] cache; delete[] cache;
cache = nullptr; cache = nullptr;
} }
memaudit::set("tmm", 0);
#endif #endif
if (nodeInfoPayload) { if (nodeInfoPayload) {
@@ -207,6 +211,7 @@ TrafficManagementModule::~TrafficManagementModule()
delete[] nodeInfoPayload; delete[] nodeInfoPayload;
nodeInfoPayload = nullptr; nodeInfoPayload = nullptr;
} }
memaudit::set("tmm_ni", 0);
} }
// ============================================================================= // =============================================================================
+1 -1
View File
@@ -1 +1 @@
27 28
+175
View File
@@ -0,0 +1,175 @@
// Unit tests for the per-subsystem heap accounting registry - src/memory/MemAudit.cpp.
// Covers add/set arithmetic, snapshot, tag reuse (pointer and strcmp fallback),
// unknown/null tags and table-full behavior. The registry is a process-global
// with no reset, so tests use distinct tags and the fill-the-table test runs last.
#include "TestUtil.h"
#include "memory/MemAudit.h" // BEFORE TestUtil.h - provides Arduino.h via configuration.h
#include <cstdio>
#include <cstring>
#include <unity.h>
#if defined(ARCH_PORTDUINO)
#define MA_TEST_ENTRY extern "C"
#else
#define MA_TEST_ENTRY
#endif
#if MESHTASTIC_MEM_AUDIT
namespace
{
// Look a tag up by text in a fresh snapshot; returns its bytes, sets *found.
int32_t bytesFor(const char *tag, bool *found = nullptr)
{
memaudit::Tag rows[memaudit::kMaxTags];
size_t n = memaudit::snapshot(rows, memaudit::kMaxTags);
for (size_t i = 0; i < n; i++) {
if (strcmp(rows[i].tag, tag) == 0) {
if (found)
*found = true;
return rows[i].bytes;
}
}
if (found)
*found = false;
return 0;
}
size_t registeredCount()
{
memaudit::Tag rows[memaudit::kMaxTags];
return memaudit::snapshot(rows, memaudit::kMaxTags);
}
} // namespace
void setUp(void) {}
void tearDown(void) {}
void test_ma_set_registersAndOverwrites()
{
bool found = false;
memaudit::set("t_set", 1234);
TEST_ASSERT_EQUAL_INT32(1234, bytesFor("t_set", &found));
TEST_ASSERT_TRUE(found);
memaudit::set("t_set", 42); // set overwrites, it never accumulates
TEST_ASSERT_EQUAL_INT32(42, bytesFor("t_set"));
memaudit::set("t_set", 0); // zero keeps the tag registered
TEST_ASSERT_EQUAL_INT32(0, bytesFor("t_set", &found));
TEST_ASSERT_TRUE(found);
}
void test_ma_add_accumulatesSignedDeltas()
{
memaudit::add("t_add", 100);
memaudit::add("t_add", 50);
TEST_ASSERT_EQUAL_INT32(150, bytesFor("t_add"));
memaudit::add("t_add", -60);
TEST_ASSERT_EQUAL_INT32(90, bytesFor("t_add"));
memaudit::add("t_neg", -5); // unbalanced frees just go negative, no clamping
TEST_ASSERT_EQUAL_INT32(-5, bytesFor("t_neg"));
}
void test_ma_sameTag_reusesSlot()
{
const size_t before = registeredCount();
memaudit::add("t_add", 10); // same literal as the previous test
TEST_ASSERT_EQUAL_INT32(100, bytesFor("t_add"));
TEST_ASSERT_EQUAL(before, registeredCount());
}
void test_ma_duplicateText_sharesSlot()
{
// Same text at different addresses (literals duplicated across translation
// units are not pointer-identical) - the strcmp fallback must merge them.
static char a[] = "t_dup";
static char b[] = "t_dup";
TEST_ASSERT_TRUE(a != b);
const size_t before = registeredCount();
memaudit::add(a, 30);
memaudit::add(b, 12);
TEST_ASSERT_EQUAL_INT32(42, bytesFor("t_dup"));
TEST_ASSERT_EQUAL(before + 1, registeredCount());
}
void test_ma_unknownAndNullTags()
{
bool found = true;
bytesFor("t_never_registered", &found); // lookups of unknown tags find nothing
TEST_ASSERT_FALSE(found);
const size_t before = registeredCount();
memaudit::add(nullptr, 99); // null tags are dropped, not crashed on
memaudit::set(nullptr, 99);
TEST_ASSERT_EQUAL(before, registeredCount());
}
void test_ma_snapshot_respectsMax()
{
TEST_ASSERT_GREATER_OR_EQUAL(2, registeredCount());
memaudit::Tag one;
TEST_ASSERT_EQUAL(1, memaudit::snapshot(&one, 1));
TEST_ASSERT_NOT_NULL(one.tag);
TEST_ASSERT_EQUAL(0, memaudit::snapshot(&one, 0));
}
// Must run last: fills every remaining slot.
void test_ma_tableFull_dropsNewTagsKeepsExisting()
{
// Storage must outlive the registry (it keeps the pointers), hence static.
static char names[memaudit::kMaxTags][8];
for (size_t i = 0; registeredCount() < memaudit::kMaxTags; i++) {
TEST_ASSERT_LESS_THAN(memaudit::kMaxTags, i);
snprintf(names[i], sizeof(names[i]), "fill%u", (unsigned)i);
memaudit::set(names[i], i + 1);
}
TEST_ASSERT_EQUAL(memaudit::kMaxTags, registeredCount());
bool found = true;
memaudit::add("t_overflow", 77); // no slot left: update dropped
memaudit::set("t_overflow", 77);
bytesFor("t_overflow", &found);
TEST_ASSERT_FALSE(found);
TEST_ASSERT_EQUAL(memaudit::kMaxTags, registeredCount());
memaudit::set("t_set", 7); // existing tags keep working at capacity
TEST_ASSERT_EQUAL_INT32(7, bytesFor("t_set"));
memaudit::logBreakdown("test"); // smoke: full table renders one log line
}
MA_TEST_ENTRY void setup()
{
initializeTestEnvironment();
UNITY_BEGIN();
RUN_TEST(test_ma_set_registersAndOverwrites);
RUN_TEST(test_ma_add_accumulatesSignedDeltas);
RUN_TEST(test_ma_sameTag_reusesSlot);
RUN_TEST(test_ma_duplicateText_sharesSlot);
RUN_TEST(test_ma_unknownAndNullTags);
RUN_TEST(test_ma_snapshot_respectsMax);
RUN_TEST(test_ma_tableFull_dropsNewTagsKeepsExisting);
exit(UNITY_END());
}
#else
void setUp(void) {}
void tearDown(void) {}
MA_TEST_ENTRY void setup()
{
initializeTestEnvironment();
UNITY_BEGIN();
exit(UNITY_END());
}
#endif // MESHTASTIC_MEM_AUDIT
MA_TEST_ENTRY void loop() {}
+1 -1
View File
@@ -340,7 +340,7 @@ build_src_filter =
+<PowerFSM.cpp> +<Power.cpp> +<airtime.cpp> +<sleep.cpp> +<PowerFSM.cpp> +<Power.cpp> +<airtime.cpp> +<sleep.cpp>
+<RedirectablePrint.cpp> +<SerialConsole.cpp> +<Observer.cpp> +<RedirectablePrint.cpp> +<SerialConsole.cpp> +<Observer.cpp>
+<FSCommon.cpp> +<SafeFile.cpp> +<MessageStore.cpp> +<meshUtils.cpp> +<FSCommon.cpp> +<SafeFile.cpp> +<MessageStore.cpp> +<meshUtils.cpp>
+<memGet.cpp> +<GpioLogic.cpp> +<PowerMon.cpp> +<SPILock.cpp> +<memGet.cpp> +<memory/> +<GpioLogic.cpp> +<PowerMon.cpp> +<SPILock.cpp>
+<xmodem.cpp> +<DisplayFormatters.cpp> +<xmodem.cpp> +<DisplayFormatters.cpp>
+<detect/ScanI2C.cpp> +<detect/ScanI2CTwoWire.cpp> +<detect/ScanI2CConsumer.cpp> +<detect/ScanI2C.cpp> +<detect/ScanI2CTwoWire.cpp> +<detect/ScanI2CConsumer.cpp>
+<power/PowerHAL.cpp> +<power/PowerHAL.cpp>