diff --git a/src/engine/helper/HL_PoolAllocator.h b/src/engine/helper/HL_PoolAllocator.h index 0e151723..f2ba70d8 100644 --- a/src/engine/helper/HL_PoolAllocator.h +++ b/src/engine/helper/HL_PoolAllocator.h @@ -11,6 +11,7 @@ /// transparent passthrough to the global allocator (i.e. allocate_shared behaves like make_shared) /// for A/B benchmarking without touching the call sites. +#include #include #include #include @@ -45,6 +46,7 @@ namespace hl { uint8_t* raw = static_cast(::operator new(size + HeaderSize)); tagOf(raw) = TagGlobal; + s_activeGlobal.fetch_add(1, std::memory_order_relaxed); return raw + HeaderSize; } @@ -54,6 +56,7 @@ namespace hl uint8_t* block = static_cast(head); head = nextOf(block); + s_activeBlocks.fetch_add(1, std::memory_order_relaxed); return block + HeaderSize; } @@ -68,14 +71,49 @@ namespace hl void*& head = freeListHead(); nextOf(block) = head; head = block; + s_activeBlocks.fetch_sub(1, std::memory_order_relaxed); } else { ::operator delete(static_cast(block)); + s_activeGlobal.fetch_sub(1, std::memory_order_relaxed); } } + /// @name Diagnostics (relaxed gauges — not synchronization) + /// @{ + /// Blocks currently handed out from the pool = pooled allocate() minus + /// pooled deallocate(). Unlike capacityBlocks() (the chunk high-water, + /// which only ever grows) this DROPS when objects are freed, so a rising + /// activeBlocks() is a genuine leak of referenced objects rather than a + /// traffic peak that merely carved extra chunks. Covers every pooled + /// shared_ptr node (allocate_shared + jitter-buffer packets). + static std::int64_t activeBlocks() noexcept + { + return s_activeBlocks.load(std::memory_order_relaxed); + } + /// Live oversized allocations that overflowed to ::operator new. + static std::int64_t activeGlobalAllocations() noexcept + { + return s_activeGlobal.load(std::memory_order_relaxed); + } + /// Total blocks ever carved (chunks x BlocksPerChunk) = high-water capacity. + static std::int64_t capacityBlocks() noexcept + { + return s_chunks.load(std::memory_order_relaxed) * static_cast(BlocksPerChunk); + } + /// Approximate live pooled payload bytes (excludes per-block header). + static std::int64_t activeBytes() noexcept + { + return activeBlocks() * static_cast(PayloadSize); + } + /// @} + private: + inline static std::atomic s_activeBlocks{0}; + inline static std::atomic s_activeGlobal{0}; + inline static std::atomic s_chunks{0}; + static constexpr std::size_t HeaderSize = alignof(std::max_align_t) >= sizeof(uint64_t) ? alignof(std::max_align_t) : sizeof(uint64_t); static constexpr std::size_t BlockSize = HeaderSize + PayloadSize; @@ -114,6 +152,7 @@ namespace hl { const std::size_t chunkBytes = BlockSize * BlocksPerChunk; uint8_t* chunk = static_cast(::operator new(chunkBytes)); + s_chunks.fetch_add(1, std::memory_order_relaxed); { std::lock_guard lock(m_Mutex); @@ -144,6 +183,21 @@ namespace hl }; #endif // HL_RTP_POOL + /// Mode-independent accessors for the pool diagnostics, so callers need not know + /// whether HL_RTP_POOL is compiled in. When pooling is disabled they return -1 + /// ("not applicable") since allocate_shared then behaves like make_shared. +#if HL_RTP_POOL + inline std::int64_t poolActiveBlocks() noexcept { return FixedBlockPool::activeBlocks(); } + inline std::int64_t poolCapacityBlocks() noexcept { return FixedBlockPool::capacityBlocks(); } + inline std::int64_t poolActiveGlobal() noexcept { return FixedBlockPool::activeGlobalAllocations(); } + inline std::int64_t poolActiveBytes() noexcept { return FixedBlockPool::activeBytes(); } +#else + inline std::int64_t poolActiveBlocks() noexcept { return -1; } + inline std::int64_t poolCapacityBlocks() noexcept { return -1; } + inline std::int64_t poolActiveGlobal() noexcept { return -1; } + inline std::int64_t poolActiveBytes() noexcept { return -1; } +#endif + /// @class PoolAllocator /// A stateless, std-conforming Allocator suitable for std::allocate_shared. When HL_RTP_POOL is /// enabled it serves single-node allocations from FixedBlockPool; otherwise (and for any request