Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,133 @@
#include <array>
#include <cstddef>
#include <functional>
#include <memory_resource>
#include <set>
#include <utility>

// --- std::pmr availability gate ---------------------------------------------
// std::pmr's runtime symbols ship in Apple's system libc++ only from
// iOS 17.0 / macOS 14.0 / tvOS 17.0 / watchOS 10.0 onward
// (_LIBCPP_AVAILABILITY_HAS_PMR maps to _LIBCPP_INTRODUCED_IN_LLVM_16).
// libc++ currently leaves the pmr availability *markup* empty
// (see llvm/llvm-project#40340), so using std::pmr below those versions
// compiles without any diagnostic but fails at dyld load time:
// Symbol not found: std::__1::pmr::memory_resource::~memory_resource()
// On those targets we fall back to a std::multiset backed by an in-object,
// non-pmr pool allocator that preserves the original design intent: a fixed
// buffer, node recycling, and no heap allocation (audio-thread / real-time
// safe). Define AUDIOAPI_HAS_PMR before including this header to override.
#if !defined(AUDIOAPI_HAS_PMR)
# if defined(_LIBCPP_VERSION) && defined(_LIBCPP_AVAILABILITY_HAS_PMR) && \
!_LIBCPP_AVAILABILITY_HAS_PMR
# define AUDIOAPI_HAS_PMR 0
# else
# define AUDIOAPI_HAS_PMR 1
# endif
#endif

#if AUDIOAPI_HAS_PMR
# include <memory_resource>
#else
# include <new>
#endif

