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
23from __future__ import print_function
24import argparse
25import copy
26import os
27import os.path
28import shutil
29import subprocess
30import sys
31import time
32
33from binary_cache_builder import BinaryCacheBuilder
34from simpleperf_report_lib import *
35from utils import *
36
37class 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
294if __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