diff --git a/Android.bp b/Android.bp index 5c6365ca55b..5df3053fe4c 100644 --- a/Android.bp +++ b/Android.bp @@ -7439,6 +7439,65 @@ genrule { ], } +// GN: //protos/perfetto/protovm:compile_config_descriptor +genrule { + name: "perfetto_protos_perfetto_protovm_compile_config_descriptor", + srcs: [ + "protos/perfetto/protovm/compile_config.proto", + ], + tools: [ + "aprotoc", + ], + cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --descriptor_set_out=$(out) $(in)", + out: [ + "perfetto_protos_perfetto_protovm_compile_config_descriptor.bin", + ], +} + +// GN: //protos/perfetto/protovm:compile_config_zero +filegroup { + name: "perfetto_protos_perfetto_protovm_compile_config_zero", + srcs: [ + "protos/perfetto/protovm/compile_config.proto", + ], +} + +// GN: //protos/perfetto/protovm:compile_config_zero +genrule { + name: "perfetto_protos_perfetto_protovm_compile_config_zero_gen", + srcs: [ + ":perfetto_protos_perfetto_protovm_compile_config_zero", + ], + tools: [ + "aprotoc", + "protozero_plugin", + ], + cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --plugin=protoc-gen-plugin=$(location protozero_plugin) --plugin_out=wrapper_namespace=pbzero:$(genDir)/external/perfetto/ $(locations :perfetto_protos_perfetto_protovm_compile_config_zero)", + out: [ + "external/perfetto/protos/perfetto/protovm/compile_config.pbzero.cc", + ], +} + +// GN: //protos/perfetto/protovm:compile_config_zero +genrule { + name: "perfetto_protos_perfetto_protovm_compile_config_zero_gen_headers", + srcs: [ + ":perfetto_protos_perfetto_protovm_compile_config_zero", + ], + tools: [ + "aprotoc", + "protozero_plugin", + ], + cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --plugin=protoc-gen-plugin=$(location protozero_plugin) --plugin_out=wrapper_namespace=pbzero:$(genDir)/external/perfetto/ $(locations :perfetto_protos_perfetto_protovm_compile_config_zero)", + out: [ + "external/perfetto/protos/perfetto/protovm/compile_config.pbzero.h", + ], + export_include_dirs: [ + ".", + "protos", + ], +} + // GN: //protos/perfetto/protovm:cpp filegroup { name: "perfetto_protos_perfetto_protovm_cpp", @@ -7483,6 +7542,21 @@ genrule { ], } +// GN: //protos/perfetto/protovm:descriptor +genrule { + name: "perfetto_protos_perfetto_protovm_descriptor", + srcs: [ + "protos/perfetto/protovm/vm_program.proto", + ], + tools: [ + "aprotoc", + ], + cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --descriptor_set_out=$(out) $(in)", + out: [ + "perfetto_protos_perfetto_protovm_descriptor.bin", + ], +} + // GN: //protos/perfetto/protovm:lite filegroup { name: "perfetto_protos_perfetto_protovm_lite", @@ -14625,6 +14699,83 @@ filegroup { ], } +// GN: //src/protovm/compiler:compiler +filegroup { + name: "perfetto_src_protovm_compiler_compiler", + srcs: [ + "src/protovm/compiler/compiler.cc", + "src/protovm/compiler/instruction_emitter.cc", + ], +} + +// GN: //src/protovm/compiler:gen_cc_compile_config_descriptor +genrule { + name: "perfetto_src_protovm_compiler_gen_cc_compile_config_descriptor", + srcs: [ + ":perfetto_protos_perfetto_protovm_compile_config_descriptor", + ], + cmd: "$(location python/tools/cpp_blob_emitter.py) --output=$(out) --gen-dir=$(genDir) --namespace perfetto --symbol-suffix Descriptor $(in)", + out: [ + "src/protovm/compiler/compile_config.descriptor.h", + ], + tool_files: [ + "python/tools/cpp_blob_emitter.py", + ], +} + +// GN: //src/protovm/compiler:gen_cc_trace_descriptor +genrule { + name: "perfetto_src_protovm_compiler_gen_cc_trace_descriptor", + srcs: [ + ":perfetto_protos_perfetto_trace_descriptor", + ], + cmd: "$(location python/tools/cpp_blob_emitter.py) --output=$(out) --gen-dir=$(genDir) --namespace perfetto --symbol-suffix Descriptor $(in)", + out: [ + "src/protovm/compiler/trace.descriptor.h", + ], + tool_files: [ + "python/tools/cpp_blob_emitter.py", + ], +} + +// GN: //src/protovm/compiler:gen_cc_vm_program_descriptor +genrule { + name: "perfetto_src_protovm_compiler_gen_cc_vm_program_descriptor", + srcs: [ + ":perfetto_protos_perfetto_protovm_descriptor", + ], + cmd: "$(location python/tools/cpp_blob_emitter.py) --output=$(out) --gen-dir=$(genDir) --namespace perfetto --symbol-suffix Descriptor $(in)", + out: [ + "src/protovm/compiler/vm_program.descriptor.h", + ], + tool_files: [ + "python/tools/cpp_blob_emitter.py", + ], +} + +// GN: //src/protovm/compiler:gen_cc_winscope_descriptor +genrule { + name: "perfetto_src_protovm_compiler_gen_cc_winscope_descriptor", + srcs: [ + ":perfetto_protos_perfetto_trace_android_winscope_descriptor", + ], + cmd: "$(location python/tools/cpp_blob_emitter.py) --output=$(out) --gen-dir=$(genDir) --namespace perfetto --symbol-suffix Descriptor $(in)", + out: [ + "src/protovm/compiler/winscope.descriptor.h", + ], + tool_files: [ + "python/tools/cpp_blob_emitter.py", + ], +} + +// GN: //src/protovm/compiler:unittests +filegroup { + name: "perfetto_src_protovm_compiler_unittests", + srcs: [ + "src/protovm/compiler/compiler_unittest.cc", + ], +} + // GN: //src/protovm:protovm filegroup { name: "perfetto_src_protovm_protovm", @@ -20178,6 +20329,7 @@ cc_test { ":perfetto_protos_perfetto_perfetto_sql_cpp_gen", ":perfetto_protos_perfetto_perfetto_sql_zero_gen", ":perfetto_protos_perfetto_proto_filtering_lite_gen", + ":perfetto_protos_perfetto_protovm_compile_config_zero_gen", ":perfetto_protos_perfetto_protovm_cpp_gen", ":perfetto_protos_perfetto_protovm_lite_gen", ":perfetto_protos_perfetto_protovm_zero_gen", @@ -20317,6 +20469,8 @@ cc_test { ":perfetto_src_proto_utils_pb_to_txt", ":perfetto_src_proto_utils_txt_to_pb", ":perfetto_src_proto_utils_unittests", + ":perfetto_src_protovm_compiler_compiler", + ":perfetto_src_protovm_compiler_unittests", ":perfetto_src_protovm_protovm", ":perfetto_src_protovm_test_messages_lite_gen", ":perfetto_src_protovm_unittests", @@ -20659,6 +20813,7 @@ cc_test { "perfetto_protos_perfetto_perfetto_sql_cpp_gen_headers", "perfetto_protos_perfetto_perfetto_sql_zero_gen_headers", "perfetto_protos_perfetto_proto_filtering_lite_gen_headers", + "perfetto_protos_perfetto_protovm_compile_config_zero_gen_headers", "perfetto_protos_perfetto_protovm_cpp_gen_headers", "perfetto_protos_perfetto_protovm_lite_gen_headers", "perfetto_protos_perfetto_protovm_zero_gen_headers", @@ -20744,6 +20899,10 @@ cc_test { "perfetto_src_perfetto_cmd_protos_cpp_gen_headers", "perfetto_src_proto_utils_gen_cc_config_descriptor", "perfetto_src_proto_utils_gen_cc_trace_summary_descriptor", + "perfetto_src_protovm_compiler_gen_cc_compile_config_descriptor", + "perfetto_src_protovm_compiler_gen_cc_trace_descriptor", + "perfetto_src_protovm_compiler_gen_cc_vm_program_descriptor", + "perfetto_src_protovm_compiler_gen_cc_winscope_descriptor", "perfetto_src_protovm_test_messages_lite_gen_headers", "perfetto_src_protozero_filtering_gen_cc_filter_util_test_descriptor", "perfetto_src_protozero_testing_messages_cpp_gen_headers", diff --git a/BUILD.gn b/BUILD.gn index 5d017fc6a12..6d666d4e0d3 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -76,6 +76,10 @@ if (enable_perfetto_traceconv) { } } +if (enable_perfetto_protovm_compiler) { + all_targets += [ "src/protovm/compiler:protovm_compiler" ] +} + if (enable_perfetto_heapprofd) { all_targets += [ "src/profiling/memory:heapprofd" ] diff --git a/gn/BUILD.gn b/gn/BUILD.gn index b18f829339d..a2990727ccc 100644 --- a/gn/BUILD.gn +++ b/gn/BUILD.gn @@ -238,6 +238,7 @@ protobuf_full_deps_allowlist = [ "../src/protozero/filtering:filter_util", "../src/trace_processor:trace_processor_shell_lib", "../src/trace_processor/shell:*", + "../src/protovm/compiler:*", "../tools/*", "../src/tools/*", "../contrib/rust-sdk/tools/*", diff --git a/gn/perfetto.gni b/gn/perfetto.gni index 73fdc196cc9..2f98374c7df 100644 --- a/gn/perfetto.gni +++ b/gn/perfetto.gni @@ -422,6 +422,9 @@ declare_args() { } declare_args() { + # Enables the protovm_compiler tool. + enable_perfetto_protovm_compiler = enable_perfetto_tools + # Enables the traceconv tool. enable_perfetto_traceconv = enable_perfetto_tools && enable_perfetto_trace_processor_sqlite diff --git a/gn/perfetto_unittests.gni b/gn/perfetto_unittests.gni index cf4e2e7093b..b01f29a361e 100644 --- a/gn/perfetto_unittests.gni +++ b/gn/perfetto_unittests.gni @@ -52,6 +52,10 @@ if (enable_perfetto_tools) { _misc_unittests_targets += [ "src/tools:unittests" ] } +if (enable_perfetto_protovm_compiler) { + _misc_unittests_targets += [ "src/protovm/compiler:unittests" ] +} + if (enable_perfetto_ipc) { _base_unittests_targets += [ "src/ipc:unittests" ] _tracing_unittests_targets += [ "src/tracing/ipc:unittests" ] diff --git a/protos/perfetto/protovm/BUILD.gn b/protos/perfetto/protovm/BUILD.gn index 9ed5f5b425a..3fda5e1c3aa 100644 --- a/protos/perfetto/protovm/BUILD.gn +++ b/protos/perfetto/protovm/BUILD.gn @@ -16,4 +16,12 @@ import("../../../gn/proto_library.gni") perfetto_proto_library("@TYPE@") { sources = [ "vm_program.proto" ] + generate_descriptor = "vm_program.descriptor" + descriptor_root_source = "vm_program.proto" +} + +perfetto_proto_library("compile_config_@TYPE@") { + sources = [ "compile_config.proto" ] + generate_descriptor = "compile_config.descriptor" + descriptor_root_source = "compile_config.proto" } diff --git a/protos/perfetto/protovm/compile_config.proto b/protos/perfetto/protovm/compile_config.proto new file mode 100644 index 00000000000..af167ca8fe4 --- /dev/null +++ b/protos/perfetto/protovm/compile_config.proto @@ -0,0 +1,67 @@ +/* + * 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. + */ + +syntax = "proto2"; + +package perfetto.protos; + +message CompileConfig { + repeated Command commands = 1; +} + +message Command { + oneof command { + DelCommand del = 1; + EnterScopeCommand enter_scope = 2; + SetCommand set = 3; + MergeCommand merge = 4; + } + + enum AbortLevel { + // Skip current command but execute following ones + SKIP_CURRENT_COMMAND = 1; + // Skip current command as well as following ones (default) + SKIP_CURRENT_COMMAND_AND_BREAK_OUTER = 2; + // Abort whole program + ABORT = 3; + }; + optional AbortLevel abort_level = 5; +} + +message DelCommand { + repeated string src = 1; + repeated string dst = 2; + optional bool if_src_present = 3; + optional string dst_key_field = 4; +} + +message EnterScopeCommand { + repeated string src = 1; + repeated string dst = 2; + repeated Command commands = 3; +} + +message SetCommand { + repeated string src = 1; + repeated string dst = 2; +} + +message MergeCommand { + repeated string src = 1; + repeated string dst = 2; + optional bool recursive = 3; + optional string key_field = 4; +} diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto index baca4799962..7ed5ff27594 100644 --- a/protos/perfetto/trace/perfetto_trace.proto +++ b/protos/perfetto/trace/perfetto_trace.proto @@ -18166,15 +18166,26 @@ message TestEvent { } optional TestPayload payload = 5; - // Simple ProtoVm patch and incremental state formats used for integration - // testing + // Simple ProtoVm patch and incremental state formats used for testing + message ProtoVmMessage { + message ProtoVmSubmessage { + optional uint32 single_int = 1; + } + optional uint32 id = 1; + optional ProtoVmSubmessage submessage = 2; + } message ProtoVmPatch { optional string string_to_merge = 1; optional int32 int_to_merge = 2; + optional ProtoVmMessage single_message = 3; + repeated ProtoVmMessage messages = 4; + repeated uint32 delete_message_ids = 5; } message ProtoVmIncrementalState { optional string string_merged = 1; optional int32 int_merged = 2; + optional ProtoVmMessage single_message = 3; + repeated ProtoVmMessage messages = 4; } optional ProtoVmPatch protovm_patch = 6; optional ProtoVmIncrementalState protovm_incremental_state = 7; diff --git a/protos/perfetto/trace/test_event.proto b/protos/perfetto/trace/test_event.proto index 8c492c1ad26..342d7b75489 100644 --- a/protos/perfetto/trace/test_event.proto +++ b/protos/perfetto/trace/test_event.proto @@ -50,15 +50,26 @@ message TestEvent { } optional TestPayload payload = 5; - // Simple ProtoVm patch and incremental state formats used for integration - // testing + // Simple ProtoVm patch and incremental state formats used for testing + message ProtoVmMessage { + message ProtoVmSubmessage { + optional uint32 single_int = 1; + } + optional uint32 id = 1; + optional ProtoVmSubmessage submessage = 2; + } message ProtoVmPatch { optional string string_to_merge = 1; optional int32 int_to_merge = 2; + optional ProtoVmMessage single_message = 3; + repeated ProtoVmMessage messages = 4; + repeated uint32 delete_message_ids = 5; } message ProtoVmIncrementalState { optional string string_merged = 1; optional int32 int_merged = 2; + optional ProtoVmMessage single_message = 3; + repeated ProtoVmMessage messages = 4; } optional ProtoVmPatch protovm_patch = 6; optional ProtoVmIncrementalState protovm_incremental_state = 7; diff --git a/src/protovm/compiler/BUILD.gn b/src/protovm/compiler/BUILD.gn new file mode 100644 index 00000000000..17f76c2af9e --- /dev/null +++ b/src/protovm/compiler/BUILD.gn @@ -0,0 +1,88 @@ +# 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. + +import("../../../gn/perfetto_cc_proto_descriptor.gni") +import("../../../gn/perfetto_host_executable.gni") +import("../../../gn/test.gni") + +source_set("compiler") { + sources = [ + "compiler.cc", + "compiler.h", + "instruction_emitter.cc", + "instruction_emitter.h", + ] + deps = [ + ":gen_cc_compile_config_descriptor", + "../../../gn:default_deps", + "../../../include/perfetto/protozero", + "../../../protos/perfetto/common:zero", + "../../../protos/perfetto/protovm:compile_config_zero", + "../../../protos/perfetto/protovm:zero", + "../../base", + "../../protozero", + "../../protozero/text_to_proto", + "../../trace_processor/util:descriptors", + ] +} + +perfetto_host_executable("protovm_compiler") { + sources = [ "main.cc" ] + deps = [ + ":compiler", + "../../../gn:default_deps", + "../../../gn:protoc_lib", + "../../base", + "../../protozero:multifile_error_collector", + ] +} + +perfetto_cc_proto_descriptor("gen_cc_trace_descriptor") { + descriptor_name = "trace.descriptor" + descriptor_target = "../../../protos/perfetto/trace:descriptor" +} + +perfetto_cc_proto_descriptor("gen_cc_winscope_descriptor") { + descriptor_name = "winscope.descriptor" + descriptor_target = + "../../../protos/perfetto/trace/android:winscope_descriptor" +} + +perfetto_cc_proto_descriptor("gen_cc_compile_config_descriptor") { + descriptor_name = "compile_config.descriptor" + descriptor_target = + "../../../protos/perfetto/protovm:compile_config_descriptor" +} + +perfetto_cc_proto_descriptor("gen_cc_vm_program_descriptor") { + descriptor_name = "vm_program.descriptor" + descriptor_target = "../../../protos/perfetto/protovm:descriptor" +} + +perfetto_unittest_source_set("unittests") { + testonly = true + deps = [ + ":compiler", + ":gen_cc_trace_descriptor", + ":gen_cc_vm_program_descriptor", + ":gen_cc_winscope_descriptor", + "../../../gn:default_deps", + "../../../gn:gtest_and_gmock", + "../../protozero/text_to_proto", + "../../trace_processor/util:descriptors", + "../../trace_processor/util:protozero_to_text", + ] + cflags = [ "-Wno-deprecated-redundant-constexpr-static-def" ] + sources = [ "compiler_unittest.cc" ] +} diff --git a/src/protovm/compiler/compiler.cc b/src/protovm/compiler/compiler.cc new file mode 100644 index 00000000000..8c3e69b1494 --- /dev/null +++ b/src/protovm/compiler/compiler.cc @@ -0,0 +1,158 @@ +/* + * 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/protovm/compiler/compiler.h" + +#include "perfetto/base/logging.h" +#include "perfetto/ext/base/status_macros.h" +#include "perfetto/protozero/scattered_heap_buffer.h" +#include "src/protovm/compiler/compile_config.descriptor.h" +#include "src/protozero/text_to_proto/text_to_proto.h" + +namespace perfetto::protovm { + +Compiler::Compiler() : emitter_(&pool_) {} + +base::Status Compiler::RegisterDescriptors(const uint8_t* data, size_t size) { + RETURN_IF_ERROR(pool_.AddFromFileDescriptorSet(data, size)); + return base::OkStatus(); +} + +base::StatusOr Compiler::Compile( + std::string_view config_textproto) { + auto idx = pool_.FindDescriptorIdx(kRootProto); + if (idx) { + root_proto_ = &pool_.descriptors()[*idx]; + } + if (!root_proto_) { + return base::ErrStatus( + "Root message %s not found in loaded " + "descriptors", + kRootProto); + } + auto status_or_proto = + protozero::TextToProto(perfetto::kCompileConfigDescriptor.data(), + perfetto::kCompileConfigDescriptor.size(), + ".perfetto.protos.CompileConfig", + "compile_config.textproto", config_textproto); + if (!status_or_proto.ok()) { + return base::ErrStatus("Failed to parse config: %s", + status_or_proto.status().c_message()); + } + + protos::pbzero::CompileConfig::Decoder config(status_or_proto->data(), + status_or_proto->size()); + protozero::HeapBuffered program; + auto root = + InstructionEmitter::Scope{program.get(), root_proto_, root_proto_}; + RETURN_IF_ERROR(ParseCommands(root, config.commands())); + return program.SerializeAsString(); +} + +base::Status Compiler::ParseCommands( + const InstructionEmitter::Scope& scope, + protozero::RepeatedFieldIterator commands) const { + for (auto it = commands; it; ++it) { + protos::pbzero::Command::Decoder command(*it); + if (command.has_set()) { + RETURN_IF_ERROR(ParseSet(scope, command)); + } else if (command.has_del()) { + RETURN_IF_ERROR(ParseDel(scope, command)); + } else if (command.has_merge()) { + RETURN_IF_ERROR(ParseMerge(scope, command)); + } else if (command.has_enter_scope()) { + RETURN_IF_ERROR(ParseEnterScope(scope, command)); + } else { + return base::ErrStatus("Unknown command type"); + } + } + return base::OkStatus(); +} + +base::Status Compiler::ParseSet( + const InstructionEmitter::Scope& scope, + const protos::pbzero::Command::Decoder& command) const { + protos::pbzero::SetCommand::Decoder set(command.set()); + auto src_path = ParsePath(set.src()); + auto dst_path = ParsePath(set.dst()); + return emitter_.Set(scope, src_path, dst_path, GetAbortLevel(command)); +} + +base::Status Compiler::ParseDel( + const InstructionEmitter::Scope& scope, + const protos::pbzero::Command::Decoder& command) const { + protos::pbzero::DelCommand::Decoder del(command.del()); + auto src_path = ParsePath(del.src()); + auto dst_path = ParsePath(del.dst()); + if (!(del.if_src_present() ^ del.has_dst_key_field())) { + return base::ErrStatus( + "del command must specify exactly one of if_src_present or " + "dst_key_field"); + } + if (del.if_src_present()) { + return emitter_.DeleteIfPresent(scope, src_path, dst_path, + GetAbortLevel(command)); + } + if (del.has_dst_key_field()) { + return emitter_.DeleteByKey(scope, src_path, dst_path, + del.dst_key_field().ToStdStringView(), + GetAbortLevel(command)); + } + return base::ErrStatus("Unsupported Del command variant"); +} + +base::Status Compiler::ParseMerge( + const InstructionEmitter::Scope& scope, + const protos::pbzero::Command::Decoder& command) const { + protos::pbzero::MergeCommand::Decoder merge(command.merge()); + auto src_path = ParsePath(merge.src()); + auto dst_path = ParsePath(merge.dst()); + auto abort_level = GetAbortLevel(command); + if (merge.has_key_field()) { + return emitter_.MergeByKey(scope, src_path, dst_path, + merge.key_field().ToStdStringView(), + merge.recursive(), abort_level); + } + return emitter_.Merge(scope, src_path, dst_path, merge.recursive(), + abort_level); +} + +base::Status Compiler::ParseEnterScope( + const InstructionEmitter::Scope& scope, + const protos::pbzero::Command::Decoder& command) const { + protos::pbzero::EnterScopeCommand::Decoder enter_scope(command.enter_scope()); + auto src_path = ParsePath(enter_scope.src()); + auto dst_path = ParsePath(enter_scope.dst()); + auto abort_level = GetAbortLevel(command); + ASSIGN_OR_RETURN(auto src_scope, + emitter_.Select(scope, src_path, Cursor::VM_CURSOR_SRC, + false, abort_level)); + ASSIGN_OR_RETURN( + auto dst_scope, + emitter_.Select(src_scope, dst_path, Cursor::VM_CURSOR_DST, true, + InstructionEmitter::kDefaultAbortLevel)); + return ParseCommands(dst_scope, enter_scope.commands()); +} + +Compiler::AbortLevel Compiler::GetAbortLevel( + const protos::pbzero::Command::Decoder& command) const { + if (!command.has_abort_level()) { + return AbortLevel::SKIP_CURRENT_INSTRUCTION_AND_BREAK_OUTER; + } + return static_cast(command.abort_level()); +} + +} // namespace perfetto::protovm diff --git a/src/protovm/compiler/compiler.h b/src/protovm/compiler/compiler.h new file mode 100644 index 00000000000..6f3766db601 --- /dev/null +++ b/src/protovm/compiler/compiler.h @@ -0,0 +1,89 @@ +/* + * 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. + */ + +#ifndef SRC_PROTOVM_COMPILER_COMPILER_H_ +#define SRC_PROTOVM_COMPILER_COMPILER_H_ + +#include +#include +#include + +#include "perfetto/base/status.h" +#include "perfetto/ext/base/status_or.h" +#include "protos/perfetto/protovm/compile_config.pbzero.h" +#include "protos/perfetto/protovm/vm_program.pbzero.h" +#include "src/protovm/compiler/instruction_emitter.h" +#include "src/trace_processor/util/descriptors.h" + +namespace perfetto::protovm { + +// Implements the compiler front-end. +// +// Parses high-level configuration commands (from CompileConfig) and delegates +// the conversion and emission of low-level bytecode instructions (VmProgram) +// to the InstructionEmitter. +class Compiler { + public: + Compiler(); + base::Status RegisterDescriptors(const uint8_t* data, size_t size); + + base::StatusOr Compile(std::string_view config_textproto); + + private: + using AbortLevel = protos::pbzero::VmInstruction::AbortLevel; + using Cursor = perfetto::protos::pbzero::VmCursorEnum; + + static constexpr const char* kRootProto = ".perfetto.protos.TracePacket"; + + base::Status ParseCommands( + const InstructionEmitter::Scope& scope, + protozero::RepeatedFieldIterator commands) const; + + base::Status ParseSet(const InstructionEmitter::Scope& scope, + const protos::pbzero::Command::Decoder& command) const; + + base::Status ParseDel(const InstructionEmitter::Scope& scope, + const protos::pbzero::Command::Decoder& command) const; + + base::Status ParseMerge( + const InstructionEmitter::Scope& scope, + const protos::pbzero::Command::Decoder& command) const; + + base::Status ParseEnterScope( + const InstructionEmitter::Scope& scope, + const protos::pbzero::Command::Decoder& command) const; + + template + std::vector ParsePath(Iterator it) const { + std::vector path; + for (; it; ++it) { + path.push_back(std::string_view(reinterpret_cast(it->data()), + it->size())); + } + return path; + } + + AbortLevel GetAbortLevel( + const protos::pbzero::Command::Decoder& command) const; + + perfetto::trace_processor::DescriptorPool pool_; + const perfetto::trace_processor::ProtoDescriptor* root_proto_ = nullptr; + InstructionEmitter emitter_; +}; + +} // namespace perfetto::protovm + +#endif // SRC_PROTOVM_COMPILER_COMPILER_H_ diff --git a/src/protovm/compiler/compiler_unittest.cc b/src/protovm/compiler/compiler_unittest.cc new file mode 100644 index 00000000000..cfad4b578ca --- /dev/null +++ b/src/protovm/compiler/compiler_unittest.cc @@ -0,0 +1,671 @@ +/* + * 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 +#include +#include + +#include "test/gtest_and_gmock.h" + +#include "src/protovm/compiler/compiler.h" +#include "src/protovm/compiler/trace.descriptor.h" +#include "src/protovm/compiler/vm_program.descriptor.h" +#include "src/protovm/compiler/winscope.descriptor.h" +#include "src/protozero/text_to_proto/text_to_proto.h" +#include "src/trace_processor/util/descriptors.h" +#include "src/trace_processor/util/protozero_to_text.h" + +namespace perfetto::protovm { +namespace { + +using perfetto::trace_processor::DescriptorPool; +using namespace perfetto::trace_processor::protozero_to_text; + +class CompilerTest : public ::testing::Test { + protected: + void SetUp() override { + pool_ = std::make_unique(); + auto status = + pool_->AddFromFileDescriptorSet(perfetto::kVmProgramDescriptor.data(), + perfetto::kVmProgramDescriptor.size()); + EXPECT_TRUE(status.ok()); + } + + void CheckCompilation(std::string_view config_textproto, + std::string_view expected_instructions_textproto) { + Compiler compiler; + EXPECT_TRUE(compiler + .RegisterDescriptors(perfetto::kTraceDescriptor.data(), + perfetto::kTraceDescriptor.size()) + .ok()); + EXPECT_TRUE(compiler + .RegisterDescriptors(perfetto::kWinscopeDescriptor.data(), + perfetto::kWinscopeDescriptor.size()) + .ok()); + base::StatusOr actual_instructions_binary = + compiler.Compile(config_textproto); + EXPECT_TRUE(actual_instructions_binary.ok()) + << actual_instructions_binary.status().message(); + + std::string actual_instructions_textproto = ProtozeroToText( + *pool_, ".perfetto.protos.VmProgram", + protozero::ConstBytes{reinterpret_cast( + actual_instructions_binary->data()), + actual_instructions_binary->size()}); + + EXPECT_EQ(actual_instructions_textproto, + NormalizeProgramTextProto(expected_instructions_textproto)); + } + + private: + std::string NormalizeProgramTextProto(std::string_view textproto) { + auto status_or_binary = protozero::TextToProto( + perfetto::kVmProgramDescriptor.data(), + perfetto::kVmProgramDescriptor.size(), ".perfetto.protos.VmProgram", + "program.textproto", textproto); + EXPECT_TRUE(status_or_binary.ok()) << status_or_binary.status().message(); + return ProtozeroToText(*pool_, ".perfetto.protos.VmProgram", + protozero::ConstBytes{status_or_binary->data(), + status_or_binary->size()}); + } + + std::unique_ptr pool_; +}; + +TEST_F(CompilerTest, Set) { + std::string config = R"( + commands { + set { + src: "for_testing" + src: "protovm_patch" + src: "string_to_merge" + dst: "for_testing" + dst: "protovm_incremental_state" + dst: "string_merged" + } + abort_level: ABORT + })"; + + std::string expected_instructions = R"( + instructions { + abort_level: ABORT + select { + relative_path { + field_id: 900 + } + relative_path { + field_id: 6 + } + relative_path { + field_id: 1 + } + } + nested_instructions { + select { + cursor: VM_CURSOR_DST + create_if_not_exist: true + relative_path { + field_id: 900 + } + relative_path { + field_id: 7 + } + relative_path { + field_id: 1 + } + } + nested_instructions { + set { + } + } + } + })"; + + CheckCompilation(config, expected_instructions); +} + +TEST_F(CompilerTest, DelIfSrcPresent) { + std::string config = R"( + commands { + del { + src: "for_testing" + src: "protovm_patch" + src: "single_message" + dst: "for_testing" + dst: "protovm_incremental_state" + if_src_present: true + } + abort_level: ABORT + })"; + + std::string expected_instructions = R"( + instructions { + abort_level: ABORT + select { + relative_path { + field_id: 900 + } + relative_path { + field_id: 6 + } + relative_path { + field_id: 3 + } + } + nested_instructions { + select { + cursor: VM_CURSOR_DST + relative_path { + field_id: 900 + } + relative_path { + field_id: 7 + } + } + nested_instructions { + del {} + } + } + })"; + + CheckCompilation(config, expected_instructions); +} + +TEST_F(CompilerTest, DelByKey) { + std::string config = R"( + commands { + del { + src: "for_testing" + src: "protovm_patch" + src: "delete_message_ids" + dst: "for_testing" + dst: "protovm_incremental_state" + dst: "messages" + dst_key_field: "id" + } + abort_level: ABORT + })"; + + std::string expected_instructions = R"( + instructions { + abort_level: ABORT + select { + relative_path { + field_id: 900 + } + relative_path { + field_id: 6 + } + relative_path { + field_id: 5 + is_repeated: true + } + } + nested_instructions { + reg_load { + dst_register: 0 + } + } + nested_instructions { + select { + cursor: VM_CURSOR_DST + relative_path { + field_id: 900 + } + relative_path { + field_id: 7 + } + relative_path { + field_id: 4 + } + relative_path { + map_key_field_id: 1 + register_to_match: 0 + } + } + nested_instructions { + del { + } + } + } + })"; + + CheckCompilation(config, expected_instructions); +} + +TEST_F(CompilerTest, Merge) { + std::string config = R"( + commands { + merge { + src: "for_testing" + src: "protovm_patch" + src: "single_message" + dst: "for_testing" + dst: "protovm_incremental_state" + dst: "single_message" + } + abort_level: ABORT + })"; + + std::string expected_instructions = R"( + instructions { + abort_level: ABORT + select { + relative_path { + field_id: 900 + } + relative_path { + field_id: 6 + } + relative_path { + field_id: 3 + } + } + nested_instructions { + select { + cursor: VM_CURSOR_DST + create_if_not_exist: true + relative_path { + field_id: 900 + } + relative_path { + field_id: 7 + } + relative_path { + field_id: 3 + } + } + nested_instructions { + merge { + del_if_src_empty: true + } + } + } + })"; + + CheckCompilation(config, expected_instructions); +} + +TEST_F(CompilerTest, MergeRecursive) { + std::string config = R"( + commands { + merge { + src: "for_testing" + src: "protovm_patch" + src: "single_message" + dst: "for_testing" + dst: "protovm_incremental_state" + dst: "single_message" + recursive: true + } + abort_level: ABORT + })"; + + std::string expected_instructions = R"( + instructions { + abort_level: ABORT + select { + relative_path { + field_id: 900 + } + relative_path { + field_id: 6 + } + relative_path { + field_id: 3 + } + } + nested_instructions { + select { + cursor: VM_CURSOR_DST + create_if_not_exist: true + relative_path { + field_id: 900 + } + relative_path { + field_id: 7 + } + relative_path { + field_id: 3 + } + } + nested_instructions { + abort_level: SKIP_CURRENT_INSTRUCTION + select { + relative_path { + field_id: 2 + } + } + nested_instructions { + select { + cursor: VM_CURSOR_DST + create_if_not_exist: true + relative_path { + field_id: 2 + } + } + nested_instructions { + merge { + del_if_src_empty: true + skip_submessages: true + } + } + } + } + nested_instructions { + merge { + del_if_src_empty: true + skip_submessages: true + } + } + } + })"; + + CheckCompilation(config, expected_instructions); +} + +TEST_F(CompilerTest, MergeByKey) { + std::string config = R"( + commands { + merge { + src: "for_testing" + src: "protovm_patch" + src: "messages" + dst: "for_testing" + dst: "protovm_incremental_state" + dst: "messages" + key_field: "id" + } + abort_level: ABORT + })"; + + std::string expected_instructions = R"( + instructions { + abort_level: ABORT + select { + relative_path { + field_id: 900 + } + relative_path { + field_id: 6 + } + relative_path { + field_id: 4 + is_repeated: true + } + } + nested_instructions { + abort_level: ABORT + select { + relative_path { + field_id: 1 + } + } + nested_instructions { + reg_load { + dst_register: 0 + } + } + } + nested_instructions { + select { + cursor: VM_CURSOR_DST + create_if_not_exist: true + relative_path { + field_id: 900 + } + relative_path { + field_id: 7 + } + relative_path { + field_id: 4 + } + relative_path { + map_key_field_id: 1 + register_to_match: 0 + } + } + nested_instructions { + merge { + del_if_src_empty: true + } + } + } + })"; + + CheckCompilation(config, expected_instructions); +} + +TEST_F(CompilerTest, EnterScope) { + std::string config = R"( + commands { + enter_scope { + src: "for_testing" + src: "protovm_patch" + dst: "for_testing" + dst: "protovm_incremental_state" + commands { + set {} + } + } + abort_level: ABORT + })"; + + std::string expected_instructions = R"( + instructions { + abort_level: ABORT + select { + relative_path { + field_id: 900 + } + relative_path { + field_id: 6 + } + } + nested_instructions { + select { + cursor: VM_CURSOR_DST + create_if_not_exist: true + relative_path { + field_id: 900 + } + relative_path { + field_id: 7 + } + } + nested_instructions { + set { + } + } + } + })"; + + CheckCompilation(config, expected_instructions); +} + +TEST_F(CompilerTest, AbortLevelTranslation) { + std::unordered_map + command_to_instruction_abort_level{ + {"", ""}, + {"abort_level: SKIP_CURRENT_COMMAND", + "abort_level: SKIP_CURRENT_INSTRUCTION"}, + {"abort_level: SKIP_CURRENT_COMMAND_AND_BREAK_OUTER", ""}, + {"abort_level: ABORT", "abort_level: ABORT"}, + }; + + for (auto [command_abort, instruction_abort] : + command_to_instruction_abort_level) { + std::string config = R"( + commands { + set { + src: "for_testing" + dst: "for_testing" + } + )" + command_abort + + R"( + })"; + + std::string expected_instructions = R"( + instructions { + )" + instruction_abort + R"( + select { + relative_path { + field_id: 900 + } + } + nested_instructions { + select { + cursor: VM_CURSOR_DST + create_if_not_exist: true + relative_path { + field_id: 900 + } + } + nested_instructions { + set { + } + } + } + })"; + + CheckCompilation(config, expected_instructions); + } +} + +TEST_F(CompilerTest, ErrorInvalidFieldName) { + std::string config = R"( + commands { + set { + src: "for_testing" + src: "protovm_patch" + src: "single_message" + dst: "for_testing" + dst: "protovm_incremental_state" + dst: "invalid_field_name" + } + })"; + + auto compiler = Compiler{}; + EXPECT_TRUE(compiler + .RegisterDescriptors(perfetto::kTraceDescriptor.data(), + perfetto::kTraceDescriptor.size()) + .ok()); + EXPECT_TRUE(compiler + .RegisterDescriptors(perfetto::kWinscopeDescriptor.data(), + perfetto::kWinscopeDescriptor.size()) + .ok()); + auto status_or = compiler.Compile(config); + EXPECT_FALSE(status_or.ok()); + EXPECT_THAT( + status_or.status().message(), + ::testing::HasSubstr("Failed to lookup field name: invalid_field_name")); +} + +TEST_F(CompilerTest, EmptyPath) { + std::string config = R"( + commands { + set {} + } + commands { + set { + src: "for_testing" + } + } + commands { + set { + dst: "for_testing" + } + } + )"; + + std::string expected_instructions = R"( + instructions { + set {} + } + instructions { + select { + relative_path { + field_id: 900 + } + } + nested_instructions { + set {} + } + } + instructions { + select { + cursor: VM_CURSOR_DST + create_if_not_exist: true + relative_path { + field_id: 900 + } + } + nested_instructions { + set {} + } + })"; + + CheckCompilation(config, expected_instructions); +} + +TEST_F(CompilerTest, CanResolveWinscopeExtensionsProtos) { + std::string config = R"( + commands { + set { + src: "for_testing" + src: "protovm_patch" + dst: "winscope_extensions" + dst: "windowmanager" + dst: "window_manager_service" + } + })"; + + std::string expected_instructions = R"( + instructions { + select { + relative_path { + field_id: 900 + } + relative_path { + field_id: 6 + } + } + nested_instructions { + select { + cursor: VM_CURSOR_DST + create_if_not_exist: true + relative_path { + field_id: 112 + } + relative_path { + field_id: 6 + } + relative_path { + field_id: 3 + } + } + nested_instructions { + set { + } + } + } + })"; + + CheckCompilation(config, expected_instructions); +} + +} // namespace +} // namespace perfetto::protovm diff --git a/src/protovm/compiler/instruction_emitter.cc b/src/protovm/compiler/instruction_emitter.cc new file mode 100644 index 00000000000..e8b02c04049 --- /dev/null +++ b/src/protovm/compiler/instruction_emitter.cc @@ -0,0 +1,297 @@ +/* + * 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/protovm/compiler/instruction_emitter.h" + +#include "perfetto/base/logging.h" +#include "perfetto/ext/base/status_macros.h" +#include "protos/perfetto/common/descriptor.pbzero.h" +#include "protos/perfetto/protovm/vm_program.pbzero.h" + +namespace perfetto::protovm { + +InstructionEmitter::Scope InstructionEmitter::Scope::AddNestedInstruction() + const { + perfetto::protos::pbzero::VmInstruction* nested_instruction = nullptr; + auto* program = + std::get_if(&instruction); + if (program) { + nested_instruction = (*program)->add_instructions(); + } else { + nested_instruction = + std::get(instruction) + ->add_nested_instructions(); + } + return {nested_instruction, src_proto, dst_proto}; +} + +perfetto::protos::pbzero::VmInstruction* +InstructionEmitter::Scope::GetInstruction() const { + return std::get(instruction); +} + +InstructionEmitter::InstructionEmitter( + perfetto::trace_processor::DescriptorPool* pool) + : pool_(pool) { + PERFETTO_CHECK(pool_); +} + +base::StatusOr InstructionEmitter::Select( + const Scope& scope, + const std::vector& relative_path, + Cursor cursor, + bool create_if_not_exist, + AbortLevel abort_level) const { + if (relative_path.empty()) { + return scope; + } + + auto new_scope = scope.AddNestedInstruction(); + MaybeSetAbortLevel(abort_level, new_scope.GetInstruction()); + + auto* select = new_scope.GetInstruction()->set_select(); + + const perfetto::trace_processor::ProtoDescriptor** current_proto = nullptr; + if (cursor == Cursor::VM_CURSOR_SRC) { + current_proto = &new_scope.src_proto; + } else { + PERFETTO_CHECK(cursor == Cursor::VM_CURSOR_DST); + current_proto = &new_scope.dst_proto; + select->set_cursor(cursor); + if (create_if_not_exist) { + select->set_create_if_not_exist(true); + } + } + + for (const auto& field_name : relative_path) { + ASSIGN_OR_RETURN(auto field, LookupProtoField(*current_proto, field_name)); + auto* path_component = select->add_relative_path(); + path_component->set_field_id(field.id); + if (field.descriptor->is_repeated()) { + path_component->set_is_repeated(true); + } + *current_proto = field.proto; + } + + return new_scope; +} + +base::StatusOr InstructionEmitter::SelectByKey( + const Scope& scope, + const std::vector& dst_relative_path, + std::string_view key_field_name, + uint32_t register_id, + bool create_if_not_exist, + AbortLevel abort_level) const { + auto new_scope = scope.AddNestedInstruction(); + MaybeSetAbortLevel(abort_level, new_scope.GetInstruction()); + + auto* select = new_scope.GetInstruction()->set_select(); + select->set_cursor(Cursor::VM_CURSOR_DST); + if (create_if_not_exist) { + select->set_create_if_not_exist(true); + } + + for (const auto& field_name : dst_relative_path) { + ASSIGN_OR_RETURN(auto field, + LookupProtoField(new_scope.dst_proto, field_name)); + select->add_relative_path()->set_field_id(field.id); + new_scope.dst_proto = field.proto; + } + + ASSIGN_OR_RETURN(auto key_field, + LookupProtoField(new_scope.dst_proto, key_field_name)); + auto* last_path_component = select->add_relative_path(); + last_path_component->set_map_key_field_id(key_field.id); + last_path_component->set_register_to_match(register_id); + + return new_scope; +} + +base::Status InstructionEmitter::Set( + const Scope& scope, + const std::vector& src_relative_path, + const std::vector& dst_relative_path, + AbortLevel abort_level) const { + ASSIGN_OR_RETURN(auto src, Select(scope, src_relative_path, + Cursor::VM_CURSOR_SRC, false, abort_level)); + ASSIGN_OR_RETURN(auto dst, + Select(src, dst_relative_path, Cursor::VM_CURSOR_DST, true, + kDefaultAbortLevel)); + auto new_scope = dst.AddNestedInstruction(); + new_scope.GetInstruction()->set_set(); + return base::OkStatus(); +} + +base::Status InstructionEmitter::Merge( + const Scope& scope, + const std::vector& src_relative_path, + const std::vector& dst_relative_path, + bool is_recursive, + AbortLevel abort_level) const { + ASSIGN_OR_RETURN(auto src, Select(scope, src_relative_path, + Cursor::VM_CURSOR_SRC, false, abort_level)); + ASSIGN_OR_RETURN(auto dst, + Select(src, dst_relative_path, Cursor::VM_CURSOR_DST, true, + kDefaultAbortLevel)); + + if (is_recursive) { + const auto* proto = dst.src_proto; + PERFETTO_CHECK(dst.src_proto == dst.dst_proto); + PERFETTO_CHECK(proto); + + for (const auto& [field_id, field_descriptor] : proto->fields()) { + if (field_descriptor.type() != + perfetto::protos::pbzero::FieldDescriptorProto::TYPE_MESSAGE) { + continue; + } + if (field_descriptor.is_repeated()) { + continue; + } + if (IsDeprecated(field_descriptor)) { + // TODO(keanmariotti): currently the recursion includes only + // non-deprecated fields. Going forward we might want a more flexible + // filtering mechanism. E.g. configure an allow/deny list of field + // options to be included/skipped. + continue; + } + RETURN_IF_ERROR(Merge(dst, {field_descriptor.name()}, + {field_descriptor.name()}, true, + AbortLevel::SKIP_CURRENT_INSTRUCTION)); + } + } + + auto new_scope = dst.AddNestedInstruction(); + auto* merge = new_scope.GetInstruction()->set_merge(); + merge->set_del_if_src_empty(true); + if (is_recursive) { + merge->set_skip_submessages(true); + } + return base::OkStatus(); +} + +base::Status InstructionEmitter::MergeByKey( + const Scope& scope, + const std::vector& src_relative_path, + const std::vector& dst_relative_path, + std::string_view key_field_name, + bool is_recursive, + AbortLevel abort_level) const { + static constexpr uint32_t REGISTER_ID = 0; + + ASSIGN_OR_RETURN(auto src_message_scope, + Select(scope, src_relative_path, Cursor::VM_CURSOR_SRC, + false, abort_level)); + ASSIGN_OR_RETURN(auto src_key_field_scope, + Select(src_message_scope, {key_field_name}, + Cursor::VM_CURSOR_SRC, false, AbortLevel::ABORT)); + + auto reg_load_scope = src_key_field_scope.AddNestedInstruction(); + reg_load_scope.GetInstruction()->set_reg_load()->set_dst_register( + REGISTER_ID); + + ASSIGN_OR_RETURN( + auto dst_message_scope, + SelectByKey(src_message_scope, dst_relative_path, key_field_name, + REGISTER_ID, true, kDefaultAbortLevel)); + RETURN_IF_ERROR( + Merge(dst_message_scope, {}, {}, is_recursive, kDefaultAbortLevel)); + return base::OkStatus(); +} + +base::Status InstructionEmitter::DeleteIfPresent( + const Scope& scope, + const std::vector& src_relative_path, + const std::vector& dst_relative_path, + AbortLevel abort_level) const { + ASSIGN_OR_RETURN(auto src_scope, + Select(scope, src_relative_path, Cursor::VM_CURSOR_SRC, + false, abort_level)); + ASSIGN_OR_RETURN(auto dst_scope, + Select(src_scope, dst_relative_path, Cursor::VM_CURSOR_DST, + false, kDefaultAbortLevel)); + dst_scope.AddNestedInstruction().GetInstruction()->set_del(); + return base::OkStatus(); +} + +base::Status InstructionEmitter::DeleteByKey( + const Scope& scope, + const std::vector& src_relative_path, + const std::vector& dst_relative_path, + std::string_view key_field_name, + AbortLevel abort_level) const { + static constexpr uint32_t REGISTER_ID = 0; + + ASSIGN_OR_RETURN(auto src_scope, + Select(scope, src_relative_path, Cursor::VM_CURSOR_SRC, + false, abort_level)); + + auto reg_load_scope = src_scope.AddNestedInstruction(); + reg_load_scope.GetInstruction()->set_reg_load()->set_dst_register( + REGISTER_ID); + + ASSIGN_OR_RETURN(auto dst_scope, + SelectByKey(src_scope, dst_relative_path, key_field_name, + REGISTER_ID, false, kDefaultAbortLevel)); + + auto del_scope = dst_scope.AddNestedInstruction(); + del_scope.GetInstruction()->set_del(); + return base::OkStatus(); +} + +void InstructionEmitter::MaybeSetAbortLevel( + AbortLevel abort_level, + protos::pbzero::VmInstruction* instruction) const { + if (abort_level != kDefaultAbortLevel) { + instruction->set_abort_level(abort_level); + } +} + +base::StatusOr InstructionEmitter::LookupProtoField( + const perfetto::trace_processor::ProtoDescriptor* proto, + std::string_view field_name) const { + PERFETTO_CHECK(proto); + auto* field_descriptor = proto->FindFieldByName(std::string(field_name)); + if (!field_descriptor) { + return base::ErrStatus("Failed to lookup field name: %s", + std::string(field_name).c_str()); + } + + if (field_descriptor->type() != + perfetto::protos::pbzero::FieldDescriptorProto::TYPE_MESSAGE) { + return Field{field_descriptor->number(), field_descriptor, nullptr}; + } + + auto new_proto_idx = + pool_->FindDescriptorIdx(field_descriptor->resolved_type_name()); + if (!new_proto_idx) { + return base::ErrStatus("Failed to find descriptor for type: %s", + field_descriptor->resolved_type_name().c_str()); + } + auto* new_proto = &pool_->descriptors()[*new_proto_idx]; + + return Field{field_descriptor->number(), field_descriptor, new_proto}; +} + +bool InstructionEmitter::IsDeprecated( + const perfetto::trace_processor::FieldDescriptor& field_descriptor) const { + protozero::ProtoDecoder options_decoder(field_descriptor.options().data(), + field_descriptor.options().size()); + auto deprecated_field = options_decoder.FindField(3); + return deprecated_field.valid() && deprecated_field.as_bool(); +} + +} // namespace perfetto::protovm diff --git a/src/protovm/compiler/instruction_emitter.h b/src/protovm/compiler/instruction_emitter.h new file mode 100644 index 00000000000..00712908a2e --- /dev/null +++ b/src/protovm/compiler/instruction_emitter.h @@ -0,0 +1,125 @@ +/* + * 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. + */ + +#ifndef SRC_PROTOVM_COMPILER_INSTRUCTION_EMITTER_H_ +#define SRC_PROTOVM_COMPILER_INSTRUCTION_EMITTER_H_ + +#include +#include +#include + +#include "perfetto/base/status.h" +#include "perfetto/ext/base/status_or.h" +#include "protos/perfetto/protovm/vm_program.pbzero.h" +#include "src/trace_processor/util/descriptors.h" + +namespace perfetto::protovm { + +// Implements the compiler back-end. +// +// Translates high-level commands into corresponding sequences of low-level +// instructions. +class InstructionEmitter { + public: + using AbortLevel = protos::pbzero::VmInstruction::AbortLevel; + using Cursor = perfetto::protos::pbzero::VmCursorEnum; + + static constexpr auto kDefaultAbortLevel = protos::pbzero::VmInstruction:: + AbortLevel::SKIP_CURRENT_INSTRUCTION_AND_BREAK_OUTER; + + struct Scope { + Scope AddNestedInstruction() const; + perfetto::protos::pbzero::VmInstruction* GetInstruction() const; + + std::variant + instruction; + const perfetto::trace_processor::ProtoDescriptor* src_proto; + const perfetto::trace_processor::ProtoDescriptor* dst_proto; + }; + + explicit InstructionEmitter(perfetto::trace_processor::DescriptorPool* pool); + + base::StatusOr Select( + const Scope& scope, + const std::vector& relative_path, + Cursor cursor, + bool create_if_not_exist, + AbortLevel abort_level) const; + + base::StatusOr SelectByKey( + const Scope& scope, + const std::vector& dst_relative_path, + std::string_view key_field_name, + uint32_t register_id, + bool create_if_not_exist, + AbortLevel abort_level) const; + + base::Status Set(const Scope& scope, + const std::vector& src_relative_path, + const std::vector& dst_relative_path, + AbortLevel abort_level) const; + + base::Status Merge(const Scope& scope, + const std::vector& src_relative_path, + const std::vector& dst_relative_path, + bool is_recursive, + AbortLevel abort_level) const; + + base::Status MergeByKey( + const Scope& scope, + const std::vector& src_relative_path, + const std::vector& dst_relative_path, + std::string_view key_field_name, + bool is_recursive, + AbortLevel abort_level) const; + + base::Status DeleteIfPresent( + const Scope& scope, + const std::vector& src_relative_path, + const std::vector& dst_relative_path, + AbortLevel abort_level) const; + + base::Status DeleteByKey( + const Scope& scope, + const std::vector& src_relative_path, + const std::vector& dst_relative_path, + std::string_view key_field_name, + AbortLevel abort_level) const; + + private: + struct Field { + uint32_t id; + const perfetto::trace_processor::FieldDescriptor* descriptor; + const perfetto::trace_processor::ProtoDescriptor* proto; + }; + + void MaybeSetAbortLevel(AbortLevel abort_level, + protos::pbzero::VmInstruction* instruction) const; + + base::StatusOr LookupProtoField( + const perfetto::trace_processor::ProtoDescriptor* proto, + std::string_view field_name) const; + + bool IsDeprecated( + const perfetto::trace_processor::FieldDescriptor& field_descriptor) const; + + perfetto::trace_processor::DescriptorPool* pool_; +}; + +} // namespace perfetto::protovm + +#endif // SRC_PROTOVM_COMPILER_INSTRUCTION_EMITTER_H_ diff --git a/src/protovm/compiler/main.cc b/src/protovm/compiler/main.cc new file mode 100644 index 00000000000..9617a37ee5c --- /dev/null +++ b/src/protovm/compiler/main.cc @@ -0,0 +1,148 @@ +/* + * 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 +#include + +#include +#include +#include + +#include "perfetto/base/logging.h" +#include "perfetto/ext/base/file_utils.h" +#include "perfetto/ext/base/getopt.h" +#include "perfetto/ext/base/status_or.h" +#include "src/protovm/compiler/compiler.h" +#include "src/protozero/multifile_error_collector.h" + +namespace { +perfetto::base::StatusOr LoadDescriptors( + const std::vector& proto_paths, + const std::vector& proto_files) { + google::protobuf::compiler::DiskSourceTree source_tree; + for (const auto& path : proto_paths) { + source_tree.MapPath("", path); + } + if (proto_paths.empty()) { + source_tree.MapPath("", "."); + } + + protozero::MultiFileErrorCollectorImpl error_collector; + google::protobuf::compiler::Importer importer(&source_tree, &error_collector); + + std::set added_files; + google::protobuf::FileDescriptorSet fds; + + for (const auto& file : proto_files) { + const auto* file_desc = importer.Import(file); + if (!file_desc) { + return perfetto::base::ErrStatus("Failed to import %s", file.c_str()); + } + + auto add_file_and_deps = + [&](auto& self, const google::protobuf::FileDescriptor* desc) -> void { + if (!added_files.insert(std::string(desc->name())).second) + return; + for (int i = 0; i < desc->dependency_count(); ++i) { + self(self, desc->dependency(i)); + } + desc->CopyTo(fds.add_file()); + }; + add_file_and_deps(add_file_and_deps, file_desc); + } + + std::string fds_bytes; + if (!fds.SerializeToString(&fds_bytes)) { + return perfetto::base::ErrStatus("Failed to serialize FileDescriptorSet"); + } + return fds_bytes; +} +} // namespace + +// Implements the ProtoVM compiler CLI +// +// Reads a CompileConfig textproto from stdin and outputs the compiled binary +// VmProgram to stdout. +int main(int argc, char** argv) { + static const option long_options[] = { + {"help", no_argument, nullptr, 'h'}, + {"proto_path", required_argument, nullptr, 'I'}, + {nullptr, 0, nullptr, 0}}; + + std::vector proto_paths; + std::vector proto_files; + + for (;;) { + int option = getopt_long(argc, argv, "hI:", long_options, nullptr); + if (option == -1) + break; + + if (option == 'h') { + std::printf( + "Usage: %s [-I ] ...\n" + "Reads ProtoVM CompileConfig textproto from stdin, compiles it and " + "outputs a VmProgram binary on stdout.\n", + argv[0]); + return 0; + } + if (option == 'I') { + proto_paths.emplace_back(optarg); + continue; + } + return 1; + } + + for (int i = optind; i < argc; ++i) { + proto_files.emplace_back(argv[i]); + } + + if (proto_files.empty()) { + PERFETTO_ELOG("At least one .proto file is required"); + return 1; + } + + auto status_or_descriptors = LoadDescriptors(proto_paths, proto_files); + if (!status_or_descriptors.ok()) { + PERFETTO_ELOG("%s", status_or_descriptors.status().c_message()); + return 1; + } + + std::string textproto; + if (!perfetto::base::ReadFileStream(stdin, &textproto)) { + PERFETTO_ELOG("Failed to read from stdin"); + return 1; + } + + auto compiler = perfetto::protovm::Compiler{}; + if (auto status = compiler.RegisterDescriptors( + reinterpret_cast(status_or_descriptors->data()), + status_or_descriptors->size()); + !status.ok()) { + PERFETTO_ELOG("Failed to register descriptors: %s", status.c_message()); + return 1; + } + + auto status_or_program = compiler.Compile(textproto); + if (!status_or_program.ok()) { + PERFETTO_ELOG("Encountered error: %s", + status_or_program.status().c_message()); + return 1; + } + + std::fwrite(status_or_program->data(), 1, status_or_program->size(), stdout); + + return 0; +}