namespace audioapi {

#if !AUDIOAPI_HAS_PMR
namespace bpq_detail {

/// @brief Fixed-capacity, in-object block pool with an intrusive free list.
/// Hands out fixed-size slots and recycles freed ones; never touches the heap.
/// Throws std::bad_alloc on exhaustion, matching the std::pmr path's
/// null_memory_resource upstream behaviour.
template <std::size_t Capacity, std::size_t BlockSize, std::size_t Align>
class FixedBlockPool {
public:
FixedBlockPool() = default;
FixedBlockPool(const FixedBlockPool &) = delete;
FixedBlockPool &operator=(const FixedBlockPool &) = delete;

void *allocate(std::size_t bytes) {
// The multiset allocates tree nodes one at a time, all the same size.
if (bytes > kSlotSize) [[unlikely]] {
throw std::bad_alloc();
}
if (freeList_ != nullptr) {
void *block = freeList_;
freeList_ = nextOf(block);
return block;
}
if (used_ >= Capacity) [[unlikely]] {
throw std::bad_alloc();
}
return &storage_[used_++ * kSlotSize];
}

void deallocate(void *block) noexcept {
nextOf(block) = freeList_;
freeList_ = block;
}

private:
// Round the requested block size up to the pool alignment. A slot is always
// >= sizeof(void*), so the free list is stored intrusively in freed slots.
static constexpr std::size_t kSlotSize =
((BlockSize + Align - 1) / Align) * Align;

static void *&nextOf(void *block) noexcept {
return *static_cast<void **>(block);
}

alignas(Align) std::array<std::byte, Capacity * kSlotSize> storage_{};
void *freeList_ = nullptr;
std::size_t used_ = 0;
};

/// @brief Stateful allocator that draws node storage from a FixedBlockPool.
/// Satisfies the C++ Allocator requirements used by std::multiset.
template <typename T, typename Pool>
class PoolAllocator {
public:
using value_type = T;

template <typename U>
struct rebind {
using other = PoolAllocator<U, Pool>;
};

explicit PoolAllocator(Pool *pool) noexcept : pool_(pool) {}

template <typename U>
PoolAllocator(const PoolAllocator<U, Pool> &other) noexcept // NOLINT(runtime/explicit)
: pool_(other.pool_) {}

T *allocate(std::size_t n) {
return static_cast<T *>(pool_->allocate(n * sizeof(T)));
}

void deallocate(T *p, std::size_t /*n*/) noexcept {
pool_->deallocate(p);
}

template <typename U>
bool operator==(const PoolAllocator<U, Pool> &o) const noexcept {
return pool_ == o.pool_;
}
template <typename U>
bool operator!=(const PoolAllocator<U, Pool> &o) const noexcept {
return pool_ != o.pool_;
}

private:
template <typename, typename>
friend class PoolAllocator;

Pool *pool_;
};

} // namespace bpq_detail
#endif // !AUDIOAPI_HAS_PMR

/// @brief A bounded priority queue with fixed capacity backed by a static pool allocator.
/// Elements are kept in ascending sorted order (smallest element at front).
/// All operations avoid heap allocation. Uses std::multiset under the hood
Expand All @@ -22,6 +143,7 @@ namespace audioapi {
template <typename T, size_t Capacity, typename Compare = std::less<T>>
class BoundedPriorityQueue {
private:
#if AUDIOAPI_HAS_PMR
using SetType = std::pmr::multiset<T, Compare>;

static constexpr size_t kNodeOverhead = 96;
Expand All @@ -39,6 +161,22 @@ class BoundedPriorityQueue {
std::pmr::pool_options{.max_blocks_per_chunk = 1, .largest_required_pool_block = 0},
&mono_};
SetType set_{Compare{}, &pool_};
#else
static constexpr size_t kNodeOverhead = 96;
// Upper bound on a std::multiset red-black-tree node holding a T (value plus
// tree links/color). Generous headroom, matching the std::pmr path above.
static constexpr size_t kBlockSize = sizeof(T) + kNodeOverhead;

using PoolType =
bpq_detail::FixedBlockPool<Capacity, kBlockSize, alignof(std::max_align_t)>;
using AllocType = bpq_detail::PoolAllocator<T, PoolType>;
using SetType = std::multiset<T, Compare, AllocType>;

// Members must be declared in this order: pool_ → set_.
// set_ allocates its nodes from pool_, so pool_ must outlive it.
PoolType pool_;
SetType set_{Compare{}, AllocType{&pool_}};
#endif

public:
explicit BoundedPriorityQueue() = default;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Exercises the non-pmr fallback path of BoundedPriorityQueue (the std::multiset
// + FixedBlockPool implementation used on runtimes whose libc++ has no std::pmr,
// e.g. iOS < 17). AUDIOAPI_HAS_PMR is forced to 0 so this path is covered even on
// hosts whose libc++ does have pmr; the default (std::pmr) path is exercised by
// the rest of the suite, which compiles the library with the auto-detected gate.
#define AUDIOAPI_HAS_PMR 0

#include <audioapi/utils/BoundedPriorityQueue.hpp>
#include <gtest/gtest.h>

#include <cstdint>
#include <utility>
#include <vector>

using namespace audioapi;

namespace {

struct Event {
int64_t time;
int id;
};

// Transparent comparator (heterogeneous key lookups), mirroring how the audio
// param queues use lowerBound/upperBound with a raw time key.
struct ByTime {
using is_transparent = void;
bool operator()(const Event &a, const Event &b) const { return a.time < b.time; }
bool operator()(const Event &a, int64_t b) const { return a.time < b; }
bool operator()(int64_t a, const Event &b) const { return a < b.time; }
};

} // namespace

TEST(BoundedPriorityQueueTest, KeepsAscendingOrderAndRespectsBounds) {
BoundedPriorityQueue<Event, 8, ByTime> q;
EXPECT_TRUE(q.isEmpty());

// Insert with descending keys; the queue must keep them ascending.
for (int i = 0; i < 8; ++i) {
EXPECT_TRUE(q.push(Event{static_cast<int64_t>(100 - i * 10), i}));
}
EXPECT_TRUE(q.isFull());
EXPECT_EQ(q.size(), 8u);
EXPECT_FALSE(q.push(Event{5, 99})); // rejected when full
EXPECT_EQ(q.size(), 8u);

EXPECT_EQ(q.peekFront().time, 30); // 100 - 7*10
EXPECT_EQ(q.peekBack().time, 100);

int64_t prev = -1;
for (auto it = q.begin(); it != q.end(); ++it) {
EXPECT_LT(prev, it->time);
prev = it->time;
}
}

TEST(BoundedPriorityQueueTest, PopReturnsSmallestFirst) {
BoundedPriorityQueue<Event, 4, ByTime> q;
q.push(Event{30, 0});
q.push(Event{10, 1});
q.push(Event{20, 2});

Event out{};
EXPECT_TRUE(q.pop(out));
EXPECT_EQ(out.time, 10);
EXPECT_TRUE(q.pop(out));
EXPECT_EQ(out.time, 20);
EXPECT_TRUE(q.pop()); // drop 30 without retrieving
EXPECT_TRUE(q.isEmpty());
EXPECT_FALSE(q.pop(out)); // empty
}

TEST(BoundedPriorityQueueTest, RecyclesNodesUnderChurn) {
// Far more insert/erase cycles than Capacity. If freed nodes were not
// recycled the fixed pool would exhaust and allocate() would throw.
BoundedPriorityQueue<Event, 8, ByTime> q;
for (int i = 0; i < 6; ++i) {
q.push(Event{static_cast<int64_t>(i), i});
}
int64_t key = 1000;
for (int round = 0; round < 100000; ++round) {
ASSERT_TRUE(q.push(Event{key++, round}));
ASSERT_TRUE(q.pop());
}
EXPECT_EQ(q.size(), 6u);
}

TEST(BoundedPriorityQueueTest, BoundsExtractMutateAndReinsert) {
BoundedPriorityQueue<Event, 8, ByTime> q;
for (int i = 0; i < 5; ++i) {
q.push(Event{static_cast<int64_t>(i * 10), i}); // 0,10,20,30,40
}

auto it = q.upperBound(static_cast<int64_t>(15)); // first key > 15 -> 20
ASSERT_NE(it, q.end());
EXPECT_EQ(it->time, 20);

auto node = q.extract(it); // detach node holding 20
node.value().time = 25; // mutate key while detached (no reallocation)
q.insert(q.upperBound(static_cast<int64_t>(25)), std::move(node));
EXPECT_EQ(q.size(), 5u);

std::vector<int64_t> times;
for (auto i = q.begin(); i != q.end(); ++i) {
times.push_back(i->time);
}
EXPECT_EQ(times, (std::vector<int64_t>{0, 10, 25, 30, 40}));

// Range erase: drop everything >= 30.
q.erase(q.lowerBound(static_cast<int64_t>(30)), q.end());
EXPECT_EQ(q.size(), 3u);
EXPECT_EQ(q.peekBack().time, 25);
}