blob: 04385c2b12c0132d8267ac4dad835f5ec1c77d88 [file] [log] [blame]
/*
* Copyright (C) 2021 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 "perfetto/ext/base/file_utils.h"
#include "perfetto/ext/base/getopt.h"
#include "perfetto/ext/base/scoped_file.h"
#include "perfetto/ext/base/string_utils.h"
#include "perfetto/ext/base/version.h"
#include "src/protozero/filtering/filter_util.h"
#include "src/protozero/filtering/message_filter.h"
namespace perfetto {
namespace proto_filter {
namespace {
const char kUsage[] =
R"(Usage: proto_filter [-s schema_in] [-i message in] [-o message out] [-f filter in] [-F filter out] [-T filter_oct_out] [-d --dedupe] [-I proto include path] [-r root message]
-s --schema-in: Path to the root .proto file. Required for most operations
-I --proto_path: Extra include directory for proto includes. If omitted assumed CWD.
-r --root_message: Fully qualified name for the root proto message (e.g. perfetto.protos.Trace)
If omitted the first message defined in the schema will be used.
-i --msg_in: Path of a binary-encoded proto message which will be filtered.
-o --msg_out: Path of the binary-encoded filtered proto message written in output.
-f --filter_in: Path of a filter bytecode file previously generated by this tool.
-F --filter_out: Path of the filter bytecode file generated from the --schema-in definition.
-T --filter_oct_out: Like --filter_out, but emits a octal-escaped C string suitable for .pbtx.
-d --dedupe: Minimize filter size by deduping leaf messages with same field ids.
Example usage:
# Convert a .proto schema file into a diff-friendly list of messages/fields>
proto_filter -r perfetto.protos.Trace -s protos/perfetto/trace/trace.proto
# Generate the filter bytecode from a .proto schema
proto_filter -r perfetto.protos.Trace -s protos/perfetto/trace/trace.proto \
-F /tmp/bytecode [--dedupe]
# List the used/filtered fields from a trace file
proto_filter -r perfetto.protos.Trace -s protos/perfetto/trace/trace.proto \
-i test/data/example_android_trace_30s.pb -f /tmp/bytecode
# Filter a trace using a filter bytecode
proto_filter -i test/data/example_android_trace_30s.pb -f /tmp/bytecode \
-o /tmp/filtered_trace
# Show which fields are allowed by a filter bytecode
proto_filter -r perfetto.protos.Trace -s protos/perfetto/trace/trace.proto \
-f /tmp/bytecode
)";
int Main(int argc, char** argv) {
static const option long_options[] = {
{"help", no_argument, nullptr, 'h'},
{"version", no_argument, nullptr, 'v'},
{"dedupe", no_argument, nullptr, 'd'},
{"proto_path", no_argument, nullptr, 'I'},
{"schema_in", no_argument, nullptr, 's'},
{"root_message", no_argument, nullptr, 'r'},
{"msg_in", no_argument, nullptr, 'i'},
{"msg_out", no_argument, nullptr, 'o'},
{"filter_in", no_argument, nullptr, 'f'},
{"filter_out", no_argument, nullptr, 'F'},
{"filter_oct_out", no_argument, nullptr, 'T'},
{nullptr, 0, nullptr, 0}};
std::string msg_in;
std::string msg_out;
std::string filter_in;
std::string schema_in;
std::string filter_out;
std::string filter_oct_out;
std::string proto_path;
std::string root_message_arg;
bool dedupe = false;
for (;;) {
int option =
getopt_long(argc, argv, "hvdI:s:r:i:o:f:F:T:", long_options, nullptr);
if (option == -1)
break; // EOF.
if (option == 'v') {
printf("%s\n", base::GetVersionString());
exit(0);
}
if (option == 'd') {
dedupe = true;
continue;
}
if (option == 'I') {
proto_path = optarg;
continue;
}
if (option == 's') {
schema_in = optarg;
continue;
}
if (option == 'r') {
root_message_arg = optarg;
continue;
}
if (option == 'i') {
msg_in = optarg;
continue;
}
if (option == 'o') {
msg_out = optarg;
continue;
}
if (option == 'f') {
filter_in = optarg;
continue;
}
if (option == 'F') {
filter_out = optarg;
continue;
}
if (option == 'T') {
filter_oct_out = optarg;
continue;
}
if (option == 'h') {
fprintf(stdout, kUsage);
exit(0);
}
fprintf(stderr, kUsage);
exit(1);
}
if (msg_in.empty() && filter_in.empty() && schema_in.empty()) {
fprintf(stderr, kUsage);
return 1;
}
std::string msg_in_data;
if (!msg_in.empty()) {
PERFETTO_LOG("Loading proto-encoded message from %s", msg_in.c_str());
if (!base::ReadFile(msg_in, &msg_in_data)) {
PERFETTO_ELOG("Could not open message file %s", msg_in.c_str());
return 1;
}
}
protozero::FilterUtil filter;
if (!schema_in.empty()) {
PERFETTO_LOG("Loading proto schema from %s", schema_in.c_str());
if (!filter.LoadMessageDefinition(schema_in, root_message_arg,
proto_path)) {
PERFETTO_ELOG("Failed to parse proto schema from %s", schema_in.c_str());
return 1;
}
if (dedupe)
filter.Dedupe();
}
protozero::MessageFilter msg_filter;
std::string filter_data;
std::string filter_data_src;
if (!filter_in.empty()) {
PERFETTO_LOG("Loading filter bytecode from %s", filter_in.c_str());
if (!base::ReadFile(filter_in, &filter_data)) {
PERFETTO_ELOG("Could not open filter file %s", filter_in.c_str());
return 1;
}
filter_data_src = filter_in;
} else if (!schema_in.empty()) {
PERFETTO_LOG("Generating filter bytecode from %s", schema_in.c_str());
filter_data = filter.GenerateFilterBytecode();
filter_data_src = schema_in;
}
if (!filter_data.empty()) {
const uint8_t* data = reinterpret_cast<const uint8_t*>(filter_data.data());
if (!msg_filter.LoadFilterBytecode(data, filter_data.size())) {
PERFETTO_ELOG("Failed to parse filter bytecode from %s",
filter_data_src.c_str());
return 1;
}
}
// Write the filter bytecode in output.
if (!filter_out.empty()) {
auto fd = base::OpenFile(filter_out, O_WRONLY | O_TRUNC | O_CREAT, 0644);
if (!fd) {
PERFETTO_ELOG("Could not open filter out path %s", filter_out.c_str());
return 1;
}
PERFETTO_LOG("Writing filter bytecode (%zu bytes) into %s",
filter_data.size(), filter_out.c_str());
base::WriteAll(*fd, filter_data.data(), filter_data.size());
}
if (!filter_oct_out.empty()) {
auto fd =
base::OpenFile(filter_oct_out, O_WRONLY | O_TRUNC | O_CREAT, 0644);
if (!fd) {
PERFETTO_ELOG("Could not open filter out path %s",
filter_oct_out.c_str());
return 1;
}
std::string oct_str;
oct_str.reserve(filter_data.size() * 4 + 64);
oct_str.append("trace_filter {\n bytecode: \"");
for (char c : filter_data) {
uint8_t octect = static_cast<uint8_t>(c);
char buf[5]{'\\', '0', '0', '0', 0};
for (uint8_t i = 0; i < 3; ++i) {
buf[3 - i] = static_cast<char>('0' + static_cast<uint8_t>(octect) % 8);
octect /= 8;
}
oct_str.append(buf);
}
oct_str.append("\"\n}\n");
PERFETTO_LOG("Writing filter bytecode (%zu bytes) into %s", oct_str.size(),
filter_oct_out.c_str());
base::WriteAll(*fd, oct_str.data(), oct_str.size());
}
// Apply the filter to the input message (if any).
std::vector<uint8_t> msg_filtered_data;
if (!msg_in.empty()) {
PERFETTO_LOG("Applying filter %s to proto message %s",
filter_data_src.c_str(), msg_in.c_str());
msg_filter.enable_field_usage_tracking(true);
auto res = msg_filter.FilterMessage(msg_in_data.data(), msg_in_data.size());
if (res.error)
PERFETTO_FATAL("Filtering failed");
msg_filtered_data.insert(msg_filtered_data.end(), res.data.get(),
res.data.get() + res.size);
}
// Write out the filtered message.
if (!msg_out.empty()) {
PERFETTO_LOG("Writing filtered proto bytes (%zu bytes) into %s",
msg_filtered_data.size(), msg_out.c_str());
auto fd = base::OpenFile(msg_out, O_WRONLY | O_CREAT, 0644);
base::WriteAll(*fd, msg_filtered_data.data(), msg_filtered_data.size());
}
if (!msg_in.empty()) {
const auto& field_usage_map = msg_filter.field_usage();
for (const auto& it : field_usage_map) {
const std::string& field_path_varint = it.first;
int32_t num_occurrences = it.second;
std::string path_str = filter.LookupField(field_path_varint);
printf("%-100s %s %d\n", path_str.c_str(),
num_occurrences < 0 ? "DROP" : "PASS", std::abs(num_occurrences));
}
} else if (!schema_in.empty()) {
filter.PrintAsText(!filter_data.empty() ? std::make_optional(filter_data)
: std::nullopt);
}
if ((!filter_out.empty() || !filter_oct_out.empty()) && !dedupe) {
PERFETTO_ELOG(
"Warning: looks like you are generating a filter without --dedupe. For "
"production use cases, --dedupe can make the output bytecode "
"significantly smaller.");
}
return 0;
}
} // namespace
} // namespace proto_filter
} // namespace perfetto
int main(int argc, char** argv) {
return perfetto::proto_filter::Main(argc, argv);
}