// 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 #include #include #include #include #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 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& requests) { for (const auto& request : requests) { HandleRequest(request); } Reset(); } bool TestHarness::HandleRequest(const Request& request) { if (allocator_ == nullptr) { allocator_ = Init(); PW_DCHECK_NOTNULL(allocator_); } return std::visit( [this](auto&& r) { using T = std::decay_t; if constexpr (std::is_same_v) { 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) { Allocation* old = RemoveAllocation(r.index); if (old == nullptr) { return false; } BeforeDeallocate(old); allocator_->Deallocate(old); AfterDeallocate(); } else if constexpr (std::is_same_v) { 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(); 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` 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(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