| // Copyright 2026 The Chromium Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "quiche/common/abiding_object_pool.h" | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include "quiche/common/platform/api/quiche_test.h" | |
| namespace quiche { | |
| namespace test { | |
| namespace { | |
| struct DestructionTracker { | |
| int value; | |
| int* destruction_count; | |
| DestructionTracker(int val, int* d_count) | |
| : value(val), destruction_count(d_count) {} | |
| ~DestructionTracker() { | |
| if (destruction_count != nullptr) { | |
| ++(*destruction_count); | |
| } | |
| } | |
| }; | |
| class Base { | |
| public: | |
| virtual ~Base() = default; | |
| virtual std::string Name() const = 0; | |
| protected: | |
| const char* name_ = nullptr; | |
| int* destruction_count_ = nullptr; | |
| }; | |
| class Derived : public Base { | |
| public: | |
| explicit Derived(const char* name, int* destruction_count = nullptr) { | |
| name_ = name; | |
| destruction_count_ = destruction_count; | |
| } | |
| ~Derived() override { | |
| if (destruction_count_ != nullptr) { | |
| ++(*destruction_count_); | |
| } | |
| } | |
| std::string Name() const override { return name_ != nullptr ? name_ : ""; } | |
| }; | |
| struct alignas(64) OveralignedStruct { | |
| uint64_t a; | |
| uint64_t b; | |
| }; | |
| TEST(AbidingObjectPoolTest, BasicAllocationAndDestruction) { | |
| AbidingObjectPool<DestructionTracker, 1024> pool; | |
| EXPECT_TRUE(pool.empty()); | |
| EXPECT_EQ(pool.allocated_slabs(), 0u); | |
| EXPECT_EQ(pool.total_capacity(), 0u); | |
| int destructions = 0; | |
| { | |
| auto ptr = pool.Create(42, &destructions); | |
| ASSERT_NE(ptr, nullptr); | |
| EXPECT_EQ(ptr->value, 42); | |
| EXPECT_FALSE(pool.empty()); | |
| EXPECT_EQ(pool.allocated_slabs(), 1u); | |
| EXPECT_GT(pool.total_capacity(), 0u); | |
| EXPECT_EQ(destructions, 0); | |
| } | |
| EXPECT_EQ(destructions, 1); | |
| EXPECT_TRUE(pool.empty()); | |
| // 1 spare empty slab is retained in reserve. | |
| EXPECT_EQ(pool.allocated_slabs(), 1u); | |
| EXPECT_EQ(pool.num_free_slots(), pool.total_capacity()); | |
| } | |
| TEST(AbidingObjectPoolTest, FreelistReuse) { | |
| AbidingObjectPool<int64_t, 256> pool; | |
| void* first_address = nullptr; | |
| { | |
| auto ptr = pool.Create(100); | |
| first_address = static_cast<void*>(ptr.get()); | |
| EXPECT_EQ(*ptr, 100); | |
| } | |
| // The next allocation should reuse the most recently deallocated slot (LIFO). | |
| { | |
| auto ptr = pool.Create(200); | |
| EXPECT_EQ(static_cast<void*>(ptr.get()), first_address); | |
| EXPECT_EQ(*ptr, 200); | |
| } | |
| } | |
| TEST(AbidingObjectPoolTest, PolymorphicDerivedCreation) { | |
| AbidingObjectPool<Base, 1024> pool; | |
| int destructions = 0; | |
| { | |
| AbidingObjectPool<Base, 1024>::Ptr<Derived> derived_ptr = | |
| pool.Create<Derived>("quiche_test", &destructions); | |
| ASSERT_NE(derived_ptr, nullptr); | |
| EXPECT_EQ(derived_ptr->Name(), "quiche_test"); | |
| EXPECT_EQ(destructions, 0); | |
| } | |
| EXPECT_EQ(destructions, 1); | |
| } | |
| TEST(AbidingObjectPoolTest, AutomaticSlabDrainingAndCompaction) { | |
| constexpr size_t kSlabSize = 256; | |
| AbidingObjectPool<int64_t, kSlabSize> pool; | |
| std::vector<AbidingObjectPool<int64_t, kSlabSize>::Ptr<int64_t>> objects; | |
| // Allocate enough objects to span multiple slabs (5 slabs at ~27 slots/slab). | |
| for (int i = 0; i < 120; ++i) { | |
| objects.push_back(pool.Create(i)); | |
| } | |
| size_t peak_slabs = pool.allocated_slabs(); | |
| EXPECT_GE(peak_slabs, 4u); | |
| // Free most objects, leaving 20 active (fits within 1 slab). | |
| objects.resize(20); | |
| // Excess drained slabs are destroyed, leaving 1 active slab + 1 spare slab. | |
| EXPECT_LT(pool.allocated_slabs(), peak_slabs); | |
| EXPECT_EQ(pool.allocated_slabs(), 2u); | |
| // Free all remaining objects. | |
| objects.clear(); | |
| // Exactly 1 spare empty slab is retained in cache. | |
| EXPECT_EQ(pool.allocated_slabs(), 1u); | |
| EXPECT_TRUE(pool.empty()); | |
| EXPECT_EQ(pool.num_free_slots(), pool.total_capacity()); | |
| } | |
| TEST(AbidingObjectPoolTest, Preallocation) { | |
| constexpr size_t kInitialSlabs = 3; | |
| constexpr size_t kSlabSize = 512; | |
| AbidingObjectPool<int64_t, kSlabSize> pool(kInitialSlabs); | |
| EXPECT_EQ(pool.allocated_slabs(), kInitialSlabs); | |
| EXPECT_GT(pool.total_capacity(), 0u); | |
| EXPECT_EQ(pool.num_free_slots(), pool.total_capacity()); | |
| EXPECT_TRUE(pool.empty()); | |
| } | |
| TEST(AbidingObjectPoolTest, MovePtrMaintainsDeleter) { | |
| AbidingObjectPool<int64_t, 256> pool; | |
| auto ptr1 = pool.Create(12345); | |
| ASSERT_NE(ptr1, nullptr); | |
| auto ptr2 = std::move(ptr1); | |
| EXPECT_EQ(ptr1, nullptr); | |
| ASSERT_NE(ptr2, nullptr); | |
| EXPECT_EQ(*ptr2, 12345); | |
| EXPECT_FALSE(pool.empty()); | |
| ptr2.reset(); | |
| EXPECT_TRUE(pool.empty()); | |
| EXPECT_EQ(pool.allocated_slabs(), 1u); | |
| } | |
| TEST(AbidingObjectPoolTest, OveralignedObject) { | |
| AbidingObjectPool<OveralignedStruct, 1024> pool; | |
| auto ptr = pool.Create(); | |
| ASSERT_NE(ptr, nullptr); | |
| auto address = reinterpret_cast<uintptr_t>(ptr.get()); | |
| EXPECT_EQ(address % alignof(OveralignedStruct), 0u); | |
| } | |
| constinit thread_local AbidingObjectPool<int64_t, 256> kConstinitPool; | |
| TEST(AbidingObjectPoolTest, ConstinitThreadLocal) { | |
| auto ptr = kConstinitPool.Create(42); | |
| ASSERT_NE(ptr, nullptr); | |
| EXPECT_EQ(*ptr, 42); | |
| } | |
| } // namespace | |
| } // namespace test | |
| } // namespace quiche |