blob: c16f0e2d0e0f3f52c0edc64ad6d3164c9c45b42f [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "zircon/system/ulib/profile/config.h"
#include <fcntl.h>
#include <lib/fitx/result.h>
#include <lib/syslog/global.h>
#include <lib/zx/profile.h>
#include <lib/zx/time.h>
#include <zircon/syscalls/profile.h>
#include <algorithm>
#include <limits>
#include <sstream>
#include <string>
#include <unordered_map>
#include <fbl/unique_fd.h>
#include <rapidjson/document.h>
#include <rapidjson/error/en.h>
#include <re2/re2.h>
#include <src/lib/files/directory.h>
#include <src/lib/files/file.h>
using zircon_profile::Profile;
using zircon_profile::ProfileScope;
namespace {
constexpr char kConfigFileExtension[] = ".profiles";
std::string ToString(const zx_profile_info_t& info) {
std::ostringstream stream;
stream << "{ ";
if (info.flags & ZX_PROFILE_INFO_FLAG_PRIORITY) {
stream << "\"priority\": " << info.priority << ", ";
}
if (info.flags & ZX_PROFILE_INFO_FLAG_DEADLINE) {
stream << "\"capacity\": " << info.deadline_params.capacity
<< ", \"deadline\": " << info.deadline_params.relative_deadline
<< ", \"period\": " << info.deadline_params.period << ", ";
}
if (info.flags & ZX_PROFILE_INFO_FLAG_CPU_MASK) {
stream << "\"affinity\": " << info.cpu_affinity_mask.mask[0] << " (0x" << std::hex
<< info.cpu_affinity_mask.mask[0] << "), " << std::dec;
}
stream << "}";
return stream.str();
}
std::string ToString(ProfileScope scope) {
switch (scope) {
case ProfileScope::Bringup:
return "bringup";
case ProfileScope::Core:
return "core";
case ProfileScope::Product:
return "product";
default:
return "none";
}
}
// Proxies an iterator over the members of the given value node. As of this
// writing, the version of rapidjson in third_party does not support range-based
// for loops. This adapter provides the missing functionality.
struct IterateMembers {
explicit IterateMembers(const rapidjson::Value& value)
: begin_iterator{value.MemberBegin()}, end_iterator{value.MemberEnd()} {}
auto begin() { return begin_iterator; }
auto end() { return end_iterator; }
rapidjson::Value::ConstMemberIterator begin_iterator;
rapidjson::Value::ConstMemberIterator end_iterator;
};
// Proxies an iterator over the values of the given array node. Provides missing
// functionality similar to the iterator above.
struct IterateValues {
explicit IterateValues(const rapidjson::Value& value)
: begin_iterator{value.Begin()}, end_iterator{value.End()} {}
auto begin() { return begin_iterator; }
auto end() { return end_iterator; }
rapidjson::Value::ConstValueIterator begin_iterator;
rapidjson::Value::ConstValueIterator end_iterator;
};
// Utility to build a fitx::result<std::string, ?> in the error state using stream operators.
//
// Example:
//
// return Error() << "Failed to open file " << filename << "!";
//
class Error {
public:
Error() = default;
explicit Error(const std::string& initial) : stream_{initial} {}
// Forwards the stream operator argument to the underlying ostream.
template <typename T>
Error& operator<<(const T& value) {
stream_ << value;
return *this;
}
// Implicit conversion to fitx::result<std::string, ?> in the error state with accumulated string
// as the error value.
template <typename... Vs>
operator fitx::result<std::string, Vs...>() const {
return fitx::error(stream_.str());
}
operator fitx::error<std::string>() const { return fitx::error(stream_.str()); }
private:
std::ostringstream stream_;
};
fitx::result<std::string, zx::duration> ParseDurationString(const std::string& duration) {
// Match one or more digits, optionally followed by time units ms, us, or ns.
static const re2::RE2 kReDuration{"^(\\d+)(ms|us|ns)?$"};
int64_t scalar;
std::string units;
const bool matched = re2::RE2::PartialMatch(duration, kReDuration, &scalar, &units);
if (!matched) {
return Error() << "String \"" << duration << "\" is not a valid duration!";
}
if (units.empty() || units == "ns") {
return fitx::ok(zx::nsec(scalar));
}
if (units == "ms") {
return fitx::ok(zx::msec(scalar));
}
if (units == "us") {
return fitx::ok(zx::usec(scalar));
}
return Error() << "String duration \"" << duration << "\" has unrecognized units \"" << units
<< "\"!";
}
fitx::result<std::string, zx::duration> ParseDuration(const rapidjson::Value& object) {
if (object.IsInt()) {
return fitx::ok(zx::nsec(object.GetInt()));
}
if (object.IsString()) {
return ParseDurationString(object.GetString());
}
return fitx::error("Duration must be an integer or duration string!");
}
struct TextPosition {
int32_t line;
int32_t column;
};
TextPosition GetLineAndColumnForOffset(const std::string& input, size_t offset) {
if (offset == 0) {
// Errors at position 0 are assumed to be related to the whole file.
return {.line = 0, .column = 0};
}
TextPosition position = {.line = 1, .column = 1};
for (size_t i = 0; i < input.size() && i < offset; i++) {
if (input[i] == '\n') {
position.line += 1;
position.column = 1;
} else {
position.column += 1;
}
}
return position;
}
std::string GetErrorMessage(const rapidjson::Document& document, const std::string& file_data) {
const auto [line, column] = GetLineAndColumnForOffset(file_data, document.GetErrorOffset());
std::ostringstream stream;
stream << line << ":" << column << ": " << GetParseError_En(document.GetParseError());
return stream.str();
}
template <typename... Context>
auto GetMember(const char* name, const rapidjson::Value& object, Context&&... context)
-> fitx::result<std::string, decltype(std::cref(object[name]))> {
if (!object.IsObject()) {
return (Error() << ... << std::forward<Context>(context)) << " must be a JSON object!";
}
if (!object.HasMember(name)) {
return (Error() << ... << std::forward<Context>(context))
<< " must have a \"" << name << "\" member!";
}
return fitx::ok(std::cref(object[name]));
}
template <typename... Context>
fitx::result<std::string, int> GetInt(const char* name, const rapidjson::Value& object,
Context&&... context) {
auto result = GetMember(name, object, std::forward<Context>(context)...);
if (result.is_error()) {
return result.take_error();
}
if (!result->get().IsInt()) {
return (Error() << ... << std::forward<Context>(context))
<< " member \"" << name << "\" must be an integer!";
}
return fitx::ok(result->get().GetInt());
}
template <typename... Context>
fitx::result<std::string, const char*> GetString(const char* name, const rapidjson::Value& object,
Context&&... context) {
auto result = GetMember(name, object, std::forward<Context>(context)...);
if (result.is_error()) {
return result.take_error();
}
if (!result->get().IsString()) {
return (Error() << ... << std::forward<Context>(context))
<< " member \"" << name << "\" must be a string!";
}
return fitx::ok(result->get().GetString());
}
template <typename... Context>
auto GetArray(const char* name, const rapidjson::Value& object, Context&&... context) {
auto result = GetMember(name, object, std::forward<Context>(context)...);
if (result.is_ok() && !result->get().IsArray()) {
return (Error() << ... << std::forward<Context>(context))
<< " member \"" << name << "\" must be an array!";
}
return result;
}
template <typename... Context>
auto GetObject(const char* name, const rapidjson::Value& object, Context&&... context) {
auto result = GetMember(name, object, std::forward<Context>(context)...);
if (result.is_ok() && !result->get().IsObject()) {
return (Error() << ... << std::forward<Context>(context))
<< " member \"" << name << "\" must be a JSON object!";
}
return result;
}
template <typename... Context>
fitx::result<std::string, unsigned int> GetUint(const char* name, const rapidjson::Value& object,
Context&&... context) {
auto result = GetMember(name, object, std::forward<Context>(context)...);
if (result.is_error()) {
return result.take_error();
}
if (!result->get().IsUint()) {
return (Error() << ... << std::forward<Context>(context)).rdbuf()
<< " member \"" << name << "\" must be an unsigned integer!";
}
return fitx::ok(result->get().GetUint());
}
void ParseProfiles(const std::string& filename, const rapidjson::Document& document,
zircon_profile::ProfileMap* profiles) {
if (!document.IsObject()) {
FX_LOG(WARNING, "ProfileProvider", "The profile config document must be a JSON object!");
return;
}
if (!document.HasMember("profiles")) {
return;
}
const rapidjson::Value& profile_member = document["profiles"];
ProfileScope scope = ProfileScope::None;
if (document.HasMember("scope")) {
auto result = GetString("scope", document);
if (result.is_ok()) {
if (!strcmp(*result, "bringup")) {
scope = ProfileScope::Bringup;
} else if (!strcmp(*result, "core")) {
scope = ProfileScope::Core;
} else if (!strcmp(*result, "product")) {
scope = ProfileScope::Product;
} else {
FX_LOGF(WARNING, "ProfileProvider", "%s: Invalid role scope \"%s\", defaulting to none!",
filename.c_str(), *result);
}
}
} else {
FX_LOGF(WARNING, "ProfileProvider", "%s: Missing role scope, defaulting to none!",
filename.c_str());
}
for (const auto& profile : IterateMembers(profile_member)) {
const char* profile_name = profile.name.GetString();
if (!profile.value.IsObject()) {
FX_LOGF(WARNING, "ProfileProvider", "%s: Profile \"%s\" value must be a JSON object!",
filename.c_str(), profile_name);
continue;
}
const bool has_priority = profile.value.HasMember("priority");
const bool has_capacity = profile.value.HasMember("capacity");
const bool has_deadline = profile.value.HasMember("deadline");
const bool has_period = profile.value.HasMember("period");
const bool has_affinity = profile.value.HasMember("affinity");
const bool has_complete_deadline = has_capacity && has_deadline && has_period;
const bool has_some_deadline = has_capacity || has_deadline || has_period;
zx_profile_info_t info{};
if (has_priority && !has_some_deadline) {
auto result = GetInt("priority", profile.value, "Profile ", profile_name);
if (result.is_ok()) {
info.flags = ZX_PROFILE_INFO_FLAG_PRIORITY;
info.priority =
std::clamp<int32_t>(result.value(), ZX_PRIORITY_LOWEST, ZX_PRIORITY_HIGHEST);
} else {
FX_LOGF(WARNING, "ProfileProvider", "\"%s\": %s", profile_name,
result.error_value().c_str());
continue;
}
} else if (!has_priority && has_complete_deadline) {
auto capacity_result = ParseDuration(profile.value["capacity"]);
if (capacity_result.is_error()) {
FX_LOGF(WARNING, "ProfileProvider", "\"%s\": %s", profile_name,
capacity_result.error_value().c_str());
continue;
}
auto deadline_result = ParseDuration(profile.value["deadline"]);
if (deadline_result.is_error()) {
FX_LOGF(WARNING, "ProfileProvider", "\"%s\": %s", profile_name,
deadline_result.error_value().c_str());
continue;
}
auto period_result = ParseDuration(profile.value["period"]);
if (period_result.is_error()) {
FX_LOGF(WARNING, "ProfileProvider", "\"%s\": %s", profile_name,
period_result.error_value().c_str());
continue;
}
info.flags = ZX_PROFILE_INFO_FLAG_DEADLINE;
info.deadline_params = zx_sched_deadline_params_t{.capacity = capacity_result->get(),
.relative_deadline = deadline_result->get(),
.period = period_result->get()};
} else if (has_priority && has_some_deadline) {
FX_LOGF(WARNING, "ProfileProvider",
"%s: \"%s\": Priority and deadline parameters are mutually exclusive!",
filename.c_str(), profile_name);
continue;
} else if (!has_priority && !has_complete_deadline && has_some_deadline) {
FX_LOGF(
WARNING, "ProfileProvider",
"%s: \"%s\": Deadline profiles must specify \"capacity\", \"deadline\", and \"period\"!",
filename.c_str(), profile_name);
continue;
}
if (has_affinity) {
const auto& affinity_member = profile.value["affinity"];
const bool is_uint = affinity_member.IsUint64();
const bool is_array = affinity_member.IsArray();
info.flags |= ZX_PROFILE_INFO_FLAG_CPU_MASK;
if (is_uint) {
static_assert(std::numeric_limits<uint64_t>::digits <= ZX_CPU_SET_BITS_PER_WORD);
info.cpu_affinity_mask.mask[0] = affinity_member.GetUint64();
} else if (is_array) {
size_t element_count = 0;
bool failed = false;
for (const auto& value : IterateValues(affinity_member)) {
if (!value.IsUint()) {
FX_LOGF(WARNING, "ProfileProvider",
"%s: \"%s\": Array element %zu of profile member \"affinity\" must be an "
"unsigned integer!",
filename.c_str(), profile_name, element_count);
failed = true;
break;
}
element_count++;
const size_t cpu_number = value.GetUint();
if (cpu_number >= ZX_CPU_SET_MAX_CPUS) {
FX_LOGF(
WARNING, "ProfileProvider",
"%s: \"%s\": Profile member \"affinity\" must be an integer in the range [0, %u)!",
filename.c_str(), profile_name, ZX_CPU_SET_MAX_CPUS);
failed = true;
break;
}
info.cpu_affinity_mask.mask[cpu_number / ZX_CPU_SET_BITS_PER_WORD] |=
uint64_t{1} << (cpu_number % ZX_CPU_SET_BITS_PER_WORD);
}
if (failed) {
continue;
}
} else {
FX_LOGF(
WARNING, "ProfileProvider",
"%s: \"%s\": Profile member \"affinity\" must be a uint64 or an array of CPU indices!",
filename.c_str(), profile_name);
continue;
}
} // if (has_affinity)
if (info.flags == 0) {
FX_LOGF(WARNING, "ProfileProvider", "%s: Ignoring empty profile \"%s\".", filename.c_str(),
profile_name);
continue;
}
const auto [iter, added] = profiles->emplace(profile_name, Profile{scope, info});
if (!added) {
if (iter->second.scope >= scope) {
FX_LOGF(WARNING, "ProfileProvider", "%s: Profile \"%s\"already exists at %s scope.",
filename.c_str(), profile_name, ToString(scope).c_str());
} else if (iter->second.scope < scope) {
FX_LOGF(INFO, "ProfileProvider", "%s: Profile \"%s\" overridden at %s scope.",
filename.c_str(), profile_name, ToString(scope).c_str());
iter->second = Profile{scope, info};
}
}
} // for (const auto& profile : IterateMembers(document))
}
} // anonymous namespace
namespace zircon_profile {
fitx::result<std::string, ProfileMap> LoadConfigs(const std::string& config_path) {
fbl::unique_fd dir_fd(openat(AT_FDCWD, config_path.c_str(), O_RDONLY | O_DIRECTORY));
if (!dir_fd.is_valid()) {
// A non-existent directory is not an error.
FX_LOGF(WARNING, "ProfileProvider", "Failed to open config dir: %s", config_path.c_str());
return fitx::ok(ProfileMap{});
}
std::vector<std::string> dir_entries;
if (!files::ReadDirContentsAt(dir_fd.get(), ".", &dir_entries)) {
return Error() << "Could not read directory contents from path " << config_path.c_str()
<< " error " << strerror(errno);
}
const auto filename_predicate = [](const std::string& filename) {
const auto pos = filename.rfind(kConfigFileExtension);
return pos != std::string::npos && pos == (filename.size() - std::strlen(kConfigFileExtension));
};
ProfileMap profiles;
for (const auto& entry : dir_entries) {
if (!files::IsFileAt(dir_fd.get(), entry) || !filename_predicate(entry)) {
continue;
}
FX_LOGF(INFO, "ProfileProvider", "Loading config: %s", entry.c_str());
std::string data;
if (!files::ReadFileToStringAt(dir_fd.get(), entry, &data)) {
FX_LOGF(WARNING, "ProfileProvider", "Failed to read file: %s", entry.c_str());
continue;
}
rapidjson::Document document;
const auto kFlags = rapidjson::kParseCommentsFlag | rapidjson::kParseTrailingCommasFlag |
rapidjson::kParseIterativeFlag;
document.Parse<kFlags>(data);
if (document.HasParseError()) {
FX_LOGF(WARNING, "ProfileProvider", "%s:%s", entry.c_str(),
GetErrorMessage(document, data).c_str());
continue;
}
ParseProfiles(entry, document, &profiles);
}
FX_LOGF(INFO, "ProfileProvider", "Loaded profiles:");
for (const auto& [key, value] : profiles) {
FX_LOGF(INFO, "ProfileProvider", " %-32s %-10s %s", key.c_str(), ToString(value.scope).c_str(),
ToString(value.info).c_str());
}
return fitx::ok(std::move(profiles));
}
} // namespace zircon_profile