| // Copyright 2025 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "base/synchronization/lock_metrics_recorder.h" |
| |
| #include <cstddef> |
| #include <memory> |
| |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/rand_util.h" |
| #include "base/synchronization/lock.h" |
| #include "base/synchronization/waitable_event.h" |
| #include "base/test/bind.h" |
| #include "base/threading/platform_thread.h" |
| #include "base/time/time.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace base { |
| |
| namespace { |
| class LockMetricsRecorderTest : public testing::Test { |
| public: |
| LockMetricsRecorderTest() { lock_metrics_recorder_.SetTargetCurrentThread(); } |
| |
| protected: |
| LockMetricsRecorder lock_metrics_recorder_; |
| |
| private: |
| MetricsSubSampler::ScopedAlwaysSampleForTesting always_sample_; |
| }; |
| } // namespace |
| |
| // Test that recording while iterating through the ring buffer is not permitted. |
| TEST_F(LockMetricsRecorderTest, TestRecordingWhileIterating) { |
| EXPECT_TRUE(lock_metrics_recorder_.ShouldRecordLockAcquisitionTime()); |
| lock_metrics_recorder_.RecordLockAcquisitionTime(Microseconds(1)); |
| lock_metrics_recorder_.ForEachSample([&](const TimeDelta& sample) { |
| EXPECT_FALSE(lock_metrics_recorder_.ShouldRecordLockAcquisitionTime()); |
| }); |
| EXPECT_TRUE(lock_metrics_recorder_.ShouldRecordLockAcquisitionTime()); |
| } |
| |
| // Test that writing more samples there is space for in the internal buffer of |
| // lock metrics recorder overwrites the oldest samples. |
| TEST_F(LockMetricsRecorderTest, TestSampleOverwrite) { |
| // Size of lock metric recorder's internal buffer. |
| constexpr size_t kBufferSize = LockMetricsRecorder::kMaxSamples; |
| // The number of additional samples written. |
| constexpr size_t kExtraSamples = 5; |
| |
| // The i-th sample has value i microseconds to allow us to check the age of |
| // the sample. |
| for (size_t i = 0; i < kBufferSize + kExtraSamples; i++) { |
| lock_metrics_recorder_.RecordLockAcquisitionTime(Microseconds(i)); |
| } |
| size_t num_samples = 0; |
| lock_metrics_recorder_.ForEachSample([&](const TimeDelta& sample) { |
| // The oldest `kExtraSamples` are expected to be overwritten, leaving us |
| // with samples starting at `kExtraSamples` microseconds. |
| EXPECT_EQ(sample, Microseconds(num_samples + kExtraSamples)); |
| num_samples++; |
| }); |
| EXPECT_EQ(num_samples, kBufferSize); |
| } |
| |
| // Test that samples are iterated over exactly once. |
| TEST_F(LockMetricsRecorderTest, TestSamplesIteratedOverExactlyOnce) { |
| constexpr size_t kSamplesPerIteration = 10; |
| static_assert(kSamplesPerIteration <= LockMetricsRecorder::kMaxSamples); |
| |
| size_t num_samples = 0; |
| for (size_t i = 0; i < 2; i++) { |
| const size_t num_samples_prev = num_samples; |
| // The j-th sample has value i microseconds to allow us to check the age of |
| // the sample. |
| for (size_t j = 0; j < kSamplesPerIteration; j++) { |
| lock_metrics_recorder_.RecordLockAcquisitionTime( |
| Microseconds(j + num_samples)); |
| } |
| lock_metrics_recorder_.ForEachSample([&](const TimeDelta& sample) { |
| EXPECT_EQ(sample, Microseconds(num_samples)); |
| num_samples++; |
| }); |
| EXPECT_EQ(num_samples - num_samples_prev, kSamplesPerIteration); |
| } |
| } |
| |
| // Test if ScopedLockAcquisitionTimer records a sample as expected |
| TEST_F(LockMetricsRecorderTest, ScopedLockAcquisitionTimerRecordsSample) { |
| size_t num_samples = 0; |
| lock_metrics_recorder_.ForEachSample( |
| [&](const TimeDelta& sample) { num_samples++; }); |
| EXPECT_EQ(num_samples, 0); |
| |
| { |
| auto timer = LockMetricsRecorder::ScopedLockAcquisitionTimer::CreateForTest( |
| &lock_metrics_recorder_); |
| PlatformThread::Sleep(Microseconds(500)); |
| } |
| lock_metrics_recorder_.ForEachSample([&](const TimeDelta& sample) { |
| EXPECT_GT(sample, Microseconds(500)); |
| num_samples++; |
| }); |
| EXPECT_EQ(num_samples, 1); |
| } |
| |
| namespace { |
| class MetricsRecorderTestThread : public PlatformThread::Delegate { |
| public: |
| MetricsRecorderTestThread(Lock* lock, WaitableEvent* should_start) |
| : lock_(lock), should_start_(should_start) {} |
| |
| 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 |
| AutoLock auto_lock(*lock_); |
| should_start_->Signal(); |
| PlatformThread::Sleep(Seconds(1)); |
| } |
| |
| private: |
| raw_ptr<Lock> lock_; |
| raw_ptr<WaitableEvent> should_start_; |
| }; |
| |
| // Two threads try to acquire the lock with very high-probability of lock |
| // contention. |
| void MakeThreadsContendOnLock() { |
| Lock lock; |
| PlatformThreadHandle handle; |
| WaitableEvent event; |
| MetricsRecorderTestThread thread(&lock, &event); |
| |
| // Create another thread and wait for it to acquire the lock before trying to |
| // acquire the lock to create contention. |
| ASSERT_TRUE(PlatformThread::Create(0, &thread, &handle)); |
| event.Wait(); |
| { |
| AutoLock auto_lock(lock); |
| } |
| |
| PlatformThread::Join(handle); |
| } |
| |
| // Creates a LockMetricsRecorder object to record lock metrics for the current |
| // thread without subsampling and sets it to record lock metrics for Lock |
| class BaseLockMetricsTest : public testing::Test { |
| public: |
| BaseLockMetricsTest() { |
| LockMetricsRecorder::Get()->SetTargetCurrentThread(); |
| } |
| |
| void SetUp() override { |
| ASSERT_TRUE(LockMetricsRecorder::Get()->ShouldRecordLockAcquisitionTime()); |
| } |
| |
| private: |
| MetricsSubSampler::ScopedAlwaysSampleForTesting always_sample_; |
| }; |
| |
| } // namespace |
| |
| // Test that no samples are recorded when there is no contention on the lock. |
| TEST_F(BaseLockMetricsTest, NoSamplesRecordedWhenUncontended) { |
| Lock lock; |
| |
| { |
| AutoLock auto_lock(lock); |
| } |
| |
| LockMetricsRecorder::Get()->ForEachSample( |
| [](const TimeDelta& sample) { GTEST_FAIL() << "No samples expected"; }); |
| } |
| |
| // Test that samples are recorded when there is contention on the lock. |
| TEST_F(BaseLockMetricsTest, SamplesRecordedWhenContended) { |
| MakeThreadsContendOnLock(); |
| bool did_record_sample = false; |
| LockMetricsRecorder::Get()->ForEachSample( |
| [&](const TimeDelta&) { did_record_sample = true; }); |
| EXPECT_TRUE(did_record_sample); |
| } |
| |
| } // namespace base |