// Copyright 2024 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/synchronized_allocator.h" #include #include #include #include "pw_allocator/test_harness.h" #include "pw_allocator/testing.h" #include "pw_containers/vector.h" #include "pw_status/status_with_size.h" #include "pw_sync/binary_semaphore.h" #include "pw_sync/interrupt_spin_lock.h" #include "pw_sync/mutex.h" #include "pw_thread/test_thread_context.h" #include "pw_thread/thread.h" #include "pw_thread/thread_core.h" #include "pw_thread/yield.h" #include "pw_unit_test/framework.h" // TODO: https://pwbug.dev/365161669 - Express joinability as a build-system // constraint. #if PW_THREAD_JOINING_ENABLED namespace { // Test fixtures. static constexpr size_t kCapacity = 8192; static constexpr size_t kMaxSize = 512; static constexpr size_t kNumAllocations = 128; static constexpr size_t kSize = 64; static constexpr size_t kAlignment = 4; static constexpr size_t kBackgroundRequests = 8; using ::pw::allocator::Layout; using ::pw::allocator::SynchronizedAllocator; using ::pw::allocator::test::TestHarness; using AllocatorForTest = ::pw::allocator::test::AllocatorForTest; struct Allocation { void* ptr; Layout layout; /// Fills a valid allocation with a pattern of data. void Paint() { if (ptr != nullptr) { auto* u8 = static_cast(ptr); for (size_t i = 0; i < layout.size(); ++i) { u8[i] = static_cast(i & 0xFF); } } } /// Checks that a valid allocation has been properly `Paint`ed. Returns the /// first index where the expected pattern doesn't match, e.g. where memory /// has been corrupted, or `layout.size()` if the memory is all correct. size_t Inspect() const { if (ptr != nullptr) { auto* u8 = static_cast(ptr); for (size_t i = 0; i < layout.size(); ++i) { if (u8[i] != (i & 0xFF)) { return i; } } } return layout.size(); } }; /// Test fixture that manages a background allocation thread. class Background final { public: Background(pw::Allocator& allocator) : Background(allocator, 1, std::numeric_limits::max()) {} Background(pw::Allocator& allocator, uint64_t seed, size_t iterations) : background_(allocator, seed, iterations) { background_thread_ = pw::Thread(context_.options(), [this] { background_.Run(); }); } ~Background() { background_.Stop(); Await(); } void Await() { background_.Await(); if (background_thread_.joinable()) { background_thread_.join(); } } private: /// Thread body that uses a test harness to perform random sequences of /// allocations on a synchronous allocator. class BackgroundThread { public: BackgroundThread(pw::Allocator& allocator, uint64_t seed, size_t iterations) : iterations_(iterations) { test_harness_.set_allocator(&allocator); test_harness_.set_prng_seed(seed); } void Run() { for (size_t i = 0; i < iterations_ && !semaphore_.try_acquire(); ++i) { test_harness_.GenerateRequests(kMaxSize, kBackgroundRequests); pw::this_thread::yield(); } semaphore_.release(); } void Stop() { semaphore_.release(); } void Await() { semaphore_.acquire(); semaphore_.release(); } private: TestHarness test_harness_; pw::sync::BinarySemaphore semaphore_; size_t iterations_; } background_; pw::thread::test::TestThreadContext context_; pw::Thread background_thread_; }; // Unit tests. // The tests below manipulate dynamically allocated memory while a background // thread simultaneously exercises the allocator. Allocations, queries and // resizes may fail, but memory must not be corrupted and the test must not // deadlock. template void TestGetCapacity() { AllocatorForTest allocator; SynchronizedAllocator synchronized(allocator); Background background(synchronized); pw::StatusWithSize capacity = synchronized.GetCapacity(); EXPECT_EQ(capacity.status(), pw::OkStatus()); EXPECT_EQ(capacity.size(), kCapacity); } TEST(SynchronizedAllocatorTest, GetCapacitySpinLock) { TestGetCapacity(); } TEST(SynchronizedAllocatorTest, GetCapacityMutex) { TestGetCapacity(); } template void TestAllocate() { AllocatorForTest allocator; SynchronizedAllocator synchronized(allocator); Background background(synchronized); pw::Vector allocations; while (!allocations.full()) { Layout layout(kSize, kAlignment); void* ptr = synchronized.Allocate(layout); Allocation allocation{ptr, layout}; allocation.Paint(); allocations.push_back(allocation); pw::this_thread::yield(); } for (const auto& allocation : allocations) { EXPECT_EQ(allocation.Inspect(), allocation.layout.size()); synchronized.Deallocate(allocation.ptr); } } TEST(SynchronizedAllocatorTest, AllocateDeallocateInterruptSpinLock) { TestAllocate(); } TEST(SynchronizedAllocatorTest, AllocateDeallocateMutex) { TestAllocate(); } template void TestResize() { AllocatorForTest allocator; SynchronizedAllocator synchronized(allocator); Background background(synchronized); pw::Vector allocations; while (!allocations.full()) { Layout layout(kSize, kAlignment); void* ptr = synchronized.Allocate(layout); Allocation allocation{ptr, layout}; allocation.Paint(); allocations.push_back(allocation); pw::this_thread::yield(); } // First, resize them smaller. for (auto& allocation : allocations) { EXPECT_EQ(allocation.Inspect(), allocation.layout.size()); size_t new_size = allocation.layout.size() / 2; if (!synchronized.Resize(allocation.ptr, new_size)) { continue; } allocation.layout = Layout(new_size, allocation.layout.alignment()); allocation.Paint(); pw::this_thread::yield(); } // Then, resize them back to their original size. for (auto& allocation : allocations) { EXPECT_EQ(allocation.Inspect(), allocation.layout.size()); size_t old_size = allocation.layout.size() * 2; if (!synchronized.Resize(allocation.ptr, old_size)) { continue; } allocation.layout = Layout(old_size, allocation.layout.alignment()); allocation.Paint(); pw::this_thread::yield(); } } TEST(SynchronizedAllocatorTest, ResizeInterruptSpinLock) { TestResize(); } TEST(SynchronizedAllocatorTest, ResizeMutex) { TestResize(); } template void TestReallocate() { AllocatorForTest allocator; SynchronizedAllocator synchronized(allocator); Background background(synchronized); pw::Vector allocations; while (!allocations.full()) { Layout layout(kSize, kAlignment); void* ptr = synchronized.Allocate(layout); Allocation allocation{ptr, layout}; allocation.Paint(); allocations.push_back(allocation); pw::this_thread::yield(); } for (auto& allocation : allocations) { EXPECT_EQ(allocation.Inspect(), allocation.layout.size()); Layout new_layout = allocation.layout.Extend(1); void* new_ptr = synchronized.Reallocate(allocation.ptr, new_layout); if (new_ptr == nullptr) { continue; } allocation.ptr = new_ptr; allocation.layout = new_layout; allocation.Paint(); pw::this_thread::yield(); } } TEST(SynchronizedAllocatorTest, ReallocateInterruptSpinLock) { TestReallocate(); } TEST(SynchronizedAllocatorTest, ReallocateMutex) { TestReallocate(); } template void TestGenerateRequests() { constexpr size_t kNumIterations = 10000; AllocatorForTest allocator; SynchronizedAllocator synchronized(allocator); Background background1(synchronized, 1, kNumIterations); Background background2(synchronized, 2, kNumIterations); background1.Await(); background2.Await(); } TEST(SynchronizedAllocatorTest, GenerateRequestsSpinLock) { TestGenerateRequests(); } TEST(SynchronizedAllocatorTest, GenerateRequestsMutex) { TestGenerateRequests(); } } // namespace #endif // PW_THREAD_JOINING_ENABLED