Files
MeshCore/MQTT_MEMORY_ANALYSIS.md
T
agessaman 2185523df6 Add sorting functions for neighbour information in MyMesh and optimize MQTTBridge for memory efficiency
- 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.
2026-01-02 13:36:41 -08:00

209 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
1. **Client Instance Creation**
```cpp
_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
2. **setServer() Calls** ✅ **OPTIMIZED - NO ALLOCATION IN WRAPPER**
```cpp
// 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)
3. **setCredentials() Calls** ✅ **NO ALLOCATION IN WRAPPER**
```cpp
// 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)
4. **publish() Calls** ⚠️ **CONFIRMED MAIN CULPRIT**
```cpp
// 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()` and `esp_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_len` bytes allocated by ESP-IDF
- For 2KB JSON payloads + ~50 byte topics: ~2KB per 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 calls `esp_mqtt_client_enqueue()`
- Enqueue likely allocates more (needs queue buffer) than direct publish
## 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 ✅
1. **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_len` bytes allocated by ESP-IDF
- **This is the main source of fragmentation**
2. **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**
3. **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) uses `esp_mqtt_client_enqueue()`
- **Option**: Use `async=false` to call `esp_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
1. ✅ **Locate Library Source**: Found in `.pio/libdeps/*/PsychicMqttClient/src/`
2. ✅ **Analyze `publish()` Implementation**:
- Wrapper doesn't copy (passes pointers)
- ESP-IDF `esp_mqtt_client_enqueue()` copies internally
- This is the root cause
3. **Test Synchronous Publishes**:
- Try `async=false` to use `esp_mqtt_client_publish()` directly
- May have different memory behavior (no queue)
- Measure fragmentation impact
4. **Reduce Number of Publishes**:
- Implement single analyzer server (US or EU)
- Or reduce custom broker count
- Measure memory improvement
5. **Profile ESP-IDF MQTT Client**:
- Check ESP-IDF documentation for buffer configuration
- Look for queue depth settings
- Investigate if we can reduce internal buffers
6. **Consider ESP-IDF MQTT Client Direct Usage**:
- Bypass PsychicMqttClient wrapper
- Use ESP-IDF API directly with custom memory management
- More control, but more complex
7. **Alternative: Switch to Different Library**:
- Consider `lwmqtt` (zero-copy, no dynamic allocations)
- Or `coreMQTT` (fixed buffers)
- Requires significant refactoring
## 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)