| // 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 |