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:
@@ -6,6 +6,7 @@
|
||||
#include "SPILock.h"
|
||||
#include "SafeFile.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "memory/MemAudit.h"
|
||||
#include <cstring> // memcpy
|
||||
|
||||
#ifndef MESSAGE_TEXT_POOL_SIZE
|
||||
@@ -28,8 +29,10 @@ static inline void resetMessagePool()
|
||||
g_messagePool = static_cast<char *>(malloc(MESSAGE_TEXT_POOL_SIZE));
|
||||
if (!g_messagePool) {
|
||||
LOG_ERROR("MessageStore: Failed to allocate %d bytes for message pool", MESSAGE_TEXT_POOL_SIZE);
|
||||
memaudit::set("msgstore", 0);
|
||||
return;
|
||||
}
|
||||
memaudit::set("msgstore", MESSAGE_TEXT_POOL_SIZE);
|
||||
}
|
||||
g_poolWritePos = 0;
|
||||
memset(g_messagePool, 0, MESSAGE_TEXT_POOL_SIZE);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
#include "memory/MemAudit.h"
|
||||
#if USE_TFTDISPLAY
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
@@ -1228,6 +1229,7 @@ TFTDisplay::~TFTDisplay()
|
||||
free(repaintChunkBuffer);
|
||||
repaintChunkBuffer = nullptr;
|
||||
}
|
||||
memaudit::set("display", 0);
|
||||
}
|
||||
|
||||
// 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");
|
||||
return false;
|
||||
}
|
||||
memaudit::add("display", sizeof(uint16_t) * displayWidth);
|
||||
}
|
||||
if (this->repaintChunkBuffer == NULL) {
|
||||
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");
|
||||
return false;
|
||||
}
|
||||
memaudit::add("display", sizeof(uint16_t) * displayWidth * kFullRepaintChunkRows);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#include "detect/einkScan.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "main.h"
|
||||
#include "memory/MemAudit.h"
|
||||
#include "mesh/generated/meshtastic/config.pb.h"
|
||||
#include "meshUtils.h"
|
||||
#include "modules/Modules.h"
|
||||
@@ -1151,6 +1152,9 @@ void setup()
|
||||
LOG_DEBUG("Free PSRAM : %7d bytes", ESP.getFreePsram());
|
||||
#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
|
||||
nodeDB->notifyObservers(true);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
#include "memGet.h"
|
||||
#include "configuration.h"
|
||||
#include "memory/MemAudit.h"
|
||||
|
||||
#if defined(MESHTASTIC_DYNAMIC_SBRK_HEAP)
|
||||
#include <malloc.h>
|
||||
@@ -118,4 +119,5 @@ void displayPercentHeapFree()
|
||||
}
|
||||
int percent = (int)((freeHeap * 100) / 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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "airtime.h"
|
||||
#include "concurrency/LockGuard.h"
|
||||
#include "configuration.h"
|
||||
#include "memory/MemAudit.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include "meshUtils.h"
|
||||
#include <Arduino.h>
|
||||
@@ -163,6 +164,7 @@ TrafficManagementModule::TrafficManagementModule() : MeshModule("TrafficManageme
|
||||
cache = new UnifiedCacheEntry[allocSize]();
|
||||
#endif
|
||||
|
||||
memaudit::set("tmm", cache ? allocSize * sizeof(UnifiedCacheEntry) : 0);
|
||||
#endif // TRAFFIC_MANAGEMENT_CACHE_SIZE > 0
|
||||
|
||||
#if defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM)
|
||||
@@ -177,6 +179,7 @@ TrafficManagementModule::TrafficManagementModule() : MeshModule("TrafficManageme
|
||||
} else {
|
||||
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
|
||||
TM_LOG_DEBUG("NodeInfo PSRAM cache not available on this target");
|
||||
#endif
|
||||
@@ -198,6 +201,7 @@ TrafficManagementModule::~TrafficManagementModule()
|
||||
delete[] cache;
|
||||
cache = nullptr;
|
||||
}
|
||||
memaudit::set("tmm", 0);
|
||||
#endif
|
||||
|
||||
if (nodeInfoPayload) {
|
||||
@@ -207,6 +211,7 @@ TrafficManagementModule::~TrafficManagementModule()
|
||||
delete[] nodeInfoPayload;
|
||||
nodeInfoPayload = nullptr;
|
||||
}
|
||||
memaudit::set("tmm_ni", 0);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -1 +1 @@
|
||||
27
|
||||
28
|
||||
|
||||
@@ -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() {}
|
||||
@@ -340,7 +340,7 @@ build_src_filter =
|
||||
+<PowerFSM.cpp> +<Power.cpp> +<airtime.cpp> +<sleep.cpp>
|
||||
+<RedirectablePrint.cpp> +<SerialConsole.cpp> +<Observer.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>
|
||||
+<detect/ScanI2C.cpp> +<detect/ScanI2CTwoWire.cpp> +<detect/ScanI2CConsumer.cpp>
|
||||
+<power/PowerHAL.cpp>
|
||||
|
||||
Reference in New Issue
Block a user