OSDN Git Service

simpleperf: replace config file with cmdline options.
authorYabin Cui <yabinc@google.com>
Thu, 13 Jul 2017 21:49:42 +0000 (14:49 -0700)
committerYabin Cui <yabinc@google.com>
Sat, 15 Jul 2017 00:40:36 +0000 (17:40 -0700)
1. For binary_cache_builder.py, annoate.py and pprof_proto_generator.py,
there are only a few options, use a config file seems overkill, so
replace them with cmdline options.

2. Add cmdline interface for app_profiler.py. It is to simplify usage,
and can be called from tests (will be added).

3. Simplify the way to find tools (including adb,readelf,addr2line),
try to find them in default install location of sdk and ndk. And output
error/warning msg if not found.

4. Raise exception in python are not always helpful for users to find
problems, because the error msg is hidden by exception stack dump. So
replace some log_fatal() with log_exit().

5. Change README.md accordingly.

Bug: http://b/32834638
Test: run scripts manually.

Change-Id: Ic60e496edbe748b801d35144da29f40c3db3e250

simpleperf/README.md
simpleperf/demo/README.md
simpleperf/scripts/annotate.config [deleted file]
simpleperf/scripts/annotate.py
simpleperf/scripts/app_profiler.config
simpleperf/scripts/app_profiler.py
simpleperf/scripts/binary_cache_builder.config [deleted file]
simpleperf/scripts/binary_cache_builder.py
simpleperf/scripts/pprof_proto_generator.config [deleted file]
simpleperf/scripts/pprof_proto_generator.py
simpleperf/scripts/utils.py

