OSDN Git Service

simpleperf: add script to manage app profiling process.
[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', 'restart_app', 'main_activity',
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['restart_app']:
151             return
152         pid = self._find_app_process()
153         if pid is not None:
154             self.adb.run(['shell', 'run-as', self.config['app_package_name'],
155                           'kill', '-9', str(pid)])
156             time.sleep(1)
157         activity = self.config['app_package_name'] + '/' + self.config['main_activity']
158         result = self.adb.run(['shell', 'am', 'start', '-n', activity])
159         if not result:
160             log_fatal("Can't start activity %s" % activity)
161         for i in range(10):
162             pid = self._find_app_process()
163             if pid is not None:
164                 return pid
165             time.sleep(1)
166             log_info('Wait for the app process for %d seconds' % (i + 1))
167         log_fatal("Can't find the app process")
168
169
170     def _find_app_process(self):
171         result, output = self.adb.run_and_return_output(
172                 ['shell', 'run-as', self.config['app_package_name'], 'ps'])
173         if not result:
174             return None
175         output = output.split('\n')
176         for line in output:
177             strs = line.split()
178             if len(strs) > 2 and strs[-1].find(self.config['app_package_name']) != -1:
179                 return int(strs[1])
180         return None
181
182
183     def _get_app_environment(self):
184         self.app_pid = self._find_app_process()
185         if self.app_pid is None:
186             log_fatal("can't find process for app [%s]" % self.config['app_package_name'])
187         if self.device_arch in ['aarch64', 'x86_64']:
188             output = self.adb.check_run_and_return_output(
189                 ['shell', 'run-as', self.config['app_package_name'],
190                 'cat', '/proc/%d/maps' % self.app_pid])
191             if output.find('linker64') != -1:
192                 self.app_arch = self.device_arch
193             else:
194                 self.app_arch = 'arm' if self.device_arch == 'aarch64' else 'x86'
195         else:
196             self.app_arch = self.device_arch
197         log_info('app_arch: %s' % self.app_arch)
198
199
200     def _download_simpleperf(self):
201         simpleperf_binary = get_target_binary_path(self.app_arch, 'simpleperf')
202         self.adb.check_run(['push', simpleperf_binary, '/data/local/tmp'])
203         self.adb.check_run(['shell', 'run-as', self.config['app_package_name'],
204                             'cp', '/data/local/tmp/simpleperf', '.'])
205         self.adb.check_run(['shell', 'run-as', self.config['app_package_name'],
206                             'chmod', 'a+x', 'simpleperf'])
207
208
209     def _download_native_libs(self):
210         if not self.config['native_lib_dir']:
211             return
212         filename_dict = dict()
213         for root, _, files in os.walk(self.config['native_lib_dir']):
214             for file in files:
215                 if not file.endswith('.so'):
216                     continue
217                 path = os.path.join(root, file)
218                 old_path = filename_dict.get(file)
219                 log_info('app_arch = %s' % self.app_arch)
220                 if self._is_lib_better(path, old_path):
221                     log_info('%s is better than %s' % (path, old_path))
222                     filename_dict[file] = path
223                 else:
224                     log_info('%s is worse than %s' % (path, old_path))
225         maps = self.adb.check_run_and_return_output(['shell', 'run-as',
226                   self.config['app_package_name'], 'cat', '/proc/%d/maps' % self.app_pid])
227         searched_lib = dict()
228         for item in maps.split():
229             if item.endswith('.so') and searched_lib.get(item) is None:
230                 searched_lib[item] = True
231                 # Use '/' as path separator as item comes from android environment.
232                 filename = item[item.rfind('/') + 1:]
233                 dirname = item[1:item.rfind('/')]
234                 path = filename_dict.get(filename)
235                 if path is None:
236                     continue
237                 self.adb.check_run(['push', path, '/data/local/tmp'])
238                 self.adb.check_run(['shell', 'run-as', self.config['app_package_name'],
239                                     'mkdir', '-p', dirname])
240                 self.adb.check_run(['shell', 'run-as', self.config['app_package_name'],
241                                     'cp', '/data/local/tmp/' + filename, dirname])
242
243
244     def _is_lib_better(self, new_path, old_path):
245         """ Return true if new_path is more likely to be used on device. """
246         if old_path is None:
247             return True
248         if self.app_arch == 'arm':
249             result1 = new_path.find('armeabi-v7a/') != -1
250             result2 = old_path.find('armeabi-v7a') != -1
251             if result1 != result2:
252                 return result1
253         arch_dir = 'arm64' if self.app_arch == 'aarch64' else self.app_arch + '/'
254         result1 = new_path.find(arch_dir) != -1
255         result2 = old_path.find(arch_dir) != -1
256         if result1 != result2:
257             return result1
258         result1 = new_path.find('obj/') != -1
259         result2 = old_path.find('obj/') != -1
260         if result1 != result2:
261             return result1
262         return False
263
264
265     def start_and_wait_profiling(self):
266         self.adb.check_run(['shell', 'run-as', self.config['app_package_name'],
267             './simpleperf', 'record', self.config['record_options'], '-p',
268             str(self.app_pid), '--symfs', '.'])
269
270
271     def collect_profiling_data(self):
272         self.adb.check_run(['shell', 'run-as', self.config['app_package_name'],
273                             '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 if __name__ == '__main__':
286     parser = argparse.ArgumentParser(
287         description='Profile an android app. See configurations in app_profiler.config.')
288     parser.add_argument('--config', default='app_profiler.config',
289                         help='Set configuration file. Default is app_profiler.config.')
290     args = parser.parse_args()
291     config = load_config(args.config)
292     profiler = AppProfiler(config)
293     profiler.profile()