blob: 5658492fcd7e77dd16c5c039eb06cdfe281abf0e [file] [log] [blame] [edit]
// 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 <vector>
#include "base/strings/string_number_conversions.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_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.data(), events.size()) == 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
}
} // namespace base