| // 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/test_harness.h" |
| |
| #include <algorithm> |
| #include <climits> |
| #include <cstddef> |
| #include <cstdint> |
| #include <type_traits> |
| |
| #include "lib/stdcompat/bit.h" |
| #include "pw_assert/assert.h" |
| #include "pw_assert/check.h" |
| |
| namespace pw::allocator::test { |
| namespace { |
| |
| // Helper to allow static_assert'ing in constexpr-if branches, e.g. that a |
| // visitor for a std::variant are exhaustive. |
| template <typename T> |
| constexpr bool not_reached(T&) { |
| return false; |
| } |
| |
| } // namespace |
| |
| size_t AlignmentFromLShift(size_t lshift, size_t size) { |
| constexpr size_t max_bits = (sizeof(size) * CHAR_BIT) - 1; |
| size_t num_bits = |
| size == 0 ? 1 : std::min(size_t(cpp20::bit_width(size)), max_bits); |
| size_t alignment = 1U << (lshift % num_bits); |
| return std::max(alignment, alignof(TestHarness::Allocation)); |
| } |
| |
| void TestHarness::GenerateRequests(size_t max_size, size_t num_requests) { |
| for (size_t i = 0; i < num_requests; ++i) { |
| GenerateRequest(max_size); |
| } |
| Reset(); |
| } |
| |
| void TestHarness::GenerateRequest(size_t max_size) { |
| Request request; |
| size_t dealloc_threshold = 40; |
| if (available_.has_value()) { |
| // This corresponds to a fixed 10% chance to reallocate and a sliding |
| // scale between: |
| // * when empty, an 80% chance to allocate and a 10% chance to deallocate |
| // * when full, a 30% chance to allocate and a 60% chance to deallocate |
| dealloc_threshold = 80 - (allocated_ * 50) / available_.value(); |
| } |
| do { |
| size_t request_type; |
| prng_->GetInt(request_type); |
| request_type %= 100; |
| if (request_type < dealloc_threshold) { |
| request = GenerateAllocationRequest(max_size); |
| } else if (request_type < 90) { |
| request = GenerateDeallocationRequest(); |
| } else { |
| request = GenerateReallocationRequest(max_size); |
| } |
| } while (!HandleRequest(request)); |
| } |
| |
| AllocationRequest TestHarness::GenerateAllocationRequest(size_t max_size) { |
| AllocationRequest request; |
| request.size = GenerateSize(max_size); |
| uint8_t lshift; |
| prng_->GetInt(lshift); |
| request.alignment = AlignmentFromLShift(lshift, request.size); |
| return request; |
| } |
| |
| DeallocationRequest TestHarness::GenerateDeallocationRequest() { |
| DeallocationRequest request; |
| prng_->GetInt(request.index); |
| return request; |
| } |
| |
| ReallocationRequest TestHarness::GenerateReallocationRequest(size_t max_size) { |
| ReallocationRequest request; |
| prng_->GetInt(request.index); |
| request.new_size = GenerateSize(max_size); |
| return request; |
| } |
| |
| size_t TestHarness::GenerateSize(size_t max_size) { |
| static constexpr size_t kMinSize = sizeof(TestHarness::Allocation); |
| size_t size = 0; |
| if (max_size_.has_value()) { |
| max_size = std::min(max_size, max_size_.value()); |
| } |
| if (max_size <= kMinSize) { |
| return kMinSize; |
| } |
| prng_->GetInt(size); |
| size %= max_size - kMinSize; |
| return kMinSize + size; |
| } |
| |
| void TestHarness::HandleRequests(const Vector<Request>& requests) { |
| for (const auto& request : requests) { |
| HandleRequest(request); |
| } |
| Reset(); |
| } |
| |
| bool TestHarness::HandleRequest(const Request& request) { |
| PW_DCHECK_NOTNULL(allocator_); |
| return std::visit( |
| [this](auto&& r) { |
| using T = std::decay_t<decltype(r)>; |
| |
| if constexpr (std::is_same_v<T, AllocationRequest>) { |
| Layout layout(r.size, r.alignment); |
| BeforeAllocate(layout); |
| void* ptr = allocator_->Allocate(layout); |
| AfterAllocate(ptr); |
| if (ptr != nullptr) { |
| AddAllocation(ptr, layout); |
| max_size_.reset(); |
| } else { |
| max_size_ = std::max(layout.size() / 2, size_t(1)); |
| } |
| } else if constexpr (std::is_same_v<T, DeallocationRequest>) { |
| Allocation* old = RemoveAllocation(r.index); |
| if (old == nullptr) { |
| return false; |
| } |
| BeforeDeallocate(old); |
| allocator_->Deallocate(old); |
| AfterDeallocate(); |
| |
| } else if constexpr (std::is_same_v<T, ReallocationRequest>) { |
| Allocation* old = RemoveAllocation(r.index); |
| if (old == nullptr) { |
| return false; |
| } |
| Layout new_layout(r.new_size, old->layout.alignment()); |
| BeforeReallocate(new_layout); |
| void* new_ptr = allocator_->Reallocate(old, new_layout); |
| AfterReallocate(new_ptr); |
| if (new_ptr == nullptr) { |
| AddAllocation(old, old->layout); |
| } else { |
| AddAllocation(new_ptr, new_layout); |
| } |
| } else { |
| static_assert(not_reached(r), "unsupported request type!"); |
| } |
| return true; |
| }, |
| request); |
| } |
| |
| void TestHarness::Reset() { |
| if (allocator_ == nullptr) { |
| return; |
| } |
| while (!allocations_.empty()) { |
| allocator_->Deallocate(RemoveAllocation(0)); |
| } |
| } |
| |
| void TestHarness::AddAllocation(void* ptr, Layout layout) { |
| constexpr Layout min_layout = Layout::Of<Allocation>(); |
| if (layout.size() < min_layout.size() || |
| layout.alignment() < min_layout.alignment()) { |
| // The harness should want to test small sizes and alignments, but it |
| // needs the layout to be at least `Layout::Of<Allocation>` in order |
| // to persist details about it. If either the size or alignment is too |
| // small, deallocate immediately. |
| BeforeDeallocate(ptr); |
| allocator_->Deallocate(ptr); |
| AfterDeallocate(); |
| return; |
| } |
| auto* bytes = static_cast<std::byte*>(ptr); |
| auto* allocation = ::new (bytes) Allocation(layout); |
| allocations_.push_back(*allocation); |
| allocated_ += layout.size(); |
| ++num_allocations_; |
| } |
| |
| TestHarness::Allocation* TestHarness::RemoveAllocation(size_t index) { |
| // Move the target allocation to the back of the list. |
| if (num_allocations_ == 0) { |
| return nullptr; |
| } |
| index %= num_allocations_; |
| auto prev = allocations_.before_begin(); |
| for (auto& allocation : allocations_) { |
| if (index == 0) { |
| PW_ASSERT(allocated_ >= allocation.layout.size()); |
| allocated_ -= allocation.layout.size(); |
| allocations_.erase_after(prev); |
| --num_allocations_; |
| return &allocation; |
| } |
| --index; |
| ++prev; |
| } |
| PW_CRASH("unreachable"); |
| } |
| |
| } // namespace pw::allocator::test |