feat(mqtt): add an opt-in mqtt.testalloc heap-contiguity probe

Diagnosing internal-heap fragmentation on non-PSRAM boards needs an answer
to one question: can a TLS handshake still get its 16 KiB inbound record
buffer? That is the largest single allocation the bridge makes and the only
one fragmentation can realistically deny.

Largest-free-block alone does not answer it, so the probe attempts the real
allocation and reports the block size while still holding it: if that equals
the size before, the request came from somewhere other than the largest free
block. It also attempts one 1024 B ArduinoJson pool block, and frees both.

Compiled out unless MQTT_DEBUG_ALLOC_TEST is defined.
This commit is contained in:
agessaman
2026-08-06 19:27:51 -07:00
parent 12a5c63a9c
commit 485c2e9cd8
+32
View File
@@ -812,6 +812,38 @@ bool CommonCLI::handleObserverGetCmd(uint32_t sender_timestamp, const char* conf
formatMQTTPresetListReply(reply, 160, start);
} else if (memcmp(config, "mqtt.stats", 10) == 0) {
MQTTBridge::formatMqttStatsReply(reply, 160);
#if defined(MQTT_DEBUG_ALLOC_TEST) && defined(ESP_PLATFORM)
} else if (memcmp(config, "mqtt.testalloc", 14) == 0) {
// Can a TLS handshake still get its inbound record buffer right now?
//
// That 16384 B contiguous request is the largest single allocation the bridge
// makes, and the only one that fragmentation can realistically deny. It normally
// succeeds even when the largest free block is smaller, because a reconnecting
// slot frees its own buffers first — this probe asks the harder question of
// whether the block is available *without* that, which is the case when a second
// slot reconnects while the first is still down.
//
// Deliberately not testing the neighbours JSON pool: ArduinoJson allocates it in
// ARDUINOJSON_POOL_CAPACITY-slot blocks, which is 128 slots = 1024 bytes on these
// 32-bit targets, so the 12288 B budget is twelve small allocations and never a
// contiguous demand.
const size_t before = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL);
void* tls_in = heap_caps_malloc(16384, MALLOC_CAP_INTERNAL);
// Measured while the block is still held. If this equals `before`, the request
// was satisfied from somewhere other than the largest free block, which is worth
// knowing: it means largest-free-block alone does not predict whether a handshake
// will get its buffer.
const size_t held = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL);
void* pool_block = heap_caps_malloc(1024, MALLOC_CAP_INTERNAL);
if (tls_in) heap_caps_free(tls_in);
if (pool_block) heap_caps_free(pool_block);
const size_t restored = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL);
sprintf(reply, "> max=%u tls16384=%s pool1024=%s held=%u restored=%u",
(unsigned)before,
tls_in ? "OK" : "FAIL",
pool_block ? "OK" : "FAIL",
(unsigned)held, (unsigned)restored);
#endif
} else if (memcmp(config, "mqtt.status", 11) == 0) {
MQTTBridge::formatMqttStatusReply(reply, 160, &_mqtt_prefs);
} else if (memcmp(config, "mqtt.packets", 12) == 0) {