OSDN Git Service

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