diff --git a/src/MessageStore.cpp b/src/MessageStore.cpp index 3360857b9..6332c0e82 100644 --- a/src/MessageStore.cpp +++ b/src/MessageStore.cpp @@ -6,6 +6,7 @@ #include "SPILock.h" #include "SafeFile.h" #include "gps/RTC.h" +#include "memory/MemAudit.h" #include // memcpy #ifndef MESSAGE_TEXT_POOL_SIZE @@ -28,8 +29,10 @@ static inline void resetMessagePool() g_messagePool = static_cast(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); diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index da3cccf5c..c24633169 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -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; } diff --git a/src/main.cpp b/src/main.cpp index 8f2a737e4..759b8e0bb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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); } diff --git a/src/memGet.cpp b/src/memGet.cpp index 0e7267dfc..cbba0f4e4 100644 --- a/src/memGet.cpp +++ b/src/memGet.cpp @@ -9,6 +9,7 @@ */ #include "memGet.h" #include "configuration.h" +#include "memory/MemAudit.h" #if defined(MESHTASTIC_DYNAMIC_SBRK_HEAP) #include @@ -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 } \ No newline at end of file diff --git a/src/memory/MemAudit.cpp b/src/memory/MemAudit.cpp new file mode 100644 index 000000000..c5a65c236 --- /dev/null +++ b/src/memory/MemAudit.cpp @@ -0,0 +1,116 @@ +#include "MemAudit.h" + +#if MESHTASTIC_MEM_AUDIT + +#include "DebugConfiguration.h" +#include +#include +#include + +namespace memaudit +{ + +namespace +{ + +struct Entry { + std::atomic tag; // registered literal; nullptr = free slot + std::atomic 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 diff --git a/src/memory/MemAudit.h b/src/memory/MemAudit.h new file mode 100644 index 000000000..52f38a466 --- /dev/null +++ b/src/memory/MemAudit.h @@ -0,0 +1,77 @@ +#pragma once + +#include "configuration.h" +#include +#include + +// 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 diff --git a/src/mesh/MemoryPool.h b/src/mesh/MemoryPool.h index eb5ac5109..d10c8cf96 100644 --- a/src/mesh/MemoryPool.h +++ b/src/mesh/MemoryPool.h @@ -7,12 +7,15 @@ #include "PointerQueue.h" #include "configuration.h" // For LOG_WARN, LOG_DEBUG, LOG_HEAP +#include "memory/MemAudit.h" template 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 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 deleter; + const char *auditTag; // memaudit tag, or nullptr for untracked pools }; /** @@ -84,6 +95,8 @@ template class Allocator template class MemoryDynamic : public Allocator { public: + explicit MemoryDynamic(const char *auditTag = nullptr) : Allocator(auditTag) {} + /// Return a buffer for use by others virtual void release(T *p) override { @@ -92,6 +105,7 @@ template class MemoryDynamic : public Allocator LOG_HEAP("Freeing 0x%x", p); + this->auditAdd(-(int32_t)sizeof(T)); free(p); } @@ -101,6 +115,7 @@ template class MemoryDynamic : public Allocator { T *p = (T *)malloc(sizeof(T)); assert(p); + this->auditAdd((int32_t)sizeof(T)); return p; } }; @@ -115,7 +130,7 @@ template class MemoryPool : public Allocator bool used[MaxSize]; public: - MemoryPool() : pool{}, used{} + explicit MemoryPool(const char *auditTag = nullptr) : Allocator(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 MemoryPool : public Allocator 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 MemoryPool : public Allocator 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]; } diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index f8e74b722..53e97351b 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -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(); diff --git a/src/mesh/PacketHistory.cpp b/src/mesh/PacketHistory.cpp index 380f021bb..9627d2425 100644 --- a/src/mesh/PacketHistory.cpp +++ b/src/mesh/PacketHistory.cpp @@ -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 } diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index e2fbcf419..c4142c7a8 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -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 dynamicPool; +// Live in-flight packet bytes are tracked under "pktpool(live)" in the MemAudit breakdown +static MemoryDynamic dynamicPool("pktpool(live)"); Allocator &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 &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 dynamicPool; +// Live in-flight packet bytes are tracked under "pktpool(live)" in the MemAudit breakdown +static MemoryDynamic dynamicPool("pktpool(live)"); Allocator &packetPool = dynamicPool; #else // Embedded targets use static memory pools with compile-time constants @@ -58,7 +60,8 @@ Allocator &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 staticPool; +// Static pool RAM is BSS, not heap; "pktpool(live)" still shows in-flight packet bytes +static MemoryPool staticPool("pktpool(live)"); Allocator &packetPool = staticPool; #endif diff --git a/src/mesh/WarmNodeStore.cpp b/src/mesh/WarmNodeStore.cpp index 10e4dda48..550549ad5 100644 --- a/src/mesh/WarmNodeStore.cpp +++ b/src/mesh/WarmNodeStore.cpp @@ -6,6 +6,7 @@ #include "SPILock.h" #include "SafeFile.h" #include "configuration.h" +#include "memory/MemAudit.h" #include "power/PowerHAL.h" #include #include @@ -62,6 +63,7 @@ WarmNodeStore::WarmNodeStore() #else entries = static_cast(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 diff --git a/src/modules/TrafficManagementModule.cpp b/src/modules/TrafficManagementModule.cpp index c94ede951..89ad128cc 100644 --- a/src/modules/TrafficManagementModule.cpp +++ b/src/modules/TrafficManagementModule.cpp @@ -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 @@ -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); } // ============================================================================= diff --git a/test/native-suite-count b/test/native-suite-count index f64f5d8d8..9902f1784 100644 --- a/test/native-suite-count +++ b/test/native-suite-count @@ -1 +1 @@ -27 +28 diff --git a/test/test_mem_audit/test_main.cpp b/test/test_mem_audit/test_main.cpp new file mode 100644 index 000000000..112ac6563 --- /dev/null +++ b/test/test_mem_audit/test_main.cpp @@ -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 +#include +#include + +#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() {} diff --git a/variants/native/portduino/platformio.ini b/variants/native/portduino/platformio.ini index d3cdeee77..e71386b67 100644 --- a/variants/native/portduino/platformio.ini +++ b/variants/native/portduino/platformio.ini @@ -340,7 +340,7 @@ build_src_filter = + + + + + + + + + + + - + + + + + + + + + + + + + + + +