opt/builder_pattern/RemoveBuilderPattern.cpp (356 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #include "RemoveBuilderPattern.h" #include <boost/regex.hpp> #include "BuilderTransform.h" #include "ConfigFiles.h" #include "CppUtil.h" #include "DexClass.h" #include "DexUtil.h" #include "PassManager.h" #include "Show.h" #include "Trace.h" #include "TypeSystem.h" #include "Walkers.h" namespace builder_pattern { constexpr size_t MAX_NUM_INLINE_ITERATION = 10; constexpr size_t MAX_NUM_INLINE_ITERATION_FOR_SIMPLE_BUILDERS = 4; namespace { /** * Example: Lcom/facebook/RandomClassName; -> RandomClassName */ std::string only_class_name(const DexType* type) { std::string type_str(type->c_str()); size_t package_delim = type_str.rfind('/'); always_assert(package_delim != std::string::npos); size_t class_start = package_delim + 1; return type_str.substr(class_start, type_str.size() - package_delim - 2); } std::unordered_set<DexType*> get_associated_buildees( const std::unordered_set<const DexType*>& builders) { std::unordered_set<DexType*> buildees; for (const auto& builder : builders) { const std::string& builder_name = builder->str(); std::string buildee_name = builder_name.substr(0, builder_name.size() - 9) + ";"; auto type = DexType::get_type(buildee_name.c_str()); if (type) { buildees.emplace(type); } } return buildees; } bool has_statics(const DexClass* cls) { always_assert(cls); auto& dmethods = cls->get_dmethods(); for (const auto* m : dmethods) { if (is_static(m)) { return true; } } return !cls->get_sfields().empty(); } class RemoveClasses { public: RemoveClasses(const DexType* super_cls, const Scope& scope, const init_classes::InitClassesWithSideEffects& init_classes_with_side_effects, const inliner::InlinerConfig& inliner_config, const std::vector<DexType*>& blocklist, bool propagate_escape_results, const size_t max_num_inline_iteration, DexStoresVector& stores) : m_root(super_cls), m_scope(scope), m_blocklist(blocklist), m_type_system(scope), m_propagate_escape_results(propagate_escape_results), m_transform(scope, m_type_system, super_cls, init_classes_with_side_effects, inliner_config, stores), m_max_num_inline_iteration(max_num_inline_iteration), m_stores(stores) { gather_classes(); } void optimize() { collect_excluded_types(); if (m_root != type::java_lang_Object()) { // We can't inline a method that has super calls. for (const DexType* builder : m_classes) { if (!m_transform.inline_super_calls_and_ctors(builder)) { TRACE(BLD_PATTERN, 2, "Excluding type %s since we cannot inline super calls for all " "methods", SHOW(builder)); m_excluded_types.emplace(builder); } } } update_usage(); } void cleanup() { m_transform.cleanup(); } void print_stats(PassManager& mgr) { auto root_name = only_class_name(m_root); mgr.set_metric(root_name + "_total_classes", m_classes.size()); mgr.set_metric(root_name + "_num_classes_excluded", m_excluded_types.size()); mgr.set_metric(root_name + "_num_total_usages", m_num_usages); mgr.set_metric(root_name + "_num_removed_usages", m_num_removed_usages); TRACE(BLD_PATTERN, 1, "num_classes_excluded %lu", m_excluded_types.size()); TRACE(BLD_PATTERN, 1, "num_classes_removed %lu", m_removed_types.size()); for (const auto& type : m_excluded_types) { TRACE(BLD_PATTERN, 2, "Excluded type: %s", SHOW(type)); } mgr.set_metric(root_name + "_num_classes_removed", m_removed_types.size()); for (const auto& type : m_removed_types) { TRACE(BLD_PATTERN, 2, "Removed type: %s", SHOW(type)); } for (const auto& pair : m_num_inline_iterations) { std::stringstream metric; metric << "_num_inline_iteration_" << pair.first; mgr.incr_metric(root_name + metric.str(), pair.second); TRACE(BLD_PATTERN, 4, "%s_num_inline_iteration %ld %ld", root_name.c_str(), pair.first, pair.second); } } private: void gather_classes() { const TypeSet& subclasses = m_type_system.get_children(m_root); auto* object_type = type::java_lang_Object(); boost::regex re("\\$Builder;$"); for (const DexType* type : subclasses) { if (!m_type_system.get_children(type).empty()) { // Only leaf classes continue; } auto cls = type_class(type); if (!cls || cls->is_external()) { continue; } if (m_root == object_type && has_statics(cls)) { // Only simple builders with no static methods or fields. continue; } // For Builders extending j/l/Object;, we filter by name. if (m_root != object_type || boost::regex_search(type->c_str(), re)) { m_classes.emplace(type); } } } void update_usage() { auto buildee_types = get_associated_buildees(m_classes); std::mutex methods_mutex; std::vector<DexMethod*> methods; walk::parallel::methods(m_scope, [&](DexMethod* method) { if (!method || !method->get_code()) { return; } if (m_classes.count(method->get_class()) || buildee_types.count(method->get_class())) { // Skip builder and associated buildee methods. return; } BuilderAnalysis analysis(m_classes, m_excluded_types, method); analysis.run_analysis(); if (analysis.has_usage()) { std::unique_lock<std::mutex> lock{methods_mutex}; methods.push_back(method); } }); if (methods.empty()) { return; } std::sort(methods.begin(), methods.end(), compare_dexmethods); for (auto method : methods) { BuilderAnalysis analysis(m_classes, m_excluded_types, method); bool have_builders_to_remove = inline_builders_and_check_method(method, &analysis); m_num_usages += analysis.get_total_num_usages(); m_num_inline_iterations[analysis.get_num_inline_iterations()]++; if (!have_builders_to_remove) { continue; } // When we get here we know that we can remove the builders. m_num_removed_usages += analysis.get_num_usages(); auto removed_types = analysis.get_instantiated_types(); TRACE(BLD_PATTERN, 2, "Removed following builders from %s", SHOW(method)); for (const auto& type : removed_types) { m_removed_types.emplace(type); TRACE(BLD_PATTERN, 2, "\t %s", SHOW(type)); } m_transform.replace_fields(analysis.get_usage(), method); } shrink_methods(methods); } void shrink_methods(const std::vector<DexMethod*>& methods) { // Run shrinking opts to optimize the changed methods. Timer t("shrink_methods"); auto post_process = [&](DexMethod* method) { m_transform.get_shrinker().shrink_method(method); }; // Walkers are over classes, so need to do this "manually." workqueue_run<DexMethod*>(post_process, methods); } void collect_excluded_types() { walk::fields(m_scope, [&](DexField* field) { auto type = field->get_type(); if (m_classes.count(type)) { TRACE(BLD_PATTERN, 2, "Excluding type since it is stored in a field: %s", SHOW(type)); m_excluded_types.emplace(type); } }); for (DexType* type : m_blocklist) { if (m_classes.count(type)) { TRACE(BLD_PATTERN, 2, "Excluding type since it was in the blocklist: %s", SHOW(type)); m_excluded_types.emplace(type); } } } /** * Returns true if there are builders that we can remove from the current * method. */ bool inline_builders_and_check_method(DexMethod* method, BuilderAnalysis* analysis) { bool builders_to_remove = false; // To be used for local excludes. We cleanup m_excluded_types at the end. std::unordered_set<const DexType*> local_excludes; std::unique_ptr<IRCode> original_code = nullptr; size_t num_iterations = 1; for (; num_iterations < m_max_num_inline_iteration; num_iterations++) { analysis->run_analysis(); std::vector<IRInstruction*> deleted_insns; // When ending the scope, free the instructions. auto deleted_guard = at_scope_exit([&deleted_insns]() { for (auto* insn : deleted_insns) { delete insn; } }); if (!analysis->has_usage()) { TRACE(BLD_PATTERN, 6, "No builder to remove from %s", SHOW(method)); break; } if (original_code == nullptr) { // Keep a copy of the code, in order to restore it, if needed. original_code = std::make_unique<IRCode>(*method->get_code()); } // First bind virtual callsites to the current implementation, if any, // in order to be able to inline them. auto vinvoke_to_instance = analysis->get_vinvokes_to_this_infered_type(); m_transform.update_virtual_calls(vinvoke_to_instance); // Inline all methods that are either called on the builder instance // or take the builder as an argument, except for the ctors. std::unordered_set<IRInstruction*> to_inline = analysis->get_all_inlinable_insns(); if (to_inline.empty()) { TRACE(BLD_PATTERN, 3, "Everything that could be inlined was inlined for %s", SHOW(method)); // Check if any of the instance builder types cannot be removed. auto non_removable_types = analysis->non_removable_types(); if (!non_removable_types.empty()) { for (const auto* type : non_removable_types) { if (m_excluded_types.count(type) == 0 && !m_propagate_escape_results) { local_excludes.emplace(type); } m_excluded_types.emplace(type); } // Restore method and re-try. We will only // try removing non-excluded types. method->set_code(std::make_unique<IRCode>(*original_code)); continue; } else { TRACE(BLD_PATTERN, 2, "Everything that could be inlined was inlined and none of " "the instances escape for %s", SHOW(method)); analysis->print_usage(); builders_to_remove = true; break; } } auto not_inlined_insns = m_transform.try_inline_calls(method, to_inline, &deleted_insns); if (!not_inlined_insns.empty()) { auto to_eliminate = analysis->get_instantiated_types(&not_inlined_insns); for (const DexType* type : to_eliminate) { if (m_excluded_types.count(type) == 0 && !m_propagate_escape_results) { local_excludes.emplace(type); } m_excluded_types.emplace(type); } if (not_inlined_insns.size() == to_inline.size()) { // Nothing left to do, since nothing was inlined. TRACE(BLD_PATTERN, 4, "Couldn't inline any of the methods in %s", SHOW(method)); for (const auto& insn : not_inlined_insns) { TRACE(BLD_PATTERN, 5, "\t%s", SHOW(insn)); } break; } else { // Restore method and re-try. We will only try inlining non-excluded // types. TRACE(BLD_PATTERN, 4, "Couldn't inline all the methods in %s", SHOW(method)); for (const auto& insn : not_inlined_insns) { TRACE(BLD_PATTERN, 5, "\t%s", SHOW(insn)); } method->set_code(std::make_unique<IRCode>(*original_code)); } } // If we inlined everything, we still need to make sure we don't have // new methods to inline (for example from something that was inlined // in this step). } for (const DexType* type : local_excludes) { m_excluded_types.erase(type); } if (!builders_to_remove && original_code != nullptr) { method->set_code(std::move(original_code)); } analysis->set_num_inline_iterations(num_iterations); return builders_to_remove; } const DexType* m_root; const Scope& m_scope; const std::vector<DexType*>& m_blocklist; TypeSystem m_type_system; bool m_propagate_escape_results; BuilderTransform m_transform; std::unordered_set<const DexType*> m_classes; std::unordered_set<const DexType*> m_excluded_types; std::unordered_set<const DexType*> m_removed_types; size_t m_num_usages{0}; size_t m_num_removed_usages{0}; size_t m_max_num_inline_iteration{0}; std::map<size_t, size_t> m_num_inline_iterations; const DexStoresVector& m_stores; }; } // namespace void RemoveBuilderPatternPass::bind_config() { std::vector<DexType*> roots; bind("roots", {}, roots, Configurable::default_doc(), Configurable::bindflags::types::warn_if_unresolvable); bind("blocklist", {}, m_blocklist, Configurable::default_doc(), Configurable::bindflags::types::warn_if_unresolvable); bind("propagate_escape_results", true, m_propagate_escape_results); // TODO(T44502473): if we could pass a binding filter lambda instead of // bindflags, this could be more simply expressed after_configuration([this, roots] { auto object_type = type::java_lang_Object(); m_roots.clear(); for (const auto& root : roots) { if (!type_class(root)) continue; if (root != object_type) { auto super_cls = type_class(root)->get_super_class(); if (super_cls != object_type) { fprintf(stderr, "[builders]: %s isn't a valid root as it extends %s\n", root->c_str(), super_cls->c_str()); continue; } } m_roots.push_back(root); } }); } void RemoveBuilderPatternPass::run_pass(DexStoresVector& stores, ConfigFiles& conf, PassManager& mgr) { auto scope = build_class_scope(stores); init_classes::InitClassesWithSideEffects init_classes_with_side_effects( scope, conf.create_init_class_insns()); // For 2nd instance or later instances of the pass, fall back the roots to // have only j/l/Object;. if (mgr.get_current_pass_info()->repeat > 0) { m_roots.clear(); m_roots.push_back(type::java_lang_Object()); } for (const auto& root : m_roots) { size_t max_num_inline_iteration = MAX_NUM_INLINE_ITERATION; if (root == type::java_lang_Object()) { max_num_inline_iteration = MAX_NUM_INLINE_ITERATION_FOR_SIMPLE_BUILDERS; } RemoveClasses rm_builder_pattern( root, scope, init_classes_with_side_effects, conf.get_inliner_config(), m_blocklist, m_propagate_escape_results, max_num_inline_iteration, stores); rm_builder_pattern.optimize(); rm_builder_pattern.print_stats(mgr); rm_builder_pattern.cleanup(); } } static RemoveBuilderPatternPass s_pass; } // namespace builder_pattern