blob: 95e42066008e32424a1710c2d4917bb65bc9e9b3 [file] [log] [blame] [edit]
/*
* Copyright (C) 2026 The Android Open Source Project
*
* 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 "src/trace_processor/shell/metrics.h"
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <optional>
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>
#include <google/protobuf/compiler/parser.h>
#include <google/protobuf/descriptor.h>
#include <google/protobuf/descriptor.pb.h>
#include <google/protobuf/dynamic_message.h>
#include <google/protobuf/io/tokenizer.h>
#include <google/protobuf/io/zero_copy_stream_impl.h>
#include "perfetto/base/logging.h"
#include "perfetto/base/status.h"
#include "perfetto/ext/base/file_utils.h"
#include "perfetto/ext/base/scoped_file.h"
#include "perfetto/ext/base/status_macros.h"
#include "perfetto/ext/base/string_splitter.h"
#include "perfetto/ext/base/string_utils.h"
#include "perfetto/trace_processor/basic_types.h"
#include "perfetto/trace_processor/trace_processor.h"
#include "src/trace_processor/metrics/all_chrome_metrics.descriptor.h"
#include "src/trace_processor/metrics/all_webview_metrics.descriptor.h"
#include "src/trace_processor/metrics/metrics.descriptor.h"
namespace perfetto::trace_processor {
namespace {
class ErrorPrinter : public google::protobuf::io::ErrorCollector {
#if GOOGLE_PROTOBUF_VERSION >= 4022000
void RecordError(int line, int col, absl::string_view msg) override {
PERFETTO_ELOG("%d:%d: %.*s", line, col, static_cast<int>(msg.size()),
msg.data());
}
void RecordWarning(int line, int col, absl::string_view msg) override {
PERFETTO_ILOG("%d:%d: %.*s", line, col, static_cast<int>(msg.size()),
msg.data());
}
#else
void AddError(int line, int col, const std::string& msg) override {
PERFETTO_ELOG("%d:%d: %s", line, col, msg.c_str());
}
void AddWarning(int line, int col, const std::string& msg) override {
PERFETTO_ILOG("%d:%d: %s", line, col, msg.c_str());
}
#endif
};
} // namespace
void MetricExtension::SetDiskPath(std::string path) {
AddTrailingSlashIfNeeded(path);
disk_path_ = std::move(path);
}
void MetricExtension::SetVirtualPath(std::string path) {
AddTrailingSlashIfNeeded(path);
virtual_path_ = std::move(path);
}
void MetricExtension::AddTrailingSlashIfNeeded(std::string& path) {
if (path.length() > 0 && path[path.length() - 1] != '/') {
path.push_back('/');
}
}
std::string BaseName(std::string metric_path) {
std::replace(metric_path.begin(), metric_path.end(), '\\', '/');
auto slash_idx = metric_path.rfind('/');
return slash_idx == std::string::npos ? metric_path
: metric_path.substr(slash_idx + 1);
}
base::Status RegisterMetric(TraceProcessor* trace_processor,
const std::string& register_metric) {
std::string sql;
base::ReadFile(register_metric, &sql);
std::string path = "shell/" + BaseName(register_metric);
return trace_processor->RegisterMetric(path, sql);
}
base::Status ParseToFileDescriptorProto(
const std::string& filename,
google::protobuf::FileDescriptorProto* file_desc) {
base::ScopedFile file(base::OpenFile(filename, O_RDONLY));
if (file.get() == -1) {
return base::ErrStatus("Failed to open proto file %s", filename.c_str());
}
google::protobuf::io::FileInputStream stream(file.get());
ErrorPrinter printer;
google::protobuf::io::Tokenizer tokenizer(&stream, &printer);
google::protobuf::compiler::Parser parser;
parser.Parse(&tokenizer, file_desc);
return base::OkStatus();
}
base::Status ExtendMetricsProto(TraceProcessor* trace_processor,
const std::string& extend_metrics_proto,
google::protobuf::DescriptorPool* pool) {
google::protobuf::FileDescriptorSet desc_set;
auto* file_desc = desc_set.add_file();
RETURN_IF_ERROR(ParseToFileDescriptorProto(extend_metrics_proto, file_desc));
file_desc->set_name(BaseName(extend_metrics_proto));
pool->BuildFile(*file_desc);
std::vector<uint8_t> metric_proto;
metric_proto.resize(desc_set.ByteSizeLong());
desc_set.SerializeToArray(metric_proto.data(),
static_cast<int>(metric_proto.size()));
return trace_processor->ExtendMetricsProto(metric_proto.data(),
metric_proto.size());
}
base::Status RunMetrics(TraceProcessor* trace_processor,
const std::vector<MetricNameAndPath>& metrics,
MetricV1OutputFormat format) {
std::vector<std::string> metric_names(metrics.size());
for (size_t i = 0; i < metrics.size(); ++i) {
metric_names[i] = metrics[i].name;
}
switch (format) {
case MetricV1OutputFormat::kBinaryProto: {
std::vector<uint8_t> metric_result;
RETURN_IF_ERROR(
trace_processor->ComputeMetric(metric_names, &metric_result));
fwrite(metric_result.data(), sizeof(uint8_t), metric_result.size(),
stdout);
break;
}
case MetricV1OutputFormat::kJson: {
std::string out;
RETURN_IF_ERROR(trace_processor->ComputeMetricText(
metric_names, TraceProcessor::MetricResultFormat::kJson, &out));
out += '\n';
fwrite(out.c_str(), sizeof(char), out.size(), stdout);
break;
}
case MetricV1OutputFormat::kTextProto: {
std::string out;
RETURN_IF_ERROR(trace_processor->ComputeMetricText(
metric_names, TraceProcessor::MetricResultFormat::kProtoText, &out));
out += '\n';
fwrite(out.c_str(), sizeof(char), out.size(), stdout);
break;
}
case MetricV1OutputFormat::kNone:
break;
}
return base::OkStatus();
}
base::Status ParseSingleMetricExtensionPath(bool dev,
const std::string& raw_extension,
MetricExtension& parsed_extension) {
// We cannot easily use ':' as a path separator because windows paths can have
// ':' in them (e.g. C:\foo\bar).
std::vector<std::string> parts = base::SplitString(raw_extension, "@");
if (parts.size() != 2 || parts[0].length() == 0 || parts[1].length() == 0) {
return base::ErrStatus(
"--metric-extension-dir must be of format disk_path@virtual_path");
}
parsed_extension.SetDiskPath(std::move(parts[0]));
parsed_extension.SetVirtualPath(std::move(parts[1]));
if (parsed_extension.virtual_path() == "/") {
if (!dev) {
return base::ErrStatus(
"Local development features must be enabled (using the "
"--dev flag) to override built-in metrics");
}
parsed_extension.SetVirtualPath("");
}
if (parsed_extension.virtual_path() == "shell/") {
return base::Status(
"Cannot have 'shell/' as metric extension virtual path.");
}
return base::OkStatus();
}
base::Status CheckForDuplicateMetricExtension(
const std::vector<MetricExtension>& metric_extensions) {
std::unordered_set<std::string> disk_paths;
std::unordered_set<std::string> virtual_paths;
for (const auto& extension : metric_extensions) {
auto ret = disk_paths.insert(extension.disk_path());
if (!ret.second) {
return base::ErrStatus(
"Another metric extension is already using disk path %s",
extension.disk_path().c_str());
}
ret = virtual_paths.insert(extension.virtual_path());
if (!ret.second) {
return base::ErrStatus(
"Another metric extension is already using virtual path %s",
extension.virtual_path().c_str());
}
}
return base::OkStatus();
}
base::Status ParseMetricExtensionPaths(
bool dev,
const std::vector<std::string>& raw_metric_extensions,
std::vector<MetricExtension>& metric_extensions) {
for (const auto& raw_extension : raw_metric_extensions) {
metric_extensions.push_back({});
RETURN_IF_ERROR(ParseSingleMetricExtensionPath(dev, raw_extension,
metric_extensions.back()));
}
return CheckForDuplicateMetricExtension(metric_extensions);
}
void ExtendPoolWithBinaryDescriptor(
google::protobuf::DescriptorPool& pool,
const void* data,
int size,
const std::vector<std::string>& skip_prefixes) {
google::protobuf::FileDescriptorSet desc_set;
PERFETTO_CHECK(desc_set.ParseFromArray(data, size));
for (const auto& file_desc : desc_set.file()) {
if (base::StartsWithAny(file_desc.name(), skip_prefixes))
continue;
pool.BuildFile(file_desc);
}
}
base::Status LoadMetricExtensionProtos(TraceProcessor* trace_processor,
const std::string& proto_root,
const std::string& mount_path,
google::protobuf::DescriptorPool& pool) {
if (!base::FileExists(proto_root)) {
return base::ErrStatus(
"Directory %s does not exist. Metric extension directory must contain "
"a 'sql/' and 'protos/' subdirectory.",
proto_root.c_str());
}
std::vector<std::string> proto_files;
RETURN_IF_ERROR(base::ListFilesRecursive(proto_root, proto_files));
google::protobuf::FileDescriptorSet parsed_protos;
for (const auto& file_path : proto_files) {
if (base::GetFileExtension(file_path) != ".proto")
continue;
auto* file_desc = parsed_protos.add_file();
ParseToFileDescriptorProto(proto_root + file_path, file_desc);
file_desc->set_name(mount_path + file_path);
}
std::vector<uint8_t> serialized_filedescset;
serialized_filedescset.resize(parsed_protos.ByteSizeLong());
parsed_protos.SerializeToArray(
serialized_filedescset.data(),
static_cast<int>(serialized_filedescset.size()));
// Extend the pool for any subsequent reflection-based operations
// (e.g. output json)
ExtendPoolWithBinaryDescriptor(
pool, serialized_filedescset.data(),
static_cast<int>(serialized_filedescset.size()), {});
return trace_processor->ExtendMetricsProto(serialized_filedescset.data(),
serialized_filedescset.size());
}
base::Status LoadMetricExtensionSql(TraceProcessor* trace_processor,
const std::string& sql_root,
const std::string& mount_path) {
if (!base::FileExists(sql_root)) {
return base::ErrStatus(
"Directory %s does not exist. Metric extension directory must contain "
"a 'sql/' and 'protos/' subdirectory.",
sql_root.c_str());
}
std::vector<std::string> sql_files;
RETURN_IF_ERROR(base::ListFilesRecursive(sql_root, sql_files));
for (const auto& file_path : sql_files) {
if (base::GetFileExtension(file_path) != ".sql")
continue;
std::string file_contents;
if (!base::ReadFile(sql_root + file_path, &file_contents)) {
return base::ErrStatus("Cannot read file %s", file_path.c_str());
}
RETURN_IF_ERROR(
trace_processor->RegisterMetric(mount_path + file_path, file_contents));
}
return base::OkStatus();
}
base::Status LoadMetricExtension(TraceProcessor* trace_processor,
const MetricExtension& extension,
google::protobuf::DescriptorPool& pool) {
const std::string& disk_path = extension.disk_path();
const std::string& virtual_path = extension.virtual_path();
if (!base::FileExists(disk_path)) {
return base::ErrStatus("Metric extension directory %s does not exist",
disk_path.c_str());
}
// Note: Proto files must be loaded first, because we determine whether an SQL
// file is a metric or not by checking if the name matches a field of the root
// TraceMetrics proto.
RETURN_IF_ERROR(
LoadMetricExtensionProtos(trace_processor, disk_path + "protos/",
kMetricProtoRoot + virtual_path, pool));
RETURN_IF_ERROR(LoadMetricExtensionSql(trace_processor, disk_path + "sql/",
virtual_path));
return base::OkStatus();
}
base::Status PopulateDescriptorPool(
google::protobuf::DescriptorPool& pool,
const std::vector<MetricExtension>& metric_extensions) {
// TODO(b/182165266): There is code duplication here with trace_processor_impl
// SetupMetrics. This will be removed when we switch the output formatter to
// use internal DescriptorPool.
std::vector<std::string> skip_prefixes;
skip_prefixes.reserve(metric_extensions.size());
for (const auto& ext : metric_extensions) {
skip_prefixes.push_back(kMetricProtoRoot + ext.virtual_path());
}
ExtendPoolWithBinaryDescriptor(pool, kMetricsDescriptor.data(),
kMetricsDescriptor.size(), skip_prefixes);
ExtendPoolWithBinaryDescriptor(pool, kAllChromeMetricsDescriptor.data(),
kAllChromeMetricsDescriptor.size(),
skip_prefixes);
ExtendPoolWithBinaryDescriptor(pool, kAllWebviewMetricsDescriptor.data(),
kAllWebviewMetricsDescriptor.size(),
skip_prefixes);
return base::OkStatus();
}
base::Status LoadMetrics(TraceProcessor* trace_processor,
const std::string& raw_metric_names,
google::protobuf::DescriptorPool& pool,
std::vector<MetricNameAndPath>& name_and_path) {
std::vector<std::string> split;
for (base::StringSplitter ss(raw_metric_names, ','); ss.Next();) {
split.emplace_back(ss.cur_token());
}
// For all metrics which are files, register them and extend the metrics
// proto.
for (const std::string& metric_or_path : split) {
// If there is no extension, we assume it is a builtin metric.
auto ext_idx = metric_or_path.rfind('.');
if (ext_idx == std::string::npos) {
name_and_path.emplace_back(
MetricNameAndPath{metric_or_path, std::nullopt});
continue;
}
std::string no_ext_path = metric_or_path.substr(0, ext_idx);
// The proto must be extended before registering the metric.
base::Status status =
ExtendMetricsProto(trace_processor, no_ext_path + ".proto", &pool);
if (!status.ok()) {
return base::ErrStatus("Unable to extend metrics proto %s: %s",
metric_or_path.c_str(), status.c_message());
}
status = RegisterMetric(trace_processor, no_ext_path + ".sql");
if (!status.ok()) {
return base::ErrStatus("Unable to register metric %s: %s",
metric_or_path.c_str(), status.c_message());
}
name_and_path.emplace_back(
MetricNameAndPath{BaseName(no_ext_path), no_ext_path});
}
return base::OkStatus();
}
base::Status LoadMetricsAndExtensionsSql(
TraceProcessor* trace_processor,
const std::vector<MetricNameAndPath>& metrics,
const std::vector<MetricExtension>& extensions) {
for (const MetricExtension& extension : extensions) {
const std::string& disk_path = extension.disk_path();
const std::string& virtual_path = extension.virtual_path();
RETURN_IF_ERROR(LoadMetricExtensionSql(trace_processor, disk_path + "sql/",
virtual_path));
}
for (const MetricNameAndPath& metric : metrics) {
// Ignore builtin metrics.
if (!metric.no_ext_path.has_value())
continue;
RETURN_IF_ERROR(
RegisterMetric(trace_processor, metric.no_ext_path.value() + ".sql"));
}
return base::OkStatus();
}
} // namespace perfetto::trace_processor