index 126fbc6..5311f03 100644 (file)
@@ -31,7 +31,6 @@ Bugs and feature requests can be submitted at http://github.com/android-ndk/ndk/
 - [Answers to common issues](#answers-to-common-issues)
     - [The correct way to pull perf.data on host](#the-correct-way-to-pull-perfdata-on-host)
 
-
 ## Simpleperf introduction
 
 ### Why simpleperf
@@ -777,23 +776,16 @@ generate profiling data in a format acceptable by pprof.
 `annotate.py` reads perf.data, binaries in `binary-cache` (collected by `app-profiler.py`)
 and source code, and generates annoated source code in `annotated_files/`.
 
-It is configured by `annotate.config`.
-
-**1. Fill `annotate.config`**
-
-    Change `source_dirs` line to source_dirs = ["../SimpleperfExamplePureJava"]
-    Change `addr2line_path` line to addr2line_path = "addr2line"
-
-`addr2line` is need to annotate source code. It can be found in Android ndk release.
-Please set `addr2line_path` to the location of `addr2line` if it can't be found
-in PATH environment variable.
-
-**2. Run `annotate.py`**
+**1. Run annotate.py**
 
-    $ python annotate.py
+    $ python annotate.py -s ../SimpleperfExamplePureJava
 
+`addr2line` is need to annotate source code. It can be found in Android ndk
+release, in paths like toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-addr2line.
+Please use `--addr2line` option to set the path of `addr2line` if annotate.py
+can't find it.
 
-**3. Read annotated code**
+**2. Read annotated code**
 
 The annotated source code is located at `annotated_files/`.
 `annotated_files/summary` shows how each source file is annotated.
index 1ecd9dd..2d2c377 100644 (file)
@@ -46,10 +46,7 @@ $ adb install -r app/build/outputs/apk/app-profiling.apk
 2. Record profiling data:
 ```
 $ cd ../../scripts/
-$ gvim app_profiler.config
-    change app_package_name line to: app_package_name = "com.example.simpleperf.simpleperfexamplepurejava"
-$ python app_profiler.py
-    It runs the application and collects profiling data in perf.data, binaries on device in binary_cache/.
+$ python app_profiler.py -p com.example.simpleperf.simpleperfexamplepurejava
 ```
 
 3. Show profiling data:
@@ -61,9 +58,7 @@ a. show call graph in txt mode
 b. show call graph in gui mode
     $ python report.py -g
 c. show samples in source code
-    $ gvim annotate.config
-        change source_dirs line to: source_dirs = ["../demo/SimpleperfExamplePureJava"]
-    $ python annotate.py
+    $ python annotate.py -s ../demo/SimpleperfExamplePureJava
     $ gvim annotated_files/java/com/example/simpleperf/simpleperfexamplepurejava/MainActivity.java
         check the annoated source file MainActivity.java.
 ```
@@ -89,9 +84,7 @@ $ adb install -r app/build/outputs/apk/app-profiling.apk
 2. Record profiling data:
 ```
 $ cd ../../scripts/
-$ gvim app_profiler.config
-    change app_package_name line to: app_package_name = "com.example.simpleperf.simpleperfexamplewithnative"
-$ python app_profiler.py
+$ python app_profiler.py -p com.example.simpleperf.simpleperfexamplewithnative
     It runs the application and collects profiling data in perf.data, binaries on device in binary_cache/.
 ```
 
@@ -104,9 +97,7 @@ a. show call graph in txt mode
 b. show call graph in gui mode
     $ python report.py -g
 c. show samples in source code
-    $ gvim annotate.config
-        change source_dirs line to: source_dirs = ["../demo/SimpleperfExampleWithNative"]
-    $ python annotate.py
+    $ python annotate.py -s ../demo/SimpleperfExampleWithNative
     $ find . -name "native-lib.cpp" | xargs gvim
         check the annoated source file native-lib.cpp.
 ```
@@ -132,9 +123,7 @@ $ adb install -r app/build/outputs/apk/profiling/app-profiling.apk
 2. Record profiling data:
 ```
 $ cd ../../scripts/
-$ gvim app_profiler.config
-    change app_package_name line to: app_package_name = "com.example.simpleperf.simpleperfexampleofkotlin"
-$ python app_profiler.py
+$ python app_profiler.py -p com.example.simpleperf.simpleperfexampleofkotlin
     It runs the application and collects profiling data in perf.data, binaries on device in binary_cache/.
 ```
 
@@ -147,9 +136,7 @@ a. show call graph in txt mode
 b. show call graph in gui mode
     $ python report.py -g
 c. show samples in source code
-    $ gvim annotate.config
-        change source_dirs line to: source_dirs = ["../demo/SimpleperfExampleOfKotlin"]
-    $ python annotate.py
+    $ python annotate.py -s ../demo/SimpleperfExampleOfKotlin
     $ find . -name "MainActivity.kt" | xargs gvim
         check the annoated source file MainActivity.kt.
 ```
diff --git a/simpleperf/scripts/annotate.config b/simpleperf/scripts/annotate.config
deleted file mode 100644 (file)
index 2e5db55..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-# This configuration is written in python and used by annotate.py.
-
-import os
-
-# A list of profiling record files. By default it only contains perf.data.
-perf_data_list = ["perf.data"]
-
-
-# Directory used to read binaries with debug info. Ideally, it should be
-# set to the path of binary_cache_dir collected by app_profiler.py.
-# Set to "" if not available.
-symfs_dir = "binary_cache"
-
-
-# File path used to find kernel symbols. Set to "" if not available.
-kallsyms = ""
-
-
-# A list of directories used to find source files.
-source_dirs = []
-
-
-# Directory used to output annotated source files.
-annotate_dest_dir = "annotated_files"
-
-
-# Sample Filters
-# Use samples only in threads with selected names.
-comm_filters = []
-# Use samples only in processes with selected process ids.
-pid_filters = []
-# Use samples only in threads with selected thread ids.
-tid_filters = []
-# Use samples only in selected binaries.
-dso_filters = []
-
-
-# We use addr2line to map virtual address to source file and source line.
-# So set the path to addr2line here.
-addr2line_path = "addr2line"
\ No newline at end of file
index 2b253bd..27414ac 100644 (file)
@@ -57,7 +57,12 @@ class Addr2Line(object):
     """
     def __init__(self, addr2line_path, symfs_dir=None):
         self.dso_dict = dict()
-        self.addr2line_path = addr2line_path
+        if addr2line_path and is_executable_available(addr2line_path):
+            self.addr2line_path = addr2line_path
+        else:
+            self.addr2line_path = find_tool_path('addr2line')
+            if not self.addr2line_path:
+                log_exit("Can't find addr2line.")
         self.symfs_dir = symfs_dir
 
 
@@ -267,27 +272,28 @@ class SourceFileAnnotator(object):
     """group code for annotating source files"""
     def __init__(self, config):
         # check config variables
-        config_names = ['perf_data_list', 'symfs_dir', 'source_dirs',
-                        'annotate_dest_dir', 'comm_filters', 'pid_filters',
-                        'tid_filters', 'dso_filters', 'addr2line_path']
+        config_names = ['perf_data_list', 'source_dirs', 'comm_filters',
+                        'pid_filters', 'tid_filters', 'dso_filters', 'addr2line_path']
         for name in config_names:
             if name not in config:
-                log_fatal('config [%s] is missing' % name)
-        symfs_dir = config['symfs_dir']
-        if symfs_dir and not os.path.isdir(symfs_dir):
-            log_fatal('[symfs_dir] "%s" is not a dir' % symfs_dir)
-        kallsyms = config['kallsyms']
-        if kallsyms and not os.path.isfile(kallsyms):
-            log_fatal('[kallsyms] "%s" is not a file' % kallsyms)
+                log_exit('config [%s] is missing' % name)
+        symfs_dir = 'binary_cache'
+        if not os.path.isdir(symfs_dir):
+            symfs_dir = None
+        kallsyms = 'binary_cache/kallsyms'
+        if not os.path.isfile(kallsyms):
+            kallsyms = None
         source_dirs = config['source_dirs']
         for dir in source_dirs:
             if not os.path.isdir(dir):
-                log_fatal('[source_dirs] "%s" is not a dir' % dir)
+                log_exit('[source_dirs] "%s" is not a dir' % dir)
+        if not config['source_dirs']:
+            log_exit('Please set source directories.')
 
         # init member variables
         self.config = config
-        self.symfs_dir = config.get('symfs_dir')
-        self.kallsyms = config.get('kallsyms')
+        self.symfs_dir = symfs_dir
+        self.kallsyms = kallsyms
         self.comm_filter = set(config['comm_filters']) if config.get('comm_filters') else None
         if config.get('pid_filters'):
             self.pid_filter = {int(x) for x in config['pid_filters']}
@@ -299,6 +305,7 @@ class SourceFileAnnotator(object):
             self.tid_filter = None
         self.dso_filter = set(config['dso_filters']) if config.get('dso_filters') else None
 
+        config['annotate_dest_dir'] = 'annotated_files'
         output_dir = config['annotate_dest_dir']
         if os.path.isdir(output_dir):
             shutil.rmtree(output_dir)
@@ -623,14 +630,41 @@ class SourceFileAnnotator(object):
                 wf.write(annotate)
                 wf.write(lines[line-1])
 
+def main():
+    parser = argparse.ArgumentParser(description=
+"""Annotate source files based on profiling data. It reads line information from
+binary_cache generated by app_profiler.py or binary_cache_builder.py, and
+generate annotated source files in annotated_files directory.""")
+    parser.add_argument('-i', '--perf_data_list', nargs='+', action='append', help=
+"""The paths of profiling data. Default is perf.data.""")
+    parser.add_argument('-s', '--source_dirs', nargs='+', action='append', help=
+"""Directories to find source files.""")
+    parser.add_argument('--comm', nargs='+', action='append', help=
+"""Use samples only in threads with selected names.""")
+    parser.add_argument('--pid', nargs='+', action='append', help=
+"""Use samples only in processes with selected process ids.""")
+    parser.add_argument('--tid', nargs='+', action='append', help=
+"""Use samples only in threads with selected thread ids.""")
+    parser.add_argument('--dso', nargs='+', action='append', help=
+"""Use samples only in selected binaries.""")
+    parser.add_argument('--addr2line', help=
+"""Set the path of addr2line.""")
 
-if __name__ == '__main__':
-    parser = argparse.ArgumentParser(
-        description='Annotate based on perf.data. See configurations in annotate.config.')
-    parser.add_argument('--config', default='annotate.config',
-                        help='Set configuration file. Default is annotate.config.')
     args = parser.parse_args()
-    config = load_config(args.config)
+    config = {}
+    config['perf_data_list'] = flatten_arg_list(args.perf_data_list)
+    if not config['perf_data_list']:
+        config['perf_data_list'].append('perf.data')
+    config['source_dirs'] = flatten_arg_list(args.source_dirs)
+    config['comm_filters'] = flatten_arg_list(args.comm)
+    config['pid_filters'] = flatten_arg_list(args.pid)
+    config['tid_filters'] = flatten_arg_list(args.tid)
+    config['dso_filters'] = flatten_arg_list(args.dso)
+    config['addr2line_path'] = args.addr2line
+
     annotator = SourceFileAnnotator(config)
     annotator.annotate()
     log_info('annotate finish successfully, please check result in annotated_files/.')
+
+if __name__ == '__main__':
+    main()
index 752b85f..9102cf0 100644 (file)
@@ -4,7 +4,7 @@ import os
 import os.path
 
 # The name of the android package, like com.example.android.
-app_package_name = "com.example.android"
+app_package_name = ""
 
 
 # Path of android studio project. It is used to find debug version of native shared libraries.
@@ -43,11 +43,6 @@ launch_activity = '.MainActivity'
 launch_inst_test = ''
 
 
-if recompile_app and not launch_activity and not launch_inst_test:
-    raise Exception('one of [launch_activity or launch_inst_test] is'
-        + 'needed for [recompile_app] to take effect.')
-
-
 # Profiling record options that will be passed directly to `simpleperf record` command on device.
 # You can set how long to profile using "--duration" option, or use Ctrl-C to stop profiling.
 record_options = "-e cpu-cycles:u -f 4000 -g --duration 10"
@@ -57,15 +52,6 @@ record_options = "-e cpu-cycles:u -f 4000 -g --duration 10"
 perf_data_path = "perf.data"
 
 
-# The path of adb.
-adb_path = "adb"
-
-
-# The path of readelf, used to read build id of files in binary cache.
-# Set to "" if not available.
-readelf_path = "readelf"
-
-
-# binary_cache_dir is used to cache binaries pulled from device. To report precisely, we pull each
-# binary hit by perf.data on host.
-binary_cache_dir = "binary_cache"
+# Collect binaries used in profiling data from device to binary_cache directory.
+# It can be used to annotate source code.
+collect_binaries = True
index e2a137f..683d6b4 100644 (file)
@@ -43,27 +43,34 @@ class AppProfiler(object):
        3. Collect profiling data.
     """
     def __init__(self, config):
-        # check config variables
+        self.check_config(config)
+        self.config = config
+        self.adb = AdbHelper()
+        self.is_root_device = False
+        self.android_version = 0
+        self.device_arch = None
+        self.app_arch = None
+        self.app_pid = None
+
+
+    def check_config(self, config):
         config_names = ['app_package_name', 'native_lib_dir', 'apk_file_path',
                         'recompile_app', 'launch_activity', 'launch_inst_test',
-                        'record_options', 'perf_data_path', 'adb_path', 'readelf_path',
-                        'binary_cache_dir']
+                        'record_options', 'perf_data_path']
         for name in config_names:
             if name not in config:
-                log_fatal('config [%s] is missing' % name)
+                log_exit('config [%s] is missing' % name)
+        if not config['app_package_name']:
+            log_exit("The package name of the application hasn't been set")
         native_lib_dir = config.get('native_lib_dir')
         if native_lib_dir and not os.path.isdir(native_lib_dir):
-            log_fatal('[native_lib_dir] "%s" is not a dir' % native_lib_dir)
+            log_exit('[native_lib_dir] "%s" is not a dir' % native_lib_dir)
         apk_file_path = config.get('apk_file_path')
         if apk_file_path and not os.path.isfile(apk_file_path):
-            log_fatal('[apk_file_path] "%s" is not a file' % apk_file_path)
-        self.config = config
-        self.adb = AdbHelper(self.config['adb_path'])
-        self.is_root_device = False
-        self.android_version = 0
-        self.device_arch = None
-        self.app_arch = None
-        self.app_pid = None
+            log_exit('[apk_file_path] "%s" is not a file' % apk_file_path)
+        if config['recompile_app']:
+            if not config['launch_activity'] and not config['launch_inst_test']:
+                log_exit('one of launch_activity and launch_inst_test is needed for recompile app')
 
 
     def profile(self):
@@ -159,13 +166,13 @@ class AppProfiler(object):
             activity = self.config['app_package_name'] + '/' + self.config['launch_activity']
             result = self.adb.run(['shell', 'am', 'start', '-n', activity])
             if not result:
-                log_fatal("Can't start activity %s" % activity)
+                log_exit("Can't start activity %s" % activity)
         else:
             runner = self.config['app_package_name'] + '/android.support.test.runner.AndroidJUnitRunner'
             result = self.adb.run(['shell', 'am', 'instrument', '-e', 'class',
                                    self.config['launch_inst_test'], runner])
             if not result:
-                log_fatal("Can't start instrumentation test  %s" % self.config['launch_inst_test'])
+                log_exit("Can't start instrumentation test  %s" % self.config['launch_inst_test'])
 
         for i in range(10):
             pid = self._find_app_process()
@@ -173,7 +180,7 @@ class AppProfiler(object):
                 return
             time.sleep(1)
             log_info('Wait for the app process for %d seconds' % (i + 1))
-        log_fatal("Can't find the app process")
+        log_exit("Can't find the app process")
 
 
     def _find_app_process(self):
@@ -192,7 +199,7 @@ class AppProfiler(object):
     def _get_app_environment(self):
         self.app_pid = self._find_app_process()
         if self.app_pid is None:
-            log_fatal("can't find process for app [%s]" % self.config['app_package_name'])
+            log_exit("can't find process for app [%s]" % self.config['app_package_name'])
         if self.device_arch in ['aarch64', 'x86_64']:
             output = self.run_in_app_dir(['cat', '/proc/%d/maps' % self.app_pid])
             if output.find('linker64') != -1:
@@ -300,12 +307,14 @@ class AppProfiler(object):
         self.run_in_app_dir(['cat perf.data | tee /data/local/tmp/perf.data >/dev/null'])
         self.adb.check_run_and_return_output(['pull', '/data/local/tmp/perf.data',
                                               self.config['perf_data_path']])
-        config = copy.copy(self.config)
-        config['symfs_dirs'] = []
-        if self.config['native_lib_dir']:
-            config['symfs_dirs'].append(self.config['native_lib_dir'])
-        binary_cache_builder = BinaryCacheBuilder(config)
-        binary_cache_builder.build_binary_cache()
+        if self.config['collect_binaries']:
+            config = copy.copy(self.config)
+            config['binary_cache_dir'] = 'binary_cache'
+            config['symfs_dirs'] = []
+            if self.config['native_lib_dir']:
+                config['symfs_dirs'].append(self.config['native_lib_dir'])
+            binary_cache_builder = BinaryCacheBuilder(config)
+            binary_cache_builder.build_binary_cache()
 
 
     def run_in_app_dir(self, args, stdout_file=None, check_result=True):
@@ -323,13 +332,62 @@ class AppProfiler(object):
         else:
             return ['shell', 'run-as', self.config['app_package_name']] + args
 
-
-if __name__ == '__main__':
+def main():
     parser = argparse.ArgumentParser(
-        description='Profile an android app. See configurations in app_profiler.config.')
-    parser.add_argument('--config', default='app_profiler.config',
-                        help='Set configuration file. Default is app_profiler.config.')
+        description=
+"""Profile an android app. See configurations in app_profiler.config.""")
+    parser.add_argument('--config', default='app_profiler.config', help=
+"""Set configuration file. Default is app_profiler.config. The configurations
+can be overridden by options in cmdline.""")
+    parser.add_argument('-p', '--package_name', help=
+"""The package name of the profiled Android app.""")
+    parser.add_argument('-lib', '--native_lib_dir', help=
+"""Path to find debug version of native shared libraries used in the app.""")
+    parser.add_argument('-nc', '--skip_recompile', action='store_true', help=
+"""By default we recompile java bytecode to native instructions to profile java
+code. It takes some time. You can skip it if the code has been compiled or you
+don't need to profile java code.""")
+    parser.add_argument('--apk', help=
+"""Apk file of the profiled app, used on Android version <= M, which needs to
+reinstall the app to recompile it.""")
+    parser.add_argument('-a', '--activity', help=
+"""Start an activity before profiling. It can be used to profile the startup
+time of an activity. Default is .MainActivity.""")
+    parser.add_argument('-t', '--test', help=
+"""Start an instrumentation test before profiling. It can be used to profile
+an instrumentation test.""")
+    parser.add_argument('-r', '--record_options', help=
+"""Set options for `simpleperf record` command. Default is "-e cpu-cycles:u -f 4000 -g --duration 10".""")
+    parser.add_argument('-o', '--perf_data_path', help=
+"""The path to store profiling data. Default is perf.data.""")
+    parser.add_argument('-nb', '--skip_collect_binaries', action='store_true', help=
+"""By default we collect binaries used in profiling data from device to
+binary_cache directory. It can be used to annotate source code. This option skips it.""")
     args = parser.parse_args()
     config = load_config(args.config)
+    if args.package_name:
+        config['app_package_name'] = args.package_name
+    if args.native_lib_dir:
+        config['native_lib_dir'] = args.native_lib_dir
+    if args.skip_recompile:
+        config['recompile_app'] = False
+    if args.apk:
+        config['apk'] = args.apk
+    if args.activity:
+        config['launch_activity'] = args.activity
+        config['launch_inst_test'] = None
+    if args.test:
+        config['launch_inst_test'] = args.test
+        config['launch_activity'] = None
+    if args.record_options:
+        config['record_options'] = args.record_options
+    if args.perf_data_path:
+        config['perf_data_path'] = args.perf_data_path
+    if args.skip_collect_binaries:
+        config['collect_binaries'] = False
+
     profiler = AppProfiler(config)
     profiler.profile()
+
+if __name__ == '__main__':
+    main()
\ No newline at end of file
diff --git a/simpleperf/scripts/binary_cache_builder.config b/simpleperf/scripts/binary_cache_builder.config
deleted file mode 100644 (file)
index 49cc5ae..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-# This configuration is written in python and used by binary_cache_builder.py.
-
-import os
-import os.path
-
-# path of profiling record data.
-perf_data_path = "perf.data"
-
-
-# directories to find binaries with symbols and debug information.
-# If binaries are found in any of these directories, having the same build_id
-# as the one recorded in perf.data, then we copy the binary in the directory
-# instead of pulling the binary from device.
-symfs_dirs = []
-
-
-# directory to cache binaries. To report precisely, we pull needed binaries
-# to host. However, We don't need to pull a binary if there is already a binary
-# in binary_cache_dir having the same build_id as the one on device.
-binary_cache_dir = "binary_cache"
-
-
-# path of adb.
-adb_path = "adb"
-
-# path of readelf, set to "" if not available.
-readelf_path = "readelf"
\ No newline at end of file
index 950e2fb..4ce4a32 100644 (file)
@@ -36,22 +36,23 @@ from utils import *
 class BinaryCacheBuilder(object):
     """Collect all binaries needed by perf.data in binary_cache."""
     def __init__(self, config):
-        config_names = ['perf_data_path', 'symfs_dirs', 'adb_path',
-                        'readelf_path', 'binary_cache_dir']
+        config_names = ['perf_data_path', 'symfs_dirs']
         for name in config_names:
             if name not in config:
-                log_fatal('config for "%s" is missing' % name)
+                log_exit('config for "%s" is missing' % name)
 
         self.perf_data_path = config.get('perf_data_path')
         if not os.path.isfile(self.perf_data_path):
-            log_fatal("can't find file %s" % self.perf_data_path)
+            log_exit("can't find file %s" % self.perf_data_path)
         self.symfs_dirs = config.get('symfs_dirs')
         for symfs_dir in self.symfs_dirs:
             if not os.path.isdir(symfs_dir):
-                log_fatal("symfs_dir '%s' is not a directory" % symfs_dir)
-        self.adb = AdbHelper(config['adb_path'])
-        self.readelf_path = config['readelf_path']
-        self.binary_cache_dir = config['binary_cache_dir']
+                log_exit("symfs_dir '%s' is not a directory" % symfs_dir)
+        self.adb = AdbHelper()
+        self.readelf_path = find_tool_path('readelf')
+        if not self.readelf_path and self.symfs_dirs:
+            log_warning("Debug shared libraries on host are not used because can't find readelf.")
+        self.binary_cache_dir = 'binary_cache'
         if not os.path.isdir(self.binary_cache_dir):
             os.makedirs(self.binary_cache_dir)
 
@@ -227,12 +228,22 @@ class BinaryCacheBuilder(object):
             self.adb.run(['pull', '/proc/kallsyms', file])
 
 
-if __name__ == '__main__':
-    parser = argparse.ArgumentParser(
-        description="Pull binaries needed by perf.data from device to binary_cache.")
-    parser.add_argument('--config', default='binary_cache_builder.config',
-                        help='Set configuration file. Default is binary_cache_builder.config.')
+def main():
+    parser = argparse.ArgumentParser(description=
+"""Pull binaries needed by perf.data from device to binary_cache directory.""")
+    parser.add_argument('-i', '--perf_data_path', default='perf.data', help=
+"""The path of profiling data.""")
+    parser.add_argument('-lib', '--native_lib_dir', nargs='+', help=
+"""Path to find debug version of native shared libraries used in the app.""",
+                        action='append')
     args = parser.parse_args()
-    config = load_config(args.config)
+    config = {}
+    config['perf_data_path'] = args.perf_data_path
+    config['symfs_dirs'] = flatten_arg_list(args.native_lib_dir)
+
     builder = BinaryCacheBuilder(config)
-    builder.build_binary_cache()
\ No newline at end of file
+    builder.build_binary_cache()
+
+
+if __name__ == '__main__':
+    main()
\ No newline at end of file
diff --git a/simpleperf/scripts/pprof_proto_generator.config b/simpleperf/scripts/pprof_proto_generator.config
deleted file mode 100644 (file)
index aa82b92..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-# This configuration is written in python and used by binary_cache_builder.py.
-
-import os
-import os.path
-
-# path of profiling record data.
-perf_data_path = "perf.data"
-
-# output path.
-output_file = "pprof.profile"
-
-
-# directory to cache binaries with symbols and debug information.
-# Can be generated by binary_cache_builder.py.
-binary_cache_dir = "binary_cache"
-
-
-# path to find kernel symbols.
-kallsyms = ""
-
-
-if binary_cache_dir:
-  path = os.path.join(binary_cache_dir, 'kallsyms')
-  if os.path.isfile(path):
-    kallsyms = path
-
-# Sample Filters
-# Use samples only in threads with selected names.
-comm_filters = []
-# Use samples only in processes with selected process ids.
-pid_filters = []
-# Use samples only in threads with selected thread ids.
-tid_filters = []
-# Use samples only in selected binaries.
-dso_filters = []
-
-# We use addr2line to map virtual address to source file and source line.
-# So set the path to addr2line here.
-addr2line_path = "addr2line"
\ No newline at end of file
index fa2fdb1..ad51911 100644 (file)
@@ -255,15 +255,16 @@ class PprofProfileGenerator(object):
         self.config = config
         self.lib = ReportLib()
 
-        if config.get('binary_cache_dir'):
-            if not os.path.isdir(config.get('binary_cache_dir')):
-                config['binary_cache_dir'] = ''
-            else:
-                self.lib.SetSymfs(config['binary_cache_dir'])
+        config['binary_cache_dir'] = 'binary_cache'
+        if not os.path.isdir(config['binary_cache_dir']):
+            config['binary_cache_dir'] = None
+        else:
+            self.lib.SetSymfs(config['binary_cache_dir'])
         if config.get('record_file'):
             self.lib.SetRecordFile(config['record_file'])
-        if config.get('kallsyms'):
-            self.lib.SetKallsymsFile(config['kallsyms'])
+        kallsyms = 'binary_cache/kallsyms'
+        if os.path.isfile(kallsyms):
+            self.lib.SetKallsymsFile(kallsyms)
         self.comm_filter = set(config['comm_filters']) if config.get('comm_filters') else None
         if config.get('pid_filters'):
             self.pid_filter = {int(x) for x in config['pid_filters']}
@@ -318,8 +319,7 @@ class PprofProfileGenerator(object):
                 self.add_sample(sample)
 
         # 2. Generate line info for locations and functions.
-        if self.config.get('binary_cache_dir'):
-            self.gen_source_lines()
+        self.gen_source_lines()
 
         # 3. Produce samples/locations/functions in profile
         for sample in self.sample_list:
@@ -446,6 +446,15 @@ class PprofProfileGenerator(object):
 
     def gen_source_lines(self):
         # 1. Create Addr2line instance
+        if not self.config.get('binary_cache_dir'):
+            log_info("Can't generate line information because binary_cache is missing.")
+            return
+        if not self.config['addr2line_path'] or not is_executable_available(
+            self.config['addr2line_path']):
+            if not find_tool_path('addr2line'):
+                log_info("Can't generate line information because can't find addr2line.")
+                return
+
         addr2line = Addr2Line(self.config['addr2line_path'], self.config['binary_cache_dir'])
 
         # 2. Put all needed addresses to it.
@@ -542,16 +551,38 @@ class PprofProfileGenerator(object):
 
 def main():
     parser = argparse.ArgumentParser(description='Generate pprof profile data in pprof.profile.')
-    parser.add_argument('--show', nargs=1, help='print existing profile.pprof')
-    parser.add_argument('--config', nargs=1, default='pprof_proto_generator.config',
-                        help='Set config file, default is gen_pprof_proto.config.')
-    args = parser.parse_args(sys.argv[1:])
+    parser.add_argument('--show', nargs='?', action='append', help='print existing pprof.profile.')
+    parser.add_argument('-i', '--perf_data_path', default='perf.data', help=
+"""The path of profiling data.""")
+    parser.add_argument('-o', '--output_file', default='pprof.profile', help=
+"""The path of generated pprof profile data.""")
+    parser.add_argument('--comm', nargs='+', action='append', help=
+"""Use samples only in threads with selected names.""")
+    parser.add_argument('--pid', nargs='+', action='append', help=
+"""Use samples only in processes with selected process ids.""")
+    parser.add_argument('--tid', nargs='+', action='append', help=
+"""Use samples only in threads with selected thread ids.""")
+    parser.add_argument('--dso', nargs='+', action='append', help=
+"""Use samples only in selected binaries.""")
+    parser.add_argument('--addr2line', help=
+"""Set the path of addr2line.""")
+
+    args = parser.parse_args()
     if args.show:
-        profile = load_pprof_profile(args.show[0])
+        show_file = args.show[0] if args.show[0] else 'pprof.profile'
+        profile = load_pprof_profile(show_file)
         printer = PprofProfilePrinter(profile)
         printer.show()
         return
-    config = load_config(args.config)
+
+    config = {}
+    config['perf_data_path'] = args.perf_data_path
+    config['output_file'] = args.output_file
+    config['comm_filters'] = flatten_arg_list(args.comm)
+    config['pid_filters'] = flatten_arg_list(args.pid)
+    config['tid_filters'] = flatten_arg_list(args.tid)
+    config['dso_filters'] = flatten_arg_list(args.dso)
+    config['addr2line_path'] = args.addr2line
     generator = PprofProfileGenerator(config)
     profile = generator.gen()
     store_pprof_profile(config['output_file'], profile)
index a0b3b3b..2e2c69a 100644 (file)
@@ -20,6 +20,7 @@
 
 from __future__ import print_function
 import logging
+import os
 import os.path
 import subprocess
 import sys
@@ -31,6 +32,9 @@ def get_script_dir():
 def is_windows():
     return sys.platform == 'win32' or sys.platform == 'cygwin'
 
+def is_darwin():
+    return sys.platform == 'darwin'
+
 def is_python3():
     return sys.version_info >= (3, 0)
 
@@ -50,6 +54,9 @@ def log_warning(msg):
 def log_fatal(msg):
     raise Exception(msg)
 
+def log_exit(msg):
+    sys.exit(msg)
+
 def str_to_bytes(str):
     if not is_python3():
         return str
@@ -95,8 +102,75 @@ def get_host_binary_path(binary_name):
     return binary_path
 
 
+def is_executable_available(executable, option='--help'):
+    """ Run an executable to see if it exists. """
+    try:
+        subproc = subprocess.Popen([executable, option], stdout=subprocess.PIPE,
+                                   stderr=subprocess.PIPE)
+        subproc.communicate()
+        return subproc.returncode == 0
+    except:
+        return False
+
+expected_tool_paths = {
+    'adb': {
+        'test_option': 'version',
+        'darwin': [(True, 'Library/Android/sdk/platform-tools/adb'),
+                   (False, '../../platform-tools/adb')],
+        'linux': [(True, 'Android/Sdk/platform-tools/adb'),
+                  (False, '../../platform-tools/adb')],
+        'windows': [(True, 'AppData/Local/Android/sdk/platform-tools/adb'),
+                    (False, '../../platform-tools/adb')],
+    },
+    'readelf': {
+        'test_option': '--help',
+        'darwin': [(True, 'Library/Android/sdk/ndk-bundle/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-readelf'),
+                   (False, '../toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-readelf')],
+        'linux': [(True, 'Android/Sdk/ndk-bundle/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-readelf'),
+                  (False, '../toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-readelf')],
+        'windows': [(True, 'AppData/Local/Android/sdk/ndk-bundle/toolchains/aarch64-linux-android-4.9/prebuilt/windows-x86_64/bin/aarch64-linux-android-readelf'),
+                    (False, '../toolchains/aarch64-linux-android-4.9/prebuilt/windows-x86_64/bin/aarch64-linux-android-readelf')],
+    },
+    'addr2line': {
+        'test_option': '--help',
+        'darwin': [(True, 'Library/Android/sdk/ndk-bundle/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line'),
+                   (False, '../toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line')],
+        'linux': [(True, 'Android/Sdk/ndk-bundle/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-addr2line'),
+                  (False, '../toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-addr2line')],
+        'windows': [(True, 'AppData/Local/Android/sdk/ndk-bundle/toolchains/aarch64-linux-android-4.9/prebuilt/windows-x86_64/bin/aarch64-linux-android-addr2line'),
+                    (False, '../toolchains/aarch64-linux-android-4.9/prebuilt/windows-x86_64/bin/aarch64-linux-android-addr2line')],
+    },
+}
+
+def find_tool_path(toolname):
+    if toolname not in expected_tool_paths:
+        return None
+    test_option = expected_tool_paths[toolname]['test_option']
+    if is_executable_available(toolname, test_option):
+        return toolname
+    platform = 'linux'
+    if is_windows():
+        platform = 'windows'
+    elif is_darwin():
+        platform = 'darwin'
+    paths = expected_tool_paths[toolname][platform]
+    home = os.environ.get('HOMEPATH') if is_windows() else os.environ.get('HOME')
+    for (relative_to_home, path) in paths:
+        path = path.replace('/', os.sep)
+        if relative_to_home:
+            path = os.path.join(home, path)
+        else:
+            path = os.path.join(get_script_dir(), path)
+        if is_executable_available(path, test_option):
+            return path
+    return None
+
+
 class AdbHelper(object):
-    def __init__(self, adb_path):
+    def __init__(self):
+        adb_path = find_tool_path('adb')
+        if not adb_path:
+            log_exit("Can't find adb in PATH environment.")
         self.adb_path = adb_path
 
 
@@ -116,7 +190,7 @@ class AdbHelper(object):
             (stdoutdata, _) = subproc.communicate()
             returncode = subproc.returncode
         result = (returncode == 0)
-        if stdoutdata:
+        if stdoutdata and adb_args[1] != 'push' and adb_args[1] != 'pull':
             stdoutdata = bytes_to_str(stdoutdata)
             log_debug(stdoutdata)
         log_debug('run adb cmd: %s  [result %s]' % (adb_args, result))
@@ -129,7 +203,7 @@ class AdbHelper(object):
     def check_run_and_return_output(self, adb_args, stdout_file=None):
         result, stdoutdata = self.run_and_return_output(adb_args, stdout_file)
         if not result:
-            log_fatal('run "adb %s" failed' % adb_args)
+            log_exit('run "adb %s" failed' % adb_args)
         return stdoutdata
 
 
@@ -161,7 +235,7 @@ class AdbHelper(object):
 
 def load_config(config_file):
     if not os.path.exists(config_file):
-        log_fatal("can't find config_file: %s" % config_file)
+        log_exit("can't find config_file: %s" % config_file)
     config = {}
     if is_python3():
         with open(config_file, 'r') as fh:
@@ -172,4 +246,12 @@ def load_config(config_file):
     return config
 
 
+def flatten_arg_list(arg_list):
+    res = []
+    if arg_list:
+        for items in arg_list:
+            res += items
+    return res
+
+
 logging.getLogger().setLevel(logging.DEBUG)