| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "partition_alloc/thread_cache.h" |
| |
| #include <algorithm> |
| #include <atomic> |
| #include <vector> |
| |
| #include "build/build_config.h" |
| #include "partition_alloc/extended_api.h" |
| #include "partition_alloc/partition_address_space.h" |
| #include "partition_alloc/partition_alloc_base/thread_annotations.h" |
| #include "partition_alloc/partition_alloc_base/threading/platform_thread_for_testing.h" |
| #include "partition_alloc/partition_alloc_buildflags.h" |
| #include "partition_alloc/partition_alloc_config.h" |
| #include "partition_alloc/partition_alloc_for_testing.h" |
| #include "partition_alloc/partition_lock.h" |
| #include "partition_alloc/partition_root.h" |
| #include "partition_alloc/tagging.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| // With *SAN, PartitionAlloc is replaced in partition_alloc.h by ASAN, so we |
| // cannot test the thread cache. |
| // |
| // Finally, the thread cache is not supported on all platforms. |
| #if !defined(MEMORY_TOOL_REPLACES_ALLOCATOR) && \ |
| PA_CONFIG(THREAD_CACHE_SUPPORTED) |
| |
| namespace partition_alloc { |
| |
| using BucketDistribution = PartitionRoot::BucketDistribution; |
| namespace { |
| |
| constexpr size_t kSmallSize = 33; // Must be large enough to fit extras. |
| constexpr size_t kDefaultCountForSmallBucket = |
| ThreadCache::kSmallBucketBaseCount * ThreadCache::kDefaultMultiplier; |
| constexpr size_t kFillCountForSmallBucket = |
| kDefaultCountForSmallBucket / ThreadCache::kBatchFillRatio; |
| |
| constexpr size_t kMediumSize = 200; |
| constexpr size_t kDefaultCountForMediumBucket = kDefaultCountForSmallBucket / 2; |
| constexpr size_t kFillCountForMediumBucket = |
| kDefaultCountForMediumBucket / ThreadCache::kBatchFillRatio; |
| |
| static_assert(kMediumSize <= ThreadCache::kDefaultSizeThreshold, ""); |
| |
| class DeltaCounter { |
| public: |
| explicit DeltaCounter(uint64_t& value) |
| : current_value_(value), initial_value_(value) {} |
| void Reset() { initial_value_ = current_value_; } |
| uint64_t Delta() const { return current_value_ - initial_value_; } |
| |
| private: |
| uint64_t& current_value_; |
| uint64_t initial_value_; |
| }; |
| |
| // Forbid extras, since they make finding out which bucket is used harder. |
| std::unique_ptr<PartitionAllocatorForTesting> CreateAllocator() { |
| PartitionOptions opts; |
| opts.aligned_alloc = PartitionOptions::kAllowed; |
| #if !BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC) |
| opts.thread_cache = PartitionOptions::kEnabled; |
| #endif // BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC) |
| opts.star_scan_quarantine = PartitionOptions::kAllowed; |
| std::unique_ptr<PartitionAllocatorForTesting> allocator = |
| std::make_unique<PartitionAllocatorForTesting>(opts); |
| allocator->root()->UncapEmptySlotSpanMemoryForTesting(); |
| |
| return allocator; |
| } |
| |
| } // namespace |
| |
| class PartitionAllocThreadCacheTest |
| : public ::testing::TestWithParam<PartitionRoot::BucketDistribution> { |
| public: |
| PartitionAllocThreadCacheTest() |
| : allocator_(CreateAllocator()), scope_(allocator_->root()) {} |
| |
| ~PartitionAllocThreadCacheTest() override { |
| ThreadCache::SetLargestCachedSize(ThreadCache::kDefaultSizeThreshold); |
| |
| // Cleanup the global state so next test can recreate ThreadCache. |
| if (ThreadCache::IsTombstone(ThreadCache::Get())) { |
| ThreadCache::RemoveTombstoneForTesting(); |
| } |
| } |
| |
| protected: |
| void SetUp() override { |
| PartitionRoot* root = allocator_->root(); |
| switch (GetParam()) { |
| case BucketDistribution::kNeutral: |
| root->ResetBucketDistributionForTesting(); |
| break; |
| case BucketDistribution::kDenser: |
| root->SwitchToDenserBucketDistribution(); |
| break; |
| } |
| |
| ThreadCacheRegistry::Instance().SetThreadCacheMultiplier( |
| ThreadCache::kDefaultMultiplier); |
| ThreadCacheRegistry::Instance().SetPurgingConfiguration( |
| kMinPurgeInterval, kMaxPurgeInterval, kDefaultPurgeInterval, |
| kMinCachedMemoryForPurgingBytes); |
| ThreadCache::SetLargestCachedSize(ThreadCache::kLargeSizeThreshold); |
| |
| // Make sure that enough slot spans have been touched, otherwise cache fill |
| // becomes unpredictable (because it doesn't take slow paths in the |
| // allocator), which is an issue for tests. |
| FillThreadCacheAndReturnIndex(kSmallSize, 1000); |
| FillThreadCacheAndReturnIndex(kMediumSize, 1000); |
| |
| // There are allocations, a thread cache is created. |
| auto* tcache = root->thread_cache_for_testing(); |
| ASSERT_TRUE(tcache); |
| |
| ThreadCacheRegistry::Instance().ResetForTesting(); |
| tcache->ResetForTesting(); |
| } |
| |
| void TearDown() override { |
| auto* tcache = root()->thread_cache_for_testing(); |
| ASSERT_TRUE(tcache); |
| tcache->Purge(); |
| |
| ASSERT_EQ(root()->get_total_size_of_allocated_bytes(), |
| GetBucketSizeForThreadCache()); |
| } |
| |
| PartitionRoot* root() { return allocator_->root(); } |
| |
| // Returns the size of the smallest bucket fitting an allocation of |
| // |sizeof(ThreadCache)| bytes. |
| size_t GetBucketSizeForThreadCache() { |
| size_t tc_bucket_index = root()->SizeToBucketIndex( |
| sizeof(ThreadCache), PartitionRoot::BucketDistribution::kNeutral); |
| auto* tc_bucket = &root()->buckets[tc_bucket_index]; |
| return tc_bucket->slot_size; |
| } |
| |
| static size_t SizeToIndex(size_t size) { |
| return PartitionRoot::SizeToBucketIndex(size, GetParam()); |
| } |
| |
| size_t FillThreadCacheAndReturnIndex(size_t raw_size, size_t count = 1) { |
| uint16_t bucket_index = SizeToIndex(raw_size); |
| std::vector<void*> allocated_data; |
| |
| for (size_t i = 0; i < count; ++i) { |
| allocated_data.push_back( |
| root()->Alloc(root()->AdjustSizeForExtrasSubtract(raw_size), "")); |
| } |
| for (void* ptr : allocated_data) { |
| root()->Free(ptr); |
| } |
| |
| return bucket_index; |
| } |
| |
| void FillThreadCacheWithMemory(size_t target_cached_memory) { |
| for (int batch : {1, 2, 4, 8, 16}) { |
| for (size_t raw_size = root()->AdjustSizeForExtrasAdd(1); |
| raw_size <= ThreadCache::kLargeSizeThreshold; raw_size++) { |
| FillThreadCacheAndReturnIndex(raw_size, batch); |
| |
| if (ThreadCache::Get()->CachedMemory() >= target_cached_memory) { |
| return; |
| } |
| } |
| } |
| |
| ASSERT_GE(ThreadCache::Get()->CachedMemory(), target_cached_memory); |
| } |
| |
| std::unique_ptr<PartitionAllocatorForTesting> allocator_; |
| internal::ThreadCacheProcessScopeForTesting scope_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(AlternateBucketDistribution, |
| PartitionAllocThreadCacheTest, |
| ::testing::Values(BucketDistribution::kNeutral, |
| BucketDistribution::kDenser)); |
| |
| TEST_P(PartitionAllocThreadCacheTest, Simple) { |
| // There is a cache. |
| auto* tcache = root()->thread_cache_for_testing(); |
| EXPECT_TRUE(tcache); |
| DeltaCounter batch_fill_counter(tcache->stats_for_testing().batch_fill_count); |
| |
| void* ptr = |
| root()->Alloc(root()->AdjustSizeForExtrasSubtract(kSmallSize), ""); |
| ASSERT_TRUE(ptr); |
| |
| uint16_t index = SizeToIndex(kSmallSize); |
| EXPECT_EQ(kFillCountForSmallBucket - 1, |
| tcache->bucket_count_for_testing(index)); |
| |
| root()->Free(ptr); |
| // Freeing fills the thread cache. |
| EXPECT_EQ(kFillCountForSmallBucket, tcache->bucket_count_for_testing(index)); |
| |
| void* ptr2 = |
| root()->Alloc(root()->AdjustSizeForExtrasSubtract(kSmallSize), ""); |
| // MTE-untag, because Free() changes tag. |
| EXPECT_EQ(UntagPtr(ptr), UntagPtr(ptr2)); |
| // Allocated from the thread cache. |
| EXPECT_EQ(kFillCountForSmallBucket - 1, |
| tcache->bucket_count_for_testing(index)); |
| |
| EXPECT_EQ(1u, batch_fill_counter.Delta()); |
| |
| root()->Free(ptr2); |
| } |
| |
| TEST_P(PartitionAllocThreadCacheTest, InexactSizeMatch) { |
| void* ptr = |
| root()->Alloc(root()->AdjustSizeForExtrasSubtract(kSmallSize), ""); |
| ASSERT_TRUE(ptr); |
| |
| // There is a cache. |
| auto* tcache = root()->thread_cache_for_testing(); |
| EXPECT_TRUE(tcache); |
| |
| uint16_t index = SizeToIndex(kSmallSize); |
| EXPECT_EQ(kFillCountForSmallBucket - 1, |
| tcache->bucket_count_for_testing(index)); |
| |
| root()->Free(ptr); |
| // Freeing fills the thread cache. |
| EXPECT_EQ(kFillCountForSmallBucket, tcache->bucket_count_for_testing(index)); |
| |
| void* ptr2 = |
| root()->Alloc(root()->AdjustSizeForExtrasSubtract(kSmallSize + 1), ""); |
| // MTE-untag, because Free() changes tag. |
| EXPECT_EQ(UntagPtr(ptr), UntagPtr(ptr2)); |
| // Allocated from the thread cache. |
| EXPECT_EQ(kFillCountForSmallBucket - 1, |
| tcache->bucket_count_for_testing(index)); |
| root()->Free(ptr2); |
| } |
| |
| TEST_P(PartitionAllocThreadCacheTest, MultipleObjectsCachedPerBucket) { |
| auto* tcache = root()->thread_cache_for_testing(); |
| DeltaCounter batch_fill_counter{tcache->stats_for_testing().batch_fill_count}; |
| size_t bucket_index = |
| FillThreadCacheAndReturnIndex(kMediumSize, kFillCountForMediumBucket + 2); |
| EXPECT_EQ(2 * kFillCountForMediumBucket, |
| tcache->bucket_count_for_testing(bucket_index)); |
| // 2 batches, since there were more than |kFillCountForMediumBucket| |
| // allocations. |
| EXPECT_EQ(2u, batch_fill_counter.Delta()); |
| } |
| |
| TEST_P(PartitionAllocThreadCacheTest, ObjectsCachedCountIsLimited) { |
| size_t bucket_index = FillThreadCacheAndReturnIndex(kMediumSize, 1000); |
| auto* tcache = root()->thread_cache_for_testing(); |
| EXPECT_LT(tcache->bucket_count_for_testing(bucket_index), 1000u); |
| } |
| |
| TEST_P(PartitionAllocThreadCacheTest, Purge) { |
| size_t allocations = 10; |
| size_t bucket_index = FillThreadCacheAndReturnIndex(kMediumSize, allocations); |
| auto* tcache = root()->thread_cache_for_testing(); |
| EXPECT_EQ( |
| (1 + allocations / kFillCountForMediumBucket) * kFillCountForMediumBucket, |
| tcache->bucket_count_for_testing(bucket_index)); |
| tcache->Purge(); |
| EXPECT_EQ(0u, tcache->bucket_count_for_testing(bucket_index)); |
| } |
| |
| TEST_P(PartitionAllocThreadCacheTest, NoCrossPartitionCache) { |
| PartitionOptions opts; |
| opts.aligned_alloc = PartitionOptions::kAllowed; |
| opts.star_scan_quarantine = PartitionOptions::kAllowed; |
| PartitionAllocatorForTesting allocator(opts); |
| |
| size_t bucket_index = FillThreadCacheAndReturnIndex(kSmallSize); |
| void* ptr = allocator.root()->Alloc( |
| allocator.root()->AdjustSizeForExtrasSubtract(kSmallSize), ""); |
| ASSERT_TRUE(ptr); |
| |
| auto* tcache = root()->thread_cache_for_testing(); |
| EXPECT_EQ(kFillCountForSmallBucket, |
| tcache->bucket_count_for_testing(bucket_index)); |
| |
| allocator.root()->Free(ptr); |
| EXPECT_EQ(kFillCountForSmallBucket, |
| tcache->bucket_count_for_testing(bucket_index)); |
| } |
| |
| // Required to record hits and misses. |
| #if PA_CONFIG(THREAD_CACHE_ENABLE_STATISTICS) |
| TEST_P(PartitionAllocThreadCacheTest, LargeAllocationsAreNotCached) { |
| auto* tcache = root()->thread_cache_for_testing(); |
| DeltaCounter alloc_miss_counter{tcache->stats_for_testing().alloc_misses}; |
| DeltaCounter alloc_miss_too_large_counter{ |
| tcache->stats_for_testing().alloc_miss_too_large}; |
| DeltaCounter cache_fill_counter{tcache->stats_for_testing().cache_fill_count}; |
| DeltaCounter cache_fill_misses_counter{ |
| tcache->stats_for_testing().cache_fill_misses}; |
| |
| FillThreadCacheAndReturnIndex(100 * 1024); |
| tcache = root()->thread_cache_for_testing(); |
| EXPECT_EQ(1u, alloc_miss_counter.Delta()); |
| EXPECT_EQ(1u, alloc_miss_too_large_counter.Delta()); |
| EXPECT_EQ(1u, cache_fill_counter.Delta()); |
| EXPECT_EQ(1u, cache_fill_misses_counter.Delta()); |
| } |
| #endif // PA_CONFIG(THREAD_CACHE_ENABLE_STATISTICS) |
| |
| TEST_P(PartitionAllocThreadCacheTest, DirectMappedAllocationsAreNotCached) { |
| FillThreadCacheAndReturnIndex(1024 * 1024); |
| // The line above would crash due to out of bounds access if this wasn't |
| // properly handled. |
| } |
| |
| // This tests that Realloc properly handles bookkeeping, specifically the path |
| // that reallocates in place. |
| TEST_P(PartitionAllocThreadCacheTest, DirectMappedReallocMetrics) { |
| root()->ResetBookkeepingForTesting(); |
| |
| size_t expected_allocated_size = root()->get_total_size_of_allocated_bytes(); |
| |
| EXPECT_EQ(expected_allocated_size, |
| root()->get_total_size_of_allocated_bytes()); |
| EXPECT_EQ(expected_allocated_size, root()->get_max_size_of_allocated_bytes()); |
| |
| void* ptr = root()->Alloc( |
| root()->AdjustSizeForExtrasSubtract(10 * internal::kMaxBucketed), ""); |
| |
| EXPECT_EQ(expected_allocated_size + 10 * internal::kMaxBucketed, |
| root()->get_total_size_of_allocated_bytes()); |
| |
| void* ptr2 = root()->Realloc( |
| ptr, root()->AdjustSizeForExtrasSubtract(9 * internal::kMaxBucketed), ""); |
| |
| ASSERT_EQ(ptr, ptr2); |
| EXPECT_EQ(expected_allocated_size + 9 * internal::kMaxBucketed, |
| root()->get_total_size_of_allocated_bytes()); |
| |
| ptr2 = root()->Realloc( |
| ptr, root()->AdjustSizeForExtrasSubtract(10 * internal::kMaxBucketed), |
| ""); |
| |
| ASSERT_EQ(ptr, ptr2); |
| EXPECT_EQ(expected_allocated_size + 10 * internal::kMaxBucketed, |
| root()->get_total_size_of_allocated_bytes()); |
| |
| root()->Free(ptr); |
| } |
| |
| namespace { |
| |
| size_t FillThreadCacheAndReturnIndex(PartitionRoot* root, |
| size_t size, |
| BucketDistribution bucket_distribution, |
| size_t count = 1) { |
| uint16_t bucket_index = |
| PartitionRoot::SizeToBucketIndex(size, bucket_distribution); |
| std::vector<void*> allocated_data; |
| |
| for (size_t i = 0; i < count; ++i) { |
| allocated_data.push_back( |
| root->Alloc(root->AdjustSizeForExtrasSubtract(size), "")); |
| } |
| for (void* ptr : allocated_data) { |
| root->Free(ptr); |
| } |
| |
| return bucket_index; |
| } |
| |
| // TODO(1151236): To remove callback from partition allocator's DEPS, |
| // rewrite the tests without BindLambdaForTesting and RepeatingClosure. |
| // However this makes a little annoying to add more tests using their |
| // own threads. Need to support an easier way to implement tests using |
| // PlatformThreadForTesting::Create(). |
| class ThreadDelegateForMultipleThreadCaches |
| : public internal::base::PlatformThreadForTesting::Delegate { |
| public: |
| ThreadDelegateForMultipleThreadCaches(ThreadCache* parent_thread_cache, |
| PartitionRoot* root, |
| BucketDistribution bucket_distribution) |
| : parent_thread_tcache_(parent_thread_cache), |
| root_(root), |
| bucket_distribution_(bucket_distribution) {} |
| |
| void ThreadMain() override { |
| EXPECT_FALSE(root_->thread_cache_for_testing()); // No allocations yet. |
| FillThreadCacheAndReturnIndex(root_, kMediumSize, bucket_distribution_); |
| auto* tcache = root_->thread_cache_for_testing(); |
| EXPECT_TRUE(tcache); |
| |
| EXPECT_NE(parent_thread_tcache_, tcache); |
| } |
| |
| private: |
| ThreadCache* parent_thread_tcache_ = nullptr; |
| PartitionRoot* root_ = nullptr; |
| PartitionRoot::BucketDistribution bucket_distribution_; |
| }; |
| |
| } // namespace |
| |
| TEST_P(PartitionAllocThreadCacheTest, MultipleThreadCaches) { |
| FillThreadCacheAndReturnIndex(kMediumSize); |
| auto* parent_thread_tcache = root()->thread_cache_for_testing(); |
| ASSERT_TRUE(parent_thread_tcache); |
| |
| ThreadDelegateForMultipleThreadCaches delegate(parent_thread_tcache, root(), |
| GetParam()); |
| |
| internal::base::PlatformThreadHandle thread_handle; |
| internal::base::PlatformThreadForTesting::Create(0, &delegate, |
| &thread_handle); |
| internal::base::PlatformThreadForTesting::Join(thread_handle); |
| } |
| |
| namespace { |
| |
| class ThreadDelegateForThreadCacheReclaimedWhenThreadExits |
| : public internal::base::PlatformThreadForTesting::Delegate { |
| public: |
| ThreadDelegateForThreadCacheReclaimedWhenThreadExits(PartitionRoot* root, |
| void*& other_thread_ptr) |
| : root_(root), other_thread_ptr_(other_thread_ptr) {} |
| |
| void ThreadMain() override { |
| EXPECT_FALSE(root_->thread_cache_for_testing()); // No allocations yet. |
| other_thread_ptr_ = |
| root_->Alloc(root_->AdjustSizeForExtrasSubtract(kMediumSize), ""); |
| root_->Free(other_thread_ptr_); |
| // |other_thread_ptr| is now in the thread cache. |
| } |
| |
| private: |
| PartitionRoot* root_ = nullptr; |
| void*& other_thread_ptr_; |
| }; |
| |
| } // namespace |
| |
| TEST_P(PartitionAllocThreadCacheTest, ThreadCacheReclaimedWhenThreadExits) { |
| // Make sure that there is always at least one object allocated in the test |
| // bucket, so that the PartitionPage is no reclaimed. |
| // |
| // Allocate enough objects to force a cache fill at the next allocation. |
| std::vector<void*> tmp; |
| for (size_t i = 0; i < kDefaultCountForMediumBucket / 4; i++) { |
| tmp.push_back( |
| root()->Alloc(root()->AdjustSizeForExtrasSubtract(kMediumSize), "")); |
| } |
| |
| void* other_thread_ptr = nullptr; |
| ThreadDelegateForThreadCacheReclaimedWhenThreadExits delegate( |
| root(), other_thread_ptr); |
| |
| internal::base::PlatformThreadHandle thread_handle; |
| internal::base::PlatformThreadForTesting::Create(0, &delegate, |
| &thread_handle); |
| internal::base::PlatformThreadForTesting::Join(thread_handle); |
| |
| void* this_thread_ptr = |
| root()->Alloc(root()->AdjustSizeForExtrasSubtract(kMediumSize), ""); |
| // |other_thread_ptr| was returned to the central allocator, and is returned |
| // here, as it comes from the freelist. |
| EXPECT_EQ(UntagPtr(this_thread_ptr), UntagPtr(other_thread_ptr)); |
| root()->Free(other_thread_ptr); |
| |
| for (void* ptr : tmp) { |
| root()->Free(ptr); |
| } |
| } |
| |
| namespace { |
| |
| class ThreadDelegateForThreadCacheRegistry |
| : public internal::base::PlatformThreadForTesting::Delegate { |
| public: |
| ThreadDelegateForThreadCacheRegistry(ThreadCache* parent_thread_cache, |
| PartitionRoot* root, |
| BucketDistribution bucket_distribution) |
| : parent_thread_tcache_(parent_thread_cache), |
| root_(root), |
| bucket_distribution_(bucket_distribution) {} |
| |
| void ThreadMain() override { |
| EXPECT_FALSE(root_->thread_cache_for_testing()); // No allocations yet. |
| FillThreadCacheAndReturnIndex(root_, kSmallSize, bucket_distribution_); |
| auto* tcache = root_->thread_cache_for_testing(); |
| EXPECT_TRUE(tcache); |
| |
| internal::ScopedGuard lock(ThreadCacheRegistry::GetLock()); |
| EXPECT_EQ(tcache->prev_for_testing(), nullptr); |
| EXPECT_EQ(tcache->next_for_testing(), parent_thread_tcache_); |
| } |
| |
| private: |
| ThreadCache* parent_thread_tcache_ = nullptr; |
| PartitionRoot* root_ = nullptr; |
| BucketDistribution bucket_distribution_; |
| }; |
| |
| } // namespace |
| |
| TEST_P(PartitionAllocThreadCacheTest, ThreadCacheRegistry) { |
| auto* parent_thread_tcache = root()->thread_cache_for_testing(); |
| ASSERT_TRUE(parent_thread_tcache); |
| |
| #if !(BUILDFLAG(IS_APPLE) || BUILDFLAG(IS_ANDROID) || \ |
| BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_LINUX)) && \ |
| BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC) |
| // iOS and MacOS 15 create worker threads internally(start_wqthread). |
| // So thread caches are created for the worker threads, because the threads |
| // allocate memory for initialization (_dispatch_calloc is invoked). |
| // We cannot assume that there is only 1 thread cache here. |
| |
| // Regarding Linux, ChromeOS and Android, some other tests may create |
| // non-joinable threads. E.g. FilePathWatcherTest will create |
| // non-joinable thread at InotifyReader::StartThread(). The thread will |
| // be still running after the tests are finished, and will break |
| // an assumption that there exists only main thread here. |
| { |
| internal::ScopedGuard lock(ThreadCacheRegistry::GetLock()); |
| EXPECT_EQ(parent_thread_tcache->prev_for_testing(), nullptr); |
| EXPECT_EQ(parent_thread_tcache->next_for_testing(), nullptr); |
| } |
| #endif |
| |
| ThreadDelegateForThreadCacheRegistry delegate(parent_thread_tcache, root(), |
| GetParam()); |
| |
| internal::base::PlatformThreadHandle thread_handle; |
| internal::base::PlatformThreadForTesting::Create(0, &delegate, |
| &thread_handle); |
| internal::base::PlatformThreadForTesting::Join(thread_handle); |
| |
| #if !(BUILDFLAG(IS_APPLE) || BUILDFLAG(IS_ANDROID) || \ |
| BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_LINUX)) && \ |
| BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC) |
| internal::ScopedGuard lock(ThreadCacheRegistry::GetLock()); |
| EXPECT_EQ(parent_thread_tcache->prev_for_testing(), nullptr); |
| EXPECT_EQ(parent_thread_tcache->next_for_testing(), nullptr); |
| #endif |
| } |
| |
| #if PA_CONFIG(THREAD_CACHE_ENABLE_STATISTICS) |
| TEST_P(PartitionAllocThreadCacheTest, RecordStats) { |
| auto* tcache = root()->thread_cache_for_testing(); |
| DeltaCounter alloc_counter{tcache->stats_for_testing().alloc_count}; |
| DeltaCounter alloc_hits_counter{tcache->stats_for_testing().alloc_hits}; |
| DeltaCounter alloc_miss_counter{tcache->stats_for_testing().alloc_misses}; |
| |
| DeltaCounter alloc_miss_empty_counter{ |
| tcache->stats_for_testing().alloc_miss_empty}; |
| |
| DeltaCounter cache_fill_counter{tcache->stats_for_testing().cache_fill_count}; |
| DeltaCounter cache_fill_hits_counter{ |
| tcache->stats_for_testing().cache_fill_hits}; |
| DeltaCounter cache_fill_misses_counter{ |
| tcache->stats_for_testing().cache_fill_misses}; |
| |
| // Cache has been purged, first allocation is a miss. |
| void* data = |
| root()->Alloc(root()->AdjustSizeForExtrasSubtract(kMediumSize), ""); |
| EXPECT_EQ(1u, alloc_counter.Delta()); |
| EXPECT_EQ(1u, alloc_miss_counter.Delta()); |
| EXPECT_EQ(0u, alloc_hits_counter.Delta()); |
| |
| // Cache fill worked. |
| root()->Free(data); |
| EXPECT_EQ(1u, cache_fill_counter.Delta()); |
| EXPECT_EQ(1u, cache_fill_hits_counter.Delta()); |
| EXPECT_EQ(0u, cache_fill_misses_counter.Delta()); |
| |
| tcache->Purge(); |
| cache_fill_counter.Reset(); |
| // Buckets are never full, fill always succeeds. |
| size_t allocations = 10; |
| size_t bucket_index = FillThreadCacheAndReturnIndex( |
| kMediumSize, kDefaultCountForMediumBucket + allocations); |
| EXPECT_EQ(kDefaultCountForMediumBucket + allocations, |
| cache_fill_counter.Delta()); |
| EXPECT_EQ(0u, cache_fill_misses_counter.Delta()); |
| |
| // Memory footprint. |
| ThreadCacheStats stats; |
| ThreadCacheRegistry::Instance().DumpStats(true, &stats); |
| // Bucket was cleared (set to kDefaultCountForMediumBucket / 2) after going |
| // above the limit (-1), then refilled by batches (1 + floor(allocations / |
| // kFillCountForSmallBucket) times). |
| size_t expected_count = |
| kDefaultCountForMediumBucket / 2 - 1 + |
| (1 + allocations / kFillCountForMediumBucket) * kFillCountForMediumBucket; |
| EXPECT_EQ(root()->buckets[bucket_index].slot_size * expected_count, |
| stats.bucket_total_memory); |
| EXPECT_EQ(sizeof(ThreadCache), stats.metadata_overhead); |
| } |
| |
| namespace { |
| |
| class ThreadDelegateForMultipleThreadCachesAccounting |
| : public internal::base::PlatformThreadForTesting::Delegate { |
| public: |
| ThreadDelegateForMultipleThreadCachesAccounting( |
| PartitionRoot* root, |
| const ThreadCacheStats& wqthread_stats, |
| int alloc_count, |
| BucketDistribution bucket_distribution) |
| : root_(root), |
| bucket_distribution_(bucket_distribution), |
| wqthread_stats_(wqthread_stats), |
| alloc_count_(alloc_count) {} |
| |
| void ThreadMain() override { |
| EXPECT_FALSE(root_->thread_cache_for_testing()); // No allocations yet. |
| size_t bucket_index = |
| FillThreadCacheAndReturnIndex(root_, kMediumSize, bucket_distribution_); |
| |
| ThreadCacheStats stats; |
| ThreadCacheRegistry::Instance().DumpStats(false, &stats); |
| // 2* for this thread and the parent one. |
| EXPECT_EQ( |
| 2 * root_->buckets[bucket_index].slot_size * kFillCountForMediumBucket, |
| stats.bucket_total_memory - wqthread_stats_.bucket_total_memory); |
| EXPECT_EQ(2 * sizeof(ThreadCache), |
| stats.metadata_overhead - wqthread_stats_.metadata_overhead); |
| |
| ThreadCacheStats this_thread_cache_stats{}; |
| root_->thread_cache_for_testing()->AccumulateStats( |
| &this_thread_cache_stats); |
| EXPECT_EQ(alloc_count_ + this_thread_cache_stats.alloc_count, |
| stats.alloc_count - wqthread_stats_.alloc_count); |
| } |
| |
| private: |
| PartitionRoot* root_ = nullptr; |
| BucketDistribution bucket_distribution_; |
| const ThreadCacheStats wqthread_stats_; |
| const int alloc_count_; |
| }; |
| |
| } // namespace |
| |
| TEST_P(PartitionAllocThreadCacheTest, MultipleThreadCachesAccounting) { |
| ThreadCacheStats wqthread_stats{0}; |
| #if (BUILDFLAG(IS_APPLE) || BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_CHROMEOS) || \ |
| BUILDFLAG(IS_LINUX)) && \ |
| BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC) |
| { |
| // iOS and MacOS 15 create worker threads internally(start_wqthread). |
| // So thread caches are created for the worker threads, because the threads |
| // allocate memory for initialization (_dispatch_calloc is invoked). |
| // We need to count worker threads created by iOS and Mac system. |
| |
| // Regarding Linux, ChromeOS and Android, some other tests may create |
| // non-joinable threads. E.g. FilePathWatcherTest will create |
| // non-joinable thread at InotifyReader::StartThread(). The thread will |
| // be still running after the tests are finished. We need to count |
| // the joinable threads here. |
| ThreadCacheRegistry::Instance().DumpStats(false, &wqthread_stats); |
| |
| // Remove this thread's thread cache stats from wqthread_stats. |
| ThreadCacheStats this_stats; |
| ThreadCacheRegistry::Instance().DumpStats(true, &this_stats); |
| |
| wqthread_stats.alloc_count -= this_stats.alloc_count; |
| wqthread_stats.metadata_overhead -= this_stats.metadata_overhead; |
| wqthread_stats.bucket_total_memory -= this_stats.bucket_total_memory; |
| } |
| #endif |
| FillThreadCacheAndReturnIndex(kMediumSize); |
| uint64_t alloc_count = |
| root()->thread_cache_for_testing()->stats_for_testing().alloc_count; |
| |
| ThreadDelegateForMultipleThreadCachesAccounting delegate( |
| root(), wqthread_stats, alloc_count, GetParam()); |
| |
| internal::base::PlatformThreadHandle thread_handle; |
| internal::base::PlatformThreadForTesting::Create(0, &delegate, |
| &thread_handle); |
| internal::base::PlatformThreadForTesting::Join(thread_handle); |
| } |
| |
| #endif // PA_CONFIG(THREAD_CACHE_ENABLE_STATISTICS) |
| |
| // TODO(https://crbug.com/1287799): Flaky on IOS. |
| #if BUILDFLAG(IS_IOS) |
| #define MAYBE_PurgeAll DISABLED_PurgeAll |
| #else |
| #define MAYBE_PurgeAll PurgeAll |
| #endif |
| |
| namespace { |
| |
| class ThreadDelegateForPurgeAll |
| : public internal::base::PlatformThreadForTesting::Delegate { |
| public: |
| ThreadDelegateForPurgeAll(PartitionRoot* root, |
| ThreadCache*& other_thread_tcache, |
| std::atomic<bool>& other_thread_started, |
| std::atomic<bool>& purge_called, |
| int bucket_index, |
| BucketDistribution bucket_distribution) |
| : root_(root), |
| other_thread_tcache_(other_thread_tcache), |
| other_thread_started_(other_thread_started), |
| purge_called_(purge_called), |
| bucket_index_(bucket_index), |
| bucket_distribution_(bucket_distribution) {} |
| |
| void ThreadMain() override PA_NO_THREAD_SAFETY_ANALYSIS { |
| FillThreadCacheAndReturnIndex(root_, kSmallSize, bucket_distribution_); |
| other_thread_tcache_ = root_->thread_cache_for_testing(); |
| |
| other_thread_started_.store(true, std::memory_order_release); |
| while (!purge_called_.load(std::memory_order_acquire)) { |
| } |
| |
| // Purge() was not triggered from the other thread. |
| EXPECT_EQ(kFillCountForSmallBucket, |
| other_thread_tcache_->bucket_count_for_testing(bucket_index_)); |
| // Allocations do not trigger Purge(). |
| void* data = |
| root_->Alloc(root_->AdjustSizeForExtrasSubtract(kSmallSize), ""); |
| EXPECT_EQ(kFillCountForSmallBucket - 1, |
| other_thread_tcache_->bucket_count_for_testing(bucket_index_)); |
| // But deallocations do. |
| root_->Free(data); |
| EXPECT_EQ(0u, |
| other_thread_tcache_->bucket_count_for_testing(bucket_index_)); |
| } |
| |
| private: |
| PartitionRoot* root_ = nullptr; |
| ThreadCache*& other_thread_tcache_; |
| std::atomic<bool>& other_thread_started_; |
| std::atomic<bool>& purge_called_; |
| const int bucket_index_; |
| BucketDistribution bucket_distribution_; |
| }; |
| |
| } // namespace |
| |
| TEST_P(PartitionAllocThreadCacheTest, MAYBE_PurgeAll) |
| PA_NO_THREAD_SAFETY_ANALYSIS { |
| std::atomic<bool> other_thread_started{false}; |
| std::atomic<bool> purge_called{false}; |
| |
| size_t bucket_index = FillThreadCacheAndReturnIndex(kSmallSize); |
| ThreadCache* this_thread_tcache = root()->thread_cache_for_testing(); |
| ThreadCache* other_thread_tcache = nullptr; |
| |
| ThreadDelegateForPurgeAll delegate(root(), other_thread_tcache, |
| other_thread_started, purge_called, |
| bucket_index, GetParam()); |
| internal::base::PlatformThreadHandle thread_handle; |
| internal::base::PlatformThreadForTesting::Create(0, &delegate, |
| &thread_handle); |
| |
| while (!other_thread_started.load(std::memory_order_acquire)) { |
| } |
| |
| EXPECT_EQ(kFillCountForSmallBucket, |
| this_thread_tcache->bucket_count_for_testing(bucket_index)); |
| EXPECT_EQ(kFillCountForSmallBucket, |
| other_thread_tcache->bucket_count_for_testing(bucket_index)); |
| |
| ThreadCacheRegistry::Instance().PurgeAll(); |
| // This thread is synchronously purged. |
| EXPECT_EQ(0u, this_thread_tcache->bucket_count_for_testing(bucket_index)); |
| // Not the other one. |
| EXPECT_EQ(kFillCountForSmallBucket, |
| other_thread_tcache->bucket_count_for_testing(bucket_index)); |
| |
| purge_called.store(true, std::memory_order_release); |
| internal::base::PlatformThreadForTesting::Join(thread_handle); |
| } |
| |
| TEST_P(PartitionAllocThreadCacheTest, PeriodicPurge) { |
| auto& registry = ThreadCacheRegistry::Instance(); |
| auto NextInterval = [®istry]() { |
| return internal::base::Microseconds( |
| registry.GetPeriodicPurgeNextIntervalInMicroseconds()); |
| }; |
| |
| EXPECT_EQ(NextInterval(), registry.default_purge_interval()); |
| |
| // Small amount of memory, the period gets longer. |
| auto* tcache = ThreadCache::Get(); |
| ASSERT_LT(tcache->CachedMemory(), |
| registry.min_cached_memory_for_purging_bytes()); |
| registry.RunPeriodicPurge(); |
| EXPECT_EQ(NextInterval(), 2 * registry.default_purge_interval()); |
| registry.RunPeriodicPurge(); |
| EXPECT_EQ(NextInterval(), 4 * registry.default_purge_interval()); |
| |
| // Check that the purge interval is clamped at the maximum value. |
| while (NextInterval() < registry.max_purge_interval()) { |
| registry.RunPeriodicPurge(); |
| } |
| registry.RunPeriodicPurge(); |
| |
| // Not enough memory to decrease the interval. |
| FillThreadCacheWithMemory(registry.min_cached_memory_for_purging_bytes() + 1); |
| registry.RunPeriodicPurge(); |
| EXPECT_EQ(NextInterval(), registry.max_purge_interval()); |
| |
| FillThreadCacheWithMemory(2 * registry.min_cached_memory_for_purging_bytes() + |
| 1); |
| registry.RunPeriodicPurge(); |
| EXPECT_EQ(NextInterval(), registry.max_purge_interval() / 2); |
| |
| // Enough memory, interval doesn't change. |
| FillThreadCacheWithMemory(registry.min_cached_memory_for_purging_bytes()); |
| registry.RunPeriodicPurge(); |
| EXPECT_EQ(NextInterval(), registry.max_purge_interval() / 2); |
| |
| // No cached memory, increase the interval. |
| registry.RunPeriodicPurge(); |
| EXPECT_EQ(NextInterval(), registry.max_purge_interval()); |
| |
| // Cannot test the very large size with only one thread, this is tested below |
| // in the multiple threads test. |
| } |
| |
| namespace { |
| |
| void FillThreadCacheWithMemory(PartitionRoot* root, |
| size_t target_cached_memory, |
| BucketDistribution bucket_distribution) { |
| for (int batch : {1, 2, 4, 8, 16}) { |
| for (size_t allocation_size = 1; |
| allocation_size <= ThreadCache::kLargeSizeThreshold; |
| allocation_size++) { |
| FillThreadCacheAndReturnIndex( |
| root, root->AdjustSizeForExtrasAdd(allocation_size), |
| bucket_distribution, batch); |
| |
| if (ThreadCache::Get()->CachedMemory() >= target_cached_memory) { |
| return; |
| } |
| } |
| } |
| |
| ASSERT_GE(ThreadCache::Get()->CachedMemory(), target_cached_memory); |
| } |
| |
| class ThreadDelegateForPeriodicPurgeSumsOverAllThreads |
| : public internal::base::PlatformThreadForTesting::Delegate { |
| public: |
| ThreadDelegateForPeriodicPurgeSumsOverAllThreads( |
| PartitionRoot* root, |
| std::atomic<int>& allocations_done, |
| std::atomic<bool>& can_finish, |
| BucketDistribution bucket_distribution) |
| : root_(root), |
| allocations_done_(allocations_done), |
| can_finish_(can_finish), |
| bucket_distribution_(bucket_distribution) {} |
| |
| void ThreadMain() override { |
| FillThreadCacheWithMemory(root_, |
| 5 * ThreadCacheRegistry::Instance() |
| .min_cached_memory_for_purging_bytes(), |
| bucket_distribution_); |
| allocations_done_.fetch_add(1, std::memory_order_release); |
| |
| // This thread needs to be alive when the next periodic purge task runs. |
| while (!can_finish_.load(std::memory_order_acquire)) { |
| } |
| } |
| |
| private: |
| PartitionRoot* root_ = nullptr; |
| std::atomic<int>& allocations_done_; |
| std::atomic<bool>& can_finish_; |
| BucketDistribution bucket_distribution_; |
| }; |
| |
| } // namespace |
| |
| // Disabled due to flakiness: crbug.com/1220371 |
| TEST_P(PartitionAllocThreadCacheTest, |
| DISABLED_PeriodicPurgeSumsOverAllThreads) { |
| auto& registry = ThreadCacheRegistry::Instance(); |
| auto NextInterval = [®istry]() { |
| return internal::base::Microseconds( |
| registry.GetPeriodicPurgeNextIntervalInMicroseconds()); |
| }; |
| EXPECT_EQ(NextInterval(), registry.default_purge_interval()); |
| |
| // Small amount of memory, the period gets longer. |
| auto* tcache = ThreadCache::Get(); |
| ASSERT_LT(tcache->CachedMemory(), |
| registry.min_cached_memory_for_purging_bytes()); |
| registry.RunPeriodicPurge(); |
| EXPECT_EQ(NextInterval(), 2 * registry.default_purge_interval()); |
| registry.RunPeriodicPurge(); |
| EXPECT_EQ(NextInterval(), 4 * registry.default_purge_interval()); |
| |
| // Check that the purge interval is clamped at the maximum value. |
| while (NextInterval() < registry.max_purge_interval()) { |
| registry.RunPeriodicPurge(); |
| } |
| registry.RunPeriodicPurge(); |
| |
| // Not enough memory on this thread to decrease the interval. |
| FillThreadCacheWithMemory(registry.min_cached_memory_for_purging_bytes() / 2); |
| registry.RunPeriodicPurge(); |
| EXPECT_EQ(NextInterval(), registry.max_purge_interval()); |
| |
| std::atomic<int> allocations_done{0}; |
| std::atomic<bool> can_finish{false}; |
| ThreadDelegateForPeriodicPurgeSumsOverAllThreads delegate( |
| root(), allocations_done, can_finish, GetParam()); |
| |
| internal::base::PlatformThreadHandle thread_handle; |
| internal::base::PlatformThreadForTesting::Create(0, &delegate, |
| &thread_handle); |
| internal::base::PlatformThreadHandle thread_handle_2; |
| internal::base::PlatformThreadForTesting::Create(0, &delegate, |
| &thread_handle_2); |
| |
| while (allocations_done.load(std::memory_order_acquire) != 2) { |
| internal::base::PlatformThreadForTesting::YieldCurrentThread(); |
| } |
| |
| // Many allocations on the other thread. |
| registry.RunPeriodicPurge(); |
| EXPECT_EQ(NextInterval(), registry.default_purge_interval()); |
| |
| can_finish.store(true, std::memory_order_release); |
| internal::base::PlatformThreadForTesting::Join(thread_handle); |
| internal::base::PlatformThreadForTesting::Join(thread_handle_2); |
| } |
| |
| // TODO(https://crbug.com/1287799): Flaky on IOS. |
| #if BUILDFLAG(IS_IOS) |
| #define MAYBE_DynamicCountPerBucket DISABLED_DynamicCountPerBucket |
| #else |
| #define MAYBE_DynamicCountPerBucket DynamicCountPerBucket |
| #endif |
| TEST_P(PartitionAllocThreadCacheTest, MAYBE_DynamicCountPerBucket) { |
| auto* tcache = root()->thread_cache_for_testing(); |
| size_t bucket_index = |
| FillThreadCacheAndReturnIndex(kMediumSize, kDefaultCountForMediumBucket); |
| |
| EXPECT_EQ(kDefaultCountForMediumBucket, |
| tcache->bucket_for_testing(bucket_index).count); |
| |
| ThreadCacheRegistry::Instance().SetThreadCacheMultiplier( |
| ThreadCache::kDefaultMultiplier / 2); |
| // No immediate batch deallocation. |
| EXPECT_EQ(kDefaultCountForMediumBucket, |
| tcache->bucket_for_testing(bucket_index).count); |
| void* data = |
| root()->Alloc(root()->AdjustSizeForExtrasSubtract(kMediumSize), ""); |
| // Not triggered by allocations. |
| EXPECT_EQ(kDefaultCountForMediumBucket - 1, |
| tcache->bucket_for_testing(bucket_index).count); |
| |
| // Free() triggers the purge within limits. |
| root()->Free(data); |
| EXPECT_LE(tcache->bucket_for_testing(bucket_index).count, |
| kDefaultCountForMediumBucket / 2); |
| |
| // Won't go above anymore. |
| FillThreadCacheAndReturnIndex(kMediumSize, 1000); |
| EXPECT_LE(tcache->bucket_for_testing(bucket_index).count, |
| kDefaultCountForMediumBucket / 2); |
| |
| // Limit can be raised. |
| ThreadCacheRegistry::Instance().SetThreadCacheMultiplier( |
| ThreadCache::kDefaultMultiplier * 2); |
| FillThreadCacheAndReturnIndex(kMediumSize, 1000); |
| EXPECT_GT(tcache->bucket_for_testing(bucket_index).count, |
| kDefaultCountForMediumBucket / 2); |
| } |
| |
| TEST_P(PartitionAllocThreadCacheTest, DynamicCountPerBucketClamping) { |
| auto* tcache = root()->thread_cache_for_testing(); |
| |
| ThreadCacheRegistry::Instance().SetThreadCacheMultiplier( |
| ThreadCache::kDefaultMultiplier / 1000.); |
| for (size_t i = 0; i < ThreadCache::kBucketCount; i++) { |
| // Invalid bucket. |
| if (!tcache->bucket_for_testing(i).limit.load(std::memory_order_relaxed)) { |
| EXPECT_EQ(root()->buckets[i].active_slot_spans_head, nullptr); |
| continue; |
| } |
| EXPECT_GE( |
| tcache->bucket_for_testing(i).limit.load(std::memory_order_relaxed), |
| 1u); |
| } |
| |
| ThreadCacheRegistry::Instance().SetThreadCacheMultiplier( |
| ThreadCache::kDefaultMultiplier * 1000.); |
| for (size_t i = 0; i < ThreadCache::kBucketCount; i++) { |
| // Invalid bucket. |
| if (!tcache->bucket_for_testing(i).limit.load(std::memory_order_relaxed)) { |
| EXPECT_EQ(root()->buckets[i].active_slot_spans_head, nullptr); |
| continue; |
| } |
| EXPECT_LT( |
| tcache->bucket_for_testing(i).limit.load(std::memory_order_relaxed), |
| 0xff); |
| } |
| } |
| |
| // TODO(https://crbug.com/1287799): Flaky on IOS. |
| #if BUILDFLAG(IS_IOS) |
| #define MAYBE_DynamicCountPerBucketMultipleThreads \ |
| DISABLED_DynamicCountPerBucketMultipleThreads |
| #else |
| #define MAYBE_DynamicCountPerBucketMultipleThreads \ |
| DynamicCountPerBucketMultipleThreads |
| #endif |
| |
| namespace { |
| |
| class ThreadDelegateForDynamicCountPerBucketMultipleThreads |
| : public internal::base::PlatformThreadForTesting::Delegate { |
| public: |
| ThreadDelegateForDynamicCountPerBucketMultipleThreads( |
| PartitionRoot* root, |
| std::atomic<bool>& other_thread_started, |
| std::atomic<bool>& threshold_changed, |
| int bucket_index, |
| BucketDistribution bucket_distribution) |
| : root_(root), |
| other_thread_started_(other_thread_started), |
| threshold_changed_(threshold_changed), |
| bucket_index_(bucket_index), |
| bucket_distribution_(bucket_distribution) {} |
| |
| void ThreadMain() override { |
| FillThreadCacheAndReturnIndex(root_, kSmallSize, bucket_distribution_, |
| kDefaultCountForSmallBucket + 10); |
| auto* this_thread_tcache = root_->thread_cache_for_testing(); |
| // More than the default since the multiplier has changed. |
| EXPECT_GT(this_thread_tcache->bucket_count_for_testing(bucket_index_), |
| kDefaultCountForSmallBucket + 10); |
| |
| other_thread_started_.store(true, std::memory_order_release); |
| while (!threshold_changed_.load(std::memory_order_acquire)) { |
| } |
| |
| void* data = |
| root_->Alloc(root_->AdjustSizeForExtrasSubtract(kSmallSize), ""); |
| // Deallocations trigger limit enforcement. |
| root_->Free(data); |
| // Since the bucket is too full, it gets halved by batched deallocation. |
| EXPECT_EQ(static_cast<uint8_t>(ThreadCache::kSmallBucketBaseCount / 2), |
| this_thread_tcache->bucket_count_for_testing(bucket_index_)); |
| } |
| |
| private: |
| PartitionRoot* root_ = nullptr; |
| std::atomic<bool>& other_thread_started_; |
| std::atomic<bool>& threshold_changed_; |
| const int bucket_index_; |
| PartitionRoot::BucketDistribution bucket_distribution_; |
| }; |
| |
| } // namespace |
| |
| TEST_P(PartitionAllocThreadCacheTest, |
| MAYBE_DynamicCountPerBucketMultipleThreads) { |
| std::atomic<bool> other_thread_started{false}; |
| std::atomic<bool> threshold_changed{false}; |
| |
| auto* tcache = root()->thread_cache_for_testing(); |
| size_t bucket_index = |
| FillThreadCacheAndReturnIndex(kSmallSize, kDefaultCountForSmallBucket); |
| EXPECT_EQ(kDefaultCountForSmallBucket, |
| tcache->bucket_for_testing(bucket_index).count); |
| |
| // Change the ratio before starting the threads, checking that it will applied |
| // to newly-created threads. |
| ThreadCacheRegistry::Instance().SetThreadCacheMultiplier( |
| ThreadCache::kDefaultMultiplier + 1); |
| |
| ThreadDelegateForDynamicCountPerBucketMultipleThreads delegate( |
| root(), other_thread_started, threshold_changed, bucket_index, |
| GetParam()); |
| |
| internal::base::PlatformThreadHandle thread_handle; |
| internal::base::PlatformThreadForTesting::Create(0, &delegate, |
| &thread_handle); |
| |
| while (!other_thread_started.load(std::memory_order_acquire)) { |
| } |
| |
| ThreadCacheRegistry::Instance().SetThreadCacheMultiplier(1.); |
| threshold_changed.store(true, std::memory_order_release); |
| |
| internal::base::PlatformThreadForTesting::Join(thread_handle); |
| } |
| |
| TEST_P(PartitionAllocThreadCacheTest, DynamicSizeThreshold) { |
| auto* tcache = root()->thread_cache_for_testing(); |
| DeltaCounter alloc_miss_counter{tcache->stats_for_testing().alloc_misses}; |
| DeltaCounter alloc_miss_too_large_counter{ |
| tcache->stats_for_testing().alloc_miss_too_large}; |
| DeltaCounter cache_fill_counter{tcache->stats_for_testing().cache_fill_count}; |
| DeltaCounter cache_fill_misses_counter{ |
| tcache->stats_for_testing().cache_fill_misses}; |
| |
| // Default threshold at first. |
| ThreadCache::SetLargestCachedSize(ThreadCache::kDefaultSizeThreshold); |
| FillThreadCacheAndReturnIndex(ThreadCache::kDefaultSizeThreshold); |
| |
| EXPECT_EQ(0u, alloc_miss_too_large_counter.Delta()); |
| EXPECT_EQ(1u, cache_fill_counter.Delta()); |
| |
| // Too large to be cached. |
| FillThreadCacheAndReturnIndex(ThreadCache::kDefaultSizeThreshold + 1); |
| EXPECT_EQ(1u, alloc_miss_too_large_counter.Delta()); |
| |
| // Increase. |
| ThreadCache::SetLargestCachedSize(ThreadCache::kLargeSizeThreshold); |
| FillThreadCacheAndReturnIndex(ThreadCache::kDefaultSizeThreshold + 1); |
| // No new miss. |
| EXPECT_EQ(1u, alloc_miss_too_large_counter.Delta()); |
| |
| // Lower. |
| ThreadCache::SetLargestCachedSize(ThreadCache::kDefaultSizeThreshold); |
| FillThreadCacheAndReturnIndex(ThreadCache::kDefaultSizeThreshold + 1); |
| EXPECT_EQ(2u, alloc_miss_too_large_counter.Delta()); |
| |
| // Value is clamped. |
| size_t too_large = 1024 * 1024; |
| ThreadCache::SetLargestCachedSize(too_large); |
| FillThreadCacheAndReturnIndex(too_large); |
| EXPECT_EQ(3u, alloc_miss_too_large_counter.Delta()); |
| } |
| |
| // Disabled due to flakiness: crbug.com/1287811 |
| TEST_P(PartitionAllocThreadCacheTest, DISABLED_DynamicSizeThresholdPurge) { |
| auto* tcache = root()->thread_cache_for_testing(); |
| DeltaCounter alloc_miss_counter{tcache->stats_for_testing().alloc_misses}; |
| DeltaCounter alloc_miss_too_large_counter{ |
| tcache->stats_for_testing().alloc_miss_too_large}; |
| DeltaCounter cache_fill_counter{tcache->stats_for_testing().cache_fill_count}; |
| DeltaCounter cache_fill_misses_counter{ |
| tcache->stats_for_testing().cache_fill_misses}; |
| |
| // Cache large allocations. |
| size_t large_allocation_size = ThreadCache::kLargeSizeThreshold; |
| ThreadCache::SetLargestCachedSize(ThreadCache::kLargeSizeThreshold); |
| size_t index = FillThreadCacheAndReturnIndex(large_allocation_size); |
| EXPECT_EQ(0u, alloc_miss_too_large_counter.Delta()); |
| |
| // Lower. |
| ThreadCache::SetLargestCachedSize(ThreadCache::kDefaultSizeThreshold); |
| FillThreadCacheAndReturnIndex(large_allocation_size); |
| EXPECT_EQ(1u, alloc_miss_too_large_counter.Delta()); |
| |
| // There is memory trapped in the cache bucket. |
| EXPECT_GT(tcache->bucket_for_testing(index).count, 0u); |
| |
| // Which is reclaimed by Purge(). |
| tcache->Purge(); |
| EXPECT_EQ(0u, tcache->bucket_for_testing(index).count); |
| } |
| |
| TEST_P(PartitionAllocThreadCacheTest, ClearFromTail) { |
| auto count_items = [](ThreadCache* tcache, size_t index) { |
| uint8_t count = 0; |
| auto* head = tcache->bucket_for_testing(index).freelist_head; |
| while (head) { |
| head = head->GetNextForThreadCache<true>( |
| tcache->bucket_for_testing(index).slot_size); |
| count++; |
| } |
| return count; |
| }; |
| |
| auto* tcache = root()->thread_cache_for_testing(); |
| size_t index = FillThreadCacheAndReturnIndex(kSmallSize, 10); |
| ASSERT_GE(count_items(tcache, index), 10); |
| void* head = tcache->bucket_for_testing(index).freelist_head; |
| |
| for (size_t limit : {8, 3, 1}) { |
| tcache->ClearBucketForTesting(tcache->bucket_for_testing(index), limit); |
| EXPECT_EQ(head, static_cast<void*>( |
| tcache->bucket_for_testing(index).freelist_head)); |
| EXPECT_EQ(count_items(tcache, index), limit); |
| } |
| tcache->ClearBucketForTesting(tcache->bucket_for_testing(index), 0); |
| EXPECT_EQ(nullptr, static_cast<void*>( |
| tcache->bucket_for_testing(index).freelist_head)); |
| } |
| |
| // TODO(https://crbug.com/1287799): Flaky on IOS. |
| #if BUILDFLAG(IS_IOS) |
| #define MAYBE_Bookkeeping DISABLED_Bookkeeping |
| #else |
| #define MAYBE_Bookkeeping Bookkeeping |
| #endif |
| TEST_P(PartitionAllocThreadCacheTest, MAYBE_Bookkeeping) { |
| void* arr[kFillCountForMediumBucket] = {}; |
| auto* tcache = root()->thread_cache_for_testing(); |
| |
| root()->PurgeMemory(PurgeFlags::kDecommitEmptySlotSpans | |
| PurgeFlags::kDiscardUnusedSystemPages); |
| root()->ResetBookkeepingForTesting(); |
| |
| // The ThreadCache is allocated before we change buckets, so its size is |
| // always based on the neutral distribution. |
| size_t tc_bucket_index = root()->SizeToBucketIndex( |
| sizeof(ThreadCache), PartitionRoot::BucketDistribution::kNeutral); |
| auto* tc_bucket = &root()->buckets[tc_bucket_index]; |
| size_t expected_allocated_size = |
| tc_bucket->slot_size; // For the ThreadCache itself. |
| size_t expected_committed_size = kUseLazyCommit |
| ? internal::SystemPageSize() |
| : tc_bucket->get_bytes_per_span(); |
| |
| EXPECT_EQ(expected_committed_size, root()->total_size_of_committed_pages); |
| EXPECT_EQ(expected_committed_size, root()->max_size_of_committed_pages); |
| EXPECT_EQ(expected_allocated_size, |
| root()->get_total_size_of_allocated_bytes()); |
| EXPECT_EQ(expected_allocated_size, root()->get_max_size_of_allocated_bytes()); |
| |
| void* ptr = |
| root()->Alloc(root()->AdjustSizeForExtrasSubtract(kMediumSize), ""); |
| |
| auto* medium_bucket = root()->buckets + SizeToIndex(kMediumSize); |
| size_t medium_alloc_size = medium_bucket->slot_size; |
| expected_allocated_size += medium_alloc_size; |
| expected_committed_size += kUseLazyCommit |
| ? internal::SystemPageSize() |
| : medium_bucket->get_bytes_per_span(); |
| |
| EXPECT_EQ(expected_committed_size, root()->total_size_of_committed_pages); |
| EXPECT_EQ(expected_committed_size, root()->max_size_of_committed_pages); |
| EXPECT_EQ(expected_allocated_size, |
| root()->get_total_size_of_allocated_bytes()); |
| EXPECT_EQ(expected_allocated_size, root()->get_max_size_of_allocated_bytes()); |
| |
| expected_allocated_size += kFillCountForMediumBucket * medium_alloc_size; |
| |
| // These allocations all come from the thread-cache. |
| for (size_t i = 0; i < kFillCountForMediumBucket; i++) { |
| arr[i] = |
| root()->Alloc(root()->AdjustSizeForExtrasSubtract(kMediumSize), ""); |
| EXPECT_EQ(expected_committed_size, root()->total_size_of_committed_pages); |
| EXPECT_EQ(expected_committed_size, root()->max_size_of_committed_pages); |
| EXPECT_EQ(expected_allocated_size, |
| root()->get_total_size_of_allocated_bytes()); |
| EXPECT_EQ(expected_allocated_size, |
| root()->get_max_size_of_allocated_bytes()); |
| EXPECT_EQ((kFillCountForMediumBucket - 1 - i) * medium_alloc_size, |
| tcache->CachedMemory()); |
| } |
| |
| EXPECT_EQ(0U, tcache->CachedMemory()); |
| |
| root()->Free(ptr); |
| |
| for (auto*& el : arr) { |
| root()->Free(el); |
| } |
| EXPECT_EQ(root()->get_total_size_of_allocated_bytes(), |
| expected_allocated_size); |
| tcache->Purge(); |
| EXPECT_EQ(root()->get_total_size_of_allocated_bytes(), |
| GetBucketSizeForThreadCache()); |
| } |
| |
| TEST_P(PartitionAllocThreadCacheTest, TryPurgeNoAllocs) { |
| auto* tcache = root()->thread_cache_for_testing(); |
| tcache->TryPurge(); |
| } |
| |
| TEST_P(PartitionAllocThreadCacheTest, TryPurgeMultipleCorrupted) { |
| auto* tcache = root()->thread_cache_for_testing(); |
| |
| void* ptr = |
| root()->Alloc(root()->AdjustSizeForExtrasSubtract(kMediumSize), ""); |
| |
| auto* medium_bucket = root()->buckets + SizeToIndex(kMediumSize); |
| |
| auto* curr = medium_bucket->active_slot_spans_head->get_freelist_head(); |
| curr = curr->GetNextForThreadCache<true>(kMediumSize); |
| curr->CorruptNextForTesting(0x12345678); |
| tcache->TryPurge(); |
| curr->SetNext(nullptr); |
| root()->Free(ptr); |
| } |
| |
| TEST(AlternateBucketDistributionTest, SizeToIndex) { |
| using internal::BucketIndexLookup; |
| |
| // The first 12 buckets are the same as the default bucket index. |
| for (size_t i = 1 << 0; i < 1 << 8; i <<= 1) { |
| for (size_t offset = 0; offset < 4; offset++) { |
| size_t n = i * (4 + offset) / 4; |
| EXPECT_EQ(BucketIndexLookup::GetIndex(n), |
| BucketIndexLookup::GetIndexForNeutralBuckets(n)); |
| } |
| } |
| |
| // The alternate bucket distribution is different in the middle values. |
| // |
| // For each order, the top two buckets are removed compared with the default |
| // distribution. Values that would be allocated in those two buckets are |
| // instead allocated in the next power of two bucket. |
| // |
| // The first two buckets (each power of two and the next bucket up) remain |
| // the same between the two bucket distributions. |
| size_t expected_index = BucketIndexLookup::GetIndex(1 << 8); |
| for (size_t i = 1 << 8; i < internal::kHighThresholdForAlternateDistribution; |
| i <<= 1) { |
| // The first two buckets in the order should match up to the normal bucket |
| // distribution. |
| for (size_t offset = 0; offset < 2; offset++) { |
| size_t n = i * (4 + offset) / 4; |
| EXPECT_EQ(BucketIndexLookup::GetIndex(n), |
| BucketIndexLookup::GetIndexForNeutralBuckets(n)); |
| EXPECT_EQ(BucketIndexLookup::GetIndex(n), expected_index); |
| expected_index += 2; |
| } |
| // The last two buckets in the order are "rounded up" to the same bucket |
| // as the next power of two. |
| expected_index += 4; |
| for (size_t offset = 2; offset < 4; offset++) { |
| size_t n = i * (4 + offset) / 4; |
| // These two are rounded up in the alternate distribution, so we expect |
| // the bucket index to be larger than the bucket index for the same |
| // allocation under the default distribution. |
| EXPECT_GT(BucketIndexLookup::GetIndex(n), |
| BucketIndexLookup::GetIndexForNeutralBuckets(n)); |
| // We expect both allocations in this loop to be rounded up to the next |
| // power of two bucket. |
| EXPECT_EQ(BucketIndexLookup::GetIndex(n), expected_index); |
| } |
| } |
| |
| // The rest of the buckets all match up exactly with the existing |
| // bucket distribution. |
| for (size_t i = internal::kHighThresholdForAlternateDistribution; |
| i < internal::kMaxBucketed; i <<= 1) { |
| for (size_t offset = 0; offset < 4; offset++) { |
| size_t n = i * (4 + offset) / 4; |
| EXPECT_EQ(BucketIndexLookup::GetIndex(n), |
| BucketIndexLookup::GetIndexForNeutralBuckets(n)); |
| } |
| } |
| } |
| |
| TEST_P(PartitionAllocThreadCacheTest, AllocationRecording) { |
| // There is a cache. |
| auto* tcache = root()->thread_cache_for_testing(); |
| EXPECT_TRUE(tcache); |
| tcache->ResetPerThreadAllocationStatsForTesting(); |
| |
| constexpr size_t kBucketedNotCached = 1 << 12; |
| constexpr size_t kDirectMapped = 4 * (1 << 20); |
| // Not a "nice" size on purpose, to check that the raw size accounting works. |
| const size_t kSingleSlot = internal::PartitionPageSize() + 1; |
| |
| size_t expected_total_size = 0; |
| void* ptr = |
| root()->Alloc(root()->AdjustSizeForExtrasSubtract(kSmallSize), ""); |
| ASSERT_TRUE(ptr); |
| expected_total_size += root()->GetUsableSize(ptr); |
| void* ptr2 = root()->Alloc( |
| root()->AdjustSizeForExtrasSubtract(kBucketedNotCached), ""); |
| ASSERT_TRUE(ptr2); |
| expected_total_size += root()->GetUsableSize(ptr2); |
| void* ptr3 = |
| root()->Alloc(root()->AdjustSizeForExtrasSubtract(kDirectMapped), ""); |
| ASSERT_TRUE(ptr3); |
| expected_total_size += root()->GetUsableSize(ptr3); |
| void* ptr4 = |
| root()->Alloc(root()->AdjustSizeForExtrasSubtract(kSingleSlot), ""); |
| ASSERT_TRUE(ptr4); |
| expected_total_size += root()->GetUsableSize(ptr4); |
| |
| EXPECT_EQ(4u, tcache->thread_alloc_stats().alloc_count); |
| EXPECT_EQ(expected_total_size, tcache->thread_alloc_stats().alloc_total_size); |
| |
| root()->Free(ptr); |
| root()->Free(ptr2); |
| root()->Free(ptr3); |
| root()->Free(ptr4); |
| |
| EXPECT_EQ(4u, tcache->thread_alloc_stats().alloc_count); |
| EXPECT_EQ(expected_total_size, tcache->thread_alloc_stats().alloc_total_size); |
| EXPECT_EQ(4u, tcache->thread_alloc_stats().dealloc_count); |
| EXPECT_EQ(expected_total_size, |
| tcache->thread_alloc_stats().dealloc_total_size); |
| |
| auto stats = internal::GetAllocStatsForCurrentThread(); |
| EXPECT_EQ(4u, stats.alloc_count); |
| EXPECT_EQ(expected_total_size, stats.alloc_total_size); |
| EXPECT_EQ(4u, stats.dealloc_count); |
| EXPECT_EQ(expected_total_size, stats.dealloc_total_size); |
| } |
| |
| TEST_P(PartitionAllocThreadCacheTest, AllocationRecordingAligned) { |
| // There is a cache. |
| auto* tcache = root()->thread_cache_for_testing(); |
| EXPECT_TRUE(tcache); |
| tcache->ResetPerThreadAllocationStatsForTesting(); |
| |
| // Aligned allocations take different paths depending on whether they are (in |
| // the same order as the test cases below): |
| // - Not really aligned (since alignment is always good-enough) |
| // - Already satisfied by PA's alignment guarantees |
| // - Requiring extra padding |
| // - Already satisfied by PA's alignment guarantees |
| // - In need of a special slot span (very large alignment) |
| // - Direct-mapped with large alignment |
| size_t alloc_count = 0; |
| size_t total_size = 0; |
| size_t size_alignments[][2] = {{128, 4}, |
| {128, 128}, |
| {1024, 128}, |
| {128, 1024}, |
| {128, 2 * internal::PartitionPageSize()}, |
| {(4 << 20) + 1, 1 << 19}}; |
| for (auto [requested_size, alignment] : size_alignments) { |
| void* ptr = root()->AlignedAlloc(alignment, requested_size); |
| ASSERT_TRUE(ptr); |
| alloc_count++; |
| total_size += root()->GetUsableSize(ptr); |
| EXPECT_EQ(alloc_count, tcache->thread_alloc_stats().alloc_count); |
| EXPECT_EQ(total_size, tcache->thread_alloc_stats().alloc_total_size); |
| root()->Free(ptr); |
| EXPECT_EQ(alloc_count, tcache->thread_alloc_stats().dealloc_count); |
| EXPECT_EQ(total_size, tcache->thread_alloc_stats().dealloc_total_size); |
| } |
| |
| EXPECT_EQ(tcache->thread_alloc_stats().alloc_total_size, |
| tcache->thread_alloc_stats().dealloc_total_size); |
| |
| auto stats = internal::GetAllocStatsForCurrentThread(); |
| EXPECT_EQ(alloc_count, stats.alloc_count); |
| EXPECT_EQ(total_size, stats.alloc_total_size); |
| EXPECT_EQ(alloc_count, stats.dealloc_count); |
| EXPECT_EQ(total_size, stats.dealloc_total_size); |
| } |
| |
| TEST_P(PartitionAllocThreadCacheTest, AllocationRecordingRealloc) { |
| // There is a cache. |
| auto* tcache = root()->thread_cache_for_testing(); |
| EXPECT_TRUE(tcache); |
| tcache->ResetPerThreadAllocationStatsForTesting(); |
| |
| size_t alloc_count = 0; |
| size_t dealloc_count = 0; |
| size_t total_alloc_size = 0; |
| size_t total_dealloc_size = 0; |
| size_t size_new_sizes[][2] = { |
| {16, 15}, |
| {16, 64}, |
| {16, internal::PartitionPageSize() + 1}, |
| {4 << 20, 8 << 20}, |
| {8 << 20, 4 << 20}, |
| {(8 << 20) - internal::SystemPageSize(), 8 << 20}}; |
| for (auto [size, new_size] : size_new_sizes) { |
| void* ptr = root()->Alloc(size); |
| ASSERT_TRUE(ptr); |
| alloc_count++; |
| size_t usable_size = root()->GetUsableSize(ptr); |
| total_alloc_size += usable_size; |
| |
| ptr = root()->Realloc(ptr, new_size, ""); |
| ASSERT_TRUE(ptr); |
| total_dealloc_size += usable_size; |
| dealloc_count++; |
| usable_size = root()->GetUsableSize(ptr); |
| total_alloc_size += usable_size; |
| alloc_count++; |
| |
| EXPECT_EQ(alloc_count, tcache->thread_alloc_stats().alloc_count); |
| EXPECT_EQ(total_alloc_size, tcache->thread_alloc_stats().alloc_total_size); |
| EXPECT_EQ(dealloc_count, tcache->thread_alloc_stats().dealloc_count); |
| EXPECT_EQ(total_dealloc_size, |
| tcache->thread_alloc_stats().dealloc_total_size) |
| << new_size; |
| |
| root()->Free(ptr); |
| dealloc_count++; |
| total_dealloc_size += usable_size; |
| |
| EXPECT_EQ(alloc_count, tcache->thread_alloc_stats().alloc_count); |
| EXPECT_EQ(total_alloc_size, tcache->thread_alloc_stats().alloc_total_size); |
| EXPECT_EQ(dealloc_count, tcache->thread_alloc_stats().dealloc_count); |
| EXPECT_EQ(total_dealloc_size, |
| tcache->thread_alloc_stats().dealloc_total_size); |
| } |
| EXPECT_EQ(tcache->thread_alloc_stats().alloc_total_size, |
| tcache->thread_alloc_stats().dealloc_total_size); |
| } |
| |
| // This test makes sure it's safe to switch to the alternate bucket distribution |
| // at runtime. This is intended to happen once, near the start of Chrome, |
| // once we have enabled features. |
| TEST(AlternateBucketDistributionTest, SwitchBeforeAlloc) { |
| std::unique_ptr<PartitionAllocatorForTesting> allocator(CreateAllocator()); |
| PartitionRoot* root = allocator->root(); |
| |
| root->SwitchToDenserBucketDistribution(); |
| constexpr size_t n = (1 << 12) * 3 / 2; |
| EXPECT_NE(internal::BucketIndexLookup::GetIndex(n), |
| internal::BucketIndexLookup::GetIndexForNeutralBuckets(n)); |
| |
| void* ptr = root->Alloc(n); |
| |
| root->ResetBucketDistributionForTesting(); |
| |
| root->Free(ptr); |
| } |
| |
| // This test makes sure it's safe to switch to the alternate bucket distribution |
| // at runtime. This is intended to happen once, near the start of Chrome, |
| // once we have enabled features. |
| TEST(AlternateBucketDistributionTest, SwitchAfterAlloc) { |
| std::unique_ptr<PartitionAllocatorForTesting> allocator(CreateAllocator()); |
| constexpr size_t n = (1 << 12) * 3 / 2; |
| EXPECT_NE(internal::BucketIndexLookup::GetIndex(n), |
| internal::BucketIndexLookup::GetIndexForNeutralBuckets(n)); |
| |
| PartitionRoot* root = allocator->root(); |
| void* ptr = root->Alloc(n); |
| |
| root->SwitchToDenserBucketDistribution(); |
| |
| void* ptr2 = root->Alloc(n); |
| |
| root->Free(ptr2); |
| root->Free(ptr); |
| } |
| |
| } // namespace partition_alloc |
| |
| #endif // !defined(MEMORY_TOOL_REPLACES_ALLOCATOR) && |
| // PA_CONFIG(THREAD_CACHE_SUPPORTED) |