Right-size nRF52 heap tiers after 2.8.0 heap-exhaustion field reports (#10898)

* Right-size nRF52 heap tiers after 2.8.0 heap-exhaustion field reports

Field reports on 2.8.0 show nRF52840 devices at 99% heap (114/115 KB)
within minutes of boot; operator new asserts on OOM, so these devices
are one allocation from a reboot. The 2.8.0 cache sizing ladders gave
nRF52 the largest non-PSRAM tiers on the assumption that a BLE-only
part has a roomy heap - the arena is actually ~125 KB shared with the
FreeRTOS task stacks.

Per-target retiers (nRF52840 unless noted):
- Traffic Management cache 1000 -> 250 entries (10 KB -> 2.5 KB); the
  unclassified fallthrough drops 1000 -> 400 to match the classic-ESP32
  tier (also affects RP2040/RP2350)
- Warm node store 200 -> 100 entries (8 KB -> 4 KB); the non-XXAA
  fallthrough drops 320 -> 100 so an unclassified RAM-constrained part
  can't boot-allocate 12.8 KB
- MESSAGE_HISTORY_LIMIT 20 -> 10 (text pool 4.4 KB -> 2.2 KB), the tier
  classic ESP32 already ships
- MAX_RX_TOPHONE 32 -> 16, shrinking the static packet pool 70 -> 54
  slots (~6.6 KB of .bss returned to the heap arena)
- PacketHistory hash index off arch-wide (1 KB); O(n) over 240 records
  is negligible at LoRa packet rates
- OLEDDISPLAY_REDUCE_MEMORY arch-wide (~1 KB OLED back buffer); the five
  TFT variants -U it because TFTDisplay.cpp needs buffer_back for
  dirty-window diffing
- Drop the stale "for testing" 1024-entry TMM override on T1000-E

Measured on rak4631: heap arena grows 124,572 -> 131,180 B and boot
allocations drop ~15.7 KB, roughly +22 KB free heap on the field-report
device class.

Migration: the nRF52840 warm flash ring replays through place() (LRU),
so the newest 100 identities survive the shrink; the file backend
rejects oversized snapshots cleanly (new test covers this). Native
suites pass (536/536 Docker, 13/13 native-macos warm store); rak4631,
heltec-mesh-node-t114 (TFT) and tracker-t1000-e build green.

* Drop stale OLEDDISPLAY_REDUCE_MEMORY -U on t114 / mesh-solar-tft

Only USE_TFTDISPLAY variants (t1, t096, wismeshtap) compile
TFTDisplay.cpp and need the lib's buffer_back; t114 and mesh-solar-tft
render through the meshtastic-st7789 driver, which handles the
reduced-memory configuration fine - as #10894 (merged from develop)
already established by defining the flag there. Remove the -U guard and
the per-variant -D (redundant with the arch-wide define in nrf52_base
on this branch). Both variants verified building.
This commit is contained in:
Ben Meadors
2026-07-06 13:18:25 -05:00
committed by GitHub
co-authored by GitHub
parent 1e66ecff60
commit ed03a69555
10 changed files with 80 additions and 24 deletions
+6 -4
View File
@@ -21,10 +21,12 @@
// How many messages are stored (RAM + flash). // How many messages are stored (RAM + flash).
// Define -DMESSAGE_HISTORY_LIMIT=N in build_flags to control memory usage. // Define -DMESSAGE_HISTORY_LIMIT=N in build_flags to control memory usage.
#ifndef MESSAGE_HISTORY_LIMIT #ifndef MESSAGE_HISTORY_LIMIT
#if defined(ARCH_ESP32) && \ #if (defined(ARCH_ESP32) && \
!(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32S2)) !(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32S2))) || \
// Baseline ESP32 (non-PSRAM variants) has limited heap; reduce message history on resource-constrained builds. defined(NRF52840_XXAA)
// Override with -DMESSAGE_HISTORY_LIMIT=N if needed. // Baseline ESP32 (non-PSRAM variants) and nRF52840 (~115 KB heap arena shared with SoftDevice +
// FreeRTOS stacks; 2.8.0 field reports hit 99% use) have limited heap; reduce message history on
// resource-constrained builds. Override with -DMESSAGE_HISTORY_LIMIT=N if needed.
#define MESSAGE_HISTORY_LIMIT 10 #define MESSAGE_HISTORY_LIMIT 10
#else #else
#define MESSAGE_HISTORY_LIMIT 20 #define MESSAGE_HISTORY_LIMIT 20
+24 -9
View File
@@ -17,6 +17,12 @@
#ifndef MAX_RX_TOPHONE #ifndef MAX_RX_TOPHONE
#if defined(ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3)) #if defined(ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3))
#define MAX_RX_TOPHONE 8 #define MAX_RX_TOPHONE 8
#elif defined(NRF52840_XXAA)
// Each slot is a ~340 B MeshPacket in the static pool (Router.cpp MAX_PACKETS_STATIC), so 32 slots
// cost ~11 KB of .bss on the RAM-tightest platform (2.8.0 field reports: 99% heap). 16 still doubles
// the 8 classic ESP32 has shipped with for years; drops start when a stalled phone/serial client has
// 16 packets queued.
#define MAX_RX_TOPHONE 16
#else #else
#define MAX_RX_TOPHONE 32 #define MAX_RX_TOPHONE 32
#endif #endif
@@ -126,7 +132,10 @@ static inline int get_max_num_nodes()
// Keyed on the NRF52840_XXAA build flag, not ARCH_NRF52: the latter (from // Keyed on the NRF52840_XXAA build flag, not ARCH_NRF52: the latter (from
// architecture.h via configuration.h) isn't defined this early in every include // architecture.h via configuration.h) isn't defined this early in every include
// chain. Backed by the raw-flash ring below LittleFS - see WarmNodeStore.h. // chain. Backed by the raw-flash ring below LittleFS - see WarmNodeStore.h.
#define WARM_NODE_COUNT 200 // 100 (was 200): the RAM cache is 40 B/entry calloc'd from the ~115 KB heap
// arena, which 2.8.0 field reports showed at 99% use; 120 hot + 100 warm
// identities still covers meshes well past the hot cap.
#define WARM_NODE_COUNT 100
#elif (defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM)) || defined(ARCH_PORTDUINO) #elif (defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM)) || defined(ARCH_PORTDUINO)
#define WARM_NODE_COUNT 2000 // PSRAM-equipped ESP32-S3 / native host; warm cache in PSRAM (~80 KB) #define WARM_NODE_COUNT 2000 // PSRAM-equipped ESP32-S3 / native host; warm cache in PSRAM (~80 KB)
#elif defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32P4) #elif defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32P4)
@@ -136,10 +145,11 @@ static inline int get_max_num_nodes()
#elif defined(ARCH_RP2040) #elif defined(ARCH_RP2040)
#define WARM_NODE_COUNT 150 // RP2040 (264 KB) / RP2350 (520 KB): bounded so warm.dat write fits the 8s watchdog (#10746) #define WARM_NODE_COUNT 150 // RP2040 (264 KB) / RP2350 (520 KB): bounded so warm.dat write fits the 8s watchdog (#10746)
#else #else
// nRF52840 is handled explicitly above (200, raw-flash ring). Any other nRF52 (non-XXAA) and any // nRF52840 is handled explicitly above (raw-flash ring). Any other nRF52 (non-XXAA) and any future
// future non-ESP32/non-RP LittleFS part fall through to this 320 default - flag for review if such a // non-ESP32/non-RP LittleFS part lands here. Reviewed after the 2.8.0 nRF52840 heap-exhaustion
// RAM-constrained nRF52 target is ever added. // reports: fail small (match nRF52840) so an unclassified RAM-constrained part can't boot-allocate
#define WARM_NODE_COUNT 320 // other LittleFS-backed parts (e.g. non-nRF52840 nRF52) // 12.8 KB; RAM-rich targets should opt up explicitly in their variant.
#define WARM_NODE_COUNT 100 // other LittleFS-backed parts (e.g. non-nRF52840 nRF52)
#endif // platform #endif // platform
#endif // WARM_NODE_COUNT #endif // WARM_NODE_COUNT
@@ -177,11 +187,16 @@ static inline int get_max_num_nodes()
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 500 // 512 KB+ SRAM, no PSRAM (S3/C6/P4): ~5 KB heap (#10705) #define TRAFFIC_MANAGEMENT_CACHE_SIZE 500 // 512 KB+ SRAM, no PSRAM (S3/C6/P4): ~5 KB heap (#10705)
#elif defined(ARCH_ESP32) #elif defined(ARCH_ESP32)
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 400 // classic ESP32 / S2 / C3: tightest free heap, ~4 KB (#10705) #define TRAFFIC_MANAGEMENT_CACHE_SIZE 400 // classic ESP32 / S2 / C3: tightest free heap, ~4 KB (#10705)
#elif defined(NRF52840_XXAA)
// Keyed on NRF52840_XXAA (not ARCH_NRF52) for the same include-order reason as WARM_NODE_COUNT above.
// The 256 KB part shares its ~115 KB heap arena with SoftDevice and the FreeRTOS task stacks; 2.8.0
// field reports hit 99% heap use with the old 1000-entry (~10 KB) cache. 250 entries (2.5 KB) still
// tracks >2x the 120-node hot store; LRU victim recycling absorbs busier meshes.
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 250
#else #else
// nRF52 (incl. nRF52840) and RP2040/RP2350 fall through here - there is no nRF/RP branch above, // RP2040/RP2350 and anything unclassified land here: match the classic-ESP32 tier rather than
// by design. These parts have no ESP32-style WiFi+BLE coexistence eating the heap, so the larger // defaulting large. RAM-rich targets should opt up explicitly in their variant.
// 1000-entry (~10 KB) cache fits: nRF52840 is BLE-only on 256 KB RAM; RP2040/RP2350 have 264/520 KB. #define TRAFFIC_MANAGEMENT_CACHE_SIZE 400
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 1000 // nRF52 / RP2040 / RP2350 / other non-ESP32
#endif #endif
#endif // TRAFFIC_MANAGEMENT_CACHE_SIZE #endif // TRAFFIC_MANAGEMENT_CACHE_SIZE
+45
View File
@@ -257,6 +257,50 @@ void test_ws_v1_migration_discardsLastHeard()
b.saveIfDirty(); b.saveIfDirty();
} }
// Shrink safety: a warm.dat snapshot recording more entries than this build's
// WARM_NODE_COUNT (e.g. written before a per-platform tier reduction) must be
// rejected cleanly at the header check - load() starts empty instead of reading
// past entries[]. (The nRF52840 raw-flash ring backend has no such cliff: it
// replays records through place(), whose LRU admission keeps the newest.)
void test_ws_load_rejectsOversizedSnapshot()
{
WarmNodeStore a;
a.absorb(0xA00, 111, NULL);
if (!a.saveIfDirty()) {
TEST_IGNORE_MESSAGE("Filesystem not available in this test environment");
return;
}
// Patch the header's count field (offset 8) to one past capacity.
std::vector<uint8_t> buf;
{
auto f = FSCom.open("/prefs/warm.dat", FILE_O_READ);
if (!f) {
TEST_IGNORE_MESSAGE("warm.dat not readable in this environment");
return;
}
buf.resize(f.size());
f.read(buf.data(), buf.size());
f.close();
}
TEST_ASSERT_TRUE(buf.size() >= 16);
const uint16_t oversized = (uint16_t)(WARM_NODE_COUNT + 1);
memcpy(buf.data() + 8, &oversized, sizeof(oversized));
{
auto f = FSCom.open("/prefs/warm.dat", FILE_O_WRITE);
TEST_ASSERT_TRUE((bool)f);
f.write(buf.data(), buf.size());
f.close();
}
WarmNodeStore b;
b.load();
TEST_ASSERT_EQUAL(0, b.count()); // rejected as invalid, started empty
b.clear();
b.saveIfDirty();
}
WS_TEST_ENTRY void setup() WS_TEST_ENTRY void setup()
{ {
initializeTestEnvironment(); initializeTestEnvironment();
@@ -273,6 +317,7 @@ WS_TEST_ENTRY void setup()
RUN_TEST(test_ws_remove_and_clear); RUN_TEST(test_ws_remove_and_clear);
RUN_TEST(test_ws_persistence_roundTrip); RUN_TEST(test_ws_persistence_roundTrip);
RUN_TEST(test_ws_v1_migration_discardsLastHeard); RUN_TEST(test_ws_v1_migration_discardsLastHeard);
RUN_TEST(test_ws_load_rejectsOversizedSnapshot);
exit(UNITY_END()); exit(UNITY_END());
} }
@@ -15,6 +15,7 @@ board_level = pr
debug_tool = jlink debug_tool = jlink
build_flags = ${nrf52840_base.build_flags} build_flags = ${nrf52840_base.build_flags}
-UOLEDDISPLAY_REDUCE_MEMORY ; TFTDisplay.cpp needs the lib's buffer_back for dirty-window diffing
-Ivariants/nrf52840/heltec_mesh_node_t096 -Ivariants/nrf52840/heltec_mesh_node_t096
-D HAS_LORA_FEM=1 -D HAS_LORA_FEM=1
-D HELTEC_MESH_NODE_T096 -D HELTEC_MESH_NODE_T096
@@ -15,6 +15,7 @@ board_level = pr
debug_tool = jlink debug_tool = jlink
build_flags = ${nrf52840_base.build_flags} build_flags = ${nrf52840_base.build_flags}
-UOLEDDISPLAY_REDUCE_MEMORY ; TFTDisplay.cpp needs the lib's buffer_back for dirty-window diffing
-Ivariants/nrf52840/heltec_mesh_node_t1 -Ivariants/nrf52840/heltec_mesh_node_t1
-DHELTEC_MESH_NODE_T1 -DHELTEC_MESH_NODE_T1
-DUSE_TFTDISPLAY -DUSE_TFTDISPLAY
@@ -18,7 +18,6 @@ debug_tool = jlink
build_flags = ${nrf52840_base.build_flags} build_flags = ${nrf52840_base.build_flags}
-Ivariants/nrf52840/heltec_mesh_node_t114 -Ivariants/nrf52840/heltec_mesh_node_t114
-DHELTEC_T114 -DHELTEC_T114
-DOLEDDISPLAY_REDUCE_MEMORY
build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_node_t114> build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_node_t114>
lib_deps = lib_deps =
@@ -107,7 +107,6 @@ extends = heltec_mesh_solar_base
build_flags = ${heltec_mesh_solar_base.build_flags} build_flags = ${heltec_mesh_solar_base.build_flags}
-DHELTEC_MESH_SOLAR_TFT -DHELTEC_MESH_SOLAR_TFT
-DSPI_INTERFACES_COUNT=2 -DSPI_INTERFACES_COUNT=2
-DOLEDDISPLAY_REDUCE_MEMORY
-DHAS_SPI_TFT=1 -DHAS_SPI_TFT=1
-DUSE_ST7789 -DUSE_ST7789
-DST7789_NSS=41 -DST7789_NSS=41
+2
View File
@@ -26,6 +26,8 @@ build_flags =
-DLFS_NO_ASSERT ; Disable LFS assertions , see https://github.com/meshtastic/firmware/pull/3818 -DLFS_NO_ASSERT ; Disable LFS assertions , see https://github.com/meshtastic/firmware/pull/3818
-DMESHTASTIC_EXCLUDE_AUDIO=1 -DMESHTASTIC_EXCLUDE_AUDIO=1
-DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1
-DMESHTASTIC_EXCLUDE_PKT_HISTORY_HASH=1 ; drop PacketHistory's 1 KB hash index; O(n) over ~240 records is negligible at LoRa packet rates
-DOLEDDISPLAY_REDUCE_MEMORY ; drop the ThingPulse OLED back buffer (~1 KB); TFT variants must -U this (TFTDisplay.cpp needs buffer_back)
-Os -Os
-std=gnu++17 -std=gnu++17
-flto ; whole-image LTO (~-60KB) on every nrf52840 target; nrf52_lto.py (pre: extra_script) keeps the interrupt handlers out of LTO so they survive -flto ; whole-image LTO (~-60KB) on every nrf52840 target; nrf52_lto.py (pre: extra_script) keeps the interrupt handlers out of LTO so they survive
@@ -12,6 +12,7 @@ custom_meshtastic_tags = RAK
extends = nrf52840_base extends = nrf52840_base
board = wiscore_rak4631 board = wiscore_rak4631
build_flags = ${nrf52840_base.build_flags} build_flags = ${nrf52840_base.build_flags}
-UOLEDDISPLAY_REDUCE_MEMORY ; TFTDisplay.cpp needs the lib's buffer_back for dirty-window diffing
-Ivariants/nrf52840/rak_wismeshtap -Ivariants/nrf52840/rak_wismeshtap
-DWISMESH_TAP -DWISMESH_TAP
-DRAK_4631 -DRAK_4631
@@ -154,15 +154,6 @@ extern "C" {
#define HAS_SCREEN 0 #define HAS_SCREEN 0
// Enable Traffic Management Module for testing on T1000-E
// NRF52840 has 256KB RAM - 1024 entries uses ~10KB
#ifndef HAS_TRAFFIC_MANAGEMENT
#define HAS_TRAFFIC_MANAGEMENT 1
#endif
#ifndef TRAFFIC_MANAGEMENT_CACHE_SIZE
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 1024
#endif
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif