simpleperf: don't hide art jni methods.

1) When hiding art frames, don't hide art jni methods.
2) Add option in CallChainReportBuilder to convert art jni methods to
their corresponding Java method names.
3) Add option in report-sample command to convert art jni methods to
their corresponding Java method names.
4) When hiding art frames, also hide art_jni_trampoline, which doesn't
seem to be useful to Java developers.

Bug: 175226454
Test: run simpleperf_unit_test
Change-Id: I5dfc57e950f5fd5f28831fcdda137b8816b22dd6
diff --git a/simpleperf/cmd_report_sample.cpp b/simpleperf/cmd_report_sample.cpp
index 5d8c532..32c3ba7 100644
--- a/simpleperf/cmd_report_sample.cpp
+++ b/simpleperf/cmd_report_sample.cpp
@@ -75,6 +75,8 @@
       return proto::Sample_CallChainEntry_ExecutionType_JIT_JVM_METHOD;
     case CallChainExecutionType::ART_METHOD:
       return proto::Sample_CallChainEntry_ExecutionType_ART_METHOD;
+    case CallChainExecutionType::ART_JNI_METHOD:
+      return proto::Sample_CallChainEntry_ExecutionType_NATIVE_METHOD;
   }
   CHECK(false) << "unexpected execution type";
   return proto::Sample_CallChainEntry_ExecutionType_NATIVE_METHOD;
@@ -115,6 +117,7 @@
 "                                 are not available in perf.data.\n"
 "--show-art-frames  Show frames of internal methods in the ART Java interpreter.\n"
 "--show-execution-type  Show execution type of a method\n"
+"--convert-art-jni-method   Convert ART JNI methods to the corresponding Java method names\n"
 "--symdir <dir>     Look for files with symbols in a directory recursively.\n"
                 // clang-format on
                 ),
@@ -169,6 +172,7 @@
   CallChainReportBuilder callchain_report_builder_;
   // map from <pid, tid> to thread name
   std::map<uint64_t, const char*> thread_names_;
