blob: 16c1e2abacf2417e05a57229a753f8efde14594b [file]
// Copyright 2026 The Abseil Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Exercises the SpinLock contention profiling hooks. The hooks can only be
// registered once per process, so this test lives in its own binary and must
// not be linked together with another profiler.
#include <atomic>
#include <cstdint>
#include <thread> // NOLINT(build/c++11)
#include "gtest/gtest.h"
#include "absl/base/config.h"
#include "absl/base/internal/cycleclock.h"
#include "absl/base/internal/scheduling_mode.h"
#include "absl/base/internal/spinlock.h"
#include "absl/synchronization/notification.h"
namespace absl {
ABSL_NAMESPACE_BEGIN
namespace base_internal {
struct SpinLockProfilingTestPeer {
// True once a waiter has marked itself as about to sleep in the lock word.
static bool HasSleeper(const SpinLock& l) {
return (l.lockword_.load(std::memory_order_relaxed) &
SpinLock::kSpinLockSleeper) != 0;
}
};
namespace {
const void* profiled_lock = nullptr;
std::atomic<int64_t> lock_profiler_calls{0};
std::atomic<int64_t> lock_profiler_cycles{0};
std::atomic<int64_t> unlock_profiler_calls{0};
std::atomic<int64_t> unlock_profiler_cycles{0};
// Number of times the unlock profiler ran for an acquisition whose lock
// profiler had not yet run.
std::atomic<int64_t> unlock_before_lock_calls{0};
void LockProfiler(const void* lock, int64_t wait_cycles) {
if (lock != profiled_lock) return;
lock_profiler_calls.fetch_add(1, std::memory_order_relaxed);
lock_profiler_cycles.fetch_add(wait_cycles, std::memory_order_relaxed);
}
void UnlockProfiler(const void* lock, int64_t wait_cycles) {
if (lock != profiled_lock) return;
if (lock_profiler_calls.load(std::memory_order_relaxed) == 0) {
unlock_before_lock_calls.fetch_add(1, std::memory_order_relaxed);
}
unlock_profiler_calls.fetch_add(1, std::memory_order_relaxed);
unlock_profiler_cycles.fetch_add(wait_cycles, std::memory_order_relaxed);
}
class SpinLockProfiling : public ::testing::Test {
protected:
void SetUp() override {
RegisterSpinLockProfiler(&UnlockProfiler);
RegisterSpinLockLockProfiler(&LockProfiler);
lock_profiler_calls.store(0, std::memory_order_relaxed);
lock_profiler_cycles.store(0, std::memory_order_relaxed);
unlock_profiler_calls.store(0, std::memory_order_relaxed);
unlock_profiler_cycles.store(0, std::memory_order_relaxed);
unlock_before_lock_calls.store(0, std::memory_order_relaxed);
}
void TearDown() override { profiled_lock = nullptr; }
};
TEST_F(SpinLockProfiling, ContentionProfilers) {
SpinLock spinlock(SCHEDULE_KERNEL_ONLY);
profiled_lock = &spinlock;
// Hold the lock until a waiter is about to sleep on it, then keep holding
// it for kContendedHoldCycles so that the waiter's wait is comfortably above
// the reporting threshold.
constexpr int64_t kContendedHoldCycles = int64_t{1} << 20;
Notification held;
std::thread holder([&] {
spinlock.lock();
held.Notify();
while (!SpinLockProfilingTestPeer::HasSleeper(spinlock)) {
std::this_thread::yield();
}
const int64_t end = CycleClock::Now() + kContendedHoldCycles;
while (CycleClock::Now() < end) {
}
spinlock.unlock();
});
held.WaitForNotification();
// Causes contention, since `holder` already holds the lock.
spinlock.lock();
spinlock.unlock();
holder.join();
profiled_lock = nullptr;
// The waiter's acquisition reported its own wait.
EXPECT_EQ(1, lock_profiler_calls.load(std::memory_order_relaxed));
EXPECT_GE(lock_profiler_cycles.load(std::memory_order_relaxed),
kContendedHoldCycles);
// The waiter's release reported the wait stored in the lock word by its
// acquisition. The holder's acquisition was uncontended.
EXPECT_EQ(1, unlock_profiler_calls.load(std::memory_order_relaxed));
EXPECT_GE(unlock_profiler_cycles.load(std::memory_order_relaxed),
kContendedHoldCycles);
// An acquisition is reported by the lock profiler before its release is
// reported by the unlock profiler.
EXPECT_EQ(0, unlock_before_lock_calls.load(std::memory_order_relaxed));
}
TEST_F(SpinLockProfiling, UncontendedAcquisitionsAreNotReported) {
SpinLock spinlock(SCHEDULE_KERNEL_ONLY);
profiled_lock = &spinlock;
for (int i = 0; i < 1000; ++i) {
spinlock.lock();
spinlock.unlock();
}
profiled_lock = nullptr;
EXPECT_EQ(0, lock_profiler_calls.load(std::memory_order_relaxed));
EXPECT_EQ(0, unlock_profiler_calls.load(std::memory_order_relaxed));
}
} // namespace
} // namespace base_internal
ABSL_NAMESPACE_END
} // namespace absl