mirror of
https://github.com/torlando-tech/pyxis.git
synced 2026-10-15 18:50:00 +00:00
Configuration moves to make pyxis buildable against vanilla
attermann/microReticulum @ 0.3.0 instead of the fork:
- lib/libbz2/ — pulled from torlando-tech/microReticulum @ ca355e5
(the previous submodule pin). Used by LXMF compression. Will
eventually move into the standalone microLXMF library (Track B).
- src-shim/ — the 37 fork-only files NOT already vendored into
pyxis/lib/ as part of the original split. Includes:
- LXMF/* (11 files) — to extract as a standalone library (Track B)
- Cryptography/{BZ2,Ratchet}.{cpp,h} — used by LXMF + RNS 1.x compat
- Instrumentation/{BootProfiler,MemoryMonitor}.{cpp,h} — pyxis-specific
- Utilities/{Print,Stream}.{cpp,h} — pyxis logging supplements
- root: Buffer, BytesPool, ChannelData, Display+Graphics, FileStream,
FileSystem, MessageBase, ObjectPool, PSRAMAllocator, SegmentAccumulator
- platformio.ini lib_deps: drop the symlink:// to deps/microReticulum/lib/libbz2
(gone in upstream), add microStore as a github URL dep (vanilla
upstream needs it for Bytes.h's microStore/Codec.h include).
- platformio.ini build_flags: add -Isrc-shim plus its subdirectories
so the vendored ../X.h-style includes (rewritten to <X.h>) resolve
through pyxis's own paths first.
src-shim/* was bulk-rewritten so `#include "../X.h"` (relative to
the fork's src/) becomes `#include <X.h>` resolved via -I deps/microReticulum/src.
Pyxis still doesn't build at this point — the next layer of breakage
is API-level (not file-level): pyxis main.cpp calls fork-only
Identity/LXMRouter/Transport methods that vanilla upstream doesn't
have. See the followup commit's notes / branch description for the
full list.
357 lines
13 KiB
C++
357 lines
13 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 (512 slots): hashes (16-32 bytes), small fields - highest traffic
|
|
* - 256 bytes (24 slots): keys, small announces
|
|
* - 512 bytes (16 slots): standard packets (MTU=500 + margin)
|
|
* - 1024 bytes (16 slots): resource advertisements, large packets
|
|
*
|
|
* 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 "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"
|
|
#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 from runtime observation (2026-01-24)
|
|
// NOTE: Each slot uses ~16 bytes internal RAM for vector metadata + stack pointer
|
|
// 512 slots = ~8KB internal RAM overhead (conservative for busy networks)
|
|
static constexpr size_t TINY_SLOTS = 512; // High traffic tier (balanced for memory)
|
|
static constexpr size_t SMALL_SLOTS = 8; // Low traffic (peak 1)
|
|
static constexpr size_t MEDIUM_SLOTS = 8; // Rare (packets, keep headroom)
|
|
static constexpr size_t LARGE_SLOTS = 8; // Rare (resource ads, keep headroom)
|
|
|
|
// 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-01-24):
|
|
* - Tiny: 512 slots x 64 bytes = 32KB backing + ~8KB metadata (internal RAM)
|
|
* - Small: 8 slots x 256 bytes = 2KB backing + ~128B metadata
|
|
* - Medium: 8 slots x 512 bytes = 4KB backing + ~128B metadata
|
|
* - Large: 8 slots x 1024 bytes = 8KB backing + ~128B metadata
|
|
* - Total: ~46KB PSRAM backing + ~8.5KB internal RAM metadata
|
|
*/
|
|
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
|
|
// Pre-allocate all pool entries
|
|
// This is done at construction (startup) to front-load allocations
|
|
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;
|
|
|
|
// Initialize a tier with pre-allocated vectors
|
|
void initializeTier(PooledData* storage, PooledData** stack, size_t& count,
|
|
size_t capacity, size_t slots) {
|
|
for (size_t i = 0; i < slots; i++) {
|
|
// Placement new to construct in storage array
|
|
new (&storage[i]) PooledData();
|
|
storage[i].reserve(capacity);
|
|
stack[i] = &storage[i];
|
|
}
|
|
count = slots;
|
|
}
|
|
|
|
// Storage for pooled vectors (fixed arrays avoid dynamic allocation)
|
|
PooledData _tiny_storage[BytesPoolConfig::TINY_SLOTS];
|
|
PooledData _small_storage[BytesPoolConfig::SMALL_SLOTS];
|
|
PooledData _medium_storage[BytesPoolConfig::MEDIUM_SLOTS];
|
|
PooledData _large_storage[BytesPoolConfig::LARGE_SLOTS];
|
|
|
|
// Stacks of available vectors (indices into storage arrays)
|
|
PooledData* _tiny_stack[BytesPoolConfig::TINY_SLOTS];
|
|
PooledData* _small_stack[BytesPoolConfig::SMALL_SLOTS];
|
|
PooledData* _medium_stack[BytesPoolConfig::MEDIUM_SLOTS];
|
|
PooledData* _large_stack[BytesPoolConfig::LARGE_SLOTS];
|
|
|
|
// 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
|