OSDN Git Service

simpleperf: fix finding app's process.
authorYabin Cui <yabinc@google.com>
Mon, 28 Aug 2017 22:49:33 +0000 (15:49 -0700)
committerYabin Cui <yabinc@google.com>
Tue, 29 Aug 2017 01:03:51 +0000 (18:03 -0700)
Also check the return value of recording, fix some tiny errors in tests.

Bug: None.
Test: run test.py.
Change-Id: I42b33c796a302b71ca1c87888b4a2e9ad53306af

simpleperf/cmd_record.cpp
simpleperf/environment.cpp
simpleperf/scripts/app_profiler.py
simpleperf/scripts/test.py

index c22129a..2bd0506 100644 (file)
@@ -111,7 +111,7 @@ class RecordCommand : public Command {
 "-f freq      Set event sample frequency. It means recording at most [freq]\n"
 "             samples every second. For non-tracepoint events, the default\n"
 "             option is -f 4000. A -f/-c option affects all event types\n"
-"             following it until meeting another -f/-c option. For example,"
+"             following it until meeting another -f/-c option. For example,\n"
 "             for \"-f 1000 cpu-cycles -c 1 -e sched:sched_switch\", cpu-cycles\n"
 "             has sample freq 1000, sched:sched_switch event has sample period 1.\n"
 "-c count     Set event sample period. It means recording one sample when\n"
index c936ac8..bf0a061 100644 (file)
@@ -522,6 +522,21 @@ void PrepareVdsoFile() {
   Dso::SetVdsoFile(std::move(tmpfile), sizeof(size_t) == sizeof(uint64_t));
 }
 
+static bool HasOpenedAppApkFile(int pid) {
+  std::string fd_path = "/proc/" + std::to_string(pid) + "/fd/";
+  std::vector<std::string> files = GetEntriesInDir(fd_path);
+  for (const auto& file : files) {
+    std::string real_path;
+    if (!android::base::Readlink(fd_path + file, &real_path)) {
+      continue;
+    }
+    if (real_path.find("app") != std::string::npos && real_path.find(".apk") != std::string::npos) {
+      return true;
+    }
+  }
+  return false;
+}
+
 int WaitForAppProcess(const std::string& package_name) {
   size_t loop_count = 0;
   while (true) {
@@ -533,12 +548,26 @@ int WaitForAppProcess(const std::string& package_name) {
         continue;
       }
       cmdline = android::base::Basename(cmdline);
-      if (cmdline == package_name) {
-        if (loop_count > 0u) {
-          LOG(INFO) << "Got process " << pid << " for package " << package_name;
-        }
-        return pid;
+      if (cmdline != package_name) {
+        continue;
+      }
+      // If a debuggable app with wrap.sh runs on Android O, the app will be started with
+      // logwrapper as below:
+      // 1. Zygote forks a child process, rename it to package_name.
+      // 2. The child process execute sh, which starts a child process running
+      //    /system/bin/logwrapper.
+      // 3. logwrapper starts a child process running sh, which interprets wrap.sh.
+      // 4. wrap.sh starts a child process running the app.
+      // The problem here is we want to profile the process started in step 4, but sometimes we
+      // run into the process started in step 1. To solve it, we can check if the process has
+      // opened an apk file in some app dirs.
+      if (!HasOpenedAppApkFile(pid)) {
+        continue;
+      }
+      if (loop_count > 0u) {
+        LOG(INFO) << "Got process " << pid << " for package " << package_name;
       }
+      return pid;
     }
     if (++loop_count == 1u) {
       LOG(INFO) << "Waiting for process of app " << package_name;
index a724e2d..9e0f551 100644 (file)
@@ -25,6 +25,7 @@ import argparse
 import copy
 import os
 import os.path
+import re
 import shutil
 import subprocess
 import sys
@@ -194,7 +195,23 @@ class AppProfiler(object):
         # On Android >= N, pidof is available. Otherwise, we can use ps.
         if self.android_version >= 7:
             result, output = self.adb.run_and_return_output(['shell', 'pidof', self.app_program])
-            return int(output) if result else None
+            if not result:
+                return None
+            pid = int(output)
+            if self.android_version >= 8 and self.config['app_package_name']:
+                # If a debuggable app with wrap.sh runs on Android O, the app will be started with
+                # logwrapper as below:
+                # 1. Zygote forks a child process, rename it to package_name.
+                # 2. The child process execute sh, which starts a child process running
+                # /system/bin/logwrapper.
+                # 3. logwrapper starts a child process running sh, which interprets wrap.sh.
+                # 4. wrap.sh starts a child process running the app.
+                # The problem here is we want to profile the process started in step 4, but
+                # sometimes we run into the process started in step 1. To solve it, we can check
+                # if the process has opened an apk file in some app dirs.
+                if not self._has_opened_apk_file(pid):
+                    return None
+            return pid
         result, output = self.adb.run_and_return_output(['shell', 'ps'], log_output=False)
         if not result:
             return None
@@ -205,6 +222,12 @@ class AppProfiler(object):
         return None
 
 
+    def _has_opened_apk_file(self, pid):
+        result, output = self.run_in_app_dir(['ls -l /proc/%d/fd' % pid],
+                                             check_result=False, log_output=False)
+        return result and re.search(r'app.*\.apk', output)
+
+
     def _get_app_environment(self):
         if not self.config['cmd']:
             if self.app_pid is None:
@@ -252,7 +275,7 @@ class AppProfiler(object):
                 searched_lib[item] = True
                 # Use '/' as path separator as item comes from android environment.
                 filename = item[item.rfind('/') + 1:]
-                dirname = '/data/local/tmp' + item[:item.rfind('/')]
+                dirname = '/data/local/tmp/native_libs' + item[:item.rfind('/')]
                 path = filename_dict.get(filename)
                 if path is None:
                     continue
@@ -295,8 +318,12 @@ class AppProfiler(object):
         except KeyboardInterrupt:
             self.stop_profiling()
             self.record_subproc = None
+            # Don't check return value of record_subproc. Because record_subproc also
+            # receives Ctrl-C, and always returns non-zero.
             returncode = 0
         log_debug('profiling result [%s]' % (returncode == 0))
+        if returncode != 0:
+            log_exit('Failed to record profiling data.')
 
 
     def start_profiling(self):
index bbee65a..c33bdb0 100644 (file)
@@ -153,7 +153,7 @@ class TestExampleBase(TestBase):
                          build_binary_cache=True, skip_compile=False, start_activity=True,
                          native_lib_dir=None, profile_from_launch=False, add_arch=False):
         args = ["app_profiler.py", "--app", self.package_name, "--apk", self.apk_path,
-                "-a", self.activity_name, "-r", record_arg, "-o", "perf.data"]
+                "-r", record_arg, "-o", "perf.data"]
         if not build_binary_cache:
             args.append("-nb")
         if skip_compile or self.__class__.compiled:
@@ -253,8 +253,6 @@ class TestExampleBase(TestBase):
         self.run_app_profiler(build_binary_cache=True)
         self.run_app_profiler(skip_compile=True)
         self.run_app_profiler(start_activity=False)
-        self.run_app_profiler(profile_from_launch=True, add_arch=True)
-
 
     def common_test_report(self):
         self.run_cmd(["report.py", "-h"])
@@ -278,7 +276,7 @@ class TestExampleBase(TestBase):
         self.run_cmd(["report_sample.py"])
         output = self.run_cmd(["report_sample.py", "perf.data"], return_output=True)
         self.check_strings_in_content(output, check_strings)
-        self.run_app_profiler(record_arg="-g --duration 3 -e cpu-cycles:u, --no-dump-symbols")
+        self.run_app_profiler(record_arg="-g --duration 3 -e cpu-cycles:u --no-dump-symbols")
         output = self.run_cmd(["report_sample.py", "--symfs", "binary_cache"], return_output=True)
         self.check_strings_in_content(output, check_strings)
 
@@ -328,6 +326,13 @@ class TestExamplePureJava(TestExampleBase):
     def test_app_profiler(self):
         self.common_test_app_profiler()
 
+    def test_app_profiler_profile_from_launch(self):
+        self.run_app_profiler(profile_from_launch=True, add_arch=True, build_binary_cache=False)
+        self.run_cmd(["report.py", "-g", "-o", "report.txt"])
+        self.check_strings_in_file("report.txt",
+            ["com.example.simpleperf.simpleperfexamplepurejava.MainActivity$1.run()",
+             "__start_thread"])
+
     def test_app_profiler_with_ctrl_c(self):
         if is_windows():
             return
@@ -447,6 +452,13 @@ class TestExampleWithNative(TestExampleBase):
         remove("binary_cache")
         self.run_app_profiler(native_lib_dir=self.example_path)
 
+    def test_app_profiler_profile_from_launch(self):
+        self.run_app_profiler(profile_from_launch=True, add_arch=True, build_binary_cache=False)
+        self.run_cmd(["report.py", "-g", "-o", "report.txt"])
+        self.check_strings_in_file("report.txt",
+            ["BusyLoopThread",
+             "__start_thread"])
+
     def test_report(self):
         self.common_test_report()
         self.run_cmd(["report.py", "-g", "-o", "report.txt"])
@@ -596,6 +608,13 @@ class TestExampleOfKotlin(TestExampleBase):
     def test_app_profiler(self):
         self.common_test_app_profiler()
 
+    def test_app_profiler_profile_from_launch(self):
+        self.run_app_profiler(profile_from_launch=True, add_arch=True, build_binary_cache=False)
+        self.run_cmd(["report.py", "-g", "-o", "report.txt"])
+        self.check_strings_in_file("report.txt",
+            ["com.example.simpleperf.simpleperfexampleofkotlin.MainActivity$createBusyThread$1.run()",
+             "__start_thread"])
+
     def test_report(self):
         self.common_test_report()
         self.run_cmd(["report.py", "-g", "-o", "report.txt"])