// Copyright 2023 The Pigweed Authors // // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy of // the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. #include "pw_allocator/tracking_allocator.h" #include #include "pw_allocator/allocator.h" #include "pw_allocator/first_fit.h" #include "pw_allocator/metrics.h" #include "pw_allocator/testing.h" #include "pw_log/log.h" #include "pw_metric/metric.h" #include "pw_unit_test/framework.h" namespace { // Test fixtures. using ::pw::allocator::Layout; using ::pw::allocator::TrackingAllocator; using TestMetrics = ::pw::allocator::internal::AllMetrics; class TrackingAllocatorForTest : public TrackingAllocator { public: TrackingAllocatorForTest(pw::metric::Token token, pw::Allocator& allocator) : TrackingAllocator(token, allocator) {} // Expose the protected allocated ``Layout`` method for test purposes. pw::Result GetAllocatedLayout(const void* ptr) const { return TrackingAllocator::GetAllocatedLayout(ptr); } }; class TrackingAllocatorTest : public ::testing::Test { protected: using BlockType = ::pw::allocator::FirstFitBlock; using AllocatorType = ::pw::allocator::FirstFitAllocator; static_assert(sizeof(uintptr_t) >= BlockType::kAlignment); constexpr static size_t kCapacity = 256; constexpr static pw::metric::Token kToken = 1U; TrackingAllocatorTest() : ::testing::Test(), tracker_(kToken, *allocator_) {} void SetUp() override { allocator_->Init(allocator_.as_bytes()); } void TearDown() override { pw::allocator::test::FreeAll(allocator_->blocks()); } pw::allocator::WithBuffer allocator_; TrackingAllocatorForTest tracker_; }; struct ExpectedValues { #define INCLUDE_EXPECTED_METRIC(metric_name) uint32_t metric_name = 0 PW_ALLOCATOR_METRICS_FOREACH(INCLUDE_EXPECTED_METRIC); #undef INCLUDE_EXPECTED_METRIC void AddRequestedBytes(uint32_t requested_bytes_) { requested_bytes += requested_bytes_; peak_requested_bytes = std::max(requested_bytes, peak_requested_bytes); cumulative_requested_bytes += requested_bytes_; } void AddAllocatedBytes(uint32_t allocated_bytes_) { allocated_bytes += allocated_bytes_; peak_allocated_bytes = std::max(allocated_bytes, peak_allocated_bytes); cumulative_allocated_bytes += allocated_bytes_; } void Check(const TestMetrics& metrics, int line) { #define EXPECT_METRIC_EQ(metric_name) \ EXPECT_EQ(metrics.metric_name.value(), metric_name); PW_ALLOCATOR_METRICS_FOREACH(EXPECT_METRIC_EQ); if (testing::Test::HasFailure()) { PW_LOG_ERROR("Metrics comparison failed at line %d.", line); } #undef EXPECT_METRIC_EQ } }; #define EXPECT_METRICS_EQ(expected, metrics) expected.Check(metrics, __LINE__) // Unit tests. TEST_F(TrackingAllocatorTest, InitialValues) { const TestMetrics& metrics = tracker_.metrics(); ExpectedValues expected; // Initially all 0. EXPECT_METRICS_EQ(expected, metrics); } TEST_F(TrackingAllocatorTest, GetCapacity) { pw::StatusWithSize capacity = tracker_.GetCapacity(); EXPECT_EQ(capacity.status(), pw::OkStatus()); EXPECT_EQ(capacity.size(), kCapacity); } TEST_F(TrackingAllocatorTest, AddTrackingAllocatorAsChild) { constexpr static pw::metric::Token kChildToken = 2U; TrackingAllocator<::pw::allocator::NoMetrics> child( kChildToken, tracker_, pw::allocator::kAddTrackingAllocatorAsChild); pw::IntrusiveList& children = tracker_.metric_group().children(); ASSERT_FALSE(children.empty()); EXPECT_EQ(children.size(), 1U); EXPECT_EQ(&(children.front()), &(child.metric_group())); } TEST_F(TrackingAllocatorTest, AllocateDeallocate) { const TestMetrics& metrics = tracker_.metrics(); ExpectedValues expected; constexpr Layout layout1 = Layout::Of(); void* ptr1 = tracker_.Allocate(layout1); size_t ptr1_allocated = tracker_.GetAllocatedLayout(ptr1)->size(); ASSERT_NE(ptr1, nullptr); expected.AddRequestedBytes(layout1.size()); expected.AddAllocatedBytes(ptr1_allocated); expected.num_allocations += 1; EXPECT_METRICS_EQ(expected, metrics); tracker_.Deallocate(ptr1); expected.requested_bytes -= layout1.size(); expected.allocated_bytes -= ptr1_allocated; expected.num_deallocations += 1; EXPECT_METRICS_EQ(expected, metrics); } TEST_F(TrackingAllocatorTest, AllocateFailure) { const TestMetrics& metrics = tracker_.metrics(); ExpectedValues expected; constexpr Layout layout = Layout::Of(); void* ptr = tracker_.Allocate(layout); EXPECT_EQ(ptr, nullptr); expected.num_failures += 1; expected.unfulfilled_bytes += layout.size(); EXPECT_METRICS_EQ(expected, metrics); } TEST_F(TrackingAllocatorTest, AllocateDeallocateMultiple) { const TestMetrics& metrics = tracker_.metrics(); ExpectedValues expected; Layout layout1 = Layout::Of(); void* ptr1 = tracker_.Allocate(layout1); ASSERT_NE(ptr1, nullptr); size_t ptr1_allocated = tracker_.GetAllocatedLayout(ptr1)->size(); expected.AddRequestedBytes(layout1.size()); expected.AddAllocatedBytes(ptr1_allocated); expected.num_allocations += 1; EXPECT_METRICS_EQ(expected, metrics); Layout layout2 = Layout::Of(); void* ptr2 = tracker_.Allocate(layout2); ASSERT_NE(ptr2, nullptr); size_t ptr2_allocated = tracker_.GetAllocatedLayout(ptr2)->size(); expected.AddRequestedBytes(layout2.size()); expected.AddAllocatedBytes(ptr2_allocated); expected.num_allocations += 1; EXPECT_METRICS_EQ(expected, metrics); tracker_.Deallocate(ptr1); expected.requested_bytes -= layout1.size(); expected.allocated_bytes -= ptr1_allocated; expected.num_deallocations += 1; EXPECT_METRICS_EQ(expected, metrics); Layout layout3 = Layout::Of(); void* ptr3 = tracker_.Allocate(layout3); ASSERT_NE(ptr3, nullptr); size_t ptr3_allocated = tracker_.GetAllocatedLayout(ptr3)->size(); expected.AddRequestedBytes(layout3.size()); expected.AddAllocatedBytes(ptr3_allocated); expected.num_allocations += 1; EXPECT_METRICS_EQ(expected, metrics); tracker_.Deallocate(ptr3); expected.requested_bytes -= layout3.size(); expected.allocated_bytes -= ptr3_allocated; expected.num_deallocations += 1; EXPECT_METRICS_EQ(expected, metrics); tracker_.Deallocate(ptr2); expected.requested_bytes -= layout2.size(); expected.allocated_bytes -= ptr2_allocated; expected.num_deallocations += 1; EXPECT_METRICS_EQ(expected, metrics); } TEST_F(TrackingAllocatorTest, ResizeLarger) { const TestMetrics& metrics = tracker_.metrics(); ExpectedValues expected; constexpr Layout layout1 = Layout::Of(); void* ptr = tracker_.Allocate(layout1); size_t ptr_allocated1 = tracker_.GetAllocatedLayout(ptr)->size(); ASSERT_NE(ptr, nullptr); expected.AddRequestedBytes(layout1.size()); expected.AddAllocatedBytes(ptr_allocated1); expected.num_allocations += 1; EXPECT_METRICS_EQ(expected, metrics); constexpr size_t size2 = sizeof(uintptr_t[5]); EXPECT_TRUE(tracker_.Resize(ptr, size2)); size_t ptr_allocated2 = tracker_.GetAllocatedLayout(ptr)->size(); expected.AddRequestedBytes(size2 - layout1.size()); expected.AddAllocatedBytes(ptr_allocated2 - ptr_allocated1); expected.num_resizes += 1; EXPECT_METRICS_EQ(expected, metrics); tracker_.Deallocate(ptr); expected.requested_bytes -= size2; expected.allocated_bytes -= ptr_allocated2; expected.num_deallocations += 1; EXPECT_METRICS_EQ(expected, metrics); } TEST_F(TrackingAllocatorTest, ResizeSmaller) { const TestMetrics& metrics = tracker_.metrics(); ExpectedValues expected; constexpr Layout layout1 = Layout::Of(); void* ptr = tracker_.Allocate(layout1); size_t ptr_allocated1 = tracker_.GetAllocatedLayout(ptr)->size(); ASSERT_NE(ptr, nullptr); expected.AddRequestedBytes(layout1.size()); expected.AddAllocatedBytes(ptr_allocated1); expected.num_allocations += 1; EXPECT_METRICS_EQ(expected, metrics); constexpr size_t size2 = sizeof(uintptr_t[1]); EXPECT_TRUE(tracker_.Resize(ptr, size2)); size_t ptr_allocated2 = tracker_.GetAllocatedLayout(ptr)->size(); expected.requested_bytes -= layout1.size() - size2; expected.allocated_bytes -= ptr_allocated1 - ptr_allocated2; expected.num_resizes += 1; EXPECT_METRICS_EQ(expected, metrics); tracker_.Deallocate(ptr); expected.requested_bytes -= size2; expected.allocated_bytes -= ptr_allocated2; expected.num_deallocations += 1; EXPECT_METRICS_EQ(expected, metrics); } TEST_F(TrackingAllocatorTest, ResizeFailure) { const TestMetrics& metrics = tracker_.metrics(); ExpectedValues expected; constexpr Layout layout = Layout::Of(); void* ptr1 = tracker_.Allocate(layout); ASSERT_NE(ptr1, nullptr); size_t ptr1_allocated = tracker_.GetAllocatedLayout(ptr1)->size(); expected.AddRequestedBytes(layout.size()); expected.AddAllocatedBytes(ptr1_allocated); expected.num_allocations += 1; EXPECT_METRICS_EQ(expected, metrics); void* ptr2 = tracker_.Allocate(layout); ASSERT_NE(ptr2, nullptr); size_t ptr2_allocated = tracker_.GetAllocatedLayout(ptr2)->size(); expected.AddRequestedBytes(layout.size()); expected.AddAllocatedBytes(ptr2_allocated); expected.num_allocations += 1; EXPECT_METRICS_EQ(expected, metrics); EXPECT_FALSE(tracker_.Resize(ptr1, layout.size() * 2)); expected.num_failures += 1; expected.unfulfilled_bytes += layout.size() * 2; EXPECT_METRICS_EQ(expected, metrics); } TEST_F(TrackingAllocatorTest, Reallocate) { const TestMetrics& metrics = tracker_.metrics(); ExpectedValues expected; constexpr Layout layout1 = Layout::Of(); void* ptr1 = tracker_.Allocate(layout1); ASSERT_NE(ptr1, nullptr); size_t ptr1_allocated = tracker_.GetAllocatedLayout(ptr1)->size(); expected.AddRequestedBytes(layout1.size()); expected.AddAllocatedBytes(ptr1_allocated); expected.num_allocations += 1; EXPECT_METRICS_EQ(expected, metrics); // If `Reallocate` just resizes, no extra memory is allocated constexpr Layout layout2 = Layout::Of(); void* ptr2 = tracker_.Reallocate(ptr1, layout2); EXPECT_EQ(ptr2, ptr1); size_t ptr2_allocated = tracker_.GetAllocatedLayout(ptr2)->size(); expected.AddRequestedBytes(layout2.size() - layout1.size()); expected.AddAllocatedBytes(ptr2_allocated - ptr1_allocated); expected.num_reallocations += 1; EXPECT_METRICS_EQ(expected, metrics); // Make a second allocation to force reallocation. constexpr Layout layout3 = layout1; void* ptr3 = tracker_.Allocate(layout1); ASSERT_NE(ptr3, nullptr); size_t ptr3_allocated = tracker_.GetAllocatedLayout(ptr3)->size(); expected.AddRequestedBytes(layout3.size()); expected.AddAllocatedBytes(ptr3_allocated); expected.num_allocations += 1; EXPECT_METRICS_EQ(expected, metrics); // If `Reallocate` must copy to a new location, it allocates before // deallocating and results in higher peaks. constexpr Layout layout4 = Layout::Of(); void* ptr4 = tracker_.Reallocate(ptr2, layout4); EXPECT_NE(ptr4, ptr2); size_t ptr4_allocated = tracker_.GetAllocatedLayout(ptr4)->size(); expected.AddRequestedBytes(layout4.size() - layout2.size()); expected.AddAllocatedBytes(ptr4_allocated); expected.allocated_bytes -= ptr2_allocated; expected.num_reallocations += 1; EXPECT_METRICS_EQ(expected, metrics); tracker_.Deallocate(ptr3); expected.requested_bytes -= layout3.size(); expected.allocated_bytes -= ptr3_allocated; expected.num_deallocations += 1; EXPECT_METRICS_EQ(expected, metrics); tracker_.Deallocate(ptr4); expected.requested_bytes -= layout4.size(); expected.allocated_bytes -= ptr4_allocated; expected.num_deallocations += 1; EXPECT_METRICS_EQ(expected, metrics); } TEST_F(TrackingAllocatorTest, ReallocateFailure) { const TestMetrics& metrics = tracker_.metrics(); ExpectedValues expected; constexpr Layout layout1 = Layout::Of(); void* ptr1 = tracker_.Allocate(layout1); ASSERT_NE(ptr1, nullptr); size_t ptr1_allocated = tracker_.GetAllocatedLayout(ptr1)->size(); expected.AddRequestedBytes(layout1.size()); expected.AddAllocatedBytes(ptr1_allocated); expected.num_allocations += 1; EXPECT_METRICS_EQ(expected, metrics); constexpr Layout layout2 = Layout(0x10000000U, 1); void* ptr2 = tracker_.Reallocate(ptr1, layout2); EXPECT_EQ(ptr2, nullptr); expected.num_failures += 1; expected.unfulfilled_bytes += layout2.size(); EXPECT_METRICS_EQ(expected, metrics); } TEST_F(TrackingAllocatorTest, CorrectlyAccountsForShiftedBytes) { const TestMetrics& metrics = tracker_.metrics(); ExpectedValues expected; // Find an alignment greater than two block headers. size_t alignment = 1; while (alignment <= (BlockType::kBlockOverhead * 2)) { alignment *= 2; } // Allocate one block to align the usable space of the following block. Layout layout1(alignment - BlockType::kBlockOverhead, alignment); void* ptr1 = tracker_.Allocate(layout1); ASSERT_NE(ptr1, nullptr); auto* block1 = BlockType::FromUsableSpace(ptr1); size_t ptr1_allocated = block1->OuterSize(); expected.AddRequestedBytes(layout1.size()); expected.AddAllocatedBytes(ptr1_allocated); expected.num_allocations += 1; EXPECT_METRICS_EQ(expected, metrics); // Allocate a second block that ends two block headers from an alignment // boundary. Layout layout2(alignment - (BlockType::kBlockOverhead * 2), alignment); void* ptr2 = tracker_.Allocate(layout2); ASSERT_NE(ptr2, nullptr); auto* block2 = BlockType::FromUsableSpace(ptr2); EXPECT_EQ(block2->InnerSize(), layout2.size()); size_t ptr2_allocated = block2->OuterSize(); expected.AddRequestedBytes(layout2.size()); expected.AddAllocatedBytes(ptr2_allocated); expected.num_allocations += 1; EXPECT_METRICS_EQ(expected, metrics); // Allocate a third block to prevent the second block from being coalesced. // The extra bytes should be appended to the second block. Layout layout3(1, alignment); void* ptr3 = tracker_.Allocate(layout3); ASSERT_NE(ptr3, nullptr); auto* block3 = BlockType::FromUsableSpace(ptr3); size_t ptr3_allocated = block3->OuterSize(); size_t shifted = block2->OuterSize() - ptr2_allocated; expected.AddRequestedBytes(layout3.size()); expected.AddAllocatedBytes(ptr3_allocated + shifted); expected.num_allocations += 1; EXPECT_METRICS_EQ(expected, metrics); // Free the second block, which is larger than when it was allocated. tracker_.Deallocate(ptr2); expected.requested_bytes -= layout2.size(); expected.allocated_bytes -= ptr2_allocated + shifted; expected.num_deallocations += 1; EXPECT_METRICS_EQ(expected, metrics); // Allocate the second block again. The trailing space should be appended. ptr2 = tracker_.Allocate(layout2); ASSERT_NE(ptr2, nullptr); block2 = BlockType::FromUsableSpace(ptr2); EXPECT_EQ(block2->OuterSize(), ptr2_allocated + shifted); expected.AddRequestedBytes(layout2.size()); expected.AddAllocatedBytes(ptr2_allocated + shifted); expected.num_allocations += 1; EXPECT_METRICS_EQ(expected, metrics); // Free the third block, which should reclaim space from the second block. tracker_.Deallocate(ptr3); expected.requested_bytes -= layout3.size(); expected.allocated_bytes -= ptr3_allocated + shifted; expected.num_deallocations += 1; EXPECT_METRICS_EQ(expected, metrics); // Free the second block, which is now smaller than when it was allocated. tracker_.Deallocate(ptr2); expected.requested_bytes -= layout2.size(); expected.allocated_bytes -= ptr2_allocated; expected.num_deallocations += 1; EXPECT_METRICS_EQ(expected, metrics); } } // namespace