OSDN Git Service

Merge "ext4_utils: Change to base/logging in ext4_crypt_init_extensions"
[android-x86/system-extras.git] / simpleperf / scripts / app_profiler.py
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()