Files
pyxis/lib/microreticulum-shim/BytesPool.h
T
torlando-agent[bot]andClaude Opus 4.8 70d4aa6be9 feat: graft pyxis onto upstream microReticulum 0.4.1
Repins microReticulum + microLXMF onto the upstream-0.4.1 graft and adapts
pyxis to the new src/microReticulum/ layout and 0.4.x APIs. The far-diverged
0.3.0 fork's Resource/Transport/Identity work is subsumed by upstream's
reimplementation; only the still-needed fixes ride on the pinned branches
(PKCS7/HMAC/X25519 crypto -- proven byte-identical to python RNS 1.3.1 --
Packet link-proof callback, Identity short-sig guard, and the bz2 layer +
decompress-on-receive in Resource::assemble()).

Consumer-side changes:
- platformio.ini: pin microReticulum @2f21fee (pyxis-fixes-on-0.4.1) and
  microLXMF @33760d0 (chore/microreticulum-0.4.1-layout); bump microStore
  ceea8f5 -> c5fb69d (0.4.x requires the new BasicFileStore::init API);
  -std=gnu++11 -> gnu++17 (upstream requires C++17).
- Namespace all microReticulum includes (angle + quote) to <microReticulum/...>
  for the relocated layout; shim-local Utilities/Stream.h|Print.h preserved.
- Interface::send_outgoing now returns bool: update TCP/BLE/SX1262/Auto
  overrides with correct success/failure returns.
- SDArchiveFileSystem::init(bool reformatOnFail=true) to match new microStore.
- Static Transport::get_path_table() -> path_table(); instance getter unchanged.
- Remove duplicate shim Cryptography/BZ2 (microReticulum provides it now; keep
  lib/libbz2 as the ESP32 bzlib provider).
- patch_littlefs_paths.py: normalize microStore's LittleFS adapter paths to a
  leading "/" -- ESP32 Arduino LittleFS rejects "./"-prefixed paths, which
  silently broke the path store (no peer paths learned, all messaging blocked).

Validated on T-Deck Plus: builds (RAM 27.5% / Flash 77.7%), boots stable
(no WDT/panic), and a full on-device LXMF e2e (DIRECT + OPPORTUNISTIC +
bz2-compressed-Resource receive) passes 5/5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-19 15:49:44 -04:00

405 lines
15 KiB
C++

