diff --git a/Makefile.am b/Makefile.am index ffda3a5510..dde5b50c75 100644 --- a/Makefile.am +++ b/Makefile.am @@ -417,8 +417,9 @@ libopt_la_SOURCES = \ opt/partial-application/PartialApplication.cpp \ opt/peephole/Peephole.cpp \ opt/peephole/RedundantCheckCastRemover.cpp \ + opt/print-kotlin-stats/PrintKotlinStats.cpp \ opt/print-members/PrintMembers.cpp \ - opt/reachable-natives/ReachableNatives.cpp \ + opt/reachable-natives/ReachableNatives.cpp \ opt/rebindrefs/ReBindRefs.cpp \ opt/regalloc/RegAlloc.cpp \ opt/regalloc-fast/FastRegAlloc.cpp \ diff --git a/libredex/ReferencedState.h b/libredex/ReferencedState.h index 9b60277a04..367f3a0f10 100644 --- a/libredex/ReferencedState.h +++ b/libredex/ReferencedState.h @@ -89,6 +89,9 @@ class ReferencedState { // Whether this member is an outlined class or method. bool m_outlined : 1; + // Is this member is a kotlin class or method + bool m_is_kotlin : 1; + bool m_name_used : 1; // Whether a field is used to indicate that an sget cannot be removed @@ -128,6 +131,7 @@ class ReferencedState { m_immutable_getter = false; m_pure_method = false; m_outlined = false; + m_is_kotlin = false; m_name_used = false; @@ -215,6 +219,8 @@ class ReferencedState { other.inner_struct.m_immutable_getter; this->inner_struct.m_pure_method = this->inner_struct.m_pure_method & other.inner_struct.m_pure_method; + this->inner_struct.m_is_kotlin = + this->inner_struct.m_is_kotlin & other.inner_struct.m_is_kotlin; this->inner_struct.m_outlined = this->inner_struct.m_outlined & other.inner_struct.m_outlined; @@ -393,6 +399,8 @@ class ReferencedState { bool outlined() const { return inner_struct.m_outlined; } void set_outlined() { inner_struct.m_outlined = true; } void reset_outlined() { inner_struct.m_outlined = false; } + bool is_cls_kotlin() const { return inner_struct.m_is_kotlin; } + void set_cls_kotlin() { inner_struct.m_is_kotlin = true; } void set_name_used() { inner_struct.m_name_used = true; } bool name_used() { return inner_struct.m_name_used; } diff --git a/opt/print-kotlin-stats/PrintKotlinStats.cpp b/opt/print-kotlin-stats/PrintKotlinStats.cpp new file mode 100644 index 0000000000..22e6afc4f9 --- /dev/null +++ b/opt/print-kotlin-stats/PrintKotlinStats.cpp @@ -0,0 +1,246 @@ +/* + * 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 "PrintKotlinStats.h" +#include "DexUtil.h" +#include "IRCode.h" +#include "KotlinNullCheckMethods.h" +#include "PassManager.h" +#include "Show.h" +#include "Walkers.h" + +namespace { +constexpr const char* LAZY_SIGNATURE = "Lkotlin/Lazy;"; +constexpr const char* R_PROP_SIGNATURE = "Lkotlin/properties/ReadProperty;"; +constexpr const char* W_PROP_SIGNATURE = "Lkotlin/properties/WriteProperty;"; +constexpr const char* RW_PROP_SIGNATURE = + "Lkotlin/properties/ReadWriteProperty;"; +constexpr const char* KPROPERTY_ARRAY = "[Lkotlin/reflect/KProperty;"; +constexpr const char* KOTLIN_LAMBDA = "Lkotlin/jvm/internal/Lambda;"; +constexpr const char* DI_BASE = "Lcom/facebook/inject/AbstractLibraryModule;"; +constexpr const char* CONTINUATION_IMPL = + "Lkotlin/coroutines/jvm/internal/ContinuationImpl;"; + +// Check if cls is from Kotlin source +bool is_kotlin_class(DexClass* cls) { + auto src_string = cls->get_source_file(); + if (src_string && boost::algorithm::ends_with(src_string->str(), ".kt")) { + return true; + } + return false; +} + +// Check cls name is in anonymous format +// Anonymous cls name ends with \$[0-9]* +bool is_anonymous(std::string_view name) { + auto last = name.find_last_of('$'); + if (last == std::string::npos) { + return false; + } + // Except for the ; at the end + for (size_t i = last + 1; i < name.length() - 1; ++i) { + if (name[i] < '0' || name[i] > '9') { + return false; + } + } + return true; +} + +} // namespace + +// Setup types/strings needed for the pass +void PrintKotlinStats::setup() { + m_kotlin_null_assertions = + kotlin_nullcheck_wrapper::get_kotlin_null_assertions(); + m_kotlin_lambdas_base = DexType::get_type(KOTLIN_LAMBDA); + m_kotlin_coroutin_continuation_base = DexType::get_type(CONTINUATION_IMPL); + m_di_base = DexType::get_type(DI_BASE); + m_instance = DexString::make_string("INSTANCE"); +} + +// Annotate Kotlin classes before StripDebugInfoPass removes it +void PrintKotlinStats::eval_pass(DexStoresVector& stores, + ConfigFiles&, + PassManager&) { + Scope scope = build_class_scope(stores); + setup(); + walk::parallel::classes(scope, [&](DexClass* cls) { + if (is_kotlin_class(cls)) { + cls->rstate.set_cls_kotlin(); + } + }); +} + +void PrintKotlinStats::run_pass(DexStoresVector& stores, + ConfigFiles&, + PassManager& mgr) { + Scope scope = build_class_scope(stores); + std::unordered_set delegate_types{ + DexType::get_type(KPROPERTY_ARRAY), + DexType::get_type(R_PROP_SIGNATURE), + DexType::get_type(W_PROP_SIGNATURE), + DexType::get_type(RW_PROP_SIGNATURE), + }; + std::unordered_set lazy_delegate_types{ + DexType::get_type(LAZY_SIGNATURE), + }; + + // Handle methods + m_stats = walk::parallel::methods( + scope, [&](DexMethod* method) { return handle_method(method); }); + + // Handle fields + // Count delegated properties + walk::fields(scope, [&](DexField* field) { + auto typ = field->get_type(); + if (lazy_delegate_types.count(typ)) { + m_stats.kotlin_lazy_delegates++; + } + if (delegate_types.count(typ)) { + m_stats.kotlin_delegates++; + } + }); + + // Handle classes + std::mutex mtx; + walk::parallel::classes(scope, [&](DexClass* cls) { + auto local_stats = handle_class(cls); + std::lock_guard g(mtx); + m_stats += local_stats; + }); + m_stats.report(mgr); +} + +PrintKotlinStats::Stats PrintKotlinStats::handle_class(DexClass* cls) { + Stats stats; + bool is_lambda = false; + if (cls->get_super_class() == m_kotlin_lambdas_base) { + stats.kotlin_lambdas++; + is_lambda = true; + } + if (cls->get_super_class() == m_kotlin_coroutin_continuation_base) { + stats.kotlin_coroutine_continuation_base++; + } + + if (cls->get_super_class() == m_di_base) { + stats.di_generated_class++; + } + + for (auto* field : cls->get_sfields()) { + if (field->get_name() == m_instance && + field->get_type() == cls->get_type()) { + if (is_lambda) { + stats.kotlin_non_capturing_lambda++; + } + stats.kotlin_class_with_instance++; + } + } + if (cls->rstate.is_cls_kotlin()) { + stats.kotlin_class++; + for (auto* method : cls->get_all_methods()) { + if (boost::algorithm::ends_with(method->get_name()->str(), "$default")) { + stats.kotlin_default_arg_method++; + } + } + if (is_anonymous(cls->get_name()->str())) { + stats.kotlin_anonymous_class++; + } + if (boost::algorithm::ends_with(cls->get_name()->str(), "$Companion;")) { + stats.kotlin_companion_class++; + } + } + return stats; +} + +PrintKotlinStats::Stats PrintKotlinStats::handle_method(DexMethod* method) { + Stats stats; + + if (!method->get_code()) { + return stats; + } + + DexClass* cls = type_class(method->get_class()); + if (!cls) { + return stats; + } + + if (method->get_access() & ACC_PUBLIC) { + auto* arg_types = method->get_proto()->get_args(); + for (auto arg_type : *arg_types) { + if (cls->rstate.is_cls_kotlin()) { + stats.kotlin_public_param_objects++; + } else { + stats.java_public_param_objects++; + } + } + } + + auto code = method->get_code(); + + for (const auto& it : InstructionIterable(code)) { + auto insn = it.insn; + switch (insn->opcode()) { + case OPCODE_INVOKE_STATIC: { + auto called_method = insn->get_method(); + if (m_kotlin_null_assertions.count(called_method)) { + stats.kotlin_null_check_insns++; + } + } break; + default: + break; + } + } + return stats; +} + +void PrintKotlinStats::Stats::report(PassManager& mgr) const { + mgr.incr_metric("kotlin_null_check_insns", kotlin_null_check_insns); + mgr.incr_metric("java_public_param_objects", java_public_param_objects); + mgr.incr_metric("kotlin_public_param_objects", kotlin_public_param_objects); + mgr.incr_metric("no_of_delegates", kotlin_delegates); + mgr.incr_metric("no_of_lazy_delegates", kotlin_lazy_delegates); + mgr.incr_metric("kotlin_lambdas", kotlin_lambdas); + mgr.incr_metric("kotlin_non_capturing_lambda", kotlin_non_capturing_lambda); + mgr.incr_metric("kotlin_classes_with_instance", kotlin_class_with_instance); + mgr.incr_metric("kotlin_class", kotlin_class); + mgr.incr_metric("Kotlin_anonymous_classes", kotlin_anonymous_class); + mgr.incr_metric("kotlin_companion_class", kotlin_companion_class); + mgr.incr_metric("di_generated_class", di_generated_class); + mgr.incr_metric("kotlin_default_arg_method", kotlin_default_arg_method); + mgr.incr_metric("kotlin_coroutine_continuation_base", + kotlin_coroutine_continuation_base); + + TRACE(KOTLIN_STATS, 1, "KOTLIN_STATS: kotlin_null_check_insns = %zu", + kotlin_null_check_insns); + TRACE(KOTLIN_STATS, 1, "KOTLIN_STATS: java_public_param_objects = %zu", + java_public_param_objects); + TRACE(KOTLIN_STATS, 1, "KOTLIN_STATS: kotlin_public_param_objects = %zu", + kotlin_public_param_objects); + TRACE(KOTLIN_STATS, 1, "KOTLIN_STATS: no_of_delegates = %zu", + kotlin_delegates); + TRACE(KOTLIN_STATS, 1, "KOTLIN_STATS: no_of_lazy_delegates = %zu", + kotlin_lazy_delegates); + TRACE(KOTLIN_STATS, 1, "KOTLIN_STATS: kotlin_lambdas = %zu", kotlin_lambdas); + TRACE(KOTLIN_STATS, 1, "KOTLIN_STATS: kotlin_non_capturing_lambda = %zu", + kotlin_non_capturing_lambda); + TRACE(KOTLIN_STATS, 1, "KOTLIN_STATS: kotlin_class_with_instance = %zuu", + kotlin_class_with_instance); + TRACE(KOTLIN_STATS, 1, "KOTLIN_STATS: kotlin_class = %zu", kotlin_class); + TRACE(KOTLIN_STATS, 1, "KOTLIN_STATS: kotlin_anonymous_class = %zu", + kotlin_anonymous_class); + TRACE(KOTLIN_STATS, 1, "KOTLIN_STATS: kotlin_companion_class = %zu", + kotlin_companion_class); + TRACE(KOTLIN_STATS, 1, "KOTLIN_STATS: di_generated_class = %zu", + di_generated_class); + TRACE(KOTLIN_STATS, 1, "KOTLIN_STATS: kotlin_default_arg_method = %zu", + kotlin_default_arg_method); + TRACE(KOTLIN_STATS, 1, + "KOTLIN_STATS: kotlin_coroutine_continuation_base = %zu", + kotlin_coroutine_continuation_base); +} + +static PrintKotlinStats s_pass; diff --git a/opt/print-kotlin-stats/PrintKotlinStats.h b/opt/print-kotlin-stats/PrintKotlinStats.h new file mode 100644 index 0000000000..dfc37df3b3 --- /dev/null +++ b/opt/print-kotlin-stats/PrintKotlinStats.h @@ -0,0 +1,87 @@ +/* + * 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. + */ + +#pragma once + +#include "DexClass.h" +#include "DexUtil.h" +#include "Pass.h" + +class PrintKotlinStats : public Pass { + + public: + struct Stats { + size_t unknown_null_check_insns{0}; + size_t kotlin_null_check_insns{0}; + size_t java_public_param_objects{0}; + size_t kotlin_public_param_objects{0}; + size_t kotlin_delegates{0}; + size_t kotlin_lazy_delegates{0}; + size_t kotlin_lambdas{0}; + size_t kotlin_non_capturing_lambda{0}; + size_t kotlin_class_with_instance{0}; + size_t kotlin_class{0}; + size_t kotlin_anonymous_class{0}; + size_t kotlin_companion_class{0}; + size_t di_generated_class{0}; + size_t kotlin_default_arg_method{0}; + size_t kotlin_coroutine_continuation_base{0}; + + Stats& operator+=(const Stats& that) { + unknown_null_check_insns += that.unknown_null_check_insns; + kotlin_null_check_insns += that.kotlin_null_check_insns; + java_public_param_objects += that.java_public_param_objects; + kotlin_public_param_objects += that.kotlin_public_param_objects; + kotlin_delegates += that.kotlin_delegates; + kotlin_lazy_delegates += that.kotlin_lazy_delegates; + kotlin_lambdas += that.kotlin_lambdas; + kotlin_non_capturing_lambda += that.kotlin_non_capturing_lambda; + kotlin_class_with_instance += that.kotlin_class_with_instance; + kotlin_class += that.kotlin_class; + kotlin_anonymous_class += that.kotlin_anonymous_class; + kotlin_companion_class += that.kotlin_companion_class; + di_generated_class += that.di_generated_class; + kotlin_default_arg_method += that.kotlin_default_arg_method; + kotlin_coroutine_continuation_base += + that.kotlin_coroutine_continuation_base; + return *this; + } + + /// Updates metrics tracked by \p mgr corresponding to these statistics. + void report(PassManager& mgr) const; + }; + + PrintKotlinStats() : Pass("PrintKotlinStatsPass") {} + + redex_properties::PropertyInteractions get_property_interactions() + const override { + using namespace redex_properties::interactions; + using namespace redex_properties::names; + return { + {DexLimitsObeyed, Preserves}, + {HasSourceBlocks, Preserves}, + {NoSpuriousGetClassCalls, Preserves}, + {UltralightCodePatterns, Preserves}, + }; + } + + void setup(); + void eval_pass(DexStoresVector&, ConfigFiles&, PassManager&) override; + bool is_cfg_legacy() override { return true; } + void run_pass(DexStoresVector&, ConfigFiles&, PassManager&) override; + Stats handle_method(DexMethod* method); + Stats handle_class(DexClass* cls); + Stats get_stats() { return m_stats; } + + private: + std::unordered_set m_kotlin_null_assertions; + DexType* m_kotlin_lambdas_base = nullptr; + DexType* m_kotlin_coroutin_continuation_base = nullptr; + const DexString* m_instance = nullptr; + DexType* m_di_base = nullptr; + Stats m_stats; +}; diff --git a/test/integ/KotlinStats.cpp b/test/integ/KotlinStats.cpp new file mode 100644 index 0000000000..338b4f519e --- /dev/null +++ b/test/integ/KotlinStats.cpp @@ -0,0 +1,70 @@ +/* + * 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 + +#include "DexUtil.h" +#include "PrintKotlinStats.h" +#include "RedexTest.h" +#include "Resolver.h" +#include "RewriteKotlinSingletonInstance.h" +#include "Show.h" + +class KotlinStatsTest : public RedexIntegrationTest {}; + +namespace { +TEST_F(KotlinStatsTest, MethodHasNoEqDefined) { + auto klr = new PrintKotlinStats(); + std::vector passes{klr}; + run_passes(passes); + + PrintKotlinStats::Stats stats = klr->get_stats(); + + ASSERT_EQ(stats.kotlin_null_check_insns, 9); + ASSERT_EQ(stats.kotlin_public_param_objects, 21); + + // LExample;.$$delegatedProperties:[Lkotlin/reflect/KProperty; + // LFooDelagates;.lazyValue$delegate:Lkotlin/Lazy; + // Lfoo;.unsafeLazy:Lkotlin/Lazy; + ASSERT_EQ(stats.kotlin_delegates, 1); + ASSERT_EQ(stats.kotlin_lazy_delegates, 2); + + // LKotlinLambdaInline$foo$1; + // LFooDelagates$lazyValue$2; + ASSERT_EQ(stats.kotlin_lambdas, 2); + + // LKotlinLambdaInline$foo$1; + ASSERT_EQ(stats.kotlin_class_with_instance, 1); + + // LKotlinLambdaInline$foo$1; + ASSERT_EQ(stats.kotlin_non_capturing_lambda, 1); + + // LDelegate1; + // LKotlinLambdaInline$foo$1; + // LKotlinLayzyKt; + // LCompanionClass$Companion; + // LKotlinLambdaInline; + // LCompanionClass; + // LDelegateTest; + // LExample; + // LAnotherCompanionClass$Test; + // LFooDelagates$lazyValue$2; + // LFooDelagates; + // LKotlinCompanionObjKt; + // Lfoo; + // LAnotherCompanionClass; + ASSERT_EQ(stats.kotlin_class, 14); + + // Named companion object is not counted yet + // LCompanionClass$Companion; + ASSERT_EQ(stats.kotlin_companion_class, 1); + + // LKotlinLambdaInline$foo$1; + // LFooDelagates$lazyValue$2; + ASSERT_EQ(stats.kotlin_anonymous_class, 2); +} +} // namespace diff --git a/test/unit/Makefile.am b/test/unit/Makefile.am index 95ec2c37bb..5e9ee30e46 100644 --- a/test/unit/Makefile.am +++ b/test/unit/Makefile.am @@ -96,6 +96,7 @@ check_PROGRAMS = \ outliner_type_analysis_test \ partial_pass_test \ peephole_test \ + print_kotlin_stats_test \ proguard_lexer_test \ proguard_map_test \ proguard_matcher_test \ @@ -334,6 +335,8 @@ partial_pass_test_SOURCES = PartialPassTest.cpp peephole_test_SOURCES = PeepholeTest.cpp +print_kotlin_stats_test_SOURCES = PrintKotlinStatsTest.cpp + proguard_lexer_test_SOURCES = ProguardLexerTest.cpp proguard_map_test_SOURCES = ProguardMapTest.cpp diff --git a/test/unit/PrintKotlinStatsTest.cpp b/test/unit/PrintKotlinStatsTest.cpp new file mode 100644 index 0000000000..1108607c30 --- /dev/null +++ b/test/unit/PrintKotlinStatsTest.cpp @@ -0,0 +1,95 @@ +/* + * 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 "PrintKotlinStats.h" +#include "Creators.h" +#include "IRAssembler.h" +#include "PassManager.h" +#include "RedexTest.h" +#include "Walkers.h" + +struct PrintKotlinStatsTest : public RedexTest { + public: + PrintKotlinStatsTest() { + m_cls_public = DexType::make_type("LPUB;"); + m_init1 = assembler::method_from_string(R"( + (method (public constructor) "LPUB;.:()V" + ( + (return-void) + ) + ) + )"); + + m_cls_private = DexType::make_type("LPRI;"); + m_init2 = assembler::method_from_string(R"( + (method (public constructor) "LPRI;.:()V" + ( + (return-void) + ) + ) + )"); + } + + protected: + void prepare_scope(Scope& scope, + DexMethod* method_public, + DexMethod* method_private) { + ClassCreator creator1(m_cls_public); + creator1.set_super(type::java_lang_Object()); + ClassCreator creator2(m_cls_private); + creator2.set_super(type::java_lang_Object()); + + creator1.add_method(m_init1); + creator1.add_method(method_public); + m_m_cls_public = creator1.create(); + + creator2.add_method(m_init2); + creator2.add_method(method_private); + m_m_cls_private = creator2.create(); + + scope.push_back(m_m_cls_public); + scope.push_back(m_m_cls_private); + } + DexClass* m_m_cls_public; + DexClass* m_m_cls_private; + DexType* m_cls_public; + DexType* m_cls_private; + DexMethod* m_init1; + DexMethod* m_init2; +}; + +TEST_F(PrintKotlinStatsTest, SimpleArgumentPassingTest) { + Scope scope; + DexMethod* method_public = assembler::method_from_string(R"( + (method (public) "LPUB;.meth1:(Ljava/lang/Object;ILjava/lang/Object;)Ljava/lang/Object;" + ( + (load-param-object v0) + (const-string "args") + (move-result-pseudo-object v1) + (invoke-static (v0 v1) "Lkotlin/jvm/internal/Intrinsics;.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V") + (invoke-static (v0 v1) "Lkotlin/jvm/internal/Intrinsics;.checkExpressionValueIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V") + (return-object v1) + ) + ) + )"); + DexMethod* method_private = assembler::method_from_string(R"( + (method (private) "LPRI;.meth2:(Ljava/lang/Object;ILjava/lang/Object;)Ljava/lang/Object;" + ( + (return-object v1) + ) + ) + )"); + + prepare_scope(scope, method_public, method_private); + PrintKotlinStats pass; + pass.setup(); + PrintKotlinStats::Stats stats = + walk::parallel::methods( + scope, [&](DexMethod* meth) { return pass.handle_method(meth); }); + + ASSERT_EQ(stats.kotlin_null_check_insns, 2); +}