diff --git a/cobalt/base/BUILD.gn b/cobalt/base/BUILD.gn index 28f956e3c916..086264720466 100644 --- a/cobalt/base/BUILD.gn +++ b/cobalt/base/BUILD.gn @@ -67,6 +67,8 @@ static_library("base") { "path_provider.h", "polymorphic_downcast.h", "polymorphic_equatable.h", + "process/process_metrics_helper.cc", + "process/process_metrics_helper.h", "ref_counted_lock.h", "source_location.cc", "source_location.h", @@ -117,6 +119,7 @@ target(gtest_target_type, "base_test") { "c_val_time_interval_timer_stats_test.cc", "circular_buffer_shell_unittest.cc", "fixed_size_lru_cache_test.cc", + "process/process_metrics_helper_test.cc", "statistics_test.cc", "token_test.cc", ] diff --git a/cobalt/base/process/process_metrics_helper.cc b/cobalt/base/process/process_metrics_helper.cc new file mode 100644 index 000000000000..2d518ced0cf8 --- /dev/null +++ b/cobalt/base/process/process_metrics_helper.cc @@ -0,0 +1,203 @@ +// Copyright 2024 The Cobalt Authors. All Rights Reserved. +// +// 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 +// +// http://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. + +#include "cobalt/base/process/process_metrics_helper.h" + +#include +#include +#include +#include +#include + +#include "base/files/file_enumerator.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/memory/ptr_util.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" +#include "base/time/time.h" + +namespace base { + +namespace { + +static std::atomic clock_ticks_per_s{0}; + +ProcessMetricsHelper::ReadCallback GetReadCallback(const FilePath& path) { + return BindOnce( + [](const FilePath& path) -> absl::optional { + std::string contents; + if (!ReadFileToString(path, &contents)) return absl::nullopt; + return std::move(contents); + }, + path); +} + +double CalculateCPUUsageSeconds(const std::string& utime_string, + const std::string& stime_string, + int ticks_per_s) { + DCHECK_NE(ticks_per_s, 0); + double utime; + if (!StringToDouble(utime_string, &utime)) return 0.0; + double stime; + if (!StringToDouble(stime_string, &stime)) return 0.0; + return (utime + stime) / static_cast(ticks_per_s); +} + +} // namespace + +// static +int ProcessMetricsHelper::GetClockTicksPerS() { + return clock_ticks_per_s.load(); +} + +// static +int ProcessMetricsHelper::GetClockTicksPerS(ReadCallback uptime_callback, + ReadCallback stat_callback) { + double uptime = 0.0; + { + auto uptime_contents = std::move(uptime_callback).Run(); + if (!uptime_contents) return 0; + auto parts = SplitString(*uptime_contents, " ", TRIM_WHITESPACE, + SPLIT_WANT_NONEMPTY); + if (parts.size() == 0 || !StringToDouble(parts[0], &uptime) || + uptime == 0.0) { + return 0; + } + } + + // Get starttime. + auto fields = GetProcStatFields(std::move(stat_callback), {21}); + if (fields.size() != 1) return 0; + double starttime; + if (!StringToDouble(fields[0], &starttime) || starttime == 0.0) return 0; + int ticks_per_s = static_cast(starttime / uptime); + int nearest_tens = 10 * ((ticks_per_s + 5) / 10); + return nearest_tens; +} + +// static +void ProcessMetricsHelper::PopulateClockTicksPerS() { + DCHECK_EQ(clock_ticks_per_s.load(), 0); + clock_ticks_per_s.store( + GetClockTicksPerS(GetReadCallback(FilePath("/proc/uptime")), + GetReadCallback(FilePath("/proc/self/stat")))); +} + +// static +PlatformThreadId ProcessMetricsHelper::GetPid() { + PlatformThreadId pid; + auto fields = GetProcStatFields(FilePath("/proc/self"), {0}); + if (fields.size() != 1 || !StringToInt(fields[0], &pid)) { + return -1; + } + return pid; +} + +// static +TimeDelta ProcessMetricsHelper::GetCumulativeCPUUsage() { + int ticks_per_s = clock_ticks_per_s.load(); + if (ticks_per_s == 0) return TimeDelta(); + return GetCPUUsage(FilePath("/proc/self"), ticks_per_s); +} + +// static +base::Value ProcessMetricsHelper::GetCumulativeCPUUsagePerThread() { + int ticks_per_s = clock_ticks_per_s.load(); + if (ticks_per_s == 0) return base::Value(); + base::Value::List cpu_per_thread; + FileEnumerator file_enum(FilePath("/proc/self/task"), /*recursive=*/false, + FileEnumerator::DIRECTORIES); + for (FilePath path = file_enum.Next(); !path.empty(); + path = file_enum.Next()) { + // tid, thread name, utime, and stime. + Fields fields = GetProcStatFields(path, {0, 1, 13, 14}); + if (fields.size() != 4) continue; + int id; + if (!StringToInt(fields[0], &id)) continue; + base::Value::Dict entry = + base::Value::Dict() + .Set("id", id) + .Set("name", fields[1]) + .Set("utime", fields[2]) + .Set("stime", fields[3]) + .Set("usage_seconds", + CalculateCPUUsageSeconds(fields[2], fields[3], ticks_per_s)); + cpu_per_thread.Append(std::move(entry)); + } + return base::Value(std::move(cpu_per_thread)); +} + +// static +ProcessMetricsHelper::Fields ProcessMetricsHelper::GetProcStatFields( + ReadCallback read_callback, std::initializer_list indices) { + absl::optional contents = std::move(read_callback).Run(); + if (!contents) return Fields(); + + // Between the first '(' and last ')' is the second field called comm. It + // contains the process name and can include spaces. + int comm_start = contents->find('(') + 1; + int comm_end = contents->rfind(')'); + // End before " (". + std::string pid = contents->substr(0, comm_start - 2); + std::string comm = contents->substr(comm_start, comm_end - comm_start); + // Field after comm is state. Start after ") ". + int state_start = comm_end + 2; + // Split the string starting with the state field. + auto parts = SplitString(contents->substr(state_start), " ", TRIM_WHITESPACE, + SPLIT_WANT_NONEMPTY); + Fields fields; + for (int index : indices) { + if (index < 0 || index >= parts.size() + 2) { + return Fields(); + } + if (index == 0) { + fields.push_back(pid); + } else if (index == 1) { + fields.push_back(comm); + } else { + // Shift index to account for pid and comm. + int offset_index = index - 2; + fields.push_back(parts[offset_index]); + } + } + return std::move(fields); +} + +// static +ProcessMetricsHelper::Fields ProcessMetricsHelper::GetProcStatFields( + const FilePath& path, std::initializer_list indices) { + return ProcessMetricsHelper::GetProcStatFields( + GetReadCallback(path.Append("stat")), indices); +} + +// static +TimeDelta ProcessMetricsHelper::GetCPUUsage(ReadCallback read_callback, + int ticks_per_s) { + // Get utime and stime. + auto fields = ProcessMetricsHelper::GetProcStatFields( + std::move(read_callback), {13, 14}); + if (fields.size() != 2) return TimeDelta(); + return TimeDelta::FromSecondsD( + CalculateCPUUsageSeconds(fields[0], fields[1], ticks_per_s)); +} + +// static +TimeDelta ProcessMetricsHelper::GetCPUUsage(const FilePath& path, + int ticks_per_s) { + return ProcessMetricsHelper::GetCPUUsage(GetReadCallback(path.Append("stat")), + ticks_per_s); +} + +} // namespace base diff --git a/cobalt/base/process/process_metrics_helper.h b/cobalt/base/process/process_metrics_helper.h new file mode 100644 index 000000000000..86aa8a4203fb --- /dev/null +++ b/cobalt/base/process/process_metrics_helper.h @@ -0,0 +1,55 @@ +// Copyright 2024 The Cobalt Authors. All Rights Reserved. +// +// 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 +// +// http://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. + +#ifndef COBALT_BASE_PROCESS_PROCESS_METRICS_HELPER_H_ +#define COBALT_BASE_PROCESS_PROCESS_METRICS_HELPER_H_ + +#include +#include +#include + +#include "base/bind.h" +#include "base/files/file_path.h" +#include "base/threading/platform_thread.h" +#include "base/time/time.h" +#include "base/values.h" +#include "third_party/abseil-cpp/absl/types/optional.h" + +namespace base { + +class ProcessMetricsHelper { + public: + using CPUUsagePerThread = std::vector>; + using ReadCallback = OnceCallback()>; + using Fields = std::vector; + + static int GetClockTicksPerS(); + static void PopulateClockTicksPerS(); + static PlatformThreadId GetPid(); + static TimeDelta GetCumulativeCPUUsage(); + static base::Value GetCumulativeCPUUsagePerThread(); + + private: + friend class ProcessMetricsHelperTest; + + static int GetClockTicksPerS(ReadCallback, ReadCallback); + static Fields GetProcStatFields(ReadCallback, std::initializer_list); + static Fields GetProcStatFields(const FilePath&, std::initializer_list); + static TimeDelta GetCPUUsage(ReadCallback, int); + static TimeDelta GetCPUUsage(const FilePath&, int); +}; + +} // namespace base + +#endif // COBALT_BASE_PROCESS_PROCESS_METRICS_HELPER_H_ diff --git a/cobalt/base/process/process_metrics_helper_test.cc b/cobalt/base/process/process_metrics_helper_test.cc new file mode 100644 index 000000000000..8c354459f557 --- /dev/null +++ b/cobalt/base/process/process_metrics_helper_test.cc @@ -0,0 +1,205 @@ +// Copyright 2024 The Cobalt Authors. All Rights Reserved. +// +// 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 +// +// http://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. + +#include "cobalt/base/process/process_metrics_helper.h" + +#include +#include +#include +#include +#include +#include + +#include "base/bind.h" +#include "base/containers/contains.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/stringprintf.h" +#include "base/threading/thread.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace base { + +namespace { + +static std::atomic keep_busy{true}; + +void BusyWork(std::vector* vec) { + int64_t test_value = 0; + while (keep_busy.load()) { + ++test_value; + vec->push_back(NumberToString(test_value)); + if (vec->size() > 100000) vec->clear(); + } +} + +} // namespace + +class ProcessMetricsHelperTest : public testing::Test { + public: + static void SetUpTestSuite() { + ProcessMetricsHelper::PopulateClockTicksPerS(); + } + + ProcessMetricsHelper::Fields GetProcStatFields( + ProcessMetricsHelper::ReadCallback cb, + std::initializer_list indices) { + return ProcessMetricsHelper::GetProcStatFields(std::move(cb), indices); + } + + int GetClockTicksPerS(ProcessMetricsHelper::ReadCallback uptime_callback, + ProcessMetricsHelper::ReadCallback stat_callback) { + return ProcessMetricsHelper::GetClockTicksPerS(std::move(uptime_callback), + std::move(stat_callback)); + } + + TimeDelta GetCPUUsage(ProcessMetricsHelper::ReadCallback stat_callback, + int ticks_per_s) { + return ProcessMetricsHelper::GetCPUUsage(std::move(stat_callback), + ticks_per_s); + } +}; + +ProcessMetricsHelper::ReadCallback GetNulloptCallback() { + return BindOnce( + []() -> absl::optional { return absl::nullopt; }); +} + +ProcessMetricsHelper::ReadCallback GetUptimeCallback() { + return BindOnce([]() -> absl::optional { + return "1635667.97 155819443.97"; + }); +} + +ProcessMetricsHelper::ReadCallback GetStatCallback() { + return BindOnce([]() -> absl::optional { + return "3669677 (name with )( ) R 477564 3669677 477564 34817 3669677 " + "4194304 91 0 0 0 1 3 0 0 20 0 1 0 163348716 9076736 384 " + "18446744073709551615 94595846197248 94595846217801 140722879734192 " + "0 0 0 0 0 0 0 0 0 17 52 0 0 0 0 0 94595846237232 94595846238880 " + "94595860643840 140722879736274 140722879736294 140722879736294 " + "140722879741931 0"; + }); +} + +TEST_F(ProcessMetricsHelperTest, GetProcStatFields) { + auto fields = GetProcStatFields(GetStatCallback(), {0, 1, 2, 21, 51}); + EXPECT_EQ(5, fields.size()); + EXPECT_EQ("3669677", fields[0]); + EXPECT_EQ("name with )( ", fields[1]); + EXPECT_EQ("R", fields[2]); + EXPECT_EQ("163348716", fields[3]); + EXPECT_EQ("0", fields[4]); + + fields = GetProcStatFields(GetStatCallback(), {0}); + EXPECT_EQ(1, fields.size()); + EXPECT_EQ("3669677", fields[0]); + + fields = GetProcStatFields(GetStatCallback(), {0, 52}); + EXPECT_EQ(0, fields.size()); + + fields = GetProcStatFields(GetNulloptCallback(), {0}); + EXPECT_EQ(0, fields.size()); +} + +TEST_F(ProcessMetricsHelperTest, GetClockTicksPerSWithCallbacks) { + EXPECT_EQ(0, GetClockTicksPerS(GetNulloptCallback(), GetNulloptCallback())); + EXPECT_EQ(0, GetClockTicksPerS(GetNulloptCallback(), GetStatCallback())); + EXPECT_EQ(0, GetClockTicksPerS(GetUptimeCallback(), GetNulloptCallback())); + EXPECT_EQ(100, GetClockTicksPerS(GetUptimeCallback(), GetStatCallback())); +} + +TEST_F(ProcessMetricsHelperTest, GetClockTicksPerS) { + EXPECT_EQ(100, ProcessMetricsHelper::GetClockTicksPerS()); +} + +TEST_F(ProcessMetricsHelperTest, GetPid) { + PlatformThreadId pid = ProcessMetricsHelper::GetPid(); + EXPECT_GT(pid, 0); + EXPECT_EQ(PlatformThread::CurrentId(), pid); +} + +TEST_F(ProcessMetricsHelperTest, GetCumulativeCPUUsage) { + TimeDelta usage = ProcessMetricsHelper::GetCumulativeCPUUsage(); + EXPECT_GE(usage.InMicroseconds(), 0); +} + +TEST_F(ProcessMetricsHelperTest, GetCPUUsage) { + TimeDelta usage = GetCPUUsage(GetStatCallback(), 100); + EXPECT_EQ(40, usage.InMilliseconds()); +} + +TEST_F(ProcessMetricsHelperTest, GetCumulativeCPUUsagePerThread) { + base::Value cpu_per_thread = + ProcessMetricsHelper::GetCumulativeCPUUsagePerThread(); + base::Value::List* list = cpu_per_thread.GetIfList(); + int initial_num_threads = list ? list->size() : 0; + + Thread thread1("thread1"); + Thread thread2("thread2"); + Thread thread3("thread3"); + + thread1.StartAndWaitForTesting(); + thread2.StartAndWaitForTesting(); + thread3.StartAndWaitForTesting(); + + ASSERT_TRUE(thread1.IsRunning()); + ASSERT_TRUE(thread2.IsRunning()); + ASSERT_TRUE(thread3.IsRunning()); + + std::vector vec1; + std::vector vec2; + std::vector vec3; + + keep_busy.store(true); + thread1.task_runner()->PostTask(FROM_HERE, BindOnce(&BusyWork, &vec1)); + thread2.task_runner()->PostTask(FROM_HERE, BindOnce(&BusyWork, &vec2)); + thread3.task_runner()->PostTask(FROM_HERE, BindOnce(&BusyWork, &vec3)); + PlatformThread::Sleep(Milliseconds(50)); + keep_busy.store(false); + cpu_per_thread = ProcessMetricsHelper::GetCumulativeCPUUsagePerThread(); + list = cpu_per_thread.GetIfList(); + + EXPECT_TRUE(!!list); + EXPECT_EQ(initial_num_threads + 3, list->size()); + std::map names{ + {thread1.GetThreadId(), "thread1"}, + {thread2.GetThreadId(), "thread2"}, + {thread3.GetThreadId(), "thread3"}, + }; + int found = 0; + for (auto& entry_value : *list) { + base::Value::Dict* entry = entry_value.GetIfDict(); + EXPECT_TRUE(!!entry); + EXPECT_TRUE(entry->contains("id")); + EXPECT_TRUE(entry->contains("name")); + EXPECT_TRUE(entry->contains("utime")); + EXPECT_TRUE(entry->contains("stime")); + EXPECT_TRUE(entry->contains("usage_seconds")); + int id = entry->FindInt("id").value(); + if (names.count(id) == 1) { + found++; + EXPECT_EQ(names[id], *(entry->FindString("name"))); + EXPECT_GT(entry->FindString("utime")->size(), 0); + EXPECT_GT(entry->FindString("stime")->size(), 0); + EXPECT_GT(entry->FindDouble("usage_seconds"), 0.0); + } + } + EXPECT_EQ(3, found); + thread1.Stop(); + thread2.Stop(); + thread3.Stop(); +} + +} // namespace base diff --git a/starboard/shared/win32/test_filters.py b/starboard/shared/win32/test_filters.py index 352adbb1c886..0b666365f380 100644 --- a/starboard/shared/win32/test_filters.py +++ b/starboard/shared/win32/test_filters.py @@ -23,6 +23,12 @@ # implementation. 'SbTimeZoneGetNameTest.IsIANAFormat', ], + 'base_test': [ + 'ProcessMetricsHelperTest.GetClockTicksPerS', + 'ProcessMetricsHelperTest.GetPid', + 'ProcessMetricsHelperTest.GetCumulativeCPUUsage', + 'ProcessMetricsHelperTest.GetCumulativeCPUUsagePerThread', + ], }