blob: 8492e2fb4359bd715a18f5e0c2289a6435352033 [file] [log] [blame]
// 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