Describe the bug
TFTView_320x240::updateStatistics() keeps one statistics entry per packet sender in a function-static std::list. Every previously unseen sender creates a small allocation, and the list is only cleared by the manual statistics-reset path (p.from == 0). It is not pruned when purgeNode() evicts a UI node at MAX_NUM_NODES_VIEW.
On ESP32-S3 targets with PSRAM, these small list-node allocations remain in internal heap. With a large or MQTT-fed node database near the 250-node view limit, sender churn therefore causes internal heap to decline slowly even though the visible node set is bounded and PSRAM remains stable. The existing // TODO: stop if memory limit is reached marks the missing bound.
packetReceived() also calls updateStatistics(p) for every packet, independently of whether the statistics panel is visible.
There is a second bookkeeping problem in the same function: the rendering loop uses for (auto it2 : stats), so assignments such as it2.row = row modify a copy and are not retained.
Relevant code on current master (44b86e1):
To Reproduce
- Build an ESP32-S3 MUI target and monitor internal free heap, minimum free heap, largest internal block, and PSRAM.
- Synchronize a large node database and receive packets from many distinct senders. MQTT traffic makes this easy to reproduce.
- Keep the UI at or near
MAX_NUM_NODES_VIEW, so old UI nodes are purged while new senders arrive.
- Run for several hours and compare internal heap against sender/node churn.
In one five-hour run with 305 one-minute samples:
- Internal free heap changed from 80,356 to 73,724 bytes (
-6,632 bytes).
- PSRAM free remained approximately 1.58 MB and stable.
- There were 89 node-add events at the 250-node cap, involving 75 distinct newly observed senders/nodes.
- No panic, watchdog reset, reboot, or allocation failure occurred.
- The largest internal free block remained stable at 8,180 bytes.
This retained memory is still reachable through the static container, so an ordinary leak detector may not classify it as a leak.
Expected behavior
Packet statistics should be bounded to the UI node capacity and should be removed when the corresponding UI node is evicted. After the container's initial capacity is allocated, observing another sender should reuse existing storage rather than create another small internal-heap allocation.
Suggested repair
I tested the following bounded design locally:
- Move the statistics container from function-static storage to a
TFTView_320x240 member.
- Replace
std::list<Stats> with a reusable std::vector<PacketStats>.
- Reserve
MAX_NUM_NODES_VIEW once on PSRAM-capable ESP32-S3 targets.
- Enforce
MAX_NUM_NODES_VIEW as a hard fallback bound even if node/statistics lifetimes become temporarily inconsistent.
- Add
purgeNodeStatistics(nodeNum) and call it from purgeNode() whenever a node is evicted.
- Erase entries without shrinking vector capacity, allowing later senders to reuse the allocation.
- Sort and render entries in place, or otherwise remove/fix the copy-based row bookkeeping in
for (auto it2 : stats).
- Optionally avoid rewriting LVGL table cells when their displayed values have not changed.
This repair changes only statistics bookkeeping. It does not discard mesh packets or messages and does not alter the radio TX/RX queues.
The local implementation builds successfully for both LoRa and NoLoRa MUI variants. In early validation with more than 160 restored statistics entries, vector capacity stayed fixed at 250 and internal heap was approximately 9 KB higher than the previous build at comparable uptime. Longer-duration validation is still useful, so this report focuses on the deterministic unbounded retention and the bounded-container repair rather than claiming that every heap trend is eliminated.
Screenshots / Photos
Not applicable; the problem is visible in heap diagnostics rather than in the rendered UI.
Device (please complete the following information):
- Type: Custom Waveshare T147 ESP32-S3 MUI target with 8 MB PSRAM
- Exact version: Meshtastic 2.8.0 local build; Device UI master
44b86e1
- Firmware download source: Local PlatformIO build
- Other attached hardware: None required
Additional logs
The measurements above were captured once per minute. Node identifiers and message content were intentionally omitted; neither is needed to reproduce the allocation pattern.
Describe the bug
TFTView_320x240::updateStatistics()keeps one statistics entry per packet sender in a function-staticstd::list. Every previously unseen sender creates a small allocation, and the list is only cleared by the manual statistics-reset path (p.from == 0). It is not pruned whenpurgeNode()evicts a UI node atMAX_NUM_NODES_VIEW.On ESP32-S3 targets with PSRAM, these small list-node allocations remain in internal heap. With a large or MQTT-fed node database near the 250-node view limit, sender churn therefore causes internal heap to decline slowly even though the visible node set is bounded and PSRAM remains stable. The existing
// TODO: stop if memory limit is reachedmarks the missing bound.packetReceived()also callsupdateStatistics(p)for every packet, independently of whether the statistics panel is visible.There is a second bookkeeping problem in the same function: the rendering loop uses
for (auto it2 : stats), so assignments such asit2.row = rowmodify a copy and are not retained.Relevant code on current master (
44b86e1):updateStatistics()and its static listpurgeNode()packetReceived()To Reproduce
MAX_NUM_NODES_VIEW, so old UI nodes are purged while new senders arrive.In one five-hour run with 305 one-minute samples:
-6,632bytes).This retained memory is still reachable through the static container, so an ordinary leak detector may not classify it as a leak.
Expected behavior
Packet statistics should be bounded to the UI node capacity and should be removed when the corresponding UI node is evicted. After the container's initial capacity is allocated, observing another sender should reuse existing storage rather than create another small internal-heap allocation.
Suggested repair
I tested the following bounded design locally:
TFTView_320x240member.std::list<Stats>with a reusablestd::vector<PacketStats>.MAX_NUM_NODES_VIEWonce on PSRAM-capable ESP32-S3 targets.MAX_NUM_NODES_VIEWas a hard fallback bound even if node/statistics lifetimes become temporarily inconsistent.purgeNodeStatistics(nodeNum)and call it frompurgeNode()whenever a node is evicted.for (auto it2 : stats).This repair changes only statistics bookkeeping. It does not discard mesh packets or messages and does not alter the radio TX/RX queues.
The local implementation builds successfully for both LoRa and NoLoRa MUI variants. In early validation with more than 160 restored statistics entries, vector capacity stayed fixed at 250 and internal heap was approximately 9 KB higher than the previous build at comparable uptime. Longer-duration validation is still useful, so this report focuses on the deterministic unbounded retention and the bounded-container repair rather than claiming that every heap trend is eliminated.
Screenshots / Photos
Not applicable; the problem is visible in heap diagnostics rather than in the rendered UI.
Device (please complete the following information):
44b86e1Additional logs
The measurements above were captured once per minute. Node identifiers and message content were intentionally omitted; neither is needed to reproduce the allocation pattern.