+  std::unordered_map<const Symbol*, const char*> override_symbol_names_;
 };
 
 bool ReportSampleCommand::Run(const std::vector<std::string>& args) {
@@ -284,6 +288,8 @@
       callchain_report_builder_.SetRemoveArtFrame(false);
     } else if (args[i] == "--show-execution-type") {
       show_execution_type_ = true;
+    } else if (args[i] == "--convert-art-jni-method") {
+      callchain_report_builder_.SetConvertArtJniMethod(true);
     } else if (args[i] == "--symdir") {
       if (!NextArgumentOrError(args, &i)) {
         return false;
@@ -555,6 +561,9 @@
     if (show_execution_type_) {
       callchain->set_execution_type(ToProtoExecutionType(node.execution_type));
     }
+    if (node.symbol_name != nullptr) {
+      override_symbol_names_[node.symbol] = node.symbol_name;
+    }
 
     // Android studio wants a clear call chain end to notify whether a call chain is complete.
     // For the main thread, the call chain ends at __libc_init in libc.so. For other threads,
@@ -617,9 +626,14 @@
 
     for (const auto& sym : dump_symbols) {
       std::string* symbol = file->add_symbol();
-      *symbol = sym->DemangledName();
       std::string* mangled_symbol = file->add_mangled_symbol();
-      *mangled_symbol = sym->Name();
+      if (auto it = override_symbol_names_.find(sym); it != override_symbol_names_.end()) {
+        *symbol = it->second;
+        *mangled_symbol = it->second;
+      } else {
+        *symbol = sym->DemangledName();
+        *mangled_symbol = sym->Name();
+      }
     }
     if (!WriteRecordInProtobuf(proto_record)) {
       return false;
@@ -657,7 +671,9 @@
   CHECK(!entries.empty());
   FprintIndented(report_fp_, 1, "vaddr_in_file: %" PRIx64 "\n", entries[0].vaddr_in_file);
   FprintIndented(report_fp_, 1, "file: %s\n", entries[0].dso->GetReportPath().data());
-  FprintIndented(report_fp_, 1, "symbol: %s\n", entries[0].symbol->DemangledName());
+  const char* symbol_name =
+      (entries[0].symbol_name ? entries[0].symbol_name : entries[0].symbol->DemangledName());
+  FprintIndented(report_fp_, 1, "symbol: %s\n", symbol_name);
   if (show_execution_type_) {
     FprintIndented(report_fp_, 1, "execution_type: %s\n",
                    ProtoExecutionTypeToString(ToProtoExecutionType(entries[0].execution_type)));
@@ -668,7 +684,9 @@
     for (size_t i = 1u; i < entries.size(); ++i) {
       FprintIndented(report_fp_, 2, "vaddr_in_file: %" PRIx64 "\n", entries[i].vaddr_in_file);
       FprintIndented(report_fp_, 2, "file: %s\n", entries[i].dso->GetReportPath().data());
-      FprintIndented(report_fp_, 2, "symbol: %s\n", entries[i].symbol->DemangledName());
+      const char* symbol_name =
+          (entries[i].symbol_name ? entries[i].symbol_name : entries[i].symbol->DemangledName());
+      FprintIndented(report_fp_, 2, "symbol: %s\n", symbol_name);
       if (show_execution_type_) {
         FprintIndented(report_fp_, 1, "execution_type: %s\n",
                        ProtoExecutionTypeToString(ToProtoExecutionType(entries[i].execution_type)));
diff --git a/simpleperf/cmd_report_sample_test.cpp b/simpleperf/cmd_report_sample_test.cpp
index 4080d15..0c0df3b 100644
--- a/simpleperf/cmd_report_sample_test.cpp
+++ b/simpleperf/cmd_report_sample_test.cpp
@@ -177,3 +177,23 @@
                     {"--symdir", GetTestDataDir() + CORRECT_SYMFS_FOR_BUILD_ID_CHECK});
   ASSERT_NE(data.find("symbol: main"), std::string::npos);
 }
+
+TEST(cmd_report_sample, no_art_jni_trampoline) {
+  std::string data;
+  GetProtobufReport("perf_display_bitmaps.data", &data, {"--show-callchain"});
+  ASSERT_EQ(data.find("art_jni_trampoline"), std::string::npos);
+}
+
+TEST(cmd_report_sample, convert_art_jni_method) {
+  std::string data;
+  // When --convert-art-jni-method isn't used, art::Method_invoke isn't converted to Java method.
+  GetProtobufReport("perf_display_bitmaps.data", &data, {"--show-callchain"});
+  ASSERT_NE(data.find("art::Method_invoke"), std::string::npos);
+  ASSERT_EQ(data.find("java.lang.reflect.Method.invoke"), std::string::npos);
+
+  // When --convert-art-jni-method is used, art::Method_invoke is converted to Java method.
+  GetProtobufReport("perf_display_bitmaps.data", &data,
+                    {"--show-callchain", "--convert-art-jni-method"});
+  ASSERT_EQ(data.find("art::Method_invoke"), std::string::npos);
+  ASSERT_NE(data.find("java.lang.reflect.Method.invoke"), std::string::npos);
+}
diff --git a/simpleperf/report_utils.cpp b/simpleperf/report_utils.cpp
index cb29e0e..f8c4748 100644
--- a/simpleperf/report_utils.cpp
+++ b/simpleperf/report_utils.cpp
@@ -22,11 +22,39 @@
 
 namespace simpleperf {
 
-static bool IsArtDso(const Dso* dso) {
-  return android::base::EndsWith(dso->Path(), "/libart.so") ||
-         android::base::EndsWith(dso->Path(), "/libartd.so");
+static bool IsArtEntry(const CallChainReportEntry& entry) {
+  return entry.execution_type == CallChainExecutionType::NATIVE_METHOD &&
+         (android::base::EndsWith(entry.dso->Path(), "/libart.so") ||
+          android::base::EndsWith(entry.dso->Path(), "/libartd.so") ||
+          strcmp(entry.symbol->Name(), "art_jni_trampoline") == 0);
 };
 
+#define ART_JNI_METHOD(java_name, native_name) java_name, native_name,
+
+static const char* art_jni_method_array[] = {
+#include "art_jni_method_table.h"
+};
+
+CallChainReportBuilder::CallChainReportBuilder(ThreadTree& thread_tree)
+    : thread_tree_(thread_tree) {
+  size_t n = sizeof(art_jni_method_array) / sizeof(art_jni_method_array[0]);
+  for (size_t i = 0; i + 1 < n; i += 2) {
+    art_jni_method_map_[art_jni_method_array[i + 1]] = art_jni_method_array[i];
+  }
+}
+
+static std::string_view GetFunctionName(std::string_view symbol_name) {
+  // Remove parameters.
+  if (auto pos = symbol_name.find('('); pos != symbol_name.npos) {
+    symbol_name = symbol_name.substr(0, pos);
+  }
+  // Remove return type.
+  if (auto pos = symbol_name.rfind(' '); pos != symbol_name.npos) {
+    symbol_name = symbol_name.substr(pos + 1);
+  }
+  return symbol_name;
+}
+
 std::vector<CallChainReportEntry> CallChainReportBuilder::Build(const ThreadEntry* thread,
                                                                 const std::vector<uint64_t>& ips,
                                                                 size_t kernel_ip_count) {
@@ -35,6 +63,9 @@
   for (size_t i = 0; i < ips.size(); i++) {
     const MapEntry* map = thread_tree_.FindMap(thread, ips[i], i < kernel_ip_count);
     Dso* dso = map->dso;
+    uint64_t vaddr_in_file;
+    const Symbol* symbol = thread_tree_.FindSymbol(map, ips[i], &vaddr_in_file, &dso);
+    const char* symbol_name = nullptr;
     CallChainExecutionType execution_type = CallChainExecutionType::NATIVE_METHOD;
     if (dso->IsForJavaMethod()) {
       if (dso->type() == DSO_DEX_FILE) {
@@ -42,13 +73,18 @@
       } else {
         execution_type = CallChainExecutionType::JIT_JVM_METHOD;
       }
+    } else if (auto it = art_jni_method_map_.find(GetFunctionName(symbol->DemangledName()));
+               it != art_jni_method_map_.end()) {
+      execution_type = CallChainExecutionType::ART_JNI_METHOD;
+      if (convert_art_jni_method_) {
+        symbol_name = it->second;
+      }
     }
-    uint64_t vaddr_in_file;
-    const Symbol* symbol = thread_tree_.FindSymbol(map, ips[i], &vaddr_in_file, &dso);
     result.resize(result.size() + 1);
     auto& entry = result.back();
     entry.ip = ips[i];
     entry.symbol = symbol;
+    entry.symbol_name = symbol_name;
     entry.dso = dso;
     entry.vaddr_in_file = vaddr_in_file;
     entry.map = map;
@@ -73,17 +109,18 @@
   for (size_t i = 0; i < callchain.size(); ++i) {
     auto& entry = callchain[i];
     if (entry.execution_type == CallChainExecutionType::INTERPRETED_JVM_METHOD ||
-        entry.execution_type == CallChainExecutionType::JIT_JVM_METHOD) {
+        entry.execution_type == CallChainExecutionType::JIT_JVM_METHOD ||
+        entry.execution_type == CallChainExecutionType::ART_JNI_METHOD) {
       near_java_method = true;
 
       // Mark art frames before this entry.
       for (int j = static_cast<int>(i) - 1; j >= 0; j--) {
-        if (!IsArtDso(callchain[j].dso)) {
+        if (!IsArtEntry(callchain[j])) {
           break;
         }
         callchain[j].execution_type = CallChainExecutionType::ART_METHOD;
       }
-    } else if (near_java_method && IsArtDso(entry.dso)) {
+    } else if (near_java_method && IsArtEntry(entry)) {
       entry.execution_type = CallChainExecutionType::ART_METHOD;
     } else {
       near_java_method = false;
diff --git a/simpleperf/report_utils.h b/simpleperf/report_utils.h
index 8293f76..6a3beb4 100644
--- a/simpleperf/report_utils.h
+++ b/simpleperf/report_utils.h
@@ -18,6 +18,7 @@
 
 #include <inttypes.h>
 
+#include <string_view>
 #include <vector>
 
 #include "dso.h"
@@ -31,11 +32,14 @@
   JIT_JVM_METHOD,
   // ART methods near interpreted/JIT JVM methods. They're shown only when RemoveArtFrame = false.
   ART_METHOD,
+  // JNI native methods implemented in libart.so
+  ART_JNI_METHOD,
 };
 
 struct CallChainReportEntry {
   uint64_t ip = 0;
   const Symbol* symbol = nullptr;
+  const char* symbol_name = nullptr;
   Dso* dso = nullptr;
   const char* dso_name = nullptr;
   uint64_t vaddr_in_file = 0;
@@ -45,13 +49,17 @@
 
 class CallChainReportBuilder {
  public:
-  CallChainReportBuilder(ThreadTree& thread_tree) : thread_tree_(thread_tree) {}
+  CallChainReportBuilder(ThreadTree& thread_tree);
   // If true, remove interpreter frames both before and after a Java frame.
   // Default is true.
   void SetRemoveArtFrame(bool enable) { remove_art_frame_ = enable; }
   // If true, convert a JIT method into its corresponding interpreted Java method. So they can be
   // merged in reports like flamegraph. Default is true.
   void SetConvertJITFrame(bool enable) { convert_jit_frame_ = enable; }
+  // If true, convert ART JNI methods to their corresponding Java method names.
+  // For example, art::Method_invoke is converted to java.lang.reflect.Method.invoke.
+  // Default is false.
+  void SetConvertArtJniMethod(bool enable) { convert_art_jni_method_ = enable; }
   std::vector<CallChainReportEntry> Build(const ThreadEntry* thread,
                                           const std::vector<uint64_t>& ips, size_t kernel_ip_count);
 
@@ -69,8 +77,10 @@
   ThreadTree& thread_tree_;
   bool remove_art_frame_ = true;
   bool convert_jit_frame_ = true;
+  bool convert_art_jni_method_ = false;
   bool java_method_initialized_ = false;
   std::unordered_map<std::string, JavaMethod> java_method_map_;
+  std::unordered_map<std::string_view, const char*> art_jni_method_map_;
 };
 
 }  // namespace simpleperf
diff --git a/simpleperf/report_utils_test.cpp b/simpleperf/report_utils_test.cpp
index ca803ab..9147579 100644
--- a/simpleperf/report_utils_test.cpp
+++ b/simpleperf/report_utils_test.cpp
@@ -41,6 +41,7 @@
     file.min_vaddr = file.file_offset_of_min_vaddr = 0;
     file.symbols = {
         Symbol("native_func1", 0x0, 0x100),
+        Symbol("art_jni_trampoline", 0x100, 0x100),
     };
     thread_tree.AddDsoInfo(file);
 
@@ -51,6 +52,7 @@
     file.symbols = {
         Symbol("art_func1", 0x0, 0x100),
         Symbol("art_func2", 0x100, 0x100),
+        Symbol("_ZN3artL13Method_invokeEP7_JNIEnvP8_jobjectS3_P13_jobjectArray", 0x200, 0x100),
     };
     thread_tree.AddDsoInfo(file);
 
@@ -230,3 +232,74 @@
   ASSERT_EQ(entries[2].vaddr_in_file, 0x0);
   ASSERT_EQ(entries[2].execution_type, CallChainExecutionType::INTERPRETED_JVM_METHOD);
 }
+
+TEST_F(CallChainReportBuilderTest, remove_art_jni_trampoline) {
+  // Test option: remove_art_frame = true.
+  // The callchain should remove art_jni_trampolines.
+  std::vector<uint64_t> fake_ips = {
+      0x100,   // art_jni_trampoline
+      0x2000,  // java_method1 in dex file
+      0x100,   // art_jni_trampoline
+  };
+  CallChainReportBuilder builder(thread_tree);
+  std::vector<CallChainReportEntry> entries = builder.Build(thread, fake_ips, 0);
+  ASSERT_EQ(entries.size(), 1);
+  ASSERT_EQ(entries[0].ip, 0x2000);
+  ASSERT_STREQ(entries[0].symbol->Name(), "java_method1");
+  ASSERT_EQ(entries[0].dso->Path(), fake_dex_file_path);
+  ASSERT_EQ(entries[0].vaddr_in_file, 0x0);
+  ASSERT_EQ(entries[0].execution_type, CallChainExecutionType::INTERPRETED_JVM_METHOD);
+}
+
+TEST_F(CallChainReportBuilderTest, not_convert_art_jni_method) {
+  // Test option: remove_art_frame = true, convert_art_jni_method = false.
+  // The callchain should not remove art_jni_methods or change their names.
+  std::vector<uint64_t> fake_ips = {
+      0x1200,  // art::Method_invoke(_JNIEnv*, _jobject*, _jobject*, _jobjectArray*)
+      0x2000,  // java_method1 in dex file
+      0x1200,  // art::Method_invoke(_JNIEnv*, _jobject*, _jobject*, _jobjectArray*)
+  };
+  CallChainReportBuilder builder(thread_tree);
+  std::vector<CallChainReportEntry> entries = builder.Build(thread, fake_ips, 0);
+  ASSERT_EQ(entries.size(), 3);
+  for (size_t i : {0, 2}) {
+    ASSERT_EQ(entries[i].ip, 0x1200);
+    ASSERT_STREQ(entries[i].symbol->DemangledName(),
+                 "art::Method_invoke(_JNIEnv*, _jobject*, _jobject*, _jobjectArray*)");
+    ASSERT_TRUE(entries[i].symbol_name == nullptr);
+    ASSERT_EQ(entries[i].dso->Path(), fake_interpreter_path);
+    ASSERT_EQ(entries[i].vaddr_in_file, 0x200);
+    ASSERT_EQ(entries[i].execution_type, CallChainExecutionType::ART_JNI_METHOD);
+  }
+  ASSERT_EQ(entries[1].ip, 0x2000);
+  ASSERT_STREQ(entries[1].symbol->Name(), "java_method1");
+  ASSERT_EQ(entries[1].dso->Path(), fake_dex_file_path);
+  ASSERT_EQ(entries[1].vaddr_in_file, 0x0);
+  ASSERT_EQ(entries[1].execution_type, CallChainExecutionType::INTERPRETED_JVM_METHOD);
+}
+
+TEST_F(CallChainReportBuilderTest, convert_art_jni_method) {
+  // Test option: remove_art_frame = true, convert_art_jni_method = true.
+  // The callchain should not remove art_jni_methods, but should convert their names.
+  std::vector<uint64_t> fake_ips = {
+      0x1200,  // art::Method_invoke(_JNIEnv*, _jobject*, _jobject*, _jobjectArray*)
+      0x2000,  // java_method1 in dex file
+      0x1200,  // art::Method_invoke(_JNIEnv*, _jobject*, _jobject*, _jobjectArray*)
+  };
+  CallChainReportBuilder builder(thread_tree);
+  builder.SetConvertArtJniMethod(true);
+  std::vector<CallChainReportEntry> entries = builder.Build(thread, fake_ips, 0);
+  ASSERT_EQ(entries.size(), 3);
+  for (size_t i : {0, 2}) {
+    ASSERT_EQ(entries[i].ip, 0x1200);
+    ASSERT_STREQ(entries[i].symbol_name, "java.lang.reflect.Method.invoke");
+    ASSERT_EQ(entries[i].dso->Path(), fake_interpreter_path);
+    ASSERT_EQ(entries[i].vaddr_in_file, 0x200);
+    ASSERT_EQ(entries[i].execution_type, CallChainExecutionType::ART_JNI_METHOD);
+  }
+  ASSERT_EQ(entries[1].ip, 0x2000);
+  ASSERT_STREQ(entries[1].symbol->Name(), "java_method1");
+  ASSERT_EQ(entries[1].dso->Path(), fake_dex_file_path);
+  ASSERT_EQ(entries[1].vaddr_in_file, 0x0);
+  ASSERT_EQ(entries[1].execution_type, CallChainExecutionType::INTERPRETED_JVM_METHOD);
+}