opt/virtual_merging/VirtualMerging.cpp (1,386 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. */ /** * VirtualMergingPass removes virtual methods that override other virtual * methods, by merging them, under certain conditions. * - we omit virtual scopes that are involved in invoke-supers (this could be * made less conservative) * - we omit virtual methods that might be involved in unresolved * invoke-virtuals. * - of, course the usual `can_rename` and not `root` conditions. * - the overriding method must be inlinable into the overridden method (using * standard inliner functionality) * * When overriding an abstract method, the body of the overriding method is * essentially just moved into the formerly abstract method, with a preceeding * cast-class instruction to make the type checker happy. (The actual * implementation is a special case of the below, using the inliner.) * * When overriding a non-abstract method, we first insert a prologue like the * following into the overridden method: * * instance-of param0, DeclaringTypeOfOverridingMethod * move-result-pseudo if_temp * if-nez if_temp, new_code * ... (old body) * * new_code: * cast-class param0, DeclaringTypeOfOverridingMethod * move-result-pseudo-object temp * invoke-virtual temp, param1, ..., paramN, OverridingMethod * move-result result_temp * return result_temp * * And then we inline the invoke-virtual instruction. Details vary depending on * the whether the method actually has a result, and if so, what kind it is. */ #include "VirtualMerging.h" #include "ABExperimentContext.h" #include "ConfigFiles.h" #include "ControlFlow.h" #include "CppUtil.h" #include "DedupVirtualMethods.h" #include "IRCode.h" #include "IRInstruction.h" #include "Inliner.h" #include "MethodProfiles.h" #include "PassManager.h" #include "Resolver.h" #include "ScopedCFG.h" #include "SourceBlocks.h" #include "StlUtil.h" #include "TypeSystem.h" #include "Walkers.h" namespace { constexpr const char* METRIC_DEDUPPED_VIRTUAL_METHODS = "num_dedupped_virtual_methods"; constexpr const char* METRIC_INVOKE_SUPER_METHODS = "num_invoke_super_methods"; constexpr const char* METRIC_INVOKE_SUPER_UNRESOLVED_METHOD_REFS = "num_invoke_super_unresolved_methods_refs"; constexpr const char* METRIC_MERGEABLE_VIRTUAL_SCOPES = "num_mergeable_virtual_scopes"; constexpr const char* METRIC_MERGEABLE_VIRTUAL_METHODS = "num_mergeable_virtual_methods"; constexpr const char* METRIC_MERGEABLE_VIRTUAL_METHODS_ANNOTATED_METHODS = "num_mergeable_virtual_method_annotated_methods"; constexpr const char* METRIC_MERGEABLE_VIRTUAL_METHODS_CROSS_STORE_REFS = "num_mergeable_virtual_method_cross_store_refs"; constexpr const char* METRIC_MERGEABLE_VIRTUAL_METHODS_CROSS_DEX_REFS = "num_mergeable_virtual_method_cross_dex_refs"; constexpr const char* METRIC_MERGEABLE_VIRTUAL_METHODS_INCONCRETE_OVERRIDDEN_METHODS = "num_mergeable_virtual_methods_inconcrete_overridden_methods"; constexpr const char* METRIC_MERGEABLE_PAIRS = "num_mergeable_pairs"; constexpr const char* METRIC_VIRTUAL_SCOPES_WITH_MERGEABLE_PAIRS = "num_virtual_scopes_with_mergeable_pairs"; constexpr const char* METRIC_UNABSTRACTED_METHODS = "num_unabstracted_methods"; constexpr const char* METRIC_UNINLINABLE_METHODS = "num_uninlinable_methods"; constexpr const char* METRIC_HUGE_METHODS = "num_huge_methods"; constexpr const char* METRIC_CALLER_SIZE_REMOVED_METHODS = "num_caller_size_removed_methods"; constexpr const char* METRIC_REMOVED_VIRTUAL_METHODS = "num_removed_virtual_methods"; constexpr const char* METRIC_EXPERIMENT_METHODS = "num_experiment_methods"; constexpr size_t kAppear100Buckets = 10; } // namespace VirtualMerging::VirtualMerging(DexStoresVector& stores, const inliner::InlinerConfig& inliner_config, size_t max_overriding_method_instructions, const api::AndroidSDK* min_sdk_api) : m_scope(build_class_scope(stores)), m_xstores(stores), m_xdexes(stores), m_type_system(m_scope), m_max_overriding_method_instructions(max_overriding_method_instructions), m_inliner_config(inliner_config), m_init_classes_with_side_effects(m_scope, /* create_init_class_insns */ false) { auto concurrent_resolver = [&](DexMethodRef* method, MethodSearch search) { return resolve_method(method, search, m_concurrent_resolved_refs); }; std::unordered_set<DexMethod*> no_default_inlinables; // disable shrinking options, minimizing initialization time m_inliner_config.shrinker = shrinker::ShrinkerConfig(); int min_sdk = 0; m_inliner.reset(new MultiMethodInliner( m_scope, m_init_classes_with_side_effects, stores, no_default_inlinables, concurrent_resolver, m_inliner_config, min_sdk, MultiMethodInlinerMode::None, /* true_virtual_callers */ {}, /* inline_for_speed */ nullptr, /* bool analyze_and_prune_inits */ false, /* const std::unordered_set<DexMethodRef*>& configured_pure_methods */ {}, min_sdk_api)); } VirtualMerging::~VirtualMerging() {} // Part 1: Identify which virtual methods get invoked via invoke-super --- we'll // stay away from those virtual scopes // TODO: Relax this. Some portions of those virtual scopes could still // be handled void VirtualMerging::find_unsupported_virtual_scopes() { ConcurrentSet<const DexMethod*> invoke_super_methods; ConcurrentSet<const DexMethodRef*> invoke_super_unresolved_method_refs; walk::parallel::opcodes( m_scope, [](const DexMethod*) { return true; }, [&](const DexMethod*, IRInstruction* insn) { if (insn->opcode() == OPCODE_INVOKE_SUPER) { auto method_ref = insn->get_method(); auto method = resolve_method(method_ref, MethodSearch::Virtual); if (method == nullptr) { invoke_super_unresolved_method_refs.insert(method_ref); } else { invoke_super_methods.insert(method); } } }); m_stats.invoke_super_methods = invoke_super_methods.size(); m_stats.invoke_super_unresolved_method_refs = invoke_super_unresolved_method_refs.size(); for (auto method : invoke_super_methods) { m_unsupported_virtual_scopes.insert( m_type_system.find_virtual_scope(method)); } for (auto method : invoke_super_unresolved_method_refs) { m_unsupported_named_protos[method->get_name()].insert(method->get_proto()); } } // Part 2: Identify all overriding virtual methods which might potentially be // mergeable into other overridden virtual methods. // Group these methods by virtual scopes. void VirtualMerging::compute_mergeable_scope_methods() { walk::parallel::methods(m_scope, [&](const DexMethod* overriding_method) { if (!overriding_method->is_virtual() || !overriding_method->is_concrete() || is_native(overriding_method) || is_abstract(overriding_method)) { return; } always_assert(overriding_method->is_def()); always_assert(overriding_method->is_concrete()); always_assert(!overriding_method->is_external()); always_assert(overriding_method->get_code()); auto virtual_scope = m_type_system.find_virtual_scope(overriding_method); if (virtual_scope == nullptr) { TRACE(VM, 1, "[VM] virtual method {%s} has no virtual scope!", SHOW(overriding_method)); return; } if (virtual_scope->type == overriding_method->get_class()) { // Actually, this method isn't overriding anything. return; } if (m_unsupported_virtual_scopes.count(virtual_scope)) { TRACE(VM, 5, "[VM] virtual method {%s} in an unsupported virtual scope", SHOW(overriding_method)); return; } auto it = m_unsupported_named_protos.find(overriding_method->get_name()); if (it != m_unsupported_named_protos.end() && it->second.count(overriding_method->get_proto())) { // Never observed in practice, but I guess it might happen TRACE(VM, 1, "[VM] virtual method {%s} has unsupported name/proto", SHOW(overriding_method)); return; } m_mergeable_scope_methods.update( virtual_scope, [&](const VirtualScope*, std::unordered_set<const DexMethod*>& s, bool /* exists */) { s.emplace(overriding_method); }); }); m_stats.mergeable_scope_methods = m_mergeable_scope_methods.size(); for (auto& p : m_mergeable_scope_methods) { m_stats.mergeable_virtual_methods += p.second.size(); } } namespace { struct LocalStats { size_t overriding_methods{0}; size_t cross_store_refs{0}; size_t cross_dex_refs{0}; size_t inconcrete_overridden_methods{0}; }; struct SimpleOrdering { using Map = std::unordered_map<const DexMethodRef*, double>; Map map; SimpleOrdering() = default; explicit SimpleOrdering(const method_profiles::MethodProfiles& profiles) : map(create_call_count_ordering(profiles)) {} double get_order(const DexMethodRef* m) const { auto it = map.find(m); if (it == map.end()) { return 0; } return it->second; } static Map create_call_count_ordering( const method_profiles::MethodProfiles& profiles) { std::unordered_map<const DexMethodRef*, std::pair<double, double>> call_counts; // Fill first part with cold-start. for (auto& p : profiles.method_stats(method_profiles::COLD_START)) { call_counts.emplace(p.first, std::make_pair(p.second.call_count, 0.0)); } // Second part with maximum of other interactions. for (auto& p : profiles.all_interactions()) { for (auto& q : p.second) { auto& cc = call_counts[q.first].second; cc = std::max(cc, q.second.call_count); } } std::vector<const DexMethodRef*> profile_methods; profile_methods.reserve(call_counts.size()); for (auto& p : call_counts) { profile_methods.push_back(p.first); } std::sort(profile_methods.begin(), profile_methods.end(), [&call_counts](const auto* lhs, const auto* rhs) { auto& lhs_p = call_counts.at(lhs); auto& rhs_p = call_counts.at(rhs); if (lhs_p.first != rhs_p.first) { return lhs_p.first < rhs_p.first; } if (lhs_p.second != rhs_p.second) { return lhs_p.second < rhs_p.second; } return compare_dexmethods(lhs, rhs); }); SimpleOrdering::Map ret; for (size_t i = 0; i < profile_methods.size(); ++i) { // +1 to have 0 empty for methods without profile. ret.emplace(profile_methods[i], ((double)i + 1) / (profile_methods.size() + 1)); } return ret; } }; struct SimpleOrderingProvider { mutable std::once_flag flag; const method_profiles::MethodProfiles& profiles; mutable SimpleOrdering ordering; explicit SimpleOrderingProvider( const method_profiles::MethodProfiles& profiles) : profiles(profiles) {} const SimpleOrdering& operator()() const { std::call_once(flag, [&]() { ordering = SimpleOrdering(profiles); }); return ordering; } }; template <typename OrderingProvider> class MergePairsBuilder { using MergablesMap = std::unordered_map<const DexMethod*, const DexMethod*>; public: using PairSeq = std::vector<std::pair<const DexMethod*, const DexMethod*>>; MergePairsBuilder(const VirtualScope* virtual_scope, const OrderingProvider& ordering_provider) : virtual_scope(virtual_scope), m_ordering_provider(ordering_provider) {} boost::optional<std::pair<LocalStats, PairSeq>> build( const std::unordered_set<const DexMethod*>& mergeable_methods, const XStoreRefs& xstores, const XDexRefs& xdexes, const method_profiles::MethodProfiles& profiles, VirtualMerging::Strategy strategy) { if (!init()) { return boost::none; } MergablesMap mergeable_pairs_map = find_overrides(mergeable_methods, xstores, xdexes); if (mergeable_pairs_map.empty()) { always_assert(stats.overriding_methods == stats.cross_store_refs + stats.cross_dex_refs + stats.inconcrete_overridden_methods); return std::make_pair(stats, PairSeq{}); } auto mergeable_pairs = create_merge_pair_sequence(mergeable_pairs_map, profiles, strategy); return std::make_pair(stats, mergeable_pairs); } private: bool init() { for (auto& p : virtual_scope->methods) { auto method = p.first; methods.push_back(method); types_to_methods.emplace(method->get_class(), method); if (!can_rename(method) || root(method) || method->rstate.no_optimizations()) { // If we find any method in this virtual scope which we shouldn't // touch, we exclude the entire virtual scope. return false; } } return true; } MergablesMap find_overrides( const std::unordered_set<const DexMethod*>& mergeable_methods, const XStoreRefs& xstores, const XDexRefs& xdexes) { MergablesMap mergeable_pairs_map; // sorting to make things deterministic std::sort(methods.begin(), methods.end(), dexmethods_comparator()); for (DexMethod* overriding_method : methods) { if (!mergeable_methods.count(overriding_method)) { continue; } stats.overriding_methods++; auto subtype = overriding_method->get_class(); always_assert(subtype != virtual_scope->type); auto overriding_cls = type_class(overriding_method->get_class()); always_assert(overriding_cls != nullptr); auto supertype = overriding_cls->get_super_class(); always_assert(supertype != nullptr); auto run_fn = [](auto fn, DexType* start, DexType* trailing, const DexType* stop) { for (;;) { if (fn(start, trailing)) { return true; } if (start == stop) { return false; } trailing = start; start = type_class(start)->get_super_class(); } }; run_fn( [this](const DexType* t, DexType* trailing) { subtypes[t].push_back(trailing); return false; }, supertype, subtype, virtual_scope->type); bool found_override = run_fn( [this, &overriding_method, &mergeable_pairs_map, &xstores, &xdexes](const DexType* t, const DexType*) { auto it = types_to_methods.find(t); if (it == types_to_methods.end()) { return false; } auto overridden_method = it->second; if (!overridden_method->is_concrete() || is_native(overridden_method)) { stats.inconcrete_overridden_methods++; } else if (xstores.cross_store_ref(overridden_method, overriding_method)) { stats.cross_store_refs++; } else if (xdexes.cross_dex_ref_override(overridden_method, overriding_method) || (xdexes.num_dexes() > 1 && xdexes.is_in_primary_dex(overridden_method))) { stats.cross_dex_refs++; } else { always_assert(overriding_method->get_code()); always_assert(is_abstract(overridden_method) || overridden_method->get_code()); mergeable_pairs_map.emplace(overriding_method, overridden_method); } return true; }, supertype, subtype, virtual_scope->type); always_assert(found_override); } return mergeable_pairs_map; } PairSeq create_merge_pair_sequence( const MergablesMap& mergeable_pairs_map, const method_profiles::MethodProfiles& profiles, VirtualMerging::Strategy strategy) { // we do a depth-first traversal of the subtype structure, adding // mergeable pairs as we find them; this ensures that mergeable pairs // can later be processed sequentially --- first inlining pairs that // appear in deeper portions of the type hierarchy PairSeq mergeable_pairs; std::unordered_set<const DexType*> visited; std::unordered_map<const DexMethod*, std::vector<std::pair<const DexMethod*, double>>> override_map; self_recursive_fn( [&](auto self, const DexType* t) { if (visited.count(t)) { return; } visited.insert(t); auto subtypes_it = subtypes.find(t); if (subtypes_it != subtypes.end()) { // This is ordered because `methods` was ordered. for (auto subtype : subtypes_it->second) { self(self, subtype); } } const DexMethod* t_method; { auto t_method_it = types_to_methods.find(t); if (t_method_it == types_to_methods.end()) { return; } t_method = t_method_it->second; } double order_value = 0; enum OrderMix { kSum, kMax, }; OrderMix order_mix = OrderMix::kSum; switch (strategy) { case VirtualMerging::Strategy::kLexicographical: break; case VirtualMerging::Strategy::kProfileCallCount: if (auto mstats = profiles.get_method_stat( method_profiles::COLD_START, t_method)) { order_value = mstats->call_count; } break; case VirtualMerging::Strategy::kProfileAppearBucketsAndCallCount: { // Using appear100 with buckets, and adding in normalized // call-count. // // To merge interactions, give precedence to cold-start for bucket. // If a method is not executed during cold-start, sort it into the // next lower bucket. auto cold_stats = profiles.get_method_stat(method_profiles::COLD_START, t_method); double appear_part; if (cold_stats) { appear_part = std::floor(cold_stats->appear_percent / kAppear100Buckets) * kAppear100Buckets; } else { double max_appear{0}; for (auto& i : profiles.all_interactions()) { auto it = i.second.find(t_method); if (it != i.second.end()) { max_appear = std::max(max_appear, it->second.appear_percent); } } appear_part = std::max(0.0, std::floor(max_appear / kAppear100Buckets - 1) * kAppear100Buckets); } double call_part = m_ordering_provider().get_order(t_method); order_value = appear_part + call_part; // Summing up does not make much sense here and would overvalue // multiple appear subcalls over single but high-call-count ones. order_mix = OrderMix::kMax; break; } } { // If there are overrides for this type's implementation, order the // overrides by their weight (and otherwise retain the original // order), then insert the overrides into the global merge // structure. auto it = override_map.find(t_method); if (it != override_map.end()) { auto& t_overrides = it->second; redex_assert(!t_overrides.empty()); // Use stable sort to retain order if other ordering is // unavailable. As insertion is pushing to front, sort low to // high. std::stable_sort(t_overrides.begin(), t_overrides.end(), [](const auto& lhs, const auto& rhs) { return lhs.second < rhs.second; }); for (const auto& p : t_overrides) { auto assert_it = mergeable_pairs_map.find(p.first); redex_assert(assert_it != mergeable_pairs_map.end() && assert_it->second == t_method); mergeable_pairs.emplace_back(t_method, p.first); switch (order_mix) { case OrderMix::kSum: order_value += p.second; break; case OrderMix::kMax: order_value = std::max(order_value, p.second); break; } } // Clear the vector. Leave it empty for the assert above // (to ensure things are not handled twice). t_overrides.clear(); t_overrides.shrink_to_fit(); } } auto overridden_method_it = mergeable_pairs_map.find(t_method); if (overridden_method_it == mergeable_pairs_map.end()) { return; } override_map[overridden_method_it->second].emplace_back(t_method, order_value); }, virtual_scope->type); for (const auto& p : override_map) { redex_assert(p.second.empty()); } always_assert(mergeable_pairs_map.size() == mergeable_pairs.size()); always_assert(stats.overriding_methods == mergeable_pairs.size() + stats.cross_store_refs + stats.cross_dex_refs + stats.inconcrete_overridden_methods); return mergeable_pairs; } const VirtualScope* virtual_scope; const OrderingProvider& m_ordering_provider; std::vector<DexMethod*> methods; std::unordered_map<const DexType*, DexMethod*> types_to_methods; std::unordered_map<const DexType*, std::vector<DexType*>> subtypes; LocalStats stats; }; } // namespace // Part 3: For each virtual scope, identify all pairs of methods where // one can be merged with another. The list of pairs is ordered in // way that it can be later processed sequentially. VirtualMerging::MergablePairsByVirtualScope VirtualMerging::compute_mergeable_pairs_by_virtual_scopes( const method_profiles::MethodProfiles& profiles, Strategy strategy, VirtualMergingStats& stats) const { ConcurrentMap<const VirtualScope*, LocalStats> local_stats; std::vector<const VirtualScope*> virtual_scopes; for (auto& p : m_mergeable_scope_methods) { virtual_scopes.push_back(p.first); } ConcurrentMap<const VirtualScope*, std::vector<std::pair<const DexMethod*, const DexMethod*>>> mergeable_pairs_by_virtual_scopes; SimpleOrderingProvider ordering_provider{profiles}; walk::parallel::virtual_scopes( virtual_scopes, [&](const VirtualScope* virtual_scope) { MergePairsBuilder mpb(virtual_scope, ordering_provider); auto res = mpb.build(m_mergeable_scope_methods.at(virtual_scope), m_xstores, m_xdexes, profiles, strategy); if (!res) { return; } local_stats.emplace(virtual_scope, res->first); if (!res->second.empty()) { mergeable_pairs_by_virtual_scopes.emplace(virtual_scope, std::move(res->second)); } }); stats.virtual_scopes_with_mergeable_pairs += mergeable_pairs_by_virtual_scopes.size(); size_t overriding_methods = 0; for (auto& p : local_stats) { overriding_methods += p.second.overriding_methods; stats.cross_store_refs += p.second.cross_store_refs; stats.cross_dex_refs += p.second.cross_dex_refs; stats.inconcrete_overridden_methods += p.second.inconcrete_overridden_methods; } always_assert(overriding_methods <= stats.mergeable_virtual_methods); stats.annotated_methods = stats.mergeable_virtual_methods - overriding_methods; MergablePairsByVirtualScope out; for (auto& p : mergeable_pairs_by_virtual_scopes) { const auto& mergeable_pairs = p.second; stats.mergeable_pairs += mergeable_pairs.size(); out.insert(p); } always_assert(mergeable_pairs_by_virtual_scopes.size() == out.size()); always_assert(stats.mergeable_pairs == stats.mergeable_virtual_methods - stats.annotated_methods - stats.cross_store_refs - stats.cross_dex_refs - stats.inconcrete_overridden_methods); return out; } namespace { using MethodData = std::pair< const DexMethod*, std::vector<std::pair<const VirtualScope*, std::vector<const DexMethod*>>>>; std::pair<std::vector<MethodData>, VirtualMergingStats> create_ordering( const VirtualMerging::MergablePairsByVirtualScope& mergable_pairs, size_t max_overriding_method_instructions, MultiMethodInliner& inliner) { std::vector<MethodData> ordering; VirtualMergingStats stats; // Fill the ordering. { std::unordered_map<const DexMethod*, size_t> method_idx; for (auto& p : mergable_pairs) { auto virtual_scope = p.first; const auto& mergeable_pairs = p.second; for (auto& q : mergeable_pairs) { auto overridden_method = q.first; auto overriding_method = q.second; MethodData* method_data; { auto it = method_idx.find(overridden_method); if (it == method_idx.end()) { ordering.emplace_back( overridden_method, std::vector<std::pair<const VirtualScope*, std::vector<const DexMethod*>>>{}); method_idx.emplace(overridden_method, ordering.size() - 1); method_data = &ordering.back(); } else { method_data = &ordering.at(it->second); } } if (method_data->second.empty() || method_data->second.back().first != virtual_scope) { method_data->second.emplace_back(virtual_scope, std::vector<const DexMethod*>{}); } std::vector<const DexMethod*>& v_data = method_data->second.back().second; v_data.push_back(overriding_method); } } for (const auto& p : ordering) { std::unordered_set<const VirtualScope*> scopes_seen; for (const auto& q : p.second) { redex_assert(scopes_seen.count(q.first) == 0); scopes_seen.insert(q.first); } } } // Sort out large methods already. for (auto& p : ordering) { auto overridden_method = const_cast<DexMethod*>(p.first); for (auto& q : p.second) { q.second.erase( std::remove_if( q.second.begin(), q.second.end(), [&](const auto* m) { size_t estimated_callee_size = m->get_code()->sum_opcode_sizes(); if (estimated_callee_size > max_overriding_method_instructions) { TRACE(VM, 5, "[VM] %s is too large to be merged into %s", SHOW(m), SHOW(overridden_method)); stats.huge_methods++; return true; } size_t estimated_caller_size = is_abstract(overridden_method) ? 64 // we'll need some extra instruction; 64 // is conservative : overridden_method->get_code()->sum_opcode_sizes(); if (!inliner.is_inlinable( overridden_method, m, nullptr /* invoke_virtual_insn */, estimated_caller_size, estimated_callee_size)) { TRACE(VM, 3, "[VM] Cannot inline %s into %s", SHOW(m), SHOW(overridden_method)); stats.uninlinable_methods++; return true; } return false; }), q.second.end()); } // Check whether it is likely that we'll be able to inline everything. { size_t sum = is_abstract(overridden_method) ? 64 // we'll need some extra instruction; 64 // is conservative : overridden_method->get_code()->sum_opcode_sizes(); auto method_inline_estimate = [](const DexMethod* m) { return 20 // if + invoke + return ~= 20. + m->get_code()->sum_opcode_sizes(); }; size_t num_methods = 0; for (auto& q : p.second) { num_methods += q.second.size(); for (auto* m : q.second) { sum += method_inline_estimate(m); } } // The inliner uses a limit of 1<<15 - 1<<12. Let's use 1<<14, which // is hopefully conservative. constexpr size_t kLimit = (1u << 15) - (1u << 13); if (kLimit < sum) { TRACE(VM, 3, "[VM] Estimated sum of inlines too large for %s: %zu", SHOW(overridden_method), sum); // To be consistent with other orderings, we need to be // any-order-deterministic when removing candidates. It would probably // be good to do this well, e.g., work towards being able to remove // the most methods. But let's be simple for now. std::unordered_map<const VirtualScope*, std::vector<const DexMethod*>*> data_map; data_map.reserve(p.second.size()); std::vector<const VirtualScope*> scopes; scopes.reserve(p.second.size()); for (auto& q : p.second) { scopes.push_back(q.first); data_map.emplace(q.first, &q.second); } // Sort scopes by root methods. This is somewhat arbitrary but stable. std::sort(scopes.begin(), scopes.end(), [](const auto* lhs, const auto* rhs) { if (lhs == rhs) { return false; } return compare_dexmethods(lhs->methods.front().first, rhs->methods.front().first); }); size_t removals = 0; for (const auto* scope : scopes) { auto m_tmp = *data_map.at(scope); // Sort methods lexicographically. Arbitrary but stable. Could include // size. std::sort(m_tmp.begin(), m_tmp.end(), compare_dexmethods); // Fetch methods to get under limit. std::unordered_set<const DexMethod*> to_remove; for (auto* m : m_tmp) { sum -= method_inline_estimate(m); to_remove.insert(m); if (sum <= kLimit) { break; } } // Remove those methods. auto* m_orig = data_map.at(scope); m_orig->erase(std::remove_if(m_orig->begin(), m_orig->end(), [&to_remove](const auto* m) { return to_remove.count(m) != 0; }), m_orig->end()); removals += to_remove.size(); if (sum <= kLimit) { break; } } TRACE(VM, 3, "[VM] Removed %zu of %zu methods to reduce estimate for %s", removals, num_methods, SHOW(overridden_method)); stats.caller_size_removed_methods += removals; } } } // Remove methods that no longer have inlinees. ordering.erase(std::remove_if(ordering.begin(), ordering.end(), [](const auto& p) { size_t sum = 0; for (const auto& q : p.second) { sum += q.second.size(); } return sum == 0; }), ordering.end()); return std::make_pair(std::move(ordering), stats); } template <typename T> std::unordered_set<typename T::key_type> get_keys(const T& c) { std::unordered_set<typename T::key_type> c_keys; std::transform(c.begin(), c.end(), std::inserter(c_keys, c_keys.end()), [](const auto& p) { return p.first; }); return c_keys; } template <typename T> void check_keys(const T& c1, const T& c2) { redex_assert(c1.size() == c2.size()); auto c1_keys = get_keys(c1); auto c2_keys = get_keys(c2); std20::erase_if(c1_keys, [&c2_keys](const auto& k) { return c2_keys.count(k) != 0; }); redex_assert(c1_keys.empty()); }; void check_remove( const std::unordered_map<DexClass*, std::vector<const DexMethod*>>& a, const std::unordered_map<DexClass*, std::vector<const DexMethod*>>& b, const std::unordered_map<const DexMethod*, DexMethod*>& clones) { check_keys(a, b); for (const auto& l : a) { std::unordered_set<const DexMethod*> as_clones; std::transform(l.second.begin(), l.second.end(), std::inserter(as_clones, as_clones.end()), [&clones](const auto* m) { return clones.at(m); }); const auto& r = b.at(l.first); std::unordered_set<const DexMethod*> as_exp(r.begin(), r.end()); redex_assert(as_clones == as_exp); } } void check_remap( const std::unordered_map<DexMethod*, DexMethod*>& a, const std::unordered_map<DexMethod*, DexMethod*>& b, const std::unordered_map<const DexMethod*, DexMethod*>& clones) { auto remap_keys = get_keys(a); { auto exp_remap_keys = get_keys(b); redex_assert(remap_keys.size() == exp_remap_keys.size()); for (auto* m : remap_keys) { redex_assert(exp_remap_keys.count(clones.at(m)) != 0); } } } void reset_sb(SourceBlock& sb, DexMethod* ref, uint32_t id) { sb.src = ref->get_deobfuscated_name_or_null(); sb.id = id; for (size_t i = 0; i < sb.vals_size; i++) { sb.vals[i] = SourceBlock::Val{0, 0}; } } struct SBHelper { DexMethod* overridden; const std::vector<const DexMethod*>& v; const bool overridden_had_source_blocks; const bool create_source_blocks; explicit SBHelper(DexMethod* overridden, const std::vector<const DexMethod*>& v) : overridden(overridden), v(v), overridden_had_source_blocks( overridden->get_code() != nullptr && method_get_first_source_block(overridden) != nullptr), create_source_blocks([&]() { for (auto* m : v) { if (method_get_first_source_block(m) != nullptr) { return true; } } return false; }()) { // Fix up the host with empty source blocks if necessary. It's easier to // do this ahead of time. if (create_source_blocks && !overridden_had_source_blocks && overridden->get_code() != nullptr) { normalize_overridden_method_withouts_sbs(overridden, get_arbitrary_first_sb()); } } static SourceBlock* method_get_first_source_block(const DexMethod* m) { auto code = m->get_code(); if (code->cfg_built()) { return source_blocks::get_first_source_block(code->cfg().entry_block()); } else { for (auto& mie : *code) { if (mie.type == MFLOW_SOURCE_BLOCK) { return mie.src_block.get(); } }; } return nullptr; } SourceBlock* get_arbitrary_first_sb() { for (auto* m : v) { auto* sb = method_get_first_source_block(m); if (sb != nullptr) { return sb; } } not_reached(); } static void normalize_overridden_method_withouts_sbs( DexMethod* overridden_method, SourceBlock* arbitrary_sb) { auto* code = overridden_method->get_code(); cfg::ScopedCFG cfg(code); for (auto* block : cfg->blocks()) { if (block == cfg->entry_block()) { // Special handling. continue; } auto new_sb = std::make_unique<SourceBlock>(*arbitrary_sb); // Bit weird, but better than making up real numbers. reset_sb(*new_sb, overridden_method, SourceBlock::kSyntheticId); auto it = block->get_first_insn(); if (it == block->end()) { block->insert_before(block->begin(), std::move(new_sb)); } else { if (opcode::is_a_move_result(it->insn->opcode())) { block->insert_after(it, std::move(new_sb)); } else { block->insert_before(it, std::move(new_sb)); } } } auto* block = cfg->entry_block(); auto new_sb = std::make_unique<SourceBlock>(*arbitrary_sb); // Bit weird, but better than making up real numbers. reset_sb(*new_sb, overridden_method, SourceBlock::kSyntheticId); auto it = block->get_first_non_param_loading_insn(); if (it == block->end()) { block->insert_before(it, std::move(new_sb)); } else { block->insert_after(it, std::move(new_sb)); } } std::unique_ptr<SourceBlock> gen_arbitrary_reset_sb() { auto src_block = std::make_unique<SourceBlock>(*get_arbitrary_first_sb()); reset_sb(*src_block, overridden, SourceBlock::kSyntheticId); return src_block; } struct ScopedSplitHelper { cfg::Block* block{nullptr}; SourceBlock* first_sb{nullptr}; DexMethod* overriding{nullptr}; SBHelper* parent{nullptr}; ScopedSplitHelper(cfg::Block* block, IRList::iterator last_it, DexMethod* overriding, SBHelper* parent) : block(block), first_sb([&]() -> SourceBlock* { for (auto it = std::next(last_it); it != block->end(); ++it) { if (it->type == MFLOW_SOURCE_BLOCK) { return it->src_block.get(); } } return nullptr; }()), overriding(overriding), parent(parent) {} ~ScopedSplitHelper() { if (block != nullptr) { auto overriding_sb = method_get_first_source_block(overriding); auto new_sb = std::make_unique<SourceBlock>( overriding_sb != nullptr ? *overriding_sb : first_sb != nullptr ? *first_sb : *parent->get_arbitrary_first_sb()); new_sb->src = parent->overridden->get_deobfuscated_name_or_null(); new_sb->id = SourceBlock::kSyntheticId; if (overriding_sb != nullptr && first_sb != nullptr) { for (size_t i = 0; i != new_sb->vals_size; ++i) { if (!new_sb->get_val(i)) { new_sb->vals[i] = first_sb->vals[i]; } else if (first_sb->get_val(i)) { new_sb->vals[i]->val += first_sb->vals[i]->val; new_sb->vals[i]->appear100 = std::max(new_sb->vals[i]->appear100, first_sb->vals[i]->val); } } } block->insert_before(block->end(), std::move(new_sb)); } } ScopedSplitHelper(const ScopedSplitHelper&) = delete; ScopedSplitHelper(ScopedSplitHelper&& rhs) noexcept : block(rhs.block), first_sb(rhs.first_sb), overriding(rhs.overriding), parent(rhs.parent) { rhs.block = nullptr; } ScopedSplitHelper& operator=(const ScopedSplitHelper&) = delete; ScopedSplitHelper& operator=(ScopedSplitHelper&& rhs) noexcept { block = rhs.block; first_sb = rhs.first_sb; overriding = rhs.overriding; parent = rhs.parent; rhs.block = nullptr; return *this; } }; std::optional<ScopedSplitHelper> handle_split(cfg::Block* block, const IRList::iterator& it, DexMethod* overriding) { if (!create_source_blocks) { return std::nullopt; } return ScopedSplitHelper(block, it, overriding, this); } void add_return_sb( DexMethod* overriding, const std::function<void(std::unique_ptr<SourceBlock>)>& push_sb) { if (create_source_blocks) { // Let's assume there's always normal return. auto o_sb = source_blocks::get_first_source_block(overriding->get_code()); if (o_sb != nullptr) { auto new_sb = std::make_unique<SourceBlock>(*o_sb); new_sb->src = overriding->get_deobfuscated_name_or_null(); new_sb->id = SourceBlock::kSyntheticId; push_sb(std::move(new_sb)); } } } }; template <typename MethodFn> VirtualMergingStats apply_ordering( MultiMethodInliner& inliner, std::vector<MethodData>& ordering, const MethodFn& method_fn, std::unordered_map<DexClass*, std::vector<const DexMethod*>>& virtual_methods_to_remove, std::unordered_map<DexMethod*, DexMethod*>& virtual_methods_to_remap, VirtualMerging::InsertionStrategy insertion_strategy) { VirtualMergingStats stats; for (auto& p : ordering) { auto overridden_method = const_cast<DexMethod*>(p.first); for (auto& q : p.second) { if (q.second.empty()) { continue; } overridden_method = method_fn(overridden_method); SBHelper sb_helper(overridden_method, q.second); auto* virtual_scope = q.first; for (auto* overriding_method_const : q.second) { auto overriding_method = const_cast<DexMethod*>(overriding_method_const); overriding_method = method_fn(overriding_method); size_t estimated_callee_size = overriding_method->get_code()->sum_opcode_sizes(); size_t estimated_insn_size = is_abstract(overridden_method) ? 64 // we'll need some extra instruction; 64 is conservative : overridden_method->get_code()->sum_opcode_sizes(); bool is_inlineable = inliner.is_inlinable(overridden_method, overriding_method, nullptr /* invoke_virtual_insn */, estimated_insn_size, estimated_callee_size); always_assert_log(is_inlineable, "[VM] Cannot inline %s into %s", SHOW(overriding_method), SHOW(overridden_method)); TRACE(VM, 4, "[VM] Merging %s into %s", SHOW(overriding_method), SHOW(overridden_method)); auto proto = overriding_method->get_proto(); always_assert(overridden_method->get_proto() == proto); std::vector<uint32_t> param_regs; std::function<void(IRInstruction*)> push_insn; std::function<void(std::unique_ptr<SourceBlock>)> push_sb; std::function<uint32_t()> allocate_temp; std::function<uint32_t()> allocate_wide_temp; std::function<void()> cleanup; IRCode* overridden_code; // We make the method public to avoid visibility issues. We could be // more conservative (i.e. taking the strongest visibility control // that encompasses the original pair) but I'm not sure it's worth the // effort. set_public(overridden_method); if (is_abstract(overridden_method)) { // We'll make the abstract method be not abstract, and give it a new // method body. // It starts out with just load-param instructions as needed, and // then we'll add an invoke-virtual instruction that will get // inlined. stats.unabstracted_methods++; overridden_method->make_concrete( (DexAccessFlags)(overridden_method->get_access() & ~ACC_ABSTRACT), std::make_unique<IRCode>(), true /* is_virtual */); overridden_code = overridden_method->get_code(); auto load_param_insn = new IRInstruction(IOPCODE_LOAD_PARAM_OBJECT); load_param_insn->set_dest(overridden_code->allocate_temp()); overridden_code->push_back(load_param_insn); param_regs.push_back(load_param_insn->dest()); for (auto t : *proto->get_args()) { if (type::is_wide_type(t)) { load_param_insn = new IRInstruction(IOPCODE_LOAD_PARAM_WIDE); load_param_insn->set_dest(overridden_code->allocate_wide_temp()); } else { load_param_insn = new IRInstruction( type::is_object(t) ? IOPCODE_LOAD_PARAM_OBJECT : IOPCODE_LOAD_PARAM); load_param_insn->set_dest(overridden_code->allocate_temp()); } overridden_code->push_back(load_param_insn); param_regs.push_back(load_param_insn->dest()); } if (sb_helper.create_source_blocks) { overridden_code->push_back(sb_helper.gen_arbitrary_reset_sb()); } // we'll define helper functions in a way that lets them mutate the // new IRCode push_insn = [=](IRInstruction* insn) { overridden_code->push_back(insn); }; push_sb = [=](std::unique_ptr<SourceBlock> sb) { overridden_code->push_back(std::move(sb)); }; allocate_temp = [=]() { return overridden_code->allocate_temp(); }; allocate_wide_temp = [=]() { return overridden_code->allocate_wide_temp(); }; cleanup = [=]() { overridden_code->build_cfg(/* editable */ true); }; } else { // We are dealing with a non-abstract method. In this case, we'll // first insert an if-instruction to decide whether to run the // overriding method that we'll inline, or whether to jump to the // old method body. overridden_code = overridden_method->get_code(); always_assert(overridden_code); overridden_code->build_cfg(/* editable */ true); auto& overridden_cfg = overridden_code->cfg(); // Find block with load-param instructions cfg::Block* block = overridden_cfg.entry_block(); while (block->get_first_insn() == block->end()) { const auto& succs = block->succs(); always_assert(succs.size() == 1); const auto& out = succs[0]; always_assert(out->type() == cfg::EDGE_GOTO); block = out->target(); } // Scan load-param instructions std::unordered_set<uint32_t> param_regs_set; auto last_it = block->end(); for (auto it = block->begin(); it != block->end(); it++) { auto& mie = *it; if (!opcode::is_a_load_param(mie.insn->opcode())) { break; } param_regs.push_back(mie.insn->dest()); param_regs_set.insert(mie.insn->dest()); last_it = it; } always_assert(param_regs.size() == param_regs_set.size()); always_assert(1 + proto->get_args()->size() == param_regs_set.size()); always_assert(last_it != block->end()); // We'll split the block right after the last load-param instruction // --- that's where we'll insert the new if-statement. { auto sb_scoped = sb_helper.handle_split(block, last_it, overriding_method); overridden_cfg.split_block(block, last_it); } auto new_block = overridden_cfg.create_block(); { // instance-of param0, DeclaringTypeOfOverridingMethod auto instance_of_insn = new IRInstruction(OPCODE_INSTANCE_OF); instance_of_insn->set_type(overriding_method->get_class()); instance_of_insn->set_src(0, param_regs.at(0)); block->push_back(instance_of_insn); // move-result-pseudo if_temp auto if_temp_reg = overridden_cfg.allocate_temp(); auto move_result_pseudo_insn = new IRInstruction(IOPCODE_MOVE_RESULT_PSEUDO); move_result_pseudo_insn->set_dest(if_temp_reg); block->push_back(move_result_pseudo_insn); switch (insertion_strategy) { case VirtualMerging::InsertionStrategy::kJumpTo: { // if-nez if_temp, new_code // (fall through to old code) auto if_insn = new IRInstruction(OPCODE_IF_NEZ); if_insn->set_src(0, if_temp_reg); overridden_cfg.create_branch( block, if_insn, /*fls=*/block->goes_to(), /*tru=*/new_block); break; } case VirtualMerging::InsertionStrategy::kFallthrough: { // if-eqz if_temp, old code // (fall through to new_code) auto if_insn = new IRInstruction(OPCODE_IF_EQZ); if_insn->set_src(0, if_temp_reg); overridden_cfg.create_branch(block, if_insn, /*fls=*/new_block, /*tru=*/block->goes_to()); break; } } } // we'll define helper functions in a way that lets them mutate the // cfg push_insn = [=](IRInstruction* insn) { new_block->push_back(insn); }; auto* cfg_ptr = &overridden_cfg; push_sb = [=](std::unique_ptr<SourceBlock> sb) { new_block->insert_before(new_block->end(), std::move(sb)); }; allocate_temp = [=]() { return cfg_ptr->allocate_temp(); }; allocate_wide_temp = [=]() { return cfg_ptr->allocate_wide_temp(); }; cleanup = []() {}; } always_assert(1 + proto->get_args()->size() == param_regs.size()); // invoke-virtual temp, param1, ..., paramN, OverridingMethod auto invoke_virtual_insn = new IRInstruction(OPCODE_INVOKE_VIRTUAL); invoke_virtual_insn->set_method(overriding_method); invoke_virtual_insn->set_srcs_size(param_regs.size()); for (size_t i = 0; i < param_regs.size(); i++) { uint32_t reg = param_regs[i]; if (i == 0) { uint32_t temp_reg = allocate_temp(); auto check_cast_insn = new IRInstruction(OPCODE_CHECK_CAST); check_cast_insn->set_type(overriding_method->get_class()); check_cast_insn->set_src(0, reg); push_insn(check_cast_insn); auto move_result_pseudo_insn = new IRInstruction(IOPCODE_MOVE_RESULT_PSEUDO_OBJECT); move_result_pseudo_insn->set_dest(temp_reg); push_insn(move_result_pseudo_insn); reg = temp_reg; } invoke_virtual_insn->set_src(i, reg); } push_insn(invoke_virtual_insn); if (proto->is_void()) { // return-void sb_helper.add_return_sb(overriding_method, push_sb); auto return_insn = new IRInstruction(OPCODE_RETURN_VOID); push_insn(return_insn); } else { // move-result result_temp auto rtype = proto->get_rtype(); auto op = opcode::move_result_for_invoke(overriding_method); auto move_result_insn = new IRInstruction(op); auto result_temp = op == OPCODE_MOVE_RESULT_WIDE ? allocate_wide_temp() : allocate_temp(); move_result_insn->set_dest(result_temp); push_insn(move_result_insn); sb_helper.add_return_sb(overriding_method, push_sb); // return result_temp op = opcode::return_opcode(rtype); auto return_insn = new IRInstruction(op); return_insn->set_src(0, result_temp); push_insn(return_insn); } cleanup(); overriding_method->get_code()->build_cfg(/* editable */ true); inliner::inline_with_cfg( overridden_method, overriding_method, invoke_virtual_insn, /* needs_receiver_cast */ nullptr, /* needs_init_class */ nullptr, overridden_method->get_code()->cfg().get_registers_size()); inliner.visibility_changes_apply_and_record_make_static( get_visibility_changes(overriding_method, overridden_method->get_class())); overriding_method->get_code()->clear_cfg(); // Check if everything was inlined. for (const auto& mie : cfg::InstructionIterable(overridden_code->cfg())) { redex_assert(invoke_virtual_insn != mie.insn); } overridden_code->clear_cfg(); virtual_methods_to_remove[type_class(overriding_method->get_class())] .push_back(overriding_method); auto virtual_scope_root = virtual_scope->methods.front(); always_assert(overriding_method != virtual_scope_root.first); virtual_methods_to_remap.emplace(overriding_method, virtual_scope_root.first); stats.removed_virtual_methods++; } } } return stats; } } // namespace // Part 4: For each virtual scope, merge all pairs in order, unless inlining // is for some reason not possible, e.g. because of code size // constraints. Record set of methods in each class which can be // removed. void VirtualMerging::merge_methods( const MergablePairsByVirtualScope& mergable_pairs, const MergablePairsByVirtualScope& exp_mergable_pairs, ab_test::ABExperimentContext* ab_experiment_context, InsertionStrategy insertion_strategy, InsertionStrategy ab_insertion_strategy) { auto ordering_pair = create_ordering( mergable_pairs, m_max_overriding_method_instructions, *m_inliner); m_stats += ordering_pair.second; const bool is_experiment = !exp_mergable_pairs.empty(); std::unordered_map<const DexMethod*, DexMethod*> clones; auto make_clone = [&clones, is_experiment](DexMethod* m) { if (!is_experiment) { return m; } if (clones.count(m) == 0) { TRACE(VM, 5, "[VM] Cloning %s", show_deobfuscated(m).c_str()); clones.emplace(m, DexMethod::make_full_method_from( m, m->get_class(), DexString::make_string( m->str() + "$VirtualMergingTemporaryClone"))); } return m; }; auto stats = apply_ordering(*m_inliner, ordering_pair.first, make_clone, m_virtual_methods_to_remove, m_virtual_methods_to_remap, insertion_strategy); m_stats += stats; always_assert(m_stats.mergeable_pairs == m_stats.huge_methods + m_stats.uninlinable_methods + m_stats.caller_size_removed_methods + m_stats.removed_virtual_methods); if (is_experiment) { TRACE(VM, 3, "[VM] Applying experiment."); // Gotta remap everything. auto exp_mergable_pairs_remapped = exp_mergable_pairs; for (auto& p : exp_mergable_pairs_remapped) { for (auto& q : p.second) { auto check_clone = [&clones](const DexMethod* m) -> const DexMethod* { // Some methods will be filtered out, so not everything is a clone. auto it = clones.find(m); if (it == clones.end()) { return m; } return it->second; }; q.first = check_clone(q.first); q.second = check_clone(q.second); } } auto exp_ordering_pair = create_ordering(exp_mergable_pairs_remapped, m_max_overriding_method_instructions, *m_inliner); // Minimal integrity check. redex_assert(ordering_pair.second == exp_ordering_pair.second); std::unordered_map<const DexMethod*, const DexMethod*> clones_rev; for (const auto& p : clones) { auto it = clones_rev.emplace(p.second, p.first); redex_assert(it.second); } // TODO: Check the orderings. std::unordered_map<DexClass*, std::vector<const DexMethod*>> exp_virtual_methods_to_remove; std::unordered_map<DexMethod*, DexMethod*> exp_virtual_methods_to_remap; auto exp_stats = apply_ordering( *m_inliner, exp_ordering_pair.first, [&clones_rev](auto* m) { always_assert_log(clones_rev.count(m) != 0, "%s not a clone!", SHOW(m)); return m; }, exp_virtual_methods_to_remove, exp_virtual_methods_to_remap, ab_insertion_strategy); redex_assert(stats == exp_stats); check_remove(m_virtual_methods_to_remove, exp_virtual_methods_to_remove, clones); check_remap(m_virtual_methods_to_remap, exp_virtual_methods_to_remap, clones); // Go and process things with an experiment now. std::unordered_set<const DexMethod*> all_methods; std::transform(ordering_pair.first.begin(), ordering_pair.first.end(), std::inserter(all_methods, all_methods.end()), [](const auto& p) { return p.first; }); auto remap_keys = get_keys(m_virtual_methods_to_remap); std20::erase_if(all_methods, [&remap_keys](const auto* m) { return remap_keys.count(const_cast<DexMethod*>(m)) != 0; }); TRACE(VM, 3, "[VM] Registering %zu methods for experiments", all_methods.size()); m_stats.experiment_methods = all_methods.size(); redex_assert(ab_experiment_context != nullptr); for (auto* m_const : all_methods) { auto* m = const_cast<DexMethod*>(m_const); redex_assert(!m->get_code()->cfg_built()); m->get_code()->build_cfg(); ab_experiment_context->try_register_method(m); m->get_code()->clear_cfg(); m->set_code(clones.at(m)->release_code()); m->get_code()->build_cfg(); } ab_experiment_context->flush(); for (auto* m_const : all_methods) { const_cast<DexMethod*>(m_const)->get_code()->clear_cfg(); } } } // Part 5: Remove methods within classes. void VirtualMerging::remove_methods() { std::vector<DexClass*> classes_with_virtual_methods_to_remove; for (auto& p : m_virtual_methods_to_remove) { classes_with_virtual_methods_to_remove.push_back(p.first); } walk::parallel::classes( classes_with_virtual_methods_to_remove, [&](DexClass* cls) { for (auto method : m_virtual_methods_to_remove.at(cls)) { cls->remove_method(method); } }); } // Part 6: Remap all invoke-virtual instructions where the associated method got // removed void VirtualMerging::remap_invoke_virtuals() { walk::parallel::opcodes( m_scope, [](const DexMethod*) { return true; }, [&](const DexMethod*, IRInstruction* insn) { if (insn->opcode() == OPCODE_INVOKE_VIRTUAL) { auto method_ref = insn->get_method(); auto method = resolve_method(method_ref, MethodSearch::Virtual); auto it = m_virtual_methods_to_remap.find(method); if (it != m_virtual_methods_to_remap.end()) { insn->set_method(it->second); } } }); } void VirtualMerging::run(const method_profiles::MethodProfiles& profiles, Strategy strategy, InsertionStrategy insertion_strategy, Strategy ab_strategy, InsertionStrategy ab_insertion_strategy, ab_test::ABExperimentContext* ab_experiment_context) { TRACE(VM, 1, "[VM] Finding unsupported virtual scopes"); find_unsupported_virtual_scopes(); TRACE(VM, 1, "[VM] Computing mergeable scope methods"); compute_mergeable_scope_methods(); TRACE(VM, 1, "[VM] Computing mergeable pairs by virtual scopes"); auto stats_copy = m_stats; auto scopes = compute_mergeable_pairs_by_virtual_scopes(profiles, strategy, m_stats); MergablePairsByVirtualScope exp_scopes; if (ab_experiment_context != nullptr && !ab_experiment_context->use_control()) { exp_scopes = compute_mergeable_pairs_by_virtual_scopes( profiles, ab_strategy, stats_copy); redex_assert(m_stats == stats_copy); } TRACE(VM, 1, "[VM] Merging methods"); merge_methods(scopes, exp_scopes, ab_experiment_context, insertion_strategy, ab_insertion_strategy); TRACE(VM, 1, "[VM] Removing methods"); remove_methods(); TRACE(VM, 1, "[VM] Remapping invoke-virtual instructions"); remap_invoke_virtuals(); TRACE(VM, 1, "[VM] Done"); } void VirtualMergingPass::bind_config() { // Merging huge overriding methods into an overridden method tends to not // be a good idea, as it may pull in many other dependencies, and all just // for some small saving in number of method refs. So we impose a configurable // limit. int64_t default_max_overriding_method_instructions = 1000; bind("max_overriding_method_instructions", default_max_overriding_method_instructions, m_max_overriding_method_instructions); std::string strategy; bind("strategy", "call-count", strategy); std::string ab_strategy; bind("ab_strategy", "lexicographical", ab_strategy); std::string insertion_strategy; bind("insertion_strategy", "jump-to", insertion_strategy); std::string ab_insertion_strategy; bind("ab_insertion_strategy", "jump-to", ab_insertion_strategy); after_configuration([this, strategy, ab_strategy, insertion_strategy, ab_insertion_strategy] { always_assert(m_max_overriding_method_instructions >= 0); auto parse_strategy = [](const std::string& s) { if (s == "call-count") { return VirtualMerging::Strategy::kProfileCallCount; } if (s == "lexicographical") { return VirtualMerging::Strategy::kLexicographical; } if (s == "appear-buckets") { return VirtualMerging::Strategy::kProfileAppearBucketsAndCallCount; } always_assert_log(false, "Unknown strategy %s", s.c_str()); }; m_strategy = parse_strategy(strategy); m_ab_strategy = parse_strategy(ab_strategy); auto parse_insertion_strategy = [](const std::string& s) { if (s == "jump-to") { return VirtualMerging::InsertionStrategy::kJumpTo; } if (s == "fallthrough") { return VirtualMerging::InsertionStrategy::kFallthrough; } always_assert_log(false, "Unknown insertion strategy %s", s.c_str()); }; m_insertion_strategy = parse_insertion_strategy(insertion_strategy); m_ab_insertion_strategy = parse_insertion_strategy(ab_insertion_strategy); }); } void VirtualMergingPass::run_pass(DexStoresVector& stores, ConfigFiles& conf, PassManager& mgr) { if (mgr.get_redex_options().instrument_pass_enabled) { TRACE(VM, 1, "Skipping VirtualMergingPass because Instrumentation is enabled"); return; } auto dedupped = dedup_vmethods::dedup(stores); const api::AndroidSDK* min_sdk_api{nullptr}; int32_t min_sdk = mgr.get_redex_options().min_sdk; mgr.incr_metric("min_sdk", min_sdk); TRACE(INLINE, 2, "min_sdk: %d", min_sdk); auto min_sdk_api_file = conf.get_android_sdk_api_file(min_sdk); if (!min_sdk_api_file) { mgr.incr_metric("min_sdk_no_file", 1); TRACE(INLINE, 2, "Android SDK API %d file cannot be found.", min_sdk); } else { min_sdk_api = &conf.get_android_sdk_api(min_sdk); } auto inliner_config = conf.get_inliner_config(); // We don't need to worry about inlining synchronized code, as we always // inline at the top-level outside of other try-catch regions. inliner_config.respect_sketchy_methods = false; auto ab_experiment_context = ab_test::ABExperimentContext::create("virtual_merging"); VirtualMerging vm(stores, inliner_config, m_max_overriding_method_instructions, min_sdk_api); vm.run(conf.get_method_profiles(), m_strategy, m_insertion_strategy, m_ab_strategy, m_ab_insertion_strategy, ab_experiment_context.get()); auto stats = vm.get_stats(); mgr.incr_metric(METRIC_DEDUPPED_VIRTUAL_METHODS, dedupped); mgr.incr_metric(METRIC_INVOKE_SUPER_METHODS, stats.invoke_super_methods); mgr.incr_metric(METRIC_INVOKE_SUPER_UNRESOLVED_METHOD_REFS, stats.invoke_super_unresolved_method_refs); mgr.incr_metric(METRIC_MERGEABLE_VIRTUAL_METHODS, stats.mergeable_virtual_methods); mgr.incr_metric(METRIC_MERGEABLE_VIRTUAL_METHODS_ANNOTATED_METHODS, stats.annotated_methods); mgr.incr_metric(METRIC_MERGEABLE_VIRTUAL_METHODS_CROSS_STORE_REFS, stats.cross_store_refs); mgr.incr_metric(METRIC_MERGEABLE_VIRTUAL_METHODS_CROSS_DEX_REFS, stats.cross_dex_refs); mgr.incr_metric( METRIC_MERGEABLE_VIRTUAL_METHODS_INCONCRETE_OVERRIDDEN_METHODS, stats.inconcrete_overridden_methods); mgr.incr_metric(METRIC_MERGEABLE_VIRTUAL_SCOPES, stats.mergeable_scope_methods); mgr.incr_metric(METRIC_MERGEABLE_PAIRS, stats.mergeable_pairs); mgr.incr_metric(METRIC_VIRTUAL_SCOPES_WITH_MERGEABLE_PAIRS, stats.virtual_scopes_with_mergeable_pairs); mgr.incr_metric(METRIC_UNABSTRACTED_METHODS, stats.unabstracted_methods); mgr.incr_metric(METRIC_UNINLINABLE_METHODS, stats.uninlinable_methods); mgr.incr_metric(METRIC_HUGE_METHODS, stats.huge_methods); mgr.incr_metric(METRIC_CALLER_SIZE_REMOVED_METHODS, stats.caller_size_removed_methods); mgr.incr_metric(METRIC_REMOVED_VIRTUAL_METHODS, stats.removed_virtual_methods); mgr.incr_metric(METRIC_EXPERIMENT_METHODS, stats.experiment_methods); } static VirtualMergingPass s_pass;