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