blob: 3380c93852e9a364d41a66eaa5561f2d6cf84760 [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/threading/platform_thread_metrics.h"
#include <array>
#include <atomic>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include "base/metrics/histogram_base.h"
#include "base/metrics/statistics_recorder.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/task/current_thread.h"
#include "base/task/thread_pool.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "base/test/test_timeouts.h"
#include "base/test/test_waitable_event.h"
#include "base/threading/platform_thread.h"
#include "base/threading/simple_thread.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "build/build_config.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#if BUILDFLAG(IS_ANDROID)
#include <linux/resource.h>
#include <sys/resource.h>
#endif // BUILDFLAG(IS_ANDROID)
#if BUILDFLAG(IS_WIN)
#include <windows.h>
#endif
namespace base {
namespace {
using ::testing::Ge;
using ::testing::Gt;
using ::testing::Optional;
void BusyWork() {
ElapsedTimer timer;
while (timer.Elapsed() < TestTimeouts::tiny_timeout()) {
std::vector<std::string> vec;
int64_t test_value = 0;
for (int i = 0; i < 100000; ++i) {
++test_value;
vec.push_back(NumberToString(test_value));
}
}
}
class MetricsTestThread final : public SimpleThread {
public:
MetricsTestThread() : SimpleThread("MetricsTestThread") {}
MetricsTestThread(const MetricsTestThread&) = delete;
MetricsTestThread& operator=(const MetricsTestThread&) = delete;
~MetricsTestThread() final {
if (HasBeenStarted() && !HasBeenJoined()) {
Stop();
}
}
PlatformThreadHandle handle() const {
handle_ready_event_.Wait();
return handle_.load(std::memory_order_relaxed);
}
// Stop the thread.
void Stop() {
ASSERT_TRUE(HasBeenStarted());
ASSERT_FALSE(HasBeenJoined());
stop_event_.Signal();
Join();
}
// Cause the thread to do busy work. The caller will block until it's done.
void DoBusyWork() {
ASSERT_TRUE(HasBeenStarted());
ASSERT_FALSE(HasBeenJoined());
do_busy_work_event_.Signal();
done_busy_work_event_.Wait();
}
// SimpleThread:
void Run() final {
#if BUILDFLAG(IS_WIN)
// CurrentHandle() returns a pseudo-handle that's the same in every thread.
// Duplicate it to get a real handle.
HANDLE win_handle;
ASSERT_TRUE(::DuplicateHandle(::GetCurrentProcess(), ::GetCurrentThread(),
::GetCurrentProcess(), &win_handle,
THREAD_QUERY_LIMITED_INFORMATION, FALSE, 0));
PlatformThreadHandle handle(win_handle);
#else
PlatformThreadHandle handle = PlatformThread::CurrentHandle();
#endif
handle_.store(handle, std::memory_order_relaxed);
handle_ready_event_.Signal();
std::array<WaitableEvent*, 2> events{&do_busy_work_event_, &stop_event_};
while (!stop_event_.IsSignaled()) {
// WaitMany returns the lowest index among signaled events.
if (WaitableEvent::WaitMany(events) == 0) {
// DoBusyWork() waits on `done_busy_work_event_` so it should be
// impossible to signal `stop_event_` too.
ASSERT_FALSE(stop_event_.IsSignaled());
BusyWork();
done_busy_work_event_.Signal();
}
}
}
private:
// When this is signaled, stop the thread.
TestWaitableEvent stop_event_;
// When `do_busy_work_event_` is signaled, do some busy work, then signal
// `done_busy_work_event_`. These are auto-reset so they can be reused to do
// more busy work.
TestWaitableEvent do_busy_work_event_{WaitableEvent::ResetPolicy::AUTOMATIC};
TestWaitableEvent done_busy_work_event_{
WaitableEvent::ResetPolicy::AUTOMATIC};
std::atomic<PlatformThreadHandle> handle_;
mutable TestWaitableEvent handle_ready_event_;
};
class PlatformThreadMetricsTest : public ::testing::Test {
public:
void SetUp() override {
#if BUILDFLAG(IS_WIN) && !defined(ARCH_CPU_ARM64)
// TSC is only initialized once TSCTicksPerSecond() is called twice at least
// 50 ms apart on the same thread to get a baseline. If the system has a
// TSC, make sure it's initialized so all GetCumulativeCPUUsage calls use
// it.
if (time_internal::HasConstantRateTSC()) {
if (time_internal::TSCTicksPerSecond() == 0) {
PlatformThread::Sleep(Milliseconds(51));
}
ASSERT_GT(time_internal::TSCTicksPerSecond(), 0);
}
#endif
}
};
} // namespace
#if BUILDFLAG(IS_APPLE) || BUILDFLAG(IS_WIN)
TEST_F(PlatformThreadMetricsTest, CreateFromHandle) {
EXPECT_FALSE(PlatformThreadMetrics::CreateFromHandle(PlatformThreadHandle()));
EXPECT_TRUE(
PlatformThreadMetrics::CreateFromHandle(PlatformThread::CurrentHandle()));
MetricsTestThread thread;
thread.Start();
PlatformThreadHandle handle = thread.handle();
ASSERT_FALSE(handle.is_null());
EXPECT_FALSE(handle.is_equal(PlatformThread::CurrentHandle()));
EXPECT_TRUE(PlatformThreadMetrics::CreateFromHandle(handle));
}
#endif
#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_CHROMEOS) || \
BUILDFLAG(IS_FUCHSIA) || BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_WIN)
TEST_F(PlatformThreadMetricsTest, CreateFromId) {
EXPECT_FALSE(PlatformThreadMetrics::CreateFromId(PlatformThreadId()));
EXPECT_FALSE(PlatformThreadMetrics::CreateFromId(kInvalidThreadId));
EXPECT_TRUE(PlatformThreadMetrics::CreateFromId(PlatformThread::CurrentId()));
MetricsTestThread thread;
thread.Start();
PlatformThreadId tid = thread.tid();
ASSERT_NE(tid, kInvalidThreadId);
EXPECT_NE(tid, PlatformThread::CurrentId());
EXPECT_TRUE(PlatformThreadMetrics::CreateFromId(tid));
}
#endif
TEST_F(PlatformThreadMetricsTest, GetCumulativeCPUUsage_CurrentThread) {
auto metrics = PlatformThreadMetrics::CreateForCurrentThread();
ASSERT_TRUE(metrics);
const auto cpu_usage = metrics->GetCumulativeCPUUsage();
ASSERT_THAT(cpu_usage, Optional(Ge(TimeDelta())));
// First call to GetCPUUsageProportion() always returns 0.
EXPECT_EQ(metrics->GetCPUUsageProportion(cpu_usage.value()), 0.0);
BusyWork();
const auto cpu_usage2 = metrics->GetCumulativeCPUUsage();
ASSERT_THAT(cpu_usage2, Optional(Gt(cpu_usage.value())));
// Should be capped at 100%, but may be higher due to rounding so there's no
// strict upper bound to test.
EXPECT_GT(metrics->GetCPUUsageProportion(cpu_usage2.value()), 0.0);
}
TEST_F(PlatformThreadMetricsTest, GetCumulativeCPUUsage_OtherThread) {
MetricsTestThread thread;
thread.Start();
#if BUILDFLAG(IS_APPLE)
// Apple is the only platform that doesn't support CreateFromId().
ASSERT_FALSE(thread.handle().is_null());
auto metrics = PlatformThreadMetrics::CreateFromHandle(thread.handle());
#else
ASSERT_NE(thread.tid(), kInvalidThreadId);
auto metrics = PlatformThreadMetrics::CreateFromId(thread.tid());
#endif
ASSERT_TRUE(metrics);
const auto cpu_usage = metrics->GetCumulativeCPUUsage();
ASSERT_THAT(cpu_usage, Optional(Ge(TimeDelta())));
// First call to GetCPUUsageProportion() always returns 0.
EXPECT_EQ(metrics->GetCPUUsageProportion(cpu_usage.value()), 0.0);
thread.DoBusyWork();
const auto cpu_usage2 = metrics->GetCumulativeCPUUsage();
ASSERT_THAT(cpu_usage2, Optional(Gt(cpu_usage.value())));
// Should be capped at 100%, but may be higher due to rounding so there's no
// strict upper bound to test.
EXPECT_GT(metrics->GetCPUUsageProportion(cpu_usage2.value()), 0.0);
thread.Stop();
// Thread is no longer running.
const auto cpu_usage3 = metrics->GetCumulativeCPUUsage();
// Ensure that measuring the CPU usage of a stopped thread doesn't give bogus
// values, although it may fail on some platforms. (If the measurement works,
// it will include any CPU used between the last measurement and the join.)
#if BUILDFLAG(IS_WIN)
// Windows can always read the final CPU usage of a stopped thread.
ASSERT_NE(cpu_usage3, std::nullopt);
#else
// POSIX platforms are racy, so the measurement may fail. Apple and Fuchsia
// seem to always fail, but if a change causes measurements to start working,
// that's good too.
if (cpu_usage3.has_value()) {
EXPECT_GE(cpu_usage3.value(), cpu_usage2.value());
}
#endif
}
#if BUILDFLAG(IS_ANDROID)
class PlatformThreadPriorityMonitorTest : public ::testing::Test {
public:
static constexpr std::string_view kMainThreadSuffix = "MainThread";
static constexpr std::string_view kChildThreadSuffix = "ChildThread";
static constexpr TimeDelta kMinSamplingInterval =
PlatformThreadPriorityMonitor::kMinSamplingInterval;
void TearDown() override { ASSERT_EQ(setpriority(PRIO_PROCESS, 0, 0), 0); }
size_t GetRegisteredThreadCount() {
auto& monitor = PlatformThreadPriorityMonitor::Get();
AutoLock lock(monitor.lock_);
return monitor.thread_id_to_histogram_.size();
}
static std::string HistogramNameForSuffix(const std::string_view& suffix) {
return PlatformThreadPriorityMonitor::Get().GetHistogramNameForSuffix(
suffix);
}
};
class PriorityMonitorTestDelegate : public PlatformThread::Delegate {
public:
PriorityMonitorTestDelegate() = default;
~PriorityMonitorTestDelegate() override = default;
void ThreadMain() override {
PlatformThreadPriorityMonitor::Get().RegisterCurrentThread(
PlatformThreadPriorityMonitorTest::kChildThreadSuffix);
registered_event_.Signal();
stop_event_.Wait();
}
void WaitUntilRegistered() { registered_event_.Wait(); }
void SignalStop() { stop_event_.Signal(); }
private:
TestWaitableEvent stop_event_;
TestWaitableEvent registered_event_;
};
// Test UnregisterCurrentThread() is called on thread exit.
TEST_F(PlatformThreadPriorityMonitorTest, UnregisterOnJoin) {
ASSERT_EQ(0u, GetRegisteredThreadCount());
PriorityMonitorTestDelegate delegate;
PlatformThreadHandle handle;
ASSERT_TRUE(PlatformThread::Create(0, &delegate, &handle));
delegate.WaitUntilRegistered();
EXPECT_EQ(1u, GetRegisteredThreadCount());
delegate.SignalStop();
PlatformThread::Join(handle);
EXPECT_EQ(0u, GetRegisteredThreadCount());
}
// Test that priority monitor reports thread priorities for all registered
// threads.
TEST_F(PlatformThreadPriorityMonitorTest, ReportThreadPriorities) {
test::TaskEnvironment task_environment{
test::TaskEnvironment::TimeSource::MOCK_TIME};
PriorityMonitorTestDelegate delegate;
PlatformThreadHandle handle;
ASSERT_TRUE(PlatformThread::Create(0, &delegate, &handle));
// Register the current thread and start monitoring thread priorities.
PlatformThreadPriorityMonitor::Get().RegisterCurrentThread(kMainThreadSuffix);
PlatformThreadPriorityMonitor::Get().Start();
// Set the priority of the current thread to a different value than the child
// thread.
constexpr int kTestNiceValue = -7;
ASSERT_EQ(setpriority(PRIO_PROCESS, 0, kTestNiceValue), 0);
delegate.WaitUntilRegistered();
EXPECT_EQ(2u, GetRegisteredThreadCount());
HistogramTester histogram_tester;
const std::string main_thread_histogram_name =
HistogramNameForSuffix(kMainThreadSuffix);
const std::string child_thread_histogram_name =
HistogramNameForSuffix(kChildThreadSuffix);
task_environment.FastForwardBy(Milliseconds(1));
histogram_tester.ExpectTotalCount(main_thread_histogram_name, 0);
histogram_tester.ExpectTotalCount(child_thread_histogram_name, 0);
// Should record a sample for each thread.
task_environment.FastForwardBy(kMinSamplingInterval - Milliseconds(1));
histogram_tester.ExpectTotalCount(main_thread_histogram_name, 1);
histogram_tester.ExpectUniqueSample(main_thread_histogram_name,
kTestNiceValue, 1);
histogram_tester.ExpectTotalCount(child_thread_histogram_name, 1);
histogram_tester.ExpectUniqueSample(child_thread_histogram_name, 0, 1);
delegate.SignalStop();
PlatformThread::Join(handle);
}
#endif // BUILDFLAG(IS_ANDROID)
} // namespace base