diff options
-rw-r--r-- | simpleperf/README.md | 22 | ||||
-rw-r--r-- | simpleperf/demo/README.md | 25 | ||||
-rw-r--r-- | simpleperf/scripts/annotate.config | 40 | ||||
-rw-r--r-- | simpleperf/scripts/annotate.py | 74 | ||||
-rw-r--r-- | simpleperf/scripts/app_profiler.config | 22 | ||||
-rw-r--r-- | simpleperf/scripts/app_profiler.py | 114 | ||||
-rw-r--r-- | simpleperf/scripts/binary_cache_builder.config | 27 | ||||
-rw-r--r-- | simpleperf/scripts/binary_cache_builder.py | 41 | ||||
-rw-r--r-- | simpleperf/scripts/pprof_proto_generator.config | 39 | ||||
-rw-r--r-- | simpleperf/scripts/pprof_proto_generator.py | 61 | ||||
-rw-r--r-- | simpleperf/scripts/utils.py | 90 |
11 files changed, 315 insertions, 240 deletions
diff --git a/simpleperf/README.md b/simpleperf/README.md index 126fbc65..5311f03d 100644 --- a/simpleperf/README.md +++ b/simpleperf/README.md @@ -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. diff --git a/simpleperf/demo/README.md b/simpleperf/demo/README.md index 1ecd9dd5..2d2c377a 100644 --- a/simpleperf/demo/README.md +++ b/simpleperf/demo/README.md @@ -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 index 2e5db555..00000000 --- a/simpleperf/scripts/annotate.config +++ /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 diff --git a/simpleperf/scripts/annotate.py b/simpleperf/scripts/annotate.py index 2b253bd7..27414ac6 100644 --- a/simpleperf/scripts/annotate.py +++ b/simpleperf/scripts/annotate.py @@ -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() diff --git a/simpleperf/scripts/app_profiler.config b/simpleperf/scripts/app_profiler.config index 752b85f7..9102cf0c 100644 --- a/simpleperf/scripts/app_profiler.config +++ b/simpleperf/scripts/app_profiler.config @@ -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 diff --git a/simpleperf/scripts/app_profiler.py b/simpleperf/scripts/app_profiler.py index e2a137f6..683d6b42 100644 --- a/simpleperf/scripts/app_profiler.py +++ b/simpleperf/scripts/app_profiler.py @@ -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 index 49cc5ae7..00000000 --- a/simpleperf/scripts/binary_cache_builder.config +++ /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 diff --git a/simpleperf/scripts/binary_cache_builder.py b/simpleperf/scripts/binary_cache_builder.py index 950e2fb7..4ce4a320 100644 --- a/simpleperf/scripts/binary_cache_builder.py +++ b/simpleperf/scripts/binary_cache_builder.py @@ -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 index aa82b92c..00000000 --- a/simpleperf/scripts/pprof_proto_generator.config +++ /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 diff --git a/simpleperf/scripts/pprof_proto_generator.py b/simpleperf/scripts/pprof_proto_generator.py index fa2fdb11..ad519110 100644 --- a/simpleperf/scripts/pprof_proto_generator.py +++ b/simpleperf/scripts/pprof_proto_generator.py @@ -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) diff --git a/simpleperf/scripts/utils.py b/simpleperf/scripts/utils.py index a0b3b3ba..2e2c69ad 100644 --- a/simpleperf/scripts/utils.py +++ b/simpleperf/scripts/utils.py @@ -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) |