mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-17 09:01:30 +10:00
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:
@@ -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
|
||||
Reference in New Issue
Block a user