Home | History | Annotate | Download | only in scripts
      1 #!/usr/bin/env python
      2 #
      3 # Copyright (C) 2016 The Android Open Source Project
      4 #
      5 # Licensed under the Apache License, Version 2.0 (the "License");
      6 # you may not use this file except in compliance with the License.
      7 # You may obtain a copy of the License at
      8 #
      9 #      http://www.apache.org/licenses/LICENSE-2.0
     10 #
     11 # Unless required by applicable law or agreed to in writing, software
     12 # distributed under the License is distributed on an "AS IS" BASIS,
     13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14 # See the License for the specific language governing permissions and
     15 # limitations under the License.
     16 #
     17 
     18 """app_profiler.py: manage the process of profiling an android app.
     19     It downloads simpleperf on device, uses it to collect samples from
     20     user's app, and pulls perf.data and needed binaries on host.
     21 """
     22 
     23 from __future__ import print_function
     24 import argparse
     25 import copy
     26 import os
     27 import os.path
     28 import shutil
     29 import subprocess
     30 import sys
     31 import time
     32 
     33 from binary_cache_builder import BinaryCacheBuilder
     34 from simpleperf_report_lib import *
     35 from utils import *
     36 
     37 class AppProfiler(object):
     38     """Used to manage the process of profiling an android app.
     39 
     40     There are three steps:
     41        1. Prepare profiling.
     42        2. Profile the app.
     43        3. Collect profiling data.
     44     """
     45     def __init__(self, config):
     46         # check config variables
     47         config_names = ['app_package_name', 'native_lib_dir', 'apk_file_path',
     48                         'recompile_app', 'launch_activity', 'launch_inst_test',
     49                         'record_options', 'perf_data_path', 'adb_path', 'readelf_path',
     50                         'binary_cache_dir']
     51         for name in config_names:
     52             if not config.has_key(name):
     53                 log_fatal('config [%s] is missing' % name)
     54         native_lib_dir = config.get('native_lib_dir')
     55         if native_lib_dir and not os.path.isdir(native_lib_dir):
     56             log_fatal('[native_lib_dir] "%s" is not a dir' % native_lib_dir)
     57         apk_file_path = config.get('apk_file_path')
     58         if apk_file_path and not os.path.isfile(apk_file_path):
     59             log_fatal('[apk_file_path] "%s" is not a file' % apk_file_path)
     60         self.config = config
     61         self.adb = AdbHelper(self.config['adb_path'])
     62         self.is_root_device = False
     63         self.android_version = 0
     64         self.device_arch = None
     65         self.app_arch = None
     66         self.app_pid = None
     67 
     68 
     69     def profile(self):
     70         log_info('prepare profiling')
     71         self.prepare_profiling()
     72         log_info('start profiling')
     73         self.start_and_wait_profiling()
     74         log_info('collect profiling data')
     75         self.collect_profiling_data()
     76         log_info('profiling is finished.')
     77 
     78 
     79     def prepare_profiling(self):
     80         self._get_device_environment()
     81         self._enable_profiling()
     82         self._recompile_app()
     83         self._restart_app()
     84         self._get_app_environment()
     85         self._download_simpleperf()
     86         self._download_native_libs()
     87 
     88 
     89     def _get_device_environment(self):
     90         self.is_root_device = self.adb.switch_to_root()
     91 
     92         # Get android version.
     93         build_version = self.adb.get_property('ro.build.version.release')
     94         if build_version:
     95             if not build_version[0].isdigit():
     96                 c = build_version[0].upper()
     97                 if c < 'L':
     98                     self.android_version = 0
     99                 else:
    100                     self.android_version = ord(c) - ord('L') + 5
    101             else:
    102                 strs = build_version.split('.')
    103                 if strs:
    104                     self.android_version = int(strs[0])
    105 
    106         # Get device architecture.
    107         output = self.adb.check_run_and_return_output(['shell', 'uname', '-m'])
    108         if output.find('aarch64') != -1:
    109             self.device_arch = 'aarch64'
    110         elif output.find('arm') != -1:
    111             self.device_arch = 'arm'
    112         elif output.find('x86_64') != -1:
    113             self.device_arch = 'x86_64'
    114         elif output.find('86') != -1:
    115             self.device_arch = 'x86'
    116         else:
    117             log_fatal('unsupported architecture: %s' % output.strip())
    118 
    119 
    120     def _enable_profiling(self):
    121         self.adb.set_property('security.perf_harden', '0')
    122         if self.is_root_device:
    123             # We can enable kernel symbols
    124             self.adb.run(['shell', 'echo', '0', '>/proc/sys/kernel/kptr_restrict'])
    125 
    126 
    127     def _recompile_app(self):
    128         if not self.config['recompile_app']:
    129             return
    130         if self.android_version == 0:
    131             log_warning("Can't fully compile an app on android version < L.")
    132         elif self.android_version == 5 or self.android_version == 6:
    133             if not self.is_root_device:
    134                 log_warning("Can't fully compile an app on android version < N on non-root devices.")
    135             elif not self.config['apk_file_path']:
    136                 log_warning("apk file is needed to reinstall the app on android version < N.")
    137             else:
    138                 flag = '-g' if self.android_version == 6 else '--include-debug-symbols'
    139                 self.adb.set_property('dalvik.vm.dex2oat-flags', flag)
    140                 self.adb.check_run(['install', '-r', self.config['apk_file_path']])
    141         elif self.android_version >= 7:
    142             self.adb.set_property('debug.generate-debug-info', 'true')
    143             self.adb.check_run(['shell', 'cmd', 'package', 'compile', '-f', '-m', 'speed',
    144                                 self.config['app_package_name']])
    145         else:
    146             log_fatal('unreachable')
    147 
    148 
    149     def _restart_app(self):
    150         if not self.config['launch_activity'] and not self.config['launch_inst_test']:
    151             return
    152 
    153         pid = self._find_app_process()
    154         if pid is not None:
    155             self.run_in_app_dir(['kill', '-9', str(pid)])
    156             time.sleep(1)
    157 
    158         if self.config['launch_activity']:
    159             activity = self.config['app_package_name'] + '/' + self.config['launch_activity']
    160             result = self.adb.run(['shell', 'am', 'start', '-n', activity])
    161             if not result:
    162                 log_fatal("Can't start activity %s" % activity)
    163         else:
    164             runner = self.config['app_package_name'] + '/android.support.test.runner.AndroidJUnitRunner'
    165             result = self.adb.run(['shell', 'am', 'instrument', '-e', 'class',
    166                                    self.config['launch_inst_test'], runner])
    167             if not result:
    168                 log_fatal("Can't start instrumentation test  %s" % self.config['launch_inst_test'])
    169 
    170         for i in range(10):
    171             pid = self._find_app_process()
    172             if pid is not None:
    173                 return
    174             time.sleep(1)
    175             log_info('Wait for the app process for %d seconds' % (i + 1))
    176         log_fatal("Can't find the app process")
    177 
    178 
    179     def _find_app_process(self):
    180         result, output = self.adb.run_and_return_output(['shell', 'ps'])
    181         if not result:
    182             return None
    183         output = output.split('\n')
    184         for line in output:
    185             strs = line.split()
    186             if len(strs) > 2 and strs[-1].find(self.config['app_package_name']) != -1:
    187                 return int(strs[1])
    188         return None
    189 
    190 
    191     def _get_app_environment(self):
    192         self.app_pid = self._find_app_process()
    193         if self.app_pid is None:
    194             log_fatal("can't find process for app [%s]" % self.config['app_package_name'])
    195         if self.device_arch in ['aarch64', 'x86_64']:
    196             output = self.run_in_app_dir(['cat', '/proc/%d/maps' % self.app_pid])
    197             if output.find('linker64') != -1:
    198                 self.app_arch = self.device_arch
    199             else:
    200                 self.app_arch = 'arm' if self.device_arch == 'aarch64' else 'x86'
    201         else:
    202             self.app_arch = self.device_arch
    203         log_info('app_arch: %s' % self.app_arch)
    204 
    205 
    206     def _download_simpleperf(self):
    207         simpleperf_binary = get_target_binary_path(self.app_arch, 'simpleperf')
    208         self.adb.check_run(['push', simpleperf_binary, '/data/local/tmp'])
    209         self.run_in_app_dir(['cp', '/data/local/tmp/simpleperf', '.'])
    210         self.run_in_app_dir(['chmod', 'a+x', 'simpleperf'])
    211 
    212 
    213     def _download_native_libs(self):
    214         if not self.config['native_lib_dir']:
    215             return
    216         filename_dict = dict()
    217         for root, _, files in os.walk(self.config['native_lib_dir']):
    218             for file in files:
    219                 if not file.endswith('.so'):
    220                     continue
    221                 path = os.path.join(root, file)
    222                 old_path = filename_dict.get(file)
    223                 log_info('app_arch = %s' % self.app_arch)
    224                 if self._is_lib_better(path, old_path):
    225                     log_info('%s is better than %s' % (path, old_path))
    226                     filename_dict[file] = path
    227                 else:
    228                     log_info('%s is worse than %s' % (path, old_path))
    229         maps = self.run_in_app_dir(['cat', '/proc/%d/maps' % self.app_pid])
    230         searched_lib = dict()
    231         for item in maps.split():
    232             if item.endswith('.so') and searched_lib.get(item) is None:
    233                 searched_lib[item] = True
    234                 # Use '/' as path separator as item comes from android environment.
    235                 filename = item[item.rfind('/') + 1:]
    236                 dirname = item[1:item.rfind('/')]
    237                 path = filename_dict.get(filename)
    238                 if path is None:
    239                     continue
    240                 self.adb.check_run(['push', path, '/data/local/tmp'])
    241                 self.run_in_app_dir(['mkdir', '-p', dirname])
    242                 self.run_in_app_dir(['cp', '/data/local/tmp/' + filename, dirname])
    243 
    244 
    245     def _is_lib_better(self, new_path, old_path):
    246         """ Return true if new_path is more likely to be used on device. """
    247         if old_path is None:
    248             return True
    249         if self.app_arch == 'arm':
    250             result1 = new_path.find('armeabi-v7a/') != -1
    251             result2 = old_path.find('armeabi-v7a') != -1
    252             if result1 != result2:
    253                 return result1
    254         arch_dir = 'arm64' if self.app_arch == 'aarch64' else self.app_arch + '/'
    255         result1 = new_path.find(arch_dir) != -1
    256         result2 = old_path.find(arch_dir) != -1
    257         if result1 != result2:
    258             return result1
    259         result1 = new_path.find('obj/') != -1
    260         result2 = old_path.find('obj/') != -1
    261         if result1 != result2:
    262             return result1
    263         return False
    264 
    265 
    266     def start_and_wait_profiling(self):
    267         self.run_in_app_dir([
    268             './simpleperf', 'record', self.config['record_options'], '-p',
    269             str(self.app_pid), '--symfs', '.'])
    270 
    271 
    272     def collect_profiling_data(self):
    273         self.run_in_app_dir(['chmod', 'a+rw', 'perf.data'])
    274         self.adb.check_run(['shell', 'cp',
    275             '/data/data/%s/perf.data' % self.config['app_package_name'], '/data/local/tmp'])
    276         self.adb.check_run(['pull', '/data/local/tmp/perf.data', self.config['perf_data_path']])
    277         config = copy.copy(self.config)
    278         config['symfs_dirs'] = []
    279         if self.config['native_lib_dir']:
    280             config['symfs_dirs'].append(self.config['native_lib_dir'])
    281         binary_cache_builder = BinaryCacheBuilder(config)
    282         binary_cache_builder.build_binary_cache()
    283 
    284 
    285     def run_in_app_dir(self, args):
    286         if self.is_root_device:
    287             cmd = 'cd /data/data/' + self.config['app_package_name'] + ' && ' + (' '.join(args))
    288             return self.adb.check_run_and_return_output(['shell', cmd])
    289         else:
    290             return self.adb.check_run_and_return_output(
    291                 ['shell', 'run-as', self.config['app_package_name']] + args)
    292 
    293 
    294 if __name__ == '__main__':
    295     parser = argparse.ArgumentParser(
    296         description='Profile an android app. See configurations in app_profiler.config.')
    297     parser.add_argument('--config', default='app_profiler.config',
    298                         help='Set configuration file. Default is app_profiler.config.')
    299     args = parser.parse_args()
    300     config = load_config(args.config)
    301     profiler = AppProfiler(config)
    302     profiler.profile()
    303