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