Skip to content

Packet statistics retain per-node heap allocations after node eviction #375

Description

@projectsape

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

  1. Build an ESP32-S3 MUI target and monitor internal free heap, minimum free heap, largest internal block, and PSRAM.
  2. Synchronize a large node database and receive packets from many distinct senders. MQTT traffic makes this easy to reproduce.
  3. Keep the UI at or near MAX_NUM_NODES_VIEW, so old UI nodes are purged while new senders arrive.
  4. 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:

  1. Move the statistics container from function-static storage to a TFTView_320x240 member.
  2. Replace std::list<Stats> with a reusable std::vector<PacketStats>.
  3. Reserve MAX_NUM_NODES_VIEW once on PSRAM-capable ESP32-S3 targets.
  4. Enforce MAX_NUM_NODES_VIEW as a hard fallback bound even if node/statistics lifetimes become temporarily inconsistent.
  5. Add purgeNodeStatistics(nodeNum) and call it from purgeNode() whenever a node is evicted.
  6. Erase entries without shrinking vector capacity, allowing later senders to reuse the allocation.
  7. Sort and render entries in place, or otherwise remove/fix the copy-based row bookkeeping in for (auto it2 : stats).
  8. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions