- Introduced comparison functions for sorting neighbours by timestamp and signal strength in MyMesh. - Implemented early exit conditions in MQTTBridge to improve processing efficiency when no neighbours are present. - Enhanced MQTTBridge to optimize memory usage by adjusting MQTT client configurations and implementing memory pressure checks. - Reduced processing limits in MQTTBridge to maintain responsiveness and prevent blocking during packet handling.
8.5 KiB
MQTT Library Memory Analysis
Library Information
- Library:
elims/PsychicMqttClient@^0.2.4 - Type: External PlatformIO library (wrapper around ESP-IDF MQTT client)
- Platform: ESP32
- Protocol: MQTT over WebSocket (WSS) for analyzer servers, standard MQTT for custom brokers
- Underlying Library: ESP-IDF
esp_mqtt_client(part of ESP-IDF framework) - Source Location:
.pio/libdeps/*/PsychicMqttClient/src/
Current Usage Patterns
Memory Allocation Points
-
Client Instance Creation
_mqtt_client = new PsychicMqttClient(); // Heap allocation _analyzer_us_client = new PsychicMqttClient(); // Heap allocation _analyzer_eu_client = new PsychicMqttClient(); // Heap allocation- Impact: 3 instances × ~few KB each = ~10-15KB base memory
-
setServer() Calls ✅ OPTIMIZED - NO ALLOCATION IN WRAPPER
// From PsychicMqttClient.cpp line 215-223 PsychicMqttClient &setServer(const char *uri) { _mqtt_cfg.broker.address.uri = uri; // Just stores pointer return *this; }- Finding: Wrapper does NOT copy URI (just stores pointer)
- Current Optimization: We only call
setServer()when URI changes (using static tracking) ✅ - ESP-IDF Behavior: When
connect()is called, ESP-IDF likely copies the URI internally - Impact: Minimal (only on connection, not per publish)
-
setCredentials() Calls ✅ NO ALLOCATION IN WRAPPER
// From PsychicMqttClient.cpp line 166-178 PsychicMqttClient &setCredentials(const char *username, const char *password) { _mqtt_cfg.credentials.username = username; // Just stores pointers _mqtt_cfg.credentials.authentication.password = password; return *this; }- Finding: Wrapper does NOT copy credentials (just stores pointers)
- Frequency: Once per broker connection
- ESP-IDF Behavior: Likely copies when connecting
- Impact: Minimal (only on connection, not per publish)
-
publish() Calls ⚠️ CONFIRMED MAIN CULPRIT
// From PsychicMqttClient.cpp line 370-389 int publish(const char *topic, int qos, bool retain, const char *payload, int length, bool async) { if (async) { return esp_mqtt_client_enqueue(_client, topic, payload, length, qos, retain, true); } else { return esp_mqtt_client_publish(_client, topic, payload, length, qos, retain); } }- Frequency: High (every packet, status updates)
- Parameters:
topic: String pointer (passed to ESP-IDF)payload: Buffer pointer + length (passed to ESP-IDF)
- Critical Finding:
- PsychicMqttClient wrapper does NOT copy payload (passes pointers directly)
- BUT: ESP-IDF
esp_mqtt_client_enqueue()andesp_mqtt_client_publish()DO copy internally - ESP-IDF copies both topic and payload into internal buffers for async processing
- Memory Impact:
- Each publish:
strlen(topic)+payload_lenbytes allocated by ESP-IDF - For 2KB JSON payloads + ~50 byte topics: ~2KB per publish
- Each publish:
- Multiple Publishes Per Packet:
- Custom brokers: 1-3 publishes (one per broker)
- Analyzer servers: 2 publishes (US + EU)
- Total: Up to 5 publishes per packet × 2KB = 10KB+ allocations per packet
- Async vs Sync:
- We use
async=true(default), which callsesp_mqtt_client_enqueue() - Enqueue likely allocates more (needs queue buffer) than direct publish
- We use
Memory Fragmentation Evidence
Observed Behavior
- When MQTT Active: Max alloc drops to ~54KB (severe fragmentation)
- When MQTT Disconnected: Max alloc recovers to ~88KB
- Pattern: Memory recovers when publishes stop
Root Cause - CONFIRMED ✅
-
ESP-IDF
esp_mqtt_client_enqueue()copies payload internally:- PsychicMqttClient wrapper passes pointers (no copy in wrapper)
- ESP-IDF MQTT client copies topic + payload for async queue processing
- Each publish:
topic_len + payload_lenbytes allocated by ESP-IDF - This is the main source of fragmentation
-
Multiple publishes per packet multiply allocations:
- 1 packet → 5 publishes (3 brokers + 2 analyzers) = 5× allocations
- Each allocation: ~2KB (topic + payload)
- Total: ~10KB per packet in heap allocations
-
Frequent publishes prevent heap coalescing:
- New allocations before old ones are freed
- ESP-IDF async queue holds messages until sent
- Creates "holes" in heap that can't be coalesced
Recommendations
1. Investigate Library Source Code
Since the library is external, we need to:
- Check if library source is available in PlatformIO cache
- Review
publish()implementation for memory allocations - Look for zero-copy or buffer reuse options
2. Potential Optimizations
A. Reduce Number of Publishes ⭐ HIGHEST IMPACT
- Current: Publish to all brokers + analyzers separately (5 publishes per packet)
- Option 1: Publish to one analyzer server only (reduce from 2 to 1)
- Impact: 20% reduction in allocations (2KB saved per packet)
- Option 2: Use single custom broker with forwarding
- Impact: 60% reduction (from 3 brokers to 1)
- Option 3: Combine both (1 broker + 1 analyzer)
- Impact: 60% reduction (from 5 to 2 publishes)
- Trade-off: Less redundancy, simpler architecture
B. Reduce Payload Sizes
- Current: Up to 2KB JSON per packet
- Option: Compress or reduce JSON size
- Impact: Reduces allocation size per publish
- Trade-off: Less data per message
C. Throttle Publishes (Already Implemented) ✅
- Current: Skip publishes when Max alloc < 60KB
- Status: ✅ Implemented
- Effect: Prevents further fragmentation when memory is low
D. Use Synchronous Publishes (Blocking)
- Current:
async=true(default) usesesp_mqtt_client_enqueue() - Option: Use
async=falseto callesp_mqtt_client_publish()directly - Impact: May reduce queue overhead, but blocks until sent
- Trade-off: Blocks execution, may impact responsiveness
- Code Change:
publish(topic, qos, retain, payload, len, false)
E. Reduce Buffer Size Configuration
- Current: Default buffer size is 1024 bytes (from
setBufferSize()) - Option: Reduce buffer size if messages are smaller
- Impact: Less memory per client instance
- Note: May cause message fragmentation if payloads exceed buffer
F. Investigate ESP-IDF MQTT Client Configuration
- ESP-IDF MQTT client has internal buffer configuration
- May be able to reduce queue depth or buffer sizes
- Requires: ESP-IDF documentation review
3. Library Alternatives (If Issues Persist)
Consider lightweight MQTT libraries with better memory management:
- lwmqtt: Zero-copy, no dynamic allocations
- coreMQTT: Fixed buffers, predictable memory
- Custom implementation: Full control over memory
Next Steps
-
✅ Locate Library Source: Found in
.pio/libdeps/*/PsychicMqttClient/src/ -
✅ Analyze
publish()Implementation:- Wrapper doesn't copy (passes pointers)
- ESP-IDF
esp_mqtt_client_enqueue()copies internally - This is the root cause
-
Test Synchronous Publishes:
- Try
async=falseto useesp_mqtt_client_publish()directly - May have different memory behavior (no queue)
- Measure fragmentation impact
- Try
-
Reduce Number of Publishes:
- Implement single analyzer server (US or EU)
- Or reduce custom broker count
- Measure memory improvement
-
Profile ESP-IDF MQTT Client:
- Check ESP-IDF documentation for buffer configuration
- Look for queue depth settings
- Investigate if we can reduce internal buffers
-
Consider ESP-IDF MQTT Client Direct Usage:
- Bypass PsychicMqttClient wrapper
- Use ESP-IDF API directly with custom memory management
- More control, but more complex
-
Alternative: Switch to Different Library:
- Consider
lwmqtt(zero-copy, no dynamic allocations) - Or
coreMQTT(fixed buffers) - Requires significant refactoring
- Consider
Current Mitigations
✅ Memory Pressure Monitoring: Skip publishes when fragmented
✅ setServer() Optimization: Only call when URI changes
✅ JSON Buffer Reuse: Build once, publish to multiple destinations
✅ Analyzer Server Throttling: Skip when memory low
Remaining Risk
⚠️ Library Internal Allocations: If publish() copies payloads internally, we can't control this without:
- Library modification
- Library replacement
- Library API changes (if supported)