partition_alloc_perftest.cc 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. // Copyright 2019 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. #include <algorithm>
  5. #include <atomic>
  6. #include <limits>
  7. #include <memory>
  8. #include <vector>
  9. #include "base/allocator/partition_allocator/partition_alloc.h"
  10. #include "base/allocator/partition_allocator/partition_alloc_base/logging.h"
  11. #include "base/allocator/partition_allocator/partition_alloc_base/strings/stringprintf.h"
  12. #include "base/allocator/partition_allocator/partition_alloc_base/threading/platform_thread_for_testing.h"
  13. #include "base/allocator/partition_allocator/partition_alloc_base/time/time.h"
  14. #include "base/allocator/partition_allocator/partition_alloc_check.h"
  15. #include "base/allocator/partition_allocator/thread_cache.h"
  16. #include "base/timer/lap_timer.h"
  17. #include "build/build_config.h"
  18. #include "testing/gtest/include/gtest/gtest.h"
  19. #include "testing/perf/perf_result_reporter.h"
  20. #if BUILDFLAG(IS_ANDROID) || defined(ARCH_CPU_32_BITS) || BUILDFLAG(IS_FUCHSIA)
  21. // Some tests allocate many GB of memory, which can cause issues on Android and
  22. // address-space exhaustion for any 32-bit process.
  23. #define MEMORY_CONSTRAINED
  24. #endif
  25. namespace partition_alloc::internal {
  26. namespace {
  27. // Change kTimeLimit to something higher if you need more time to capture a
  28. // trace.
  29. constexpr ::base::TimeDelta kTimeLimit = ::base::Seconds(2);
  30. constexpr int kWarmupRuns = 10000;
  31. constexpr int kTimeCheckInterval = 100000;
  32. constexpr size_t kAllocSize = 40;
  33. // Size constants are mostly arbitrary, but try to simulate something like CSS
  34. // parsing which consists of lots of relatively small objects.
  35. constexpr int kMultiBucketMinimumSize = 24;
  36. constexpr int kMultiBucketIncrement = 13;
  37. // Final size is 24 + (13 * 22) = 310 bytes.
  38. constexpr int kMultiBucketRounds = 22;
  39. constexpr char kMetricPrefixMemoryAllocation[] = "MemoryAllocation.";
  40. constexpr char kMetricThroughput[] = "throughput";
  41. constexpr char kMetricTimePerAllocation[] = "time_per_allocation";
  42. perf_test::PerfResultReporter SetUpReporter(const std::string& story_name) {
  43. perf_test::PerfResultReporter reporter(kMetricPrefixMemoryAllocation,
  44. story_name);
  45. reporter.RegisterImportantMetric(kMetricThroughput, "runs/s");
  46. reporter.RegisterImportantMetric(kMetricTimePerAllocation, "ns");
  47. return reporter;
  48. }
  49. enum class AllocatorType {
  50. kSystem,
  51. kPartitionAlloc,
  52. kPartitionAllocWithThreadCache
  53. };
  54. class Allocator {
  55. public:
  56. Allocator() = default;
  57. virtual ~Allocator() = default;
  58. virtual void* Alloc(size_t size) = 0;
  59. virtual void Free(void* data) = 0;
  60. };
  61. class SystemAllocator : public Allocator {
  62. public:
  63. SystemAllocator() = default;
  64. ~SystemAllocator() override = default;
  65. void* Alloc(size_t size) override { return malloc(size); }
  66. void Free(void* data) override { free(data); }
  67. };
  68. class PartitionAllocator : public Allocator {
  69. public:
  70. PartitionAllocator() = default;
  71. ~PartitionAllocator() override { alloc_.DestructForTesting(); }
  72. void* Alloc(size_t size) override {
  73. return alloc_.AllocWithFlagsNoHooks(0, size, PartitionPageSize());
  74. }
  75. void Free(void* data) override { ThreadSafePartitionRoot::FreeNoHooks(data); }
  76. private:
  77. ThreadSafePartitionRoot alloc_{{
  78. PartitionOptions::AlignedAlloc::kDisallowed,
  79. PartitionOptions::ThreadCache::kDisabled,
  80. PartitionOptions::Quarantine::kDisallowed,
  81. PartitionOptions::Cookie::kAllowed,
  82. PartitionOptions::BackupRefPtr::kDisabled,
  83. PartitionOptions::BackupRefPtrZapping::kDisabled,
  84. PartitionOptions::UseConfigurablePool::kNo,
  85. }};
  86. };
  87. // Only one partition with a thread cache.
  88. ThreadSafePartitionRoot* g_partition_root = nullptr;
  89. class PartitionAllocatorWithThreadCache : public Allocator {
  90. public:
  91. explicit PartitionAllocatorWithThreadCache(bool use_alternate_bucket_dist) {
  92. if (!g_partition_root) {
  93. g_partition_root = new ThreadSafePartitionRoot({
  94. PartitionOptions::AlignedAlloc::kDisallowed,
  95. PartitionOptions::ThreadCache::kEnabled,
  96. PartitionOptions::Quarantine::kDisallowed,
  97. PartitionOptions::Cookie::kAllowed,
  98. PartitionOptions::BackupRefPtr::kDisabled,
  99. PartitionOptions::BackupRefPtrZapping::kDisabled,
  100. PartitionOptions::UseConfigurablePool::kNo,
  101. });
  102. }
  103. ThreadCacheRegistry::Instance().PurgeAll();
  104. if (!use_alternate_bucket_dist)
  105. g_partition_root->SwitchToDenserBucketDistribution();
  106. else
  107. g_partition_root->ResetBucketDistributionForTesting();
  108. }
  109. ~PartitionAllocatorWithThreadCache() override = default;
  110. void* Alloc(size_t size) override {
  111. return g_partition_root->AllocWithFlagsNoHooks(0, size,
  112. PartitionPageSize());
  113. }
  114. void Free(void* data) override { ThreadSafePartitionRoot::FreeNoHooks(data); }
  115. };
  116. class TestLoopThread : public base::PlatformThreadForTesting::Delegate {
  117. public:
  118. TestLoopThread(float (*test_fn)(Allocator*), Allocator* allocator)
  119. : test_fn_(test_fn), allocator_(allocator) {
  120. PA_CHECK(base::PlatformThreadForTesting::Create(0, this, &thread_handle_));
  121. }
  122. float Run() {
  123. base::PlatformThreadForTesting::Join(thread_handle_);
  124. return laps_per_second_;
  125. }
  126. void ThreadMain() override { laps_per_second_ = test_fn_(allocator_); }
  127. float (*test_fn_)(Allocator*) = nullptr;
  128. Allocator* allocator_ = nullptr;
  129. base::PlatformThreadHandle thread_handle_;
  130. std::atomic<float> laps_per_second_;
  131. };
  132. void DisplayResults(const std::string& story_name,
  133. float iterations_per_second) {
  134. auto reporter = SetUpReporter(story_name);
  135. reporter.AddResult(kMetricThroughput, iterations_per_second);
  136. reporter.AddResult(kMetricTimePerAllocation,
  137. static_cast<size_t>(1e9 / iterations_per_second));
  138. }
  139. class MemoryAllocationPerfNode {
  140. public:
  141. MemoryAllocationPerfNode* GetNext() const { return next_; }
  142. void SetNext(MemoryAllocationPerfNode* p) { next_ = p; }
  143. static void FreeAll(MemoryAllocationPerfNode* first, Allocator* alloc) {
  144. MemoryAllocationPerfNode* cur = first;
  145. while (cur != nullptr) {
  146. MemoryAllocationPerfNode* next = cur->GetNext();
  147. alloc->Free(cur);
  148. cur = next;
  149. }
  150. }
  151. private:
  152. MemoryAllocationPerfNode* next_ = nullptr;
  153. };
  154. #if !defined(MEMORY_CONSTRAINED)
  155. float SingleBucket(Allocator* allocator) {
  156. auto* first =
  157. reinterpret_cast<MemoryAllocationPerfNode*>(allocator->Alloc(kAllocSize));
  158. size_t allocated_memory = kAllocSize;
  159. base::LapTimer timer(kWarmupRuns, kTimeLimit, kTimeCheckInterval);
  160. MemoryAllocationPerfNode* cur = first;
  161. do {
  162. auto* next = reinterpret_cast<MemoryAllocationPerfNode*>(
  163. allocator->Alloc(kAllocSize));
  164. PA_CHECK(next != nullptr);
  165. cur->SetNext(next);
  166. cur = next;
  167. timer.NextLap();
  168. allocated_memory += kAllocSize;
  169. // With multiple threads, can get OOM otherwise.
  170. if (allocated_memory > 200e6) {
  171. cur->SetNext(nullptr);
  172. MemoryAllocationPerfNode::FreeAll(first->GetNext(), allocator);
  173. cur = first;
  174. allocated_memory = kAllocSize;
  175. }
  176. } while (!timer.HasTimeLimitExpired());
  177. // next_ = nullptr only works if the class constructor is called (it's not
  178. // called in this case because then we can allocate arbitrary-length
  179. // payloads.)
  180. cur->SetNext(nullptr);
  181. MemoryAllocationPerfNode::FreeAll(first, allocator);
  182. return timer.LapsPerSecond();
  183. }
  184. #endif // defined(MEMORY_CONSTRAINED)
  185. float SingleBucketWithFree(Allocator* allocator) {
  186. // Allocate an initial element to make sure the bucket stays set up.
  187. void* elem = allocator->Alloc(kAllocSize);
  188. base::LapTimer timer(kWarmupRuns, kTimeLimit, kTimeCheckInterval);
  189. do {
  190. void* cur = allocator->Alloc(kAllocSize);
  191. PA_CHECK(cur != nullptr);
  192. allocator->Free(cur);
  193. timer.NextLap();
  194. } while (!timer.HasTimeLimitExpired());
  195. allocator->Free(elem);
  196. return timer.LapsPerSecond();
  197. }
  198. #if !defined(MEMORY_CONSTRAINED)
  199. float MultiBucket(Allocator* allocator) {
  200. auto* first =
  201. reinterpret_cast<MemoryAllocationPerfNode*>(allocator->Alloc(kAllocSize));
  202. MemoryAllocationPerfNode* cur = first;
  203. size_t allocated_memory = kAllocSize;
  204. base::LapTimer timer(kWarmupRuns, kTimeLimit, kTimeCheckInterval);
  205. do {
  206. for (int i = 0; i < kMultiBucketRounds; i++) {
  207. size_t size = kMultiBucketMinimumSize + (i * kMultiBucketIncrement);
  208. auto* next =
  209. reinterpret_cast<MemoryAllocationPerfNode*>(allocator->Alloc(size));
  210. PA_CHECK(next != nullptr);
  211. cur->SetNext(next);
  212. cur = next;
  213. allocated_memory += size;
  214. }
  215. // Can OOM with multiple threads.
  216. if (allocated_memory > 100e6) {
  217. cur->SetNext(nullptr);
  218. MemoryAllocationPerfNode::FreeAll(first->GetNext(), allocator);
  219. cur = first;
  220. allocated_memory = kAllocSize;
  221. }
  222. timer.NextLap();
  223. } while (!timer.HasTimeLimitExpired());
  224. cur->SetNext(nullptr);
  225. MemoryAllocationPerfNode::FreeAll(first, allocator);
  226. return timer.LapsPerSecond() * kMultiBucketRounds;
  227. }
  228. #endif // defined(MEMORY_CONSTRAINED)
  229. float MultiBucketWithFree(Allocator* allocator) {
  230. std::vector<void*> elems;
  231. elems.reserve(kMultiBucketRounds);
  232. // Do an initial round of allocation to make sure that the buckets stay in
  233. // use (and aren't accidentally released back to the OS).
  234. for (int i = 0; i < kMultiBucketRounds; i++) {
  235. void* cur =
  236. allocator->Alloc(kMultiBucketMinimumSize + (i * kMultiBucketIncrement));
  237. PA_CHECK(cur != nullptr);
  238. elems.push_back(cur);
  239. }
  240. base::LapTimer timer(kWarmupRuns, kTimeLimit, kTimeCheckInterval);
  241. do {
  242. for (int i = 0; i < kMultiBucketRounds; i++) {
  243. void* cur = allocator->Alloc(kMultiBucketMinimumSize +
  244. (i * kMultiBucketIncrement));
  245. PA_CHECK(cur != nullptr);
  246. allocator->Free(cur);
  247. }
  248. timer.NextLap();
  249. } while (!timer.HasTimeLimitExpired());
  250. for (void* ptr : elems) {
  251. allocator->Free(ptr);
  252. }
  253. return timer.LapsPerSecond() * kMultiBucketRounds;
  254. }
  255. float DirectMapped(Allocator* allocator) {
  256. constexpr size_t kSize = 2 * 1000 * 1000;
  257. base::LapTimer timer(kWarmupRuns, kTimeLimit, kTimeCheckInterval);
  258. do {
  259. void* cur = allocator->Alloc(kSize);
  260. PA_CHECK(cur != nullptr);
  261. allocator->Free(cur);
  262. timer.NextLap();
  263. } while (!timer.HasTimeLimitExpired());
  264. return timer.LapsPerSecond();
  265. }
  266. std::unique_ptr<Allocator> CreateAllocator(AllocatorType type,
  267. bool use_alternate_bucket_dist) {
  268. switch (type) {
  269. case AllocatorType::kSystem:
  270. return std::make_unique<SystemAllocator>();
  271. case AllocatorType::kPartitionAlloc:
  272. return std::make_unique<PartitionAllocator>();
  273. case AllocatorType::kPartitionAllocWithThreadCache:
  274. return std::make_unique<PartitionAllocatorWithThreadCache>(
  275. use_alternate_bucket_dist);
  276. }
  277. }
  278. void LogResults(int thread_count,
  279. AllocatorType alloc_type,
  280. uint64_t total_laps_per_second,
  281. uint64_t min_laps_per_second) {
  282. PA_LOG(INFO) << "RESULTSCSV: " << thread_count << ","
  283. << static_cast<int>(alloc_type) << "," << total_laps_per_second
  284. << "," << min_laps_per_second;
  285. }
  286. void RunTest(int thread_count,
  287. bool use_alternate_bucket_dist,
  288. AllocatorType alloc_type,
  289. float (*test_fn)(Allocator*),
  290. float (*noisy_neighbor_fn)(Allocator*),
  291. const char* story_base_name) {
  292. auto alloc = CreateAllocator(alloc_type, use_alternate_bucket_dist);
  293. std::unique_ptr<TestLoopThread> noisy_neighbor_thread = nullptr;
  294. if (noisy_neighbor_fn) {
  295. noisy_neighbor_thread =
  296. std::make_unique<TestLoopThread>(noisy_neighbor_fn, alloc.get());
  297. }
  298. std::vector<std::unique_ptr<TestLoopThread>> threads;
  299. for (int i = 0; i < thread_count; ++i) {
  300. threads.push_back(std::make_unique<TestLoopThread>(test_fn, alloc.get()));
  301. }
  302. uint64_t total_laps_per_second = 0;
  303. uint64_t min_laps_per_second = std::numeric_limits<uint64_t>::max();
  304. for (int i = 0; i < thread_count; ++i) {
  305. uint64_t laps_per_second = threads[i]->Run();
  306. min_laps_per_second = std::min(laps_per_second, min_laps_per_second);
  307. total_laps_per_second += laps_per_second;
  308. }
  309. if (noisy_neighbor_thread)
  310. noisy_neighbor_thread->Run();
  311. char const* alloc_type_str;
  312. switch (alloc_type) {
  313. case AllocatorType::kSystem:
  314. alloc_type_str = "System";
  315. break;
  316. case AllocatorType::kPartitionAlloc:
  317. alloc_type_str = "PartitionAlloc";
  318. break;
  319. case AllocatorType::kPartitionAllocWithThreadCache:
  320. alloc_type_str = "PartitionAllocWithThreadCache";
  321. break;
  322. }
  323. std::string name = base::TruncatingStringPrintf(
  324. "%s%s_%s_%d", kMetricPrefixMemoryAllocation, story_base_name,
  325. alloc_type_str, thread_count);
  326. DisplayResults(name + "_total", total_laps_per_second);
  327. DisplayResults(name + "_worst", min_laps_per_second);
  328. LogResults(thread_count, alloc_type, total_laps_per_second,
  329. min_laps_per_second);
  330. }
  331. class PartitionAllocMemoryAllocationPerfTest
  332. : public testing::TestWithParam<std::tuple<int, bool, AllocatorType>> {};
  333. // Only one partition with a thread cache: cannot use the thread cache when
  334. // PartitionAlloc is malloc().
  335. INSTANTIATE_TEST_SUITE_P(
  336. ,
  337. PartitionAllocMemoryAllocationPerfTest,
  338. ::testing::Combine(
  339. ::testing::Values(1, 2, 3, 4),
  340. ::testing::Values(false, true),
  341. ::testing::Values(AllocatorType::kSystem,
  342. AllocatorType::kPartitionAlloc,
  343. AllocatorType::kPartitionAllocWithThreadCache)));
  344. // This test (and the other one below) allocates a large amount of memory, which
  345. // can cause issues on Android.
  346. #if !defined(MEMORY_CONSTRAINED)
  347. TEST_P(PartitionAllocMemoryAllocationPerfTest, SingleBucket) {
  348. auto params = GetParam();
  349. RunTest(std::get<int>(params), std::get<bool>(params),
  350. std::get<AllocatorType>(params), SingleBucket, nullptr,
  351. "SingleBucket");
  352. }
  353. #endif // defined(MEMORY_CONSTRAINED)
  354. TEST_P(PartitionAllocMemoryAllocationPerfTest, SingleBucketWithFree) {
  355. auto params = GetParam();
  356. RunTest(std::get<int>(params), std::get<bool>(params),
  357. std::get<AllocatorType>(params), SingleBucketWithFree, nullptr,
  358. "SingleBucketWithFree");
  359. }
  360. #if !defined(MEMORY_CONSTRAINED)
  361. TEST_P(PartitionAllocMemoryAllocationPerfTest, MultiBucket) {
  362. auto params = GetParam();
  363. RunTest(std::get<int>(params), std::get<bool>(params),
  364. std::get<AllocatorType>(params), MultiBucket, nullptr, "MultiBucket");
  365. }
  366. #endif // defined(MEMORY_CONSTRAINED)
  367. TEST_P(PartitionAllocMemoryAllocationPerfTest, MultiBucketWithFree) {
  368. auto params = GetParam();
  369. RunTest(std::get<int>(params), std::get<bool>(params),
  370. std::get<AllocatorType>(params), MultiBucketWithFree, nullptr,
  371. "MultiBucketWithFree");
  372. }
  373. TEST_P(PartitionAllocMemoryAllocationPerfTest, DirectMapped) {
  374. auto params = GetParam();
  375. RunTest(std::get<int>(params), std::get<bool>(params),
  376. std::get<AllocatorType>(params), DirectMapped, nullptr,
  377. "DirectMapped");
  378. }
  379. #if !defined(MEMORY_CONSTRAINED)
  380. TEST_P(PartitionAllocMemoryAllocationPerfTest,
  381. DISABLED_MultiBucketWithNoisyNeighbor) {
  382. auto params = GetParam();
  383. RunTest(std::get<int>(params), std::get<bool>(params),
  384. std::get<AllocatorType>(params), MultiBucket, DirectMapped,
  385. "MultiBucketWithNoisyNeighbor");
  386. }
  387. #endif // !defined(MEMORY_CONSTRAINED)
  388. } // namespace
  389. } // namespace partition_alloc::internal