1 // Copyright 2024 The Pigweed Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License"); you may not
4 // use this file except in compliance with the License. You may obtain a copy of
5 // the License at
6 //
7 // https://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 // License for the specific language governing permissions and limitations under
13 // the License.
14
15 #include "pw_allocator/synchronized_allocator.h"
16
17 #include <cstddef>
18 #include <cstdint>
19 #include <limits>
20
21 #include "pw_allocator/test_harness.h"
22 #include "pw_allocator/testing.h"
23 #include "pw_containers/vector.h"
24 #include "pw_status/status_with_size.h"
25 #include "pw_sync/binary_semaphore.h"
26 #include "pw_sync/interrupt_spin_lock.h"
27 #include "pw_sync/mutex.h"
28 #include "pw_thread/test_thread_context.h"
29 #include "pw_thread/thread.h"
30 #include "pw_thread/thread_core.h"
31 #include "pw_thread/yield.h"
32 #include "pw_unit_test/framework.h"
33
34 // TODO: https://pwbug.dev/365161669 - Express joinability as a build-system
35 // constraint.
36 #if PW_THREAD_JOINING_ENABLED
37
38 namespace {
39
40 // Test fixtures.
41
42 static constexpr size_t kCapacity = 8192;
43 static constexpr size_t kMaxSize = 512;
44 static constexpr size_t kNumAllocations = 128;
45 static constexpr size_t kSize = 64;
46 static constexpr size_t kAlignment = 4;
47 static constexpr size_t kBackgroundRequests = 8;
48
49 using ::pw::allocator::Layout;
50 using ::pw::allocator::SynchronizedAllocator;
51 using AllocatorForTest = ::pw::allocator::test::AllocatorForTest<kCapacity>;
52
53 struct Allocation {
54 void* ptr;
55 Layout layout;
56
57 /// Fills a valid allocation with a pattern of data.
Paint__anone07a60840111::Allocation58 void Paint() {
59 if (ptr != nullptr) {
60 auto* u8 = static_cast<uint8_t*>(ptr);
61 for (size_t i = 0; i < layout.size(); ++i) {
62 u8[i] = static_cast<uint8_t>(i & 0xFF);
63 }
64 }
65 }
66
67 /// Checks that a valid allocation has been properly `Paint`ed. Returns the
68 /// first index where the expected pattern doesn't match, e.g. where memory
69 /// has been corrupted, or `layout.size()` if the memory is all correct.
Inspect__anone07a60840111::Allocation70 size_t Inspect() const {
71 if (ptr != nullptr) {
72 auto* u8 = static_cast<uint8_t*>(ptr);
73 for (size_t i = 0; i < layout.size(); ++i) {
74 if (u8[i] != (i & 0xFF)) {
75 return i;
76 }
77 }
78 }
79 return layout.size();
80 }
81 };
82
83 /// Test fixture that manages a background allocation thread.
84 class Background final {
85 public:
Background(pw::Allocator & allocator)86 Background(pw::Allocator& allocator)
87 : Background(allocator, 1, std::numeric_limits<size_t>::max()) {}
88
Background(pw::Allocator & allocator,uint64_t seed,size_t iterations)89 Background(pw::Allocator& allocator, uint64_t seed, size_t iterations)
90 : background_(allocator, seed, iterations) {
91 background_thread_ = pw::Thread(context_.options(), background_);
92 }
93
~Background()94 ~Background() {
95 background_.Stop();
96 Await();
97 }
98
Await()99 void Await() {
100 background_.Await();
101 if (background_thread_.joinable()) {
102 background_thread_.join();
103 }
104 }
105
106 private:
107 struct TestHarness : public pw::allocator::test::TestHarness {
108 pw::Allocator* allocator = nullptr;
Init__anone07a60840111::Background::TestHarness109 pw::Allocator* Init() override { return allocator; }
110 };
111
112 /// Thread body that uses a test harness to perform random sequences of
113 /// allocations on a synchronous allocator.
114 class BackgroundThreadCore : public pw::thread::ThreadCore {
115 public:
BackgroundThreadCore(pw::Allocator & allocator,uint64_t seed,size_t iterations)116 BackgroundThreadCore(pw::Allocator& allocator,
117 uint64_t seed,
118 size_t iterations)
119 : iterations_(iterations) {
120 test_harness_.allocator = &allocator;
121 test_harness_.set_prng_seed(seed);
122 }
123
Stop()124 void Stop() { semaphore_.release(); }
125
Await()126 void Await() {
127 semaphore_.acquire();
128 semaphore_.release();
129 }
130
131 private:
Run()132 void Run() override {
133 for (size_t i = 0; i < iterations_ && !semaphore_.try_acquire(); ++i) {
134 test_harness_.GenerateRequests(kMaxSize, kBackgroundRequests);
135 pw::this_thread::yield();
136 }
137 semaphore_.release();
138 }
139
140 TestHarness test_harness_;
141 pw::sync::BinarySemaphore semaphore_;
142 size_t iterations_;
143 } background_;
144
145 pw::thread::test::TestThreadContext context_;
146 pw::Thread background_thread_;
147 };
148
149 // Unit tests.
150
151 // The tests below manipulate dynamically allocated memory while a background
152 // thread simultaneously exercises the allocator. Allocations, queries and
153 // resizes may fail, but memory must not be corrupted and the test must not
154 // deadlock.
155
156 template <typename LockType>
TestGetCapacity()157 void TestGetCapacity() {
158 AllocatorForTest allocator;
159 SynchronizedAllocator<LockType> synchronized(allocator);
160 Background background(synchronized);
161
162 pw::StatusWithSize capacity = synchronized.GetCapacity();
163 EXPECT_EQ(capacity.status(), pw::OkStatus());
164 EXPECT_EQ(capacity.size(), kCapacity);
165 }
166
TEST(SynchronizedAllocatorTest,GetCapacitySpinLock)167 TEST(SynchronizedAllocatorTest, GetCapacitySpinLock) {
168 TestGetCapacity<pw::sync::InterruptSpinLock>();
169 }
170
TEST(SynchronizedAllocatorTest,GetCapacityMutex)171 TEST(SynchronizedAllocatorTest, GetCapacityMutex) {
172 TestGetCapacity<pw::sync::Mutex>();
173 }
174
175 template <typename LockType>
TestAllocate()176 void TestAllocate() {
177 AllocatorForTest allocator;
178 SynchronizedAllocator<LockType> synchronized(allocator);
179 Background background(synchronized);
180
181 pw::Vector<Allocation, kNumAllocations> allocations;
182 while (!allocations.full()) {
183 Layout layout(kSize, kAlignment);
184 void* ptr = synchronized.Allocate(layout);
185 Allocation allocation{ptr, layout};
186 allocation.Paint();
187 allocations.push_back(allocation);
188 pw::this_thread::yield();
189 }
190
191 for (const auto& allocation : allocations) {
192 EXPECT_EQ(allocation.Inspect(), allocation.layout.size());
193 synchronized.Deallocate(allocation.ptr);
194 }
195 }
196
TEST(SynchronizedAllocatorTest,AllocateDeallocateInterruptSpinLock)197 TEST(SynchronizedAllocatorTest, AllocateDeallocateInterruptSpinLock) {
198 TestAllocate<pw::sync::InterruptSpinLock>();
199 }
200
TEST(SynchronizedAllocatorTest,AllocateDeallocateMutex)201 TEST(SynchronizedAllocatorTest, AllocateDeallocateMutex) {
202 TestAllocate<pw::sync::Mutex>();
203 }
204
205 template <typename LockType>
TestResize()206 void TestResize() {
207 AllocatorForTest allocator;
208 SynchronizedAllocator<LockType> synchronized(allocator);
209 Background background(synchronized);
210
211 pw::Vector<Allocation, kNumAllocations> allocations;
212 while (!allocations.full()) {
213 Layout layout(kSize, kAlignment);
214 void* ptr = synchronized.Allocate(layout);
215 Allocation allocation{ptr, layout};
216 allocation.Paint();
217 allocations.push_back(allocation);
218 pw::this_thread::yield();
219 }
220
221 // First, resize them smaller.
222 for (auto& allocation : allocations) {
223 EXPECT_EQ(allocation.Inspect(), allocation.layout.size());
224 size_t new_size = allocation.layout.size() / 2;
225 if (!synchronized.Resize(allocation.ptr, new_size)) {
226 continue;
227 }
228 allocation.layout = Layout(new_size, allocation.layout.alignment());
229 allocation.Paint();
230 pw::this_thread::yield();
231 }
232
233 // Then, resize them back to their original size.
234 for (auto& allocation : allocations) {
235 EXPECT_EQ(allocation.Inspect(), allocation.layout.size());
236 size_t old_size = allocation.layout.size() * 2;
237 if (!synchronized.Resize(allocation.ptr, old_size)) {
238 continue;
239 }
240 allocation.layout = Layout(old_size, allocation.layout.alignment());
241 allocation.Paint();
242 pw::this_thread::yield();
243 }
244 }
245
TEST(SynchronizedAllocatorTest,ResizeInterruptSpinLock)246 TEST(SynchronizedAllocatorTest, ResizeInterruptSpinLock) {
247 TestResize<pw::sync::InterruptSpinLock>();
248 }
249
TEST(SynchronizedAllocatorTest,ResizeMutex)250 TEST(SynchronizedAllocatorTest, ResizeMutex) { TestResize<pw::sync::Mutex>(); }
251
252 template <typename LockType>
TestReallocate()253 void TestReallocate() {
254 AllocatorForTest allocator;
255 SynchronizedAllocator<LockType> synchronized(allocator);
256 Background background(synchronized);
257
258 pw::Vector<Allocation, kNumAllocations> allocations;
259 while (!allocations.full()) {
260 Layout layout(kSize, kAlignment);
261 void* ptr = synchronized.Allocate(layout);
262 Allocation allocation{ptr, layout};
263 allocation.Paint();
264 allocations.push_back(allocation);
265 pw::this_thread::yield();
266 }
267
268 for (auto& allocation : allocations) {
269 EXPECT_EQ(allocation.Inspect(), allocation.layout.size());
270 Layout new_layout = allocation.layout.Extend(1);
271 void* new_ptr = synchronized.Reallocate(allocation.ptr, new_layout);
272 if (new_ptr == nullptr) {
273 continue;
274 }
275 allocation.ptr = new_ptr;
276 allocation.layout = new_layout;
277 allocation.Paint();
278 pw::this_thread::yield();
279 }
280 }
281
TEST(SynchronizedAllocatorTest,ReallocateInterruptSpinLock)282 TEST(SynchronizedAllocatorTest, ReallocateInterruptSpinLock) {
283 TestReallocate<pw::sync::InterruptSpinLock>();
284 }
285
TEST(SynchronizedAllocatorTest,ReallocateMutex)286 TEST(SynchronizedAllocatorTest, ReallocateMutex) {
287 TestReallocate<pw::sync::Mutex>();
288 }
289
290 template <typename LockType>
TestGenerateRequests()291 void TestGenerateRequests() {
292 constexpr size_t kNumIterations = 10000;
293 AllocatorForTest allocator;
294 SynchronizedAllocator<LockType> synchronized(allocator);
295 Background background1(synchronized, 1, kNumIterations);
296 Background background2(synchronized, 2, kNumIterations);
297 background1.Await();
298 background2.Await();
299 }
300
TEST(SynchronizedAllocatorTest,GenerateRequestsSpinLock)301 TEST(SynchronizedAllocatorTest, GenerateRequestsSpinLock) {
302 TestGenerateRequests<pw::sync::InterruptSpinLock>();
303 }
304
TEST(SynchronizedAllocatorTest,GenerateRequestsMutex)305 TEST(SynchronizedAllocatorTest, GenerateRequestsMutex) {
306 TestGenerateRequests<pw::sync::Mutex>();
307 }
308
309 } // namespace
310
311 #endif // PW_THREAD_JOINING_ENABLED
312