• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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: Record cpu profiling data of an android app or native program.
19
20    It downloads simpleperf on device, uses it to collect profiling data on the selected app,
21    and pulls profiling data and related binaries on host.
22"""
23
24from __future__ import print_function
25import argparse
26import os
27import os.path
28import subprocess
29import sys
30import time
31
32from utils import AdbHelper, bytes_to_str, extant_dir, get_script_dir, get_target_binary_path
33from utils import log_debug, log_info, log_exit, ReadElf, remove, str_to_bytes
34
35NATIVE_LIBS_DIR_ON_DEVICE = '/data/local/tmp/native_libs/'
36
37class HostElfEntry(object):
38    """Represent a native lib on host in NativeLibDownloader."""
39    def __init__(self, path, name, score):
40        self.path = path
41        self.name = name
42        self.score = score
43
44    def __repr__(self):
45        return self.__str__()
46
47    def __str__(self):
48        return '[path: %s, name %s, score %s]' % (self.path, self.name, self.score)
49
50
51class NativeLibDownloader(object):
52    """Download native libs on device.
53
54    1. Collect info of all native libs in the native_lib_dir on host.
55    2. Check the available native libs in /data/local/tmp/native_libs on device.
56    3. Sync native libs on device.
57    """
58    def __init__(self, ndk_path, device_arch, adb):
59        self.adb = adb
60        self.readelf = ReadElf(ndk_path)
61        self.device_arch = device_arch
62        self.need_archs = self._get_need_archs()
63        self.host_build_id_map = {}  # Map from build_id to HostElfEntry.
64        self.device_build_id_map = {}  # Map from build_id to relative_path on device.
65        self.name_count_map = {}  # Used to give a unique name for each library.
66        self.dir_on_device = NATIVE_LIBS_DIR_ON_DEVICE
67        self.build_id_list_file = 'build_id_list'
68
69    def _get_need_archs(self):
70        """Return the archs of binaries needed on device."""
71        if self.device_arch == 'arm64':
72            return ['arm', 'arm64']
73        if self.device_arch == 'arm':
74            return ['arm']
75        if self.device_arch == 'x86_64':
76            return ['x86', 'x86_64']
77        if self.device_arch == 'x86':
78            return ['x86']
79        return []
80
81    def collect_native_libs_on_host(self, native_lib_dir):
82        self.host_build_id_map.clear()
83        for root, _, files in os.walk(native_lib_dir):
84            for name in files:
85                if not name.endswith('.so'):
86                    continue
87                self.add_native_lib_on_host(os.path.join(root, name), name)
88
89    def add_native_lib_on_host(self, path, name):
90        build_id = self.readelf.get_build_id(path)
91        if not build_id:
92            return
93        arch = self.readelf.get_arch(path)
94        if arch not in self.need_archs:
95            return
96        sections = self.readelf.get_sections(path)
97        score = 0
98        if '.debug_info' in sections:
99            score = 3
100        elif '.gnu_debugdata' in sections:
101            score = 2
102        elif '.symtab' in sections:
103            score = 1
104        entry = self.host_build_id_map.get(build_id)
105        if entry:
106            if entry.score < score:
107                entry.path = path
108                entry.score = score
109        else:
110            repeat_count = self.name_count_map.get(name, 0)
111            self.name_count_map[name] = repeat_count + 1
112            unique_name = name if repeat_count == 0 else name + '_' + str(repeat_count)
113            self.host_build_id_map[build_id] = HostElfEntry(path, unique_name, score)
114
115    def collect_native_libs_on_device(self):
116        self.device_build_id_map.clear()
117        self.adb.check_run(['shell', 'mkdir', '-p', self.dir_on_device])
118        if os.path.exists(self.build_id_list_file):
119            os.remove(self.build_id_list_file)
120        self.adb.run(['pull', self.dir_on_device + self.build_id_list_file])
121        if os.path.exists(self.build_id_list_file):
122            with open(self.build_id_list_file, 'rb') as fh:
123                for line in fh.readlines():
124                    line = bytes_to_str(line).strip()
125                    items = line.split('=')
126                    if len(items) == 2:
127                        self.device_build_id_map[items[0]] = items[1]
128            remove(self.build_id_list_file)
129
130    def sync_natives_libs_on_device(self):
131        # Push missing native libs on device.
132        for build_id in self.host_build_id_map:
133            if build_id not in self.device_build_id_map:
134                entry = self.host_build_id_map[build_id]
135                self.adb.check_run(['push', entry.path, self.dir_on_device + entry.name])
136        # Remove native libs not exist on host.
137        for build_id in self.device_build_id_map:
138            if build_id not in self.host_build_id_map:
139                name = self.device_build_id_map[build_id]
140                self.adb.run(['shell', 'rm', self.dir_on_device + name])
141        # Push new build_id_list on device.
142        with open(self.build_id_list_file, 'wb') as fh:
143            for build_id in self.host_build_id_map:
144                s = str_to_bytes('%s=%s\n' % (build_id, self.host_build_id_map[build_id].name))
145                fh.write(s)
146        self.adb.check_run(['push', self.build_id_list_file,
147                            self.dir_on_device + self.build_id_list_file])
148        os.remove(self.build_id_list_file)
149
150
151class ProfilerBase(object):
152    """Base class of all Profilers."""
153    def __init__(self, args):
154        self.args = args
155        self.adb = AdbHelper(enable_switch_to_root=not args.disable_adb_root)
156        self.is_root_device = self.adb.switch_to_root()
157        self.android_version = self.adb.get_android_version()
158        if self.android_version < 7:
159            log_exit("""app_profiler.py isn't supported on Android < N, please switch to use
160                        simpleperf binary directly.""")
161        self.device_arch = self.adb.get_device_arch()
162        self.record_subproc = None
163
164    def profile(self):
165        log_info('prepare profiling')
166        self.prepare()
167        log_info('start profiling')
168        self.start()
169        self.wait_profiling()
170        log_info('collect profiling data')
171        self.collect_profiling_data()
172        log_info('profiling is finished.')
173
174    def prepare(self):
175        """Prepare recording. """
176        self.download_simpleperf()
177        if self.args.native_lib_dir:
178            self.download_libs()
179
180    def download_simpleperf(self):
181        simpleperf_binary = get_target_binary_path(self.device_arch, 'simpleperf')
182        self.adb.check_run(['push', simpleperf_binary, '/data/local/tmp'])
183        self.adb.check_run(['shell', 'chmod', 'a+x', '/data/local/tmp/simpleperf'])
184
185    def download_libs(self):
186        downloader = NativeLibDownloader(self.args.ndk_path, self.device_arch, self.adb)
187        downloader.collect_native_libs_on_host(self.args.native_lib_dir)
188        downloader.collect_native_libs_on_device()
189        downloader.sync_natives_libs_on_device()
190
191    def start(self):
192        raise NotImplementedError
193
194    def start_profiling(self, target_args):
195        """Start simpleperf reocrd process on device."""
196        args = ['/data/local/tmp/simpleperf', 'record', '-o', '/data/local/tmp/perf.data',
197                self.args.record_options]
198        if self.adb.run(['shell', 'ls', NATIVE_LIBS_DIR_ON_DEVICE]):
199            args += ['--symfs', NATIVE_LIBS_DIR_ON_DEVICE]
200        args += target_args
201        adb_args = [self.adb.adb_path, 'shell'] + args
202        log_debug('run adb cmd: %s' % adb_args)
203        self.record_subproc = subprocess.Popen(adb_args)
204
205    def wait_profiling(self):
206        """Wait until profiling finishes, or stop profiling when user presses Ctrl-C."""
207        returncode = None
208        try:
209            returncode = self.record_subproc.wait()
210        except KeyboardInterrupt:
211            self.stop_profiling()
212            self.record_subproc = None
213            # Don't check return value of record_subproc. Because record_subproc also
214            # receives Ctrl-C, and always returns non-zero.
215            returncode = 0
216        log_debug('profiling result [%s]' % (returncode == 0))
217        if returncode != 0:
218            log_exit('Failed to record profiling data.')
219
220    def stop_profiling(self):
221        """Stop profiling by sending SIGINT to simpleperf, and wait until it exits
222           to make sure perf.data is completely generated."""
223        has_killed = False
224        while True:
225            (result, _) = self.adb.run_and_return_output(['shell', 'pidof', 'simpleperf'])
226            if not result:
227                break
228            if not has_killed:
229                has_killed = True
230                self.adb.run_and_return_output(['shell', 'pkill', '-l', '2', 'simpleperf'])
231            time.sleep(1)
232
233    def collect_profiling_data(self):
234        self.adb.check_run_and_return_output(['pull', '/data/local/tmp/perf.data',
235                                              self.args.perf_data_path])
236        if not self.args.skip_collect_binaries:
237            binary_cache_args = [sys.executable,
238                                 os.path.join(get_script_dir(), 'binary_cache_builder.py')]
239            binary_cache_args += ['-i', self.args.perf_data_path]
240            if self.args.native_lib_dir:
241                binary_cache_args += ['-lib', self.args.native_lib_dir]
242            if self.args.disable_adb_root:
243                binary_cache_args += ['--disable_adb_root']
244            if self.args.ndk_path:
245                binary_cache_args += ['--ndk_path', self.args.ndk_path]
246            subprocess.check_call(binary_cache_args)
247
248
249class AppProfiler(ProfilerBase):
250    """Profile an Android app."""
251    def prepare(self):
252        super(AppProfiler, self).prepare()
253        if self.args.compile_java_code:
254            self.compile_java_code()
255
256    def compile_java_code(self):
257        self.kill_app_process()
258        # Fully compile Java code on Android >= N.
259        self.adb.set_property('debug.generate-debug-info', 'true')
260        self.adb.check_run(['shell', 'cmd', 'package', 'compile', '-f', '-m', 'speed',
261                            self.args.app])
262
263    def kill_app_process(self):
264        if self.find_app_process():
265            self.adb.check_run(['shell', 'am', 'force-stop', self.args.app])
266            count = 0
267            while True:
268                time.sleep(1)
269                pid = self.find_app_process()
270                if not pid:
271                    break
272                # When testing on Android N, `am force-stop` sometimes can't kill
273                # com.example.simpleperf.simpleperfexampleofkotlin. So use kill when this happens.
274                count += 1
275                if count >= 3:
276                    self.run_in_app_dir(['kill', '-9', str(pid)])
277
278    def find_app_process(self):
279        result, output = self.adb.run_and_return_output(['shell', 'pidof', self.args.app])
280        return int(output) if result else None
281
282    def run_in_app_dir(self, args):
283        if self.is_root_device:
284            adb_args = ['shell', 'cd /data/data/' + self.args.app + ' && ' + (' '.join(args))]
285        else:
286            adb_args = ['shell', 'run-as', self.args.app] + args
287        return self.adb.run_and_return_output(adb_args, log_output=False)
288
289    def start(self):
290        if self.args.activity or self.args.test:
291            self.kill_app_process()
292        self.start_profiling(['--app', self.args.app])
293        if self.args.activity:
294            self.start_activity()
295        elif self.args.test:
296            self.start_test()
297        # else: no need to start an activity or test.
298
299    def start_activity(self):
300        activity = self.args.app + '/' + self.args.activity
301        result = self.adb.run(['shell', 'am', 'start', '-n', activity])
302        if not result:
303            self.record_subproc.terminate()
304            log_exit("Can't start activity %s" % activity)
305
306    def start_test(self):
307        runner = self.args.app + '/androidx.test.runner.AndroidJUnitRunner'
308        result = self.adb.run(['shell', 'am', 'instrument', '-e', 'class',
309                               self.args.test, runner])
310        if not result:
311            self.record_subproc.terminate()
312            log_exit("Can't start instrumentation test  %s" % self.args.test)
313
314
315class NativeProgramProfiler(ProfilerBase):
316    """Profile a native program."""
317    def start(self):
318        pid = int(self.adb.check_run_and_return_output(['shell', 'pidof',
319                                                        self.args.native_program]))
320        self.start_profiling(['-p', str(pid)])
321
322
323class NativeCommandProfiler(ProfilerBase):
324    """Profile running a native command."""
325    def start(self):
326        self.start_profiling([self.args.cmd])
327
328
329class NativeProcessProfiler(ProfilerBase):
330    """Profile processes given their pids."""
331    def start(self):
332        self.start_profiling(['-p', ','.join(self.args.pid)])
333
334
335class NativeThreadProfiler(ProfilerBase):
336    """Profile threads given their tids."""
337    def start(self):
338        self.start_profiling(['-t', ','.join(self.args.tid)])
339
340
341class SystemWideProfiler(ProfilerBase):
342    """Profile system wide."""
343    def start(self):
344        self.start_profiling(['-a'])
345
346
347def main():
348    parser = argparse.ArgumentParser(description=__doc__,
349                                     formatter_class=argparse.RawDescriptionHelpFormatter)
350
351    target_group = parser.add_argument_group(title='Select profiling target'
352                                            ).add_mutually_exclusive_group(required=True)
353    target_group.add_argument('-p', '--app', help="""Profile an Android app, given the package name.
354                              Like `-p com.example.android.myapp`.""")
355
356    target_group.add_argument('-np', '--native_program', help="""Profile a native program running on
357                              the Android device. Like `-np surfaceflinger`.""")
358
359    target_group.add_argument('-cmd', help="""Profile running a command on the Android device.
360                              Like `-cmd "pm -l"`.""")
361
362    target_group.add_argument('--pid', nargs='+', help="""Profile native processes running on device
363                              given their process ids.""")
364
365    target_group.add_argument('--tid', nargs='+', help="""Profile native threads running on device
366                              given their thread ids.""")
367
368    target_group.add_argument('--system_wide', action='store_true', help="""Profile system wide.""")
369
370    app_target_group = parser.add_argument_group(title='Extra options for profiling an app')
371    app_target_group.add_argument('--compile_java_code', action='store_true', help="""Used with -p.
372                                  On Android N and Android O, we need to compile Java code into
373                                  native instructions to profile Java code. Android O also needs
374                                  wrap.sh in the apk to use the native instructions.""")
375
376    app_start_group = app_target_group.add_mutually_exclusive_group()
377    app_start_group.add_argument('-a', '--activity', help="""Used with -p. Profile the launch time
378                                 of an activity in an Android app. The app will be started or
379                                 restarted to run the activity. Like `-a .MainActivity`.""")
380
381    app_start_group.add_argument('-t', '--test', help="""Used with -p. Profile the launch time of an
382                                 instrumentation test in an Android app. The app will be started or
383                                 restarted to run the instrumentation test. Like
384                                 `-t test_class_name`.""")
385
386    record_group = parser.add_argument_group('Select recording options')
387    record_group.add_argument('-r', '--record_options',
388                              default='-e task-clock:u -f 1000 -g --duration 10', help="""Set
389                              recording options for `simpleperf record` command. Use
390                              `run_simpleperf_on_device.py record -h` to see all accepted options.
391                              Default is "-e task-clock:u -f 1000 -g --duration 10".""")
392
393    record_group.add_argument('-lib', '--native_lib_dir', type=extant_dir,
394                              help="""When profiling an Android app containing native libraries,
395                                      the native libraries are usually stripped and lake of symbols
396                                      and debug information to provide good profiling result. By
397                                      using -lib, you tell app_profiler.py the path storing
398                                      unstripped native libraries, and app_profiler.py will search
399                                      all shared libraries with suffix .so in the directory. Then
400                                      the native libraries will be downloaded on device and
401                                      collected in build_cache.""")
402
403    record_group.add_argument('-o', '--perf_data_path', default='perf.data',
404                              help='The path to store profiling data. Default is perf.data.')
405
406    record_group.add_argument('-nb', '--skip_collect_binaries', action='store_true',
407                              help="""By default we collect binaries used in profiling data from
408                                      device to binary_cache directory. It can be used to annotate
409                                      source code and disassembly. This option skips it.""")
410
411    other_group = parser.add_argument_group('Other options')
412    other_group.add_argument('--ndk_path', type=extant_dir,
413                             help="""Set the path of a ndk release. app_profiler.py needs some
414                                     tools in ndk, like readelf.""")
415
416    other_group.add_argument('--disable_adb_root', action='store_true',
417                             help="""Force adb to run in non root mode. By default, app_profiler.py
418                                     will try to switch to root mode to be able to profile released
419                                     Android apps.""")
420
421    def check_args(args):
422        if (not args.app) and (args.compile_java_code or args.activity or args.test):
423            log_exit('--compile_java_code, -a, -t can only be used when profiling an Android app.')
424
425    args = parser.parse_args()
426    check_args(args)
427    if args.app:
428        profiler = AppProfiler(args)
429    elif args.native_program:
430        profiler = NativeProgramProfiler(args)
431    elif args.cmd:
432        profiler = NativeCommandProfiler(args)
433    elif args.pid:
434        profiler = NativeProcessProfiler(args)
435    elif args.tid:
436        profiler = NativeThreadProfiler(args)
437    elif args.system_wide:
438        profiler = SystemWideProfiler(args)
439    profiler.profile()
440
441if __name__ == '__main__':
442    main()
443