#pragma once
/**
* BytesPool.h - Pool for Bytes Data objects to prevent heap fragmentation
*
* Problem: Bytes uses shared_ptr<Data> where Data = vector<uint8_t>.
* Each make_shared allocates a control block (24 bytes) + vector metadata.
* In long-running firmware, this causes heap fragmentation.
*
* Solution: Pool the Data objects (vectors) themselves. When a Bytes object
* needs storage for common sizes (<=1024 bytes), it gets a pre-allocated
* Data from the pool with capacity already reserved. When the shared_ptr
* refcount hits 0, a custom deleter returns the Data to the pool instead
* of destroying it.
*
* The pool has four tiers sized for Reticulum packet processing:
* - 64 bytes (1024 slots): hashes (16-32 bytes), small fields - highest traffic
* - 256 bytes (16 slots): keys, small announces
* - 512 bytes (12 slots): standard packets (MTU=500 + margin)
* - 1024 bytes (12 slots): resource advertisements, large packets
*
* All storage arrays are dynamically allocated in PSRAM on ESP32 to avoid
* consuming internal RAM (BSS). Only the pointers (~32 bytes) stay in BSS.
*
* Thread-safe via FreeRTOS spinlock (ESP32) or std::mutex (native).
*
* Usage (in Bytes.cpp):
* auto [data, tier] = BytesPool::instance().acquire(capacity);
* if (data) {
* // Got pooled Data - use with custom deleter
* _data = SharedData(data, BytesPoolDeleter{tier});
* } else {
* // Pool exhausted - fall back to make_shared
* _data = std::make_shared<Data>();
* }
*/
#include "PSRAMAllocator.h"
#include <microReticulum/Log.h>
#include <cstddef>
#include <cstdint>
#include <utility>
#include <vector>
// FreeRTOS spinlock support - only on ESP32
#if defined(ESP_PLATFORM) || defined(ARDUINO)
#include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h"
#include <esp_heap_caps.h>
#define BYTESPOOL_USE_SPINLOCK 1
#else
// Native build - use std::mutex instead
#include <mutex>
#define BYTESPOOL_USE_SPINLOCK 0
#endif
namespace RNS {
// Pool configuration - sized for Reticulum packet processing
// MTU=500, most packets fit in 512 bytes, large resources may need 1024
namespace BytesPoolConfig {
static constexpr size_t TIER_TINY = 64; // Hashes (16-32 bytes), small fields
static constexpr size_t TIER_SMALL = 256; // Small packets, keys
static constexpr size_t TIER_MEDIUM = 512; // Standard packets
static constexpr size_t TIER_LARGE = 1024; // Large packets, resource ads
// Slot counts per tier — tuned 2026-02-19
// Storage arrays now live in PSRAM, so internal RAM cost is only pointers (~32B).
// Tiny tier for transient packet processing only. Known destinations now use
// fixed buffers (zero pool slots). 1024 provides ample headroom for packet
// hashes, Transport tables, and burst announce processing.
static constexpr size_t TINY_SLOTS = 1024; // Transient packet processing (hashes, keys, fields)
static constexpr size_t SMALL_SLOTS = 16; // Keys, small announces
static constexpr size_t MEDIUM_SLOTS = 12; // Standard packets
static constexpr size_t LARGE_SLOTS = 12; // Resource ads, large packets
// Tier identifiers for deleter
enum Tier : uint8_t {
TIER_NONE = 0, // Not from pool (fallback allocation)
TIER_64 = 1,
TIER_256 = 2,
TIER_512 = 3,
TIER_1024 = 4
};
}
// Forward declaration - Data is vector with PSRAMAllocator
using PooledData = std::vector<uint8_t, PSRAMAllocator<uint8_t>>;
/**
* Pool for Bytes Data objects (vectors).
*
* Each tier maintains a stack of pre-allocated vectors with capacity reserved.
* Vectors are cleared (size=0) but capacity preserved when returned to pool.
*
* This eliminates:
* - Repeated vector construction/destruction
* - Repeated capacity reservation allocations
* - shared_ptr control block allocations (via make_shared replacement)
*
* Memory footprint (tuned 2026-02-19, all storage in PSRAM):
* - Tiny: 1024 slots x 64 bytes = 64KB backing + ~16KB metadata (PSRAM)
* - Small: 16 slots x 256 bytes = 4KB backing + ~256B metadata (PSRAM)
* - Medium: 12 slots x 512 bytes = 6KB backing + ~192B metadata (PSRAM)
* - Large: 12 slots x 1024 bytes = 12KB backing + ~192B metadata (PSRAM)
* - Total: ~103KB PSRAM, ~32 bytes internal RAM (pointers only)
*/
class BytesPool {
public:
// Singleton access - pool must be global for custom deleter
static BytesPool& instance() {
static BytesPool pool;
return pool;
}
/**
* Acquire a Data object from pool.
* Returns {pointer, tier} or {nullptr, TIER_NONE} if pool exhausted.
*
* The returned Data is empty (size=0) but has capacity >= requested.
* Caller must use the tier value to construct BytesPoolDeleter.
*/
std::pair<PooledData*, BytesPoolConfig::Tier> acquire(size_t requested_capacity) {
_total_requests++;
#if BYTESPOOL_USE_SPINLOCK
portENTER_CRITICAL(&_mux);
#else
std::lock_guard<std::mutex> lock(_mutex);
#endif
PooledData* result = nullptr;
BytesPoolConfig::Tier tier = BytesPoolConfig::TIER_NONE;
// Try smallest tier that fits
if (requested_capacity <= BytesPoolConfig::TIER_TINY) {
if (_tiny_count > 0) {
result = _tiny_stack[--_tiny_count];
tier = BytesPoolConfig::TIER_64;
_pool_hits++;
}
}
else if (requested_capacity <= BytesPoolConfig::TIER_SMALL) {
if (_small_count > 0) {
result = _small_stack[--_small_count];
tier = BytesPoolConfig::TIER_256;
_pool_hits++;
}
}
else if (requested_capacity <= BytesPoolConfig::TIER_MEDIUM) {
if (_medium_count > 0) {
result = _medium_stack[--_medium_count];
tier = BytesPoolConfig::TIER_512;
_pool_hits++;
}
}
else if (requested_capacity <= BytesPoolConfig::TIER_LARGE) {
if (_large_count > 0) {
result = _large_stack[--_large_count];
tier = BytesPoolConfig::TIER_1024;
_pool_hits++;
}
}
// Oversized requests fall through with nullptr
if (!result) {
_pool_misses++;
}
#if BYTESPOOL_USE_SPINLOCK
portEXIT_CRITICAL(&_mux);
#endif
return {result, tier};
}
/**
* Release a Data object back to pool.
* Called by BytesPoolDeleter when shared_ptr refcount hits 0.
*
* The Data is cleared (preserving capacity) and pushed to tier stack.
*/
void release(PooledData* data, BytesPoolConfig::Tier tier) {
if (!data || tier == BytesPoolConfig::TIER_NONE) {
// Not from pool - should not happen, but defensive
return;
}
// Clear but preserve capacity
data->clear();
#if BYTESPOOL_USE_SPINLOCK
portENTER_CRITICAL(&_mux);
#else
std::lock_guard<std::mutex> lock(_mutex);
#endif
switch (tier) {
case BytesPoolConfig::TIER_64:
if (_tiny_count < BytesPoolConfig::TINY_SLOTS) {
_tiny_stack[_tiny_count++] = data;
}
// else pool full - data leaks (shouldn't happen in normal operation)
break;
case BytesPoolConfig::TIER_256:
if (_small_count < BytesPoolConfig::SMALL_SLOTS) {
_small_stack[_small_count++] = data;
}
break;
case BytesPoolConfig::TIER_512:
if (_medium_count < BytesPoolConfig::MEDIUM_SLOTS) {
_medium_stack[_medium_count++] = data;
}
break;
case BytesPoolConfig::TIER_1024:
if (_large_count < BytesPoolConfig::LARGE_SLOTS) {
_large_stack[_large_count++] = data;
}
break;
default:
break;
}
#if BYTESPOOL_USE_SPINLOCK
portEXIT_CRITICAL(&_mux);
#endif
}
// Instrumentation
size_t total_requests() const { return _total_requests; }
size_t pool_hits() const { return _pool_hits; }
size_t pool_misses() const { return _pool_misses; }
size_t fallback_count() const { return _fallback_count; }
float hit_rate() const {
return _total_requests > 0 ? (float)_pool_hits / _total_requests : 0.0f;
}
/**
* Record a fallback to heap allocation and log WARNING.
* Called by Bytes.cpp when pool is exhausted but fallback succeeds.
*/
void recordFallback(size_t requested_size) {
_fallback_count++;
WARNINGF("BytesPool: exhausted, falling back to heap (requested=%zu bytes, "
"tiny=%zu/%zu small=%zu/%zu med=%zu/%zu large=%zu/%zu)",
requested_size,
tiny_in_use(), BytesPoolConfig::TINY_SLOTS,
small_in_use(), BytesPoolConfig::SMALL_SLOTS,
medium_in_use(), BytesPoolConfig::MEDIUM_SLOTS,
large_in_use(), BytesPoolConfig::LARGE_SLOTS);
}
// Current pool state
size_t tiny_available() const { return _tiny_count; }
size_t small_available() const { return _small_count; }
size_t medium_available() const { return _medium_count; }
size_t large_available() const { return _large_count; }
size_t tiny_in_use() const { return BytesPoolConfig::TINY_SLOTS - _tiny_count; }
size_t small_in_use() const { return BytesPoolConfig::SMALL_SLOTS - _small_count; }
size_t medium_in_use() const { return BytesPoolConfig::MEDIUM_SLOTS - _medium_count; }
size_t large_in_use() const { return BytesPoolConfig::LARGE_SLOTS - _large_count; }
// Log statistics for tuning
void logStats() const {
INFOF("BytesPool: requests=%zu hits=%zu misses=%zu fallbacks=%zu hit_rate=%d%% "
"tiny=%zu/%zu small=%zu/%zu med=%zu/%zu large=%zu/%zu",
_total_requests, _pool_hits, _pool_misses, _fallback_count,
(int)(hit_rate() * 100),
tiny_in_use(), BytesPoolConfig::TINY_SLOTS,
small_in_use(), BytesPoolConfig::SMALL_SLOTS,
medium_in_use(), BytesPoolConfig::MEDIUM_SLOTS,
large_in_use(), BytesPoolConfig::LARGE_SLOTS);
}
private:
BytesPool() {
#if BYTESPOOL_USE_SPINLOCK
portMUX_INITIALIZE(&_mux);
#endif
allocateStorage();
initializeTier(_tiny_storage, _tiny_stack, _tiny_count,
BytesPoolConfig::TIER_TINY, BytesPoolConfig::TINY_SLOTS);
initializeTier(_small_storage, _small_stack, _small_count,
BytesPoolConfig::TIER_SMALL, BytesPoolConfig::SMALL_SLOTS);
initializeTier(_medium_storage, _medium_stack, _medium_count,
BytesPoolConfig::TIER_MEDIUM, BytesPoolConfig::MEDIUM_SLOTS);
initializeTier(_large_storage, _large_stack, _large_count,
BytesPoolConfig::TIER_LARGE, BytesPoolConfig::LARGE_SLOTS);
}
// Non-copyable
BytesPool(const BytesPool&) = delete;
BytesPool& operator=(const BytesPool&) = delete;
// Allocate storage and stack arrays in PSRAM (ESP32) or heap (native)
void allocateStorage() {
#if BYTESPOOL_USE_SPINLOCK
// ESP32: allocate in PSRAM to avoid consuming internal RAM (BSS)
#define POOL_ALLOC(ptr, type, count) do { \
ptr = static_cast<type*>(heap_caps_aligned_alloc( \
alignof(type), (count) * sizeof(type), \
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)); \
if (!ptr) { \
ptr = static_cast<type*>(heap_caps_aligned_alloc( \
alignof(type), (count) * sizeof(type), \
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)); \
if (ptr) WARNING("BytesPool: " #ptr " fell back to internal RAM"); \
} \
} while(0)
POOL_ALLOC(_tiny_storage, PooledData, BytesPoolConfig::TINY_SLOTS);
POOL_ALLOC(_tiny_stack, PooledData*, BytesPoolConfig::TINY_SLOTS);
POOL_ALLOC(_small_storage, PooledData, BytesPoolConfig::SMALL_SLOTS);
POOL_ALLOC(_small_stack, PooledData*, BytesPoolConfig::SMALL_SLOTS);
POOL_ALLOC(_medium_storage, PooledData, BytesPoolConfig::MEDIUM_SLOTS);
POOL_ALLOC(_medium_stack, PooledData*, BytesPoolConfig::MEDIUM_SLOTS);
POOL_ALLOC(_large_storage, PooledData, BytesPoolConfig::LARGE_SLOTS);
POOL_ALLOC(_large_stack, PooledData*, BytesPoolConfig::LARGE_SLOTS);
#undef POOL_ALLOC
#else
// Native: use new[]
_tiny_storage = new PooledData[BytesPoolConfig::TINY_SLOTS];
_tiny_stack = new PooledData*[BytesPoolConfig::TINY_SLOTS];
_small_storage = new PooledData[BytesPoolConfig::SMALL_SLOTS];
_small_stack = new PooledData*[BytesPoolConfig::SMALL_SLOTS];
_medium_storage = new PooledData[BytesPoolConfig::MEDIUM_SLOTS];
_medium_stack = new PooledData*[BytesPoolConfig::MEDIUM_SLOTS];
_large_storage = new PooledData[BytesPoolConfig::LARGE_SLOTS];
_large_stack = new PooledData*[BytesPoolConfig::LARGE_SLOTS];
#endif
}
// Initialize a tier with pre-allocated vectors
void initializeTier(PooledData* storage, PooledData** stack, size_t& count,
size_t capacity, size_t slots) {
if (!storage || !stack) {
ERROR("BytesPool: allocation failed for tier, pool will be undersized");
count = 0;
return;
}
for (size_t i = 0; i < slots; i++) {
// Placement new to construct in allocated storage
new (&storage[i]) PooledData();
storage[i].reserve(capacity);
stack[i] = &storage[i];
}
count = slots;
}
// Storage for pooled vectors — dynamically allocated (PSRAM on ESP32)
PooledData* _tiny_storage = nullptr;
PooledData* _small_storage = nullptr;
PooledData* _medium_storage = nullptr;
PooledData* _large_storage = nullptr;
// Stacks of available vectors (pointers into storage arrays)
PooledData** _tiny_stack = nullptr;
PooledData** _small_stack = nullptr;
PooledData** _medium_stack = nullptr;
PooledData** _large_stack = nullptr;
// Stack counts (how many available in each tier)
size_t _tiny_count = 0;
size_t _small_count = 0;
size_t _medium_count = 0;
size_t _large_count = 0;
// Instrumentation counters
size_t _total_requests = 0;
size_t _pool_hits = 0;
size_t _pool_misses = 0;
size_t _fallback_count = 0; // Heap fallbacks due to pool exhaustion
#if BYTESPOOL_USE_SPINLOCK
portMUX_TYPE _mux;
#else
std::mutex _mutex;
#endif
};
/**
* Custom deleter for shared_ptr that returns Data to pool.
*
* When shared_ptr refcount hits 0, this deleter is called instead of delete.
* The Data is returned to the appropriate tier in BytesPool.
*/
struct BytesPoolDeleter {
BytesPoolConfig::Tier tier;
explicit BytesPoolDeleter(BytesPoolConfig::Tier t = BytesPoolConfig::TIER_NONE)
: tier(t) {}
void operator()(PooledData* data) const {
if (data && tier != BytesPoolConfig::TIER_NONE) {
// Return to pool instead of destroying
BytesPool::instance().release(data, tier);
}
// Note: We never delete the data - it lives in BytesPool's storage arrays
// If tier is TIER_NONE, this is a fallback allocation and won't be called
}
};
} // namespace RNS