1 //===-- primary_test.cpp ----------------------------------------*- C++ -*-===//
2 //
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6 //
7 //===----------------------------------------------------------------------===//
8 
9 #include "primary32.h"
10 #include "primary64.h"
11 #include "size_class_map.h"
12 
13 #include "gtest/gtest.h"
14 
15 #include <condition_variable>
16 #include <mutex>
17 #include <thread>
18 
19 // Note that with small enough regions, the SizeClassAllocator64 also works on
20 // 32-bit architectures. It's not something we want to encourage, but we still
21 // should ensure the tests pass.
22 
23 template <typename Primary> static void testPrimary() {
24   const scudo::uptr NumberOfAllocations = 32U;
25   auto Deleter = [](Primary *P) {
26     P->unmapTestOnly();
27     delete P;
28   };
29   std::unique_ptr<Primary, decltype(Deleter)> Allocator(new Primary, Deleter);
30   Allocator->init(/*ReleaseToOsInterval=*/-1);
31   typename Primary::CacheT Cache;
32   Cache.init(nullptr, Allocator.get());
33   for (scudo::uptr I = 0; I <= 16U; I++) {
34     const scudo::uptr Size = 1UL << I;
35     if (!Primary::canAllocate(Size))
36       continue;
37     const scudo::uptr ClassId = Primary::SizeClassMap::getClassIdBySize(Size);
38     void *Pointers[NumberOfAllocations];
39     for (scudo::uptr J = 0; J < NumberOfAllocations; J++) {
40       void *P = Cache.allocate(ClassId);
41       memset(P, 'B', Size);
42       Pointers[J] = P;
43     }
44     for (scudo::uptr J = 0; J < NumberOfAllocations; J++)
45       Cache.deallocate(ClassId, Pointers[J]);
46   }
47   Cache.destroy(nullptr);
48   Allocator->releaseToOS();
49   scudo::ScopedString Str(1024);
50   Allocator->getStats(&Str);
51   Str.output();
52 }
53 
54 TEST(ScudoPrimaryTest, BasicPrimary) {
55   using SizeClassMap = scudo::DefaultSizeClassMap;
56   testPrimary<scudo::SizeClassAllocator32<SizeClassMap, 18U>>();
57   testPrimary<scudo::SizeClassAllocator64<SizeClassMap, 24U>>();
58 }
59 
60 // The 64-bit SizeClassAllocator can be easily OOM'd with small region sizes.
61 // For the 32-bit one, it requires actually exhausting memory, so we skip it.
62 TEST(ScudoPrimaryTest, Primary64OOM) {
63   using Primary = scudo::SizeClassAllocator64<scudo::DefaultSizeClassMap, 20U>;
64   using TransferBatch = Primary::CacheT::TransferBatch;
65   Primary Allocator;
66   Allocator.init(/*ReleaseToOsInterval=*/-1);
67   typename Primary::CacheT Cache;
68   scudo::GlobalStats Stats;
69   Stats.init();
70   Cache.init(&Stats, &Allocator);
71   bool AllocationFailed = false;
72   std::vector<TransferBatch *> Batches;
73   const scudo::uptr ClassId = Primary::SizeClassMap::LargestClassId;
74   const scudo::uptr Size = Primary::getSizeByClassId(ClassId);
75   for (scudo::uptr I = 0; I < 10000U; I++) {
76     TransferBatch *B = Allocator.popBatch(&Cache, ClassId);
77     if (!B) {
78       AllocationFailed = true;
79       break;
80     }
81     for (scudo::uptr J = 0; J < B->getCount(); J++)
82       memset(B->get(J), 'B', Size);
83     Batches.push_back(B);
84   }
85   while (!Batches.empty()) {
86     Allocator.pushBatch(ClassId, Batches.back());
87     Batches.pop_back();
88   }
89   Cache.destroy(nullptr);
90   Allocator.releaseToOS();
91   scudo::ScopedString Str(1024);
92   Allocator.getStats(&Str);
93   Str.output();
94   EXPECT_EQ(AllocationFailed, true);
95   Allocator.unmapTestOnly();
96 }
97 
98 template <typename Primary> static void testIteratePrimary() {
99   auto Deleter = [](Primary *P) {
100     P->unmapTestOnly();
101     delete P;
102   };
103   std::unique_ptr<Primary, decltype(Deleter)> Allocator(new Primary, Deleter);
104   Allocator->init(/*ReleaseToOsInterval=*/-1);
105   typename Primary::CacheT Cache;
106   Cache.init(nullptr, Allocator.get());
107   std::vector<std::pair<scudo::uptr, void *>> V;
108   for (scudo::uptr I = 0; I < 64U; I++) {
109     const scudo::uptr Size = std::rand() % Primary::SizeClassMap::MaxSize;
110     const scudo::uptr ClassId = Primary::SizeClassMap::getClassIdBySize(Size);
111     void *P = Cache.allocate(ClassId);
112     V.push_back(std::make_pair(ClassId, P));
113   }
114   scudo::uptr Found = 0;
115   auto Lambda = [V, &Found](scudo::uptr Block) {
116     for (const auto &Pair : V) {
117       if (Pair.second == reinterpret_cast<void *>(Block))
118         Found++;
119     }
120   };
121   Allocator->disable();
122   Allocator->iterateOverBlocks(Lambda);
123   Allocator->enable();
124   EXPECT_EQ(Found, V.size());
125   while (!V.empty()) {
126     auto Pair = V.back();
127     Cache.deallocate(Pair.first, Pair.second);
128     V.pop_back();
129   }
130   Cache.destroy(nullptr);
131   Allocator->releaseToOS();
132   scudo::ScopedString Str(1024);
133   Allocator->getStats(&Str);
134   Str.output();
135 }
136 
137 TEST(ScudoPrimaryTest, PrimaryIterate) {
138   using SizeClassMap = scudo::DefaultSizeClassMap;
139   testIteratePrimary<scudo::SizeClassAllocator32<SizeClassMap, 18U>>();
140   testIteratePrimary<scudo::SizeClassAllocator64<SizeClassMap, 24U>>();
141 }
142 
143 static std::mutex Mutex;
144 static std::condition_variable Cv;
145 static bool Ready = false;
146 
147 template <typename Primary> static void performAllocations(Primary *Allocator) {
148   static THREADLOCAL typename Primary::CacheT Cache;
149   Cache.init(nullptr, Allocator);
150   std::vector<std::pair<scudo::uptr, void *>> V;
151   {
152     std::unique_lock<std::mutex> Lock(Mutex);
153     while (!Ready)
154       Cv.wait(Lock);
155   }
156   for (scudo::uptr I = 0; I < 256U; I++) {
157     const scudo::uptr Size = std::rand() % Primary::SizeClassMap::MaxSize / 4;
158     const scudo::uptr ClassId = Primary::SizeClassMap::getClassIdBySize(Size);
159     void *P = Cache.allocate(ClassId);
160     if (P)
161       V.push_back(std::make_pair(ClassId, P));
162   }
163   while (!V.empty()) {
164     auto Pair = V.back();
165     Cache.deallocate(Pair.first, Pair.second);
166     V.pop_back();
167   }
168   Cache.destroy(nullptr);
169 }
170 
171 template <typename Primary> static void testPrimaryThreaded() {
172   auto Deleter = [](Primary *P) {
173     P->unmapTestOnly();
174     delete P;
175   };
176   std::unique_ptr<Primary, decltype(Deleter)> Allocator(new Primary, Deleter);
177   Allocator->init(/*ReleaseToOsInterval=*/-1);
178   std::thread Threads[32];
179   for (scudo::uptr I = 0; I < ARRAY_SIZE(Threads); I++)
180     Threads[I] = std::thread(performAllocations<Primary>, Allocator.get());
181   {
182     std::unique_lock<std::mutex> Lock(Mutex);
183     Ready = true;
184     Cv.notify_all();
185   }
186   for (auto &T : Threads)
187     T.join();
188   Allocator->releaseToOS();
189   scudo::ScopedString Str(1024);
190   Allocator->getStats(&Str);
191   Str.output();
192 }
193 
194 TEST(ScudoPrimaryTest, PrimaryThreaded) {
195   using SizeClassMap = scudo::SvelteSizeClassMap;
196   testPrimaryThreaded<scudo::SizeClassAllocator32<SizeClassMap, 18U>>();
197   testPrimaryThreaded<scudo::SizeClassAllocator64<SizeClassMap, 24U>>();
198 }
199 
200 // Through a simple allocation that spans two pages, verify that releaseToOS
201 // actually releases some bytes (at least one page worth). This is a regression
202 // test for an error in how the release criteria were computed.
203 template <typename Primary> static void testReleaseToOS() {
204   auto Deleter = [](Primary *P) {
205     P->unmapTestOnly();
206     delete P;
207   };
208   std::unique_ptr<Primary, decltype(Deleter)> Allocator(new Primary, Deleter);
209   Allocator->init(/*ReleaseToOsInterval=*/-1);
210   typename Primary::CacheT Cache;
211   Cache.init(nullptr, Allocator.get());
212   const scudo::uptr Size = scudo::getPageSizeCached() * 2;
213   EXPECT_TRUE(Primary::canAllocate(Size));
214   const scudo::uptr ClassId = Primary::SizeClassMap::getClassIdBySize(Size);
215   void *P = Cache.allocate(ClassId);
216   EXPECT_NE(P, nullptr);
217   Cache.deallocate(ClassId, P);
218   Cache.destroy(nullptr);
219   EXPECT_GT(Allocator->releaseToOS(), 0U);
220 }
221 
222 TEST(ScudoPrimaryTest, ReleaseToOS) {
223   using SizeClassMap = scudo::DefaultSizeClassMap;
224   testReleaseToOS<scudo::SizeClassAllocator32<SizeClassMap, 18U>>();
225   testReleaseToOS<scudo::SizeClassAllocator64<SizeClassMap, 24U>>();
226 }
227