-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Add MemAudit: per-subsystem heap accounting in the boot log #10900
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: meshtastic/firmware
Length of output: 1578
🏁 Script executed:
Repository: meshtastic/firmware
Length of output: 389
Guard
release()against already-free slots before adjusting audit state.alloc()already skipsauditAdd()when the pool is full, butrelease()only checks range/null and then decrements after anassert(used[index]). In release builds, an already-free slot would still change the counter and can drift over time.🤖 Prompt for AI Agents