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:
+19
-2
@@ -7,12 +7,15 @@
|
||||
|
||||
#include "PointerQueue.h"
|
||||
#include "configuration.h" // For LOG_WARN, LOG_DEBUG, LOG_HEAP
|
||||
#include "memory/MemAudit.h"
|
||||
|
||||
template <class T> class Allocator
|
||||
{
|
||||
|
||||
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() {}
|
||||
|
||||
/// 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
|
||||
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:
|
||||
// std::unique_ptr Deleter function; calls release().
|
||||
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>
|
||||
{
|
||||
public:
|
||||
explicit MemoryDynamic(const char *auditTag = nullptr) : Allocator<T>(auditTag) {}
|
||||
|
||||
/// Return a buffer for use by others
|
||||
virtual void release(T *p) override
|
||||
{
|
||||
@@ -92,6 +105,7 @@ template <class T> class MemoryDynamic : public Allocator<T>
|
||||
|
||||
LOG_HEAP("Freeing 0x%x", p);
|
||||
|
||||
this->auditAdd(-(int32_t)sizeof(T));
|
||||
free(p);
|
||||
}
|
||||
|
||||
@@ -101,6 +115,7 @@ template <class T> class MemoryDynamic : public Allocator<T>
|
||||
{
|
||||
T *p = (T *)malloc(sizeof(T));
|
||||
assert(p);
|
||||
this->auditAdd((int32_t)sizeof(T));
|
||||
return p;
|
||||
}
|
||||
};
|
||||
@@ -115,7 +130,7 @@ template <class T, int MaxSize> class MemoryPool : public Allocator<T>
|
||||
bool used[MaxSize];
|
||||
|
||||
public:
|
||||
MemoryPool() : pool{}, used{}
|
||||
explicit MemoryPool(const char *auditTag = nullptr) : Allocator<T>(auditTag), pool{}, used{}
|
||||
{
|
||||
// Arrays are now zero-initialized by member initializer list
|
||||
// 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) {
|
||||
assert(used[index]); // Should be marked as used
|
||||
used[index] = false;
|
||||
this->auditAdd(-(int32_t)sizeof(T));
|
||||
LOG_HEAP("Released static pool item %d at 0x%x", index, p);
|
||||
} else {
|
||||
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++) {
|
||||
if (!used[i]) {
|
||||
used[i] = true;
|
||||
this->auditAdd((int32_t)sizeof(T));
|
||||
LOG_HEAP("Allocated static pool item %d at 0x%x", i, &pool[i]);
|
||||
return &pool[i];
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "TypeConversions.h"
|
||||
#include "error.h"
|
||||
#include "main.h"
|
||||
#include "memory/MemAudit.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include "mesh/generated/meshtastic/deviceonly_legacy.pb.h"
|
||||
#include "meshUtils.h"
|
||||
@@ -1796,6 +1797,25 @@ bool NodeDB::enforceSatelliteCaps()
|
||||
#endif
|
||||
|
||||
(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;
|
||||
}
|
||||
|
||||
@@ -2080,6 +2100,7 @@ void NodeDB::nodeDBSelfCare()
|
||||
// Normalise the backing store to the hot cap so getOrCreateMeshNode always
|
||||
// has spare slots to append into (it indexes meshNodes->at(numMeshNodes++)).
|
||||
meshNodes->resize(MAX_NUM_NODES);
|
||||
memaudit::set("nodedb", MAX_NUM_NODES * sizeof(meshtastic_NodeInfoLite));
|
||||
|
||||
const bool satsTrimmed = enforceSatelliteCaps();
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "PacketHistory.h"
|
||||
#include "configuration.h"
|
||||
#include "memory/MemAudit.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include "meshUtils.h"
|
||||
|
||||
@@ -44,6 +45,7 @@ PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0) // Initia
|
||||
|
||||
// Initialize the recent packets array to zero
|
||||
memset(recentPackets.get(), 0, sizeof(PacketRecord) * recentPacketsCapacity);
|
||||
memaudit::set("pkthist", sizeof(PacketRecord) * recentPacketsCapacity);
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
||||
// 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;
|
||||
}
|
||||
memset(hashIndex.get(), 0xFF, sizeof(uint16_t) * hashCapacity); // Fill with HASH_EMPTY (0xFFFF)
|
||||
memaudit::set("pkthist", sizeof(PacketRecord) * recentPacketsCapacity + sizeof(uint16_t) * hashCapacity);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
+6
-3
@@ -41,7 +41,8 @@
|
||||
(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)
|
||||
|
||||
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;
|
||||
#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.
|
||||
@@ -50,7 +51,8 @@ Allocator<meshtastic_MeshPacket> &packetPool = dynamicPool;
|
||||
(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)
|
||||
|
||||
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;
|
||||
#else
|
||||
// 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 + \
|
||||
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;
|
||||
#endif
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "SPILock.h"
|
||||
#include "SafeFile.h"
|
||||
#include "configuration.h"
|
||||
#include "memory/MemAudit.h"
|
||||
#include "power/PowerHAL.h"
|
||||
#include <ErriezCRC32.h>
|
||||
#include <vector>
|
||||
@@ -62,6 +63,7 @@ WarmNodeStore::WarmNodeStore()
|
||||
#else
|
||||
entries = static_cast<WarmNodeEntry *>(calloc(WARM_NODE_COUNT, sizeof(WarmNodeEntry)));
|
||||
#endif
|
||||
memaudit::set("warm", entries ? WARM_NODE_COUNT * sizeof(WarmNodeEntry) : 0);
|
||||
#if defined(NRF52840_XXAA)
|
||||
memset(pageOf, kNoPage, sizeof(pageOf));
|
||||
#endif
|
||||
@@ -71,6 +73,7 @@ WarmNodeStore::~WarmNodeStore()
|
||||
{
|
||||
free(entries); // always malloc-family (calloc / ps_calloc)
|
||||
entries = nullptr;
|
||||
memaudit::set("warm", 0);
|
||||
}
|
||||
|
||||
WarmNodeEntry *WarmNodeStore::find(NodeNum num) const
|
||||
|
||||
Reference in New Issue
Block a user