Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimistically read from /proc for process stats. #3374

Merged
merged 1 commit into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions base/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -1065,6 +1065,13 @@ component("base") {
]
}

if (use_cobalt_customizations) {
sources += [
"process/internal_linux.cc",
"process/internal_linux.h",
]
}

if (!use_cobalt_customizations && (is_linux || is_chromeos || is_android)) {
sources += [
"files/file_path_watcher_inotify.cc",
Expand Down
31 changes: 21 additions & 10 deletions base/process/internal_linux.cc
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const char kProcDir[] = "/proc";

const char kStatFile[] = "stat";

#if !defined(STARBOARD)
FilePath GetProcPidDir(pid_t pid) {
return FilePath(kProcDir).Append(NumberToString(pid));
}
Expand All @@ -56,6 +57,7 @@ pid_t ProcDirSlotToPid(const char* d_name) {
}
return pid;
}
#endif // !defined(STARBOARD)

bool ReadProcFile(const FilePath& file, std::string* buffer) {
DCHECK(FilePath(kProcDir).IsParent(file));
Expand All @@ -70,10 +72,12 @@ bool ReadProcFile(const FilePath& file, std::string* buffer) {
return !buffer->empty();
}

#if !defined(STARBOARD)
bool ReadProcStats(pid_t pid, std::string* buffer) {
FilePath stat_file = internal::GetProcPidDir(pid).Append(kStatFile);
return ReadProcFile(stat_file, buffer);
}
#endif // !defined(STARBOARD)

bool ParseProcStats(const std::string& stats_data,
std::vector<std::string>* proc_stats) {
Expand All @@ -86,28 +90,29 @@ bool ParseProcStats(const std::string& stats_data,
// pid (process name) data1 data2 .... dataN
// Look for the closing paren by scanning backwards, to avoid being fooled by
// processes with ')' in the name.
size_t open_parens_idx = stats_data.find(" (");
size_t close_parens_idx = stats_data.rfind(") ");
if (open_parens_idx == std::string::npos ||
close_parens_idx == std::string::npos ||
open_parens_idx > close_parens_idx) {
size_t pid_end = stats_data.find(" (");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the existing logic broken in upstream, or is it not a "hard broken" and its just that the upstream returns the thread name with the parentheses and you changed that for us to leave those out?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pid has an extra space at the end. Everything else is parsed correctly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to keep variable names the same where possible and keep our changes in STARBOARD/USE_COBALT_CUSTOMIZATIONS blocks, the only thing I see changing is comm_start

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need the start and end positions for pid and comm, and we need the start position for state.

open_parens_idx was the first occurrence of " (. This is the pid end. pid start position is 0. open_parens_idx is incremented and the pid substring is off by 1.

Naming the positions we need avoids the modifying the value and using it to represent two different things.

stats_data.substr(start, end - start) returns the value we want.

After the last ") ", the rest of the fields are space delimited.

If I wanted to leave the code mostly the same, I would need to either move the open_parens_idx++ after getting the pid or removing the increment and fix how comm is retrieved.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option is to parse the /proc/pid/stat in cobalt/base/process/process_metrics_helper.cc.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless we upstream this code (the ideal solution) or guard it with ifdefs (an easier immediate solution), someone will eventually try to revert this back to the original code i.e. during an update as right now it looks like a mistaken diff from the upstream. Using the ifdefs we usually use makes it clear to readers that it's a conscious decision instead

size_t comm_start = pid_end + 2;
size_t comm_end = stats_data.rfind(") ");
size_t state_start = comm_end + 2;
if (pid_end == std::string::npos ||
comm_end == std::string::npos ||
pid_end > comm_end) {
aee-google marked this conversation as resolved.
Show resolved Hide resolved
DLOG(WARNING) << "Failed to find matched parens in '" << stats_data << "'";
NOTREACHED();
return false;
}
open_parens_idx++;

proc_stats->clear();
// PID.
proc_stats->push_back(stats_data.substr(0, open_parens_idx));
proc_stats->push_back(stats_data.substr(0, pid_end));
aee-google marked this conversation as resolved.
Show resolved Hide resolved
// Process name without parentheses.
proc_stats->push_back(
stats_data.substr(open_parens_idx + 1,
close_parens_idx - (open_parens_idx + 1)));
stats_data.substr(comm_start,
comm_end - comm_start));

// Split the rest.
std::vector<std::string> other_stats = SplitString(
stats_data.substr(close_parens_idx + 2), " ",
stats_data.substr(state_start), " ",
base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
for (const auto& i : other_stats)
proc_stats->push_back(i);
Expand Down Expand Up @@ -152,16 +157,19 @@ int64_t ReadStatFileAndGetFieldAsInt64(const FilePath& stat_file,
return GetProcStatsFieldAsInt64(proc_stats, field_num);
}

#if !defined(STARBOARD)
int64_t ReadProcStatsAndGetFieldAsInt64(pid_t pid, ProcStatsFields field_num) {
FilePath stat_file = internal::GetProcPidDir(pid).Append(kStatFile);
return ReadStatFileAndGetFieldAsInt64(stat_file, field_num);
aee-google marked this conversation as resolved.
Show resolved Hide resolved
}
#endif // !defined(STARBOARD)

int64_t ReadProcSelfStatsAndGetFieldAsInt64(ProcStatsFields field_num) {
FilePath stat_file = FilePath(kProcDir).Append("self").Append(kStatFile);
return ReadStatFileAndGetFieldAsInt64(stat_file, field_num);
}

#if !defined(STARBOARD)
size_t ReadProcStatsAndGetFieldAsSizeT(pid_t pid,
ProcStatsFields field_num) {
std::string stats_data;
Expand All @@ -172,6 +180,7 @@ size_t ReadProcStatsAndGetFieldAsSizeT(pid_t pid,
return 0;
return GetProcStatsFieldAsSizeT(proc_stats, field_num);
}
#endif // !defined(STARBOARD)

Time GetBootTime() {
FilePath path("/proc/stat");
Expand All @@ -189,6 +198,7 @@ Time GetBootTime() {
return Time::FromTimeT(btime);
}

#if !defined(STARBOARD)
TimeDelta GetUserCpuTimeSinceBoot() {
FilePath path("/proc/stat");
std::string contents;
Expand Down Expand Up @@ -228,6 +238,7 @@ TimeDelta ClockTicksToTimeDelta(int64_t clock_ticks) {

return Microseconds(Time::kMicrosecondsPerSecond * clock_ticks / kHertz);
}
#endif // !defined(STARBOARD)

} // namespace internal
} // namespace base
12 changes: 12 additions & 0 deletions base/process/internal_linux.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
#include <string>
#include <vector>

#if !defined(STARBOARD)
#include "base/files/dir_reader_posix.h"
#endif // !defined(STARBOARD)
#include "base/files/file_path.h"
#include "base/process/process_handle.h"
#include "base/strings/string_number_conversions.h"
Expand All @@ -33,14 +35,17 @@ extern const char kProcDir[];
// "stat"
extern const char kStatFile[];

#if !defined(STARBOARD)
// Returns a FilePath to "/proc/pid".
base::FilePath GetProcPidDir(pid_t pid);
#endif // !defined(STARBOARD)

// Reads a file from /proc into a string. This is allowed on any thread as
// reading from /proc does not hit the disk. Returns true if the file can be
// read and is non-empty.
bool ReadProcFile(const FilePath& file, std::string* buffer);

#if !defined(STARBOARD)
// Take a /proc directory entry named |d_name|, and if it is the directory for
// a process, convert it to a pid_t.
// Returns 0 on failure.
Expand All @@ -50,6 +55,7 @@ pid_t ProcDirSlotToPid(const char* d_name);
// Reads /proc/<pid>/stat into |buffer|. Returns true if the file can be read
// and is non-empty.
bool ReadProcStats(pid_t pid, std::string* buffer);
#endif // !defined(STARBOARD)

// Takes |stats_data| and populates |proc_stats| with the values split by
// spaces. Taking into account the 2nd field may, in itself, contain spaces.
Expand Down Expand Up @@ -89,16 +95,21 @@ size_t GetProcStatsFieldAsSizeT(const std::vector<std::string>& proc_stats,
// ReadProcStats(). See GetProcStatsFieldAsInt64() for details.
int64_t ReadStatsFilendGetFieldAsInt64(const FilePath& stat_file,
ProcStatsFields field_num);
#if !defined(STARBOARD)
int64_t ReadProcStatsAndGetFieldAsInt64(pid_t pid, ProcStatsFields field_num);
#endif // !defined(STARBOARD)
int64_t ReadProcSelfStatsAndGetFieldAsInt64(ProcStatsFields field_num);

#if !defined(STARBOARD)
// Same as ReadProcStatsAndGetFieldAsInt64() but for size_t values.
size_t ReadProcStatsAndGetFieldAsSizeT(pid_t pid,
ProcStatsFields field_num);
#endif // !defined(STARBOARD)

// Returns the time that the OS started. Clock ticks are relative to this.
Time GetBootTime();

#if !defined(STARBOARD)
// Returns the amount of time spent in user space since boot across all CPUs.
TimeDelta GetUserCpuTimeSinceBoot();

Expand Down Expand Up @@ -130,6 +141,7 @@ void ForEachProcessTask(base::ProcessHandle process, Lambda&& lambda) {
lambda(tid, task_path);
}
}
#endif // !defined(STARBOARD)

} // namespace internal
} // namespace base
Expand Down
3 changes: 3 additions & 0 deletions cobalt/base/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
]
Expand Down
181 changes: 181 additions & 0 deletions cobalt/base/process/process_metrics_helper.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// 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 <atomic>
#include <cmath>
#include <memory>
#include <string>
#include <utility>
#include <vector>

#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/process/internal_linux.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"

namespace base {

namespace {

static std::atomic<int> clock_ticks_per_s{0};

ProcessMetricsHelper::ReadCallback GetReadCallback(const FilePath& path) {
return BindOnce(
[](const FilePath& path) -> absl::optional<std::string> {
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<double>(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 current_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], &current_uptime) ||
current_uptime == 0.0) {
return 0;
}
}

auto fields = GetProcStatFields(std::move(stat_callback),
{internal::ProcStatsFields::VM_STARTTIME});
if (fields.size() != 1) return 0;
double process_starttime;
if (!StringToDouble(fields[0], &process_starttime) ||
process_starttime == 0.0)
return 0;
double ticks_per_s = process_starttime / current_uptime;
int rounded_up = 10 * static_cast<int>(std::ceil(ticks_per_s / 10.0));
return rounded_up;
}

// 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
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
Value ProcessMetricsHelper::GetCumulativeCPUUsagePerThread() {
int ticks_per_s = clock_ticks_per_s.load();
if (ticks_per_s == 0) return Value();
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()) {
Fields fields =
GetProcStatFields(path, {0, internal::ProcStatsFields::VM_COMM,
internal::ProcStatsFields::VM_UTIME,
internal::ProcStatsFields::VM_STIME});
if (fields.size() != 4) continue;
int id;
if (!StringToInt(fields[0], &id)) continue;
Value::Dict entry =
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 Value(std::move(cpu_per_thread));
}

// static
ProcessMetricsHelper::Fields ProcessMetricsHelper::GetProcStatFields(
aee-google marked this conversation as resolved.
Show resolved Hide resolved
ReadCallback read_callback, std::initializer_list<int> indices) {
absl::optional<std::string> contents = std::move(read_callback).Run();
if (!contents) return Fields();

std::vector<std::string> proc_stats;
if (!internal::ParseProcStats(*contents, &proc_stats)) return Fields();

Fields fields;
for (int index : indices) {
if (index < 0 || index >= proc_stats.size()) return Fields();
fields.push_back(std::move(proc_stats[index]));
}
return std::move(fields);
}

// static
ProcessMetricsHelper::Fields ProcessMetricsHelper::GetProcStatFields(
const FilePath& path, std::initializer_list<int> indices) {
return ProcessMetricsHelper::GetProcStatFields(
GetReadCallback(path.Append("stat")), indices);
}

// static
TimeDelta ProcessMetricsHelper::GetCPUUsage(ReadCallback read_callback,
int ticks_per_s) {
auto fields = ProcessMetricsHelper::GetProcStatFields(
std::move(read_callback), {internal::ProcStatsFields::VM_UTIME,
internal::ProcStatsFields::VM_STIME});
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
Loading
Loading