blob: c3d455ba2fd3a6d53bb959369d89ff36edff5746 [file]
// 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/partition_lock.h"
#include <algorithm>
#include <atomic>
#include <cstddef>
#include "partition_alloc/partition_alloc_base/compiler_specific.h"
#if PA_BUILDFLAG(IS_POSIX)
#include <sys/resource.h>
#include <sys/utsname.h>
#endif
#include "partition_alloc/build_config.h"
#include "partition_alloc/buildflags.h"
#include "partition_alloc/partition_alloc_base/rand_util.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_base/time/time.h"
#include "partition_alloc/partition_root.h"
#include "partition_alloc/spinning_mutex.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace partition_alloc::internal {
TEST(PartitionAllocLockTest, Simple) {
Lock lock;
lock.Acquire();
lock.Release();
}
namespace {
Lock g_lock;
} // namespace
TEST(PartitionAllocLockTest, StaticLockStartsUnlocked) {
g_lock.Acquire();
g_lock.Release();
}
namespace {
class ThreadDelegateForContended
: public base::PlatformThreadForTesting::Delegate {
public:
explicit ThreadDelegateForContended(Lock& start_lock,
Lock& lock,
int iterations,
int& counter)
: start_lock_(start_lock),
lock_(lock),
iterations_(iterations),
counter_(counter) {}
void ThreadMain() override {
start_lock_.Acquire();
start_lock_.Release();
for (int i = 0; i < iterations_; i++) {
lock_.Acquire();
++counter_;
lock_.Release();
}
}
private:
Lock& start_lock_;
Lock& lock_;
const int iterations_;
int& counter_;
};
} // namespace
TEST(PartitionAllocLockTest, Contended) {
int counter = 0; // *Not* atomic.
std::vector<internal::base::PlatformThreadHandle> thread_handles;
constexpr int iterations_per_thread = 1000000;
constexpr int num_threads = 4;
Lock lock;
Lock start_lock;
ThreadDelegateForContended delegate(start_lock, lock, iterations_per_thread,
counter);
start_lock.Acquire(); // Make sure that the threads compete, by waiting until
// all of them have at least been created.
for (int i = 0; i < num_threads; ++i) {
base::PlatformThreadHandle handle;
base::PlatformThreadForTesting::Create(0, &delegate, &handle);
thread_handles.push_back(handle);
}
start_lock.Release();
for (int i = 0; i < num_threads; ++i) {
base::PlatformThreadForTesting::Join(thread_handles[i]);
}
EXPECT_EQ(iterations_per_thread * num_threads, counter);
}
namespace {
class ThreadDelegateForSlowThreads
: public base::PlatformThreadForTesting::Delegate {
public:
explicit ThreadDelegateForSlowThreads(Lock& start_lock,
Lock& lock,
int iterations,
int& counter)
: start_lock_(start_lock),
lock_(lock),
iterations_(iterations),
counter_(counter) {}
void ThreadMain() override {
start_lock_.Acquire();
start_lock_.Release();
for (int i = 0; i < iterations_; i++) {
lock_.Acquire();
++counter_;
// Hold the lock for a while, to force futex()-based locks to sleep.
base::PlatformThread::Sleep(base::Milliseconds(1));
lock_.Release();
}
}
private:
Lock& start_lock_;
Lock& lock_;
const int iterations_;
int& counter_;
};
} // namespace
TEST(PartitionAllocLockTest, SlowThreads) {
int counter = 0; // *Not* atomic.
std::vector<base::PlatformThreadHandle> thread_handles;
constexpr int iterations_per_thread = 100;
constexpr int num_threads = 4;
Lock lock;
Lock start_lock;
ThreadDelegateForSlowThreads delegate(start_lock, lock, iterations_per_thread,
counter);
start_lock.Acquire(); // Make sure that the threads compete, by waiting until
// all of them have at least been created.
for (int i = 0; i < num_threads; i++) {
base::PlatformThreadHandle handle;
base::PlatformThreadForTesting::Create(0, &delegate, &handle);
thread_handles.push_back(handle);
}
start_lock.Release();
for (int i = 0; i < num_threads; i++) {
base::PlatformThreadForTesting::Join(thread_handles[i]);
}
EXPECT_EQ(iterations_per_thread * num_threads, counter);
}
TEST(PartitionAllocLockTest, AssertAcquired) {
Lock lock;
lock.Acquire();
lock.AssertAcquired();
lock.Release();
}
#if defined(GTEST_HAS_DEATH_TEST) && \
(PA_BUILDFLAG(DCHECKS_ARE_ON) || \
PA_BUILDFLAG(ENABLE_PARTITION_LOCK_REENTRANCY_CHECK))
// Need to bypass `-Wthread-safety-analysis` for this test.
#if __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wthread-safety-analysis"
#endif
TEST(PartitionAllocLockTest, ReentrancyDeathTest) {
Lock lock;
lock.Acquire();
EXPECT_DEATH(lock.Acquire(), "");
}
#if __clang__
#pragma clang diagnostic pop
#endif
#endif // defined(GTEST_HAS_DEATH_TEST) && (PA_BUILDFLAG(DCHECKS_ARE_ON) ||
// PA_BUILDFLAG(ENABLE_PARTITION_LOCK_REENTRANCY_CHECK))
// AssertAcquired() is only enforced with DCHECK()s.
// DCHECKs don't work with EXPECT_DEATH on official builds.
#if defined(GTEST_HAS_DEATH_TEST) && PA_BUILDFLAG(DCHECKS_ARE_ON) && \
(!defined(OFFICIAL_BUILD) || PA_BUILDFLAG(IS_DEBUG))
TEST(PartitionAllocLockTest, AssertAcquiredDeathTest) {
Lock lock;
EXPECT_DEATH(lock.AssertAcquired(), "");
}
namespace {
class ThreadDelegateForAssertAcquiredAnotherThreadHoldsTheLock
: public base::PlatformThreadForTesting::Delegate {
public:
explicit ThreadDelegateForAssertAcquiredAnotherThreadHoldsTheLock(Lock& lock)
: lock_(lock) {}
void ThreadMain() PA_NO_THREAD_SAFETY_ANALYSIS override { lock_.Acquire(); }
private:
Lock& lock_;
};
} // namespace
TEST(PartitionAllocLockTest, AssertAcquiredAnotherThreadHoldsTheLock) {
Lock lock;
// PA_NO_THREAD_SAFETY_ANALYSIS: The checker rightfully points out that the
// lock is still held at the end of the function, which is what we want here.
ThreadDelegateForAssertAcquiredAnotherThreadHoldsTheLock delegate(lock);
base::PlatformThreadHandle handle;
base::PlatformThreadForTesting::Create(0, &delegate, &handle);
// Join before the test, otherwise some platforms' gtest have trouble with
// EXPECT_DEATH() and multiple live threads.
base::PlatformThreadForTesting::Join(handle);
// DCHECKs don't work with EXPECT_DEATH on official builds.
#if PA_BUILDFLAG(DCHECKS_ARE_ON) && \
(!defined(OFFICIAL_BUILD) || PA_BUILDFLAG(IS_DEBUG))
EXPECT_DEATH(lock.AssertAcquired(), "");
#endif
}
#if PA_BUILDFLAG(IS_APPLE)
namespace {
class ThreadDelegateForReinitInOtherThread
: public base::PlatformThreadForTesting::Delegate {
public:
explicit ThreadDelegateForReinitInOtherThread(Lock& lock) : lock_(lock) {}
void ThreadMain() PA_NO_THREAD_SAFETY_ANALYSIS override {
lock_.Reinit();
lock_.Acquire();
lock_.Release();
}
private:
Lock& lock_;
};
} // namespace
// On Apple OSes, it is not allowed to unlock a lock from another thread, so
// we need to re-initialize it.
TEST(PartitionAllocLockTest, ReinitInOtherThread) PA_NO_THREAD_SAFETY_ANALYSIS {
Lock lock;
lock.Acquire();
ThreadDelegateForReinitInOtherThread delegate(lock);
base::PlatformThreadHandle handle;
base::PlatformThreadForTesting::Create(0, &delegate, &handle);
base::PlatformThreadForTesting::Join(handle);
}
#endif // PA_BUILDFLAG(IS_APPLE)
#endif // defined(GTEST_HAS_DEATH_TEST) && PA_BUILDFLAG(DCHECKS_ARE_ON)
// Priority Inheritance Tests --------------------------------------------------
#if PA_BUILDFLAG(ENABLE_PARTITION_LOCK_PRIORITY_INHERITANCE)
namespace {
// CPU bound work for the threads to eat up CPU cycles.
void BusyLoop(size_t n) {
auto generator = base::InsecureRandomGenerator::ConstructForTesting();
int sum = 0;
for (size_t i = 0; i < n; i++) {
if (i == 0 || (generator.RandUint32() & 1)) {
sum++;
}
}
ASSERT_GT(sum, 0) << "The busy loop was optimized out.";
}
class TestThreadBase : public base::PlatformThreadForTesting::Delegate {
public:
TestThreadBase() = default;
explicit TestThreadBase(int nice_value) : nice_value_(nice_value) {}
void Create() {
ASSERT_TRUE(base::PlatformThreadForTesting::Create(0, this, &handle_));
}
void Join() { base::PlatformThreadForTesting::Join(handle_); }
void ThreadMain() override {
if (nice_value_) {
ASSERT_EQ(setpriority(PRIO_PROCESS, 0, nice_value_), 0);
}
Body();
}
virtual void Body() = 0;
private:
int nice_value_;
base::PlatformThreadHandle handle_;
};
class PriorityInheritanceTest {
public:
// The average value of MeasureRunTime() over |num_samples| iterations.
static base::TimeDelta MeasureAverageRunTime(int num_samples = 10) {
base::TimeDelta total_run_time;
for (int i = 0; i < num_samples; i++) {
total_run_time += MeasureRunTime();
}
return total_run_time / num_samples;
}
// Measure the time taken for a low-priority thread (nice value = 2) to
// perform CPU bound work when it holds a lock that is awaited by a
// high-priority thread (nice value = -16).
static base::TimeDelta MeasureRunTime() {
Lock lock;
{
// Migrate the lock to use the PI futex, if enabled, by acquiring and
// releasing the lock.
ScopedGuard guard(lock);
}
HighPriorityThread thread_a(&lock);
LowPriorityThread thread_b(&lock, &thread_a);
// Create the low-priority thread which is responsible for creating the
// high-priority thread. Wait for both threads to finish before recording
// the elapsed time.
thread_b.Create();
thread_b.Join();
thread_a.Join();
return thread_b.GetTestRunTime();
}
// Class that spawns a thread on object creation to perform CPU bound work
// until the object is destroyed.
class CPUBoundWorkerThread : public TestThreadBase {
public:
explicit CPUBoundWorkerThread() : should_shutdown_(false) { Create(); }
~CPUBoundWorkerThread() override {
should_shutdown_.store(true, std::memory_order_relaxed);
Join();
}
void Body() override {
while (!should_shutdown_.load(std::memory_order_relaxed)) {
BusyLoop(10);
}
}
private:
std::atomic<bool> should_shutdown_;
};
class HighPriorityThread : public TestThreadBase {
public:
explicit HighPriorityThread(Lock* lock)
: TestThreadBase(-16), lock_(lock) {}
void Body() override {
// Wait on the lock to be released once the low-priority thread is
// done. In the case when priority inheritance mutexes are enabled,
// this should boost the priority of the low-priority thread to the
// priority of the highest priority waiter (i.e. the current thread).
ScopedGuard guard(*lock_);
BusyLoop(10);
}
private:
Lock* lock_;
};
class LowPriorityThread : public TestThreadBase {
public:
LowPriorityThread(Lock* lock, HighPriorityThread* thread_a)
: TestThreadBase(2), lock_(lock), thread_a_(thread_a) {}
void Body() override {
// Acquire the lock before creating the high-priority thread, so that
// the higher priority thread is blocked on the current thread while
// the current thread performs CPU-bound work.
ScopedGuard guard(*lock_);
thread_a_->Create();
// Before performing the CPU bound work, wait for the thread A to
// begin waiting on the lock.
//
// NOTE: Unlike //base/synchronization/lock_unittest.cc where we use an
// atomic to signal the low-priority thread to start its work, we instead
// inspect the lock itself to know if there is another thread waiting on
// the lock. This is necessary because a thread trying to acquire a
// partition allocator lock spins in userspace yielding the CPU multiple
// times between attempts to to acquire the lock before waiting in the
// kernel and this process can take longer than the low-priority thread's
// workload itself on x64 emulators. By waiting for the high-priority
// thread to enter the kernel and potentially boost the low-priority
// thread, the test beomes more consistent and reliable.
while (!lock_->HasWaitersForTesting()) {
(void)0;
}
base::TimeTicks start_time = base::TimeTicks::Now();
BusyLoop(2000000);
test_run_time_ = base::TimeTicks::Now() - start_time;
}
base::TimeDelta GetTestRunTime() const { return test_run_time_; }
private:
Lock* lock_;
HighPriorityThread* thread_a_;
base::TimeDelta test_run_time_;
};
};
static int32_t major_version, minor_version, bugfix_version;
bool PriorityInheritanceLocksSupported() {
struct utsname info;
EXPECT_EQ(uname(&info), 0);
int num_read = PA_UNSAFE_TODO(sscanf(info.release, "%d.%d.%d", &major_version,
&minor_version, &bugfix_version));
if (num_read < 1) {
major_version = 0;
}
if (num_read < 2) {
minor_version = 0;
}
if (num_read < 3) {
bugfix_version = 0;
}
// Same as ::base::KernelSupportsPriorityInheritanceFutex()
return (major_version > 6) ||
((major_version == 6) &&
((minor_version > 12) ||
(minor_version == 12 && bugfix_version > 13) ||
(minor_version == 6 && bugfix_version > 29) ||
(minor_version == 1 && bugfix_version > 75)));
}
} // namespace
// Tests that the time taken by a higher-priority thread to acquire a lock held
// by a lower-priority thread is indeed reduced by priority inheritance.
//
// NOTE: This test is a reimplementation of LockTest.PriorityIsInherited from
// //base/synchronization/lock_unittest.cc for partition alloc with a few key
// differences to prevent the CQ bots from timing out during the test:
// a) The low-priority thread is set to nice value 2, since it might not make
// progress quick enough in the non-PI case.
// b) The CPU-bound worker threads are created only once instead of
// per-iteration to minimize the number of threads created. This is because
// test run time on emulators is significantly affected by the time taken
// to join all the threads.
TEST(PartitionAllocLockTest, PriorityIsInherited) {
base::TimeDelta avg_test_run_time_with_pi, avg_test_run_time_without_pi;
// Priority inheritance locks are not supported on Android kernels < 6.1
if (!PriorityInheritanceLocksSupported()) {
GTEST_SKIP()
<< "internal::Lock does not handle multiple thread priorities.";
}
// Keep all the cores busy with a workload of CPU bound thread to reduce
// flakiness in the test by skewing the CPU time between the high-priority
// and low-priority measurement threads.
std::vector<PriorityInheritanceTest::CPUBoundWorkerThread>
cpu_bound_worker_threads(15);
// Since we only support migration of the lock from a non-PI lock to a PI
// lock, the the test without PI locks needs to be run first This is because
// unlike base::Lock, where the PI-ness of the lock is stored in the lock
// object at lock creation time and the global state is not queried later, for
// partition_alloc::internal::Lock the PI-ness of the lock is set at the first
// unlock after the feature is enabled but the global feature state is still
// queried at every unlock.
{
avg_test_run_time_without_pi =
PriorityInheritanceTest::MeasureAverageRunTime();
}
{
SpinningMutex::EnableUsePriorityInheritance();
avg_test_run_time_with_pi =
PriorityInheritanceTest::MeasureAverageRunTime();
}
// During the time in which the thread A is waiting on the lock to be released
// by the thread B, the thread B runs at priority 130 in the non-PI
// case and at priority 104 in the PI case.
//
// Based on the Linux kernel's allocation of CPU shares documented in
// https://elixir.bootlin.com/linux/v6.12.5/source/kernel/sched/core.c#L9998,
// a thread running at priority 104 (nice value = -16) gets 36291 shares of
// the CPU, a thread at priority 120 (nice value = 0) gets 1024 shares and a
// thread at priority 122 (nice value = 2) gets 655 shares of the CPU.
//
// Assuming no other threads except the ones created by this test are running,
// during the time in which thread A is waiting on the lock to be released by
// thread B, thread B gets 655/(15*1024 + 655) ≈ 4.1% of the CPU time in the
// non-PI case and 36291/(36291 + 15*1024) ≈ 70% of the CPU time in the PI
// case. This is approximately a 17x difference in CPU shares allocated to
// the thread B when it is doing CPU-bound work.
//
// The test is thus designed such that the measured run time is thread B's CPU
// bound work. While there are other factors at play that determine the
// measured run time such as the frequency at which the CPU is running, we can
// expect that there will be at least an order of magnitude of disparity in
// the test run times with and without PI.
//
// In order to reduce test flakiness while still eliminating the possibility
// of variance in measurements accounting for the test results, we
// conservatively expect a 3x improvement.
EXPECT_GT(avg_test_run_time_without_pi, 3 * avg_test_run_time_with_pi)
<< " on kernel version " << major_version << "." << minor_version << "."
<< bugfix_version;
}
namespace {
constexpr int kNumThreads = 64, kNumAcquiresPerThread = 16;
class FutexMigrationTestThread : public TestThreadBase {
public:
FutexMigrationTestThread(Lock* lock,
std::atomic<int>* num_threads_started,
int thread_id)
: TestThreadBase(0),
lock_(lock),
num_threads_started_(num_threads_started),
thread_id_(thread_id) {}
void Body() override {
num_threads_started_->fetch_add(1, std::memory_order_relaxed);
for (int j = 0; j < kNumAcquiresPerThread; j++) {
// Only one of the many threads we spawn triggers the migration of the
// lock futex. We want to make it likely that there are threads waiting
// on the non-PI futex in the kernel when the migration happens to try
// and get better code coverage for futex migration. So we try and make
// the migration happen somewhere in the middle of the test.
if (j == kNumAcquiresPerThread / 4 && thread_id_ == kNumThreads / 4) {
SpinningMutex::EnableUsePriorityInheritance();
}
ScopedGuard guard(*lock_);
BusyLoop(100000);
}
}
private:
Lock* lock_;
std::atomic<int>* num_threads_started_;
int thread_id_;
};
// FUTEX_LOCK_PI2 was introduced in kernel 5.14.
bool FutexLockPI2Supported() {
struct utsname info;
if (uname(&info) != 0) {
return false;
}
int32_t major = 0, minor = 0;
PA_UNSAFE_TODO(sscanf(info.release, "%d.%d", &major, &minor));
return major > 5 || (major == 5 && minor >= 14);
}
} // namespace
// The |PartitionAllocUsePriorityInheritanceLocks| feature is enabled after the
// PartitionRoot lock is initialized. This requires the lock to internally
// migrate all its waiters from the non-PI futex to the PI futex. So we test if
// the migration works correctly by enabling the feature with multiple threads
// contending for the same lock.
TEST(PartitionAllocLockTest, FutexMigration) {
if (!FutexLockPI2Supported()) {
GTEST_SKIP() << "FUTEX_LOCK_PI2 requires kernel >= 5.14";
}
Lock lock;
std::atomic<int> num_threads_started{0};
std::vector<FutexMigrationTestThread> threads;
for (int i = 0; i < kNumThreads; i++) {
threads.emplace_back(&lock, &num_threads_started, i);
}
{
ScopedGuard guard(lock);
// Create the threads after acquiring the lock and release it only after all
// threads have started to ensure heavy and consistent contention on the
// lock.
std::ranges::for_each(threads, &FutexMigrationTestThread::Create);
while (num_threads_started.load(std::memory_order_relaxed) < kNumThreads) {
usleep(10);
}
}
// The test is complete if all the threads manage to acquire the lock
// post-migration.
std::ranges::for_each(threads, &FutexMigrationTestThread::Join);
}
#endif // PA_BUILDFLAG(ENABLE_PARTITION_LOCK_PRIORITY_INHERITANCE)
namespace {
class SimpleBarrier {
public:
explicit SimpleBarrier(size_t n) : num_threads_(n) {}
void Synchronize() {
num_threads_.fetch_sub(1, std::memory_order_release);
while (num_threads_.load(std::memory_order_acquire)) {
base::PlatformThread::Sleep(base::Microseconds(10));
}
}
private:
std::atomic<size_t> num_threads_;
};
class MetricsRecorderTestThread
: public base::PlatformThreadForTesting::Delegate {
public:
MetricsRecorderTestThread(Lock& lock, SimpleBarrier& sync_point)
: lock_(lock), sync_point_(sync_point) {}
void ThreadMain() override {
// Signal that this thread has taken the lock, then go to sleep for a
// long-time holding the lock to make sure the other thread takes the slow
// path of acquire and will record a sample
ScopedGuard guard(lock_);
sync_point_.Synchronize();
base::PlatformThread::Sleep(base::Seconds(1));
}
private:
Lock& lock_;
SimpleBarrier& sync_point_;
};
// Two threads try to acquire the lock with very high-probability of lock
// contention.
void MakeThreadsContendOnLock() {
Lock lock;
base::PlatformThreadHandle handle;
SimpleBarrier sync_point{2};
MetricsRecorderTestThread thread(lock, sync_point);
// Create another thread and wait for it to acquire the lock before trying to
// acquire the lock to create contention.
base::PlatformThreadForTesting::Create(0, &thread, &handle);
sync_point.Synchronize();
{
ScopedGuard guard(lock);
}
base::PlatformThreadForTesting::Join(handle);
}
// Checks if the number of samples recorded until the object goes out of scope
// (with sampling ratio set to 1) is equal to the expected value.
class LockMetricsTestHelper : public LockMetricsRecorderInterface {
public:
LockMetricsTestHelper() {
SpinningMutex::SetLockMetricsRecorderForTesting(this);
}
~LockMetricsTestHelper() override {
SpinningMutex::SetLockMetricsRecorderForTesting(nullptr);
}
bool ShouldRecordLockAcquisitionTime() const override { return true; }
void RecordLockAcquisitionTime(base::TimeDelta sample) override {
num_samples_recorded_++;
}
size_t GetRecordedSamplesCount() const { return num_samples_recorded_; }
private:
size_t num_samples_recorded_ = 0;
};
} // namespace
// Test that no samples are recorded when there is no contention on the lock.
TEST(PartitionAllocLockTest, MetricsNotRecordedWhenUncontended) {
LockMetricsTestHelper helper;
{
Lock lock;
ScopedGuard guard(lock);
}
size_t num_samples_recorded = helper.GetRecordedSamplesCount();
EXPECT_EQ(num_samples_recorded, 0U);
}
// Test that samples are recorded when there is contention on the lock.
TEST(PartitionAllocLockTest, MetricsRecordedWhenContended) {
LockMetricsTestHelper helper;
MakeThreadsContendOnLock();
size_t num_samples_recorded = helper.GetRecordedSamplesCount();
EXPECT_GE(num_samples_recorded, 1U);
}
} // namespace partition_alloc::internal