OSDN Git Service

Merge "simpleperf: fix the way to pull perf.data on windows."
[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 name not in config:
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         ps_args = ['-e'] if self.android_version >= 8 else []
181         result, output = self.adb.run_and_return_output(['shell', 'ps'] + ps_args)
182         if not result:
183             return None
184         output = output.split('\n')
185         for line in output:
186             strs = line.split()
187             if len(strs) > 2 and strs[-1].find(self.config['app_package_name']) != -1:
188                 return int(strs[1])
189         return None
190
191
192     def _get_app_environment(self):
193         self.app_pid = self._find_app_process()
194         if self.app_pid is None:
195             log_fatal("can't find process for app [%s]" % self.config['app_package_name'])
196         if self.device_arch in ['aarch64', 'x86_64']:
197             output = self.run_in_app_dir(['cat', '/proc/%d/maps' % self.app_pid])
198             if output.find('linker64') != -1:
199                 self.app_arch = self.device_arch
200             else:
201                 self.app_arch = 'arm' if self.device_arch == 'aarch64' else 'x86'
202         else:
203             self.app_arch = self.device_arch
204         log_info('app_arch: %s' % self.app_arch)
205
206
207     def _download_simpleperf(self):
208         simpleperf_binary = get_target_binary_path(self.app_arch, 'simpleperf')
209         self.adb.check_run(['push', simpleperf_binary, '/data/local/tmp'])
210         self.run_in_app_dir(['cp', '/data/local/tmp/simpleperf', '.'])
211         self.run_in_app_dir(['chmod', 'a+x', 'simpleperf'])
212
213
214     def _download_native_libs(self):
215         if not self.config['native_lib_dir']:
216             return
217         filename_dict = dict()
218         for root, _, files in os.walk(self.config['native_lib_dir']):
219             for file in files:
220                 if not file.endswith('.so'):
221                     continue
222                 path = os.path.join(root, file)
223                 old_path = filename_dict.get(file)
224                 log_info('app_arch = %s' % self.app_arch)
225                 if self._is_lib_better(path, old_path):
226                     log_info('%s is better than %s' % (path, old_path))
227                     filename_dict[file] = path
228                 else:
229                     log_info('%s is worse than %s' % (path, old_path))
230         maps = self.run_in_app_dir(['cat', '/proc/%d/maps' % self.app_pid])
231         searched_lib = dict()
232         for item in maps.split():
233             if item.endswith('.so') and searched_lib.get(item) is None:
234                 searched_lib[item] = True
235                 # Use '/' as path separator as item comes from android environment.
236                 filename = item[item.rfind('/') + 1:]
237                 dirname = item[1:item.rfind('/')]
238                 path = filename_dict.get(filename)
239                 if path is None:
240                     continue
241                 self.adb.check_run(['push', path, '/data/local/tmp'])
242                 self.run_in_app_dir(['mkdir', '-p', dirname])
243                 self.run_in_app_dir(['cp', '/data/local/tmp/' + filename, dirname])
244
245
246     def _is_lib_better(self, new_path, old_path):
247         """ Return true if new_path is more likely to be used on device. """
248         if old_path is None:
249             return True
250         if self.app_arch == 'arm':
251             result1 = new_path.find('armeabi-v7a/') != -1
252             result2 = old_path.find('armeabi-v7a') != -1
253             if result1 != result2:
254                 return result1
255         arch_dir = 'arm64' if self.app_arch == 'aarch64' else self.app_arch + '/'
256         result1 = new_path.find(arch_dir) != -1
257         result2 = old_path.find(arch_dir) != -1
258         if result1 != result2:
259             return result1
260         result1 = new_path.find('obj/') != -1
261         result2 = old_path.find('obj/') != -1
262         if result1 != result2:
263             return result1
264         return False
265
266
267     def start_and_wait_profiling(self):
268         subproc = None
269         returncode = None
270         try:
271             args = self.get_run_in_app_dir_args([
272                 './simpleperf', 'record', self.config['record_options'], '-p',
273                 str(self.app_pid), '--symfs', '.'])
274             adb_args = [self.adb.adb_path] + args
275             log_debug('run adb cmd: %s' % adb_args)
276             subproc = subprocess.Popen(adb_args)
277             returncode = subproc.wait()
278         except KeyboardInterrupt:
279             if subproc:
280                 self.stop_profiling()
281                 returncode = 0
282         log_debug('run adb cmd: %s [result %s]' % (adb_args, returncode == 0))
283
284
285     def stop_profiling(self):
286         """ Stop profiling by sending SIGINT to simpleperf, and wait until it exits
287             to make sure perf.data is completely generated."""
288         has_killed = False
289         while True:
290             (result, _) = self.run_in_app_dir(['pidof', 'simpleperf'], check_result=False)
291             if not result:
292                 break
293             if not has_killed:
294                 has_killed = True
295                 self.run_in_app_dir(['pkill', '-l', '2', 'simpleperf'], check_result=False)
296             time.sleep(1)
297
298
299     def collect_profiling_data(self):
300         self.run_in_app_dir(['cat perf.data | tee /data/local/tmp/perf.data >/dev/null'])
301         self.adb.check_run_and_return_output(['pull', '/data/local/tmp/perf.data',
302                                               self.config['perf_data_path']])
303         config = copy.copy(self.config)
304         config['symfs_dirs'] = []
305         if self.config['native_lib_dir']:
306             config['symfs_dirs'].append(self.config['native_lib_dir'])
307         binary_cache_builder = BinaryCacheBuilder(config)
308         binary_cache_builder.build_binary_cache()
309
310
311     def run_in_app_dir(self, args, stdout_file=None, check_result=True):
312         args = self.get_run_in_app_dir_args(args)
313         if check_result:
314             return self.adb.check_run_and_return_output(args, stdout_file)
315         else:
316             return self.adb.run_and_return_output(args, stdout_file)
317
318
319     def get_run_in_app_dir_args(self, args):
320         if self.is_root_device:
321             return ['shell', 'cd /data/data/' + self.config['app_package_name'] + ' && ' +
322                       (' '.join(args))]
323         else:
324             return ['shell', 'run-as', self.config['app_package_name']] + args
325
326
327 if __name__ == '__main__':
328     parser = argparse.ArgumentParser(
329         description='Profile an android app. See configurations in app_profiler.config.')
330     parser.add_argument('--config', default='app_profiler.config',
331                         help='Set configuration file. Default is app_profiler.config.')
332     args = parser.parse_args()
333     config = load_config(args.config)
334     profiler = AppProfiler(config)
335     profiler.profile()