* 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.
176 lines
5.1 KiB
C++
176 lines
5.1 KiB
C++
// 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() {}
|