xref: /aosp_15_r20/system/extras/simpleperf/scripts/app_profiler.py (revision 288bf5226967eb3dac5cce6c939ccc2a7f2b4fe5)
1*288bf522SAndroid Build Coastguard Worker#!/usr/bin/env python3
2*288bf522SAndroid Build Coastguard Worker#
3*288bf522SAndroid Build Coastguard Worker# Copyright (C) 2016 The Android Open Source Project
4*288bf522SAndroid Build Coastguard Worker#
5*288bf522SAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
6*288bf522SAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
7*288bf522SAndroid Build Coastguard Worker# You may obtain a copy of the License at
8*288bf522SAndroid Build Coastguard Worker#
9*288bf522SAndroid Build Coastguard Worker#      http://www.apache.org/licenses/LICENSE-2.0
10*288bf522SAndroid Build Coastguard Worker#
11*288bf522SAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
12*288bf522SAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
13*288bf522SAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14*288bf522SAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
15*288bf522SAndroid Build Coastguard Worker# limitations under the License.
16*288bf522SAndroid Build Coastguard Worker#
17*288bf522SAndroid Build Coastguard Worker
18*288bf522SAndroid Build Coastguard Worker"""app_profiler.py: Record cpu profiling data of an android app or native program.
19*288bf522SAndroid Build Coastguard Worker
20*288bf522SAndroid Build Coastguard Worker    It downloads simpleperf on device, uses it to collect profiling data on the selected app,
21*288bf522SAndroid Build Coastguard Worker    and pulls profiling data and related binaries on host.
22*288bf522SAndroid Build Coastguard Worker"""
23*288bf522SAndroid Build Coastguard Worker
24*288bf522SAndroid Build Coastguard Workerimport logging
25*288bf522SAndroid Build Coastguard Workerimport os
26*288bf522SAndroid Build Coastguard Workerimport os.path
27*288bf522SAndroid Build Coastguard Workerimport re
28*288bf522SAndroid Build Coastguard Workerimport subprocess
29*288bf522SAndroid Build Coastguard Workerimport sys
30*288bf522SAndroid Build Coastguard Workerimport time
31*288bf522SAndroid Build Coastguard Workerfrom typing import Optional
32*288bf522SAndroid Build Coastguard Worker
33*288bf522SAndroid Build Coastguard Workerfrom simpleperf_utils import (
34*288bf522SAndroid Build Coastguard Worker    AdbHelper, BaseArgumentParser, bytes_to_str, extant_dir, get_script_dir, get_target_binary_path,
35*288bf522SAndroid Build Coastguard Worker    log_exit, ReadElf, remove, str_to_bytes)
36*288bf522SAndroid Build Coastguard Worker
37*288bf522SAndroid Build Coastguard WorkerNATIVE_LIBS_DIR_ON_DEVICE = '/data/local/tmp/native_libs/'
38*288bf522SAndroid Build Coastguard Worker
39*288bf522SAndroid Build Coastguard WorkerSHELL_PS_UID_PATTERN = re.compile(r'USER.*\nu(\d+)_.*')
40*288bf522SAndroid Build Coastguard Worker
41*288bf522SAndroid Build Coastguard Worker
42*288bf522SAndroid Build Coastguard Workerclass HostElfEntry(object):
43*288bf522SAndroid Build Coastguard Worker    """Represent a native lib on host in NativeLibDownloader."""
44*288bf522SAndroid Build Coastguard Worker
45*288bf522SAndroid Build Coastguard Worker    def __init__(self, path, name, score):
46*288bf522SAndroid Build Coastguard Worker        self.path = path
47*288bf522SAndroid Build Coastguard Worker        self.name = name
48*288bf522SAndroid Build Coastguard Worker        self.score = score
49*288bf522SAndroid Build Coastguard Worker
50*288bf522SAndroid Build Coastguard Worker    def __repr__(self):
51*288bf522SAndroid Build Coastguard Worker        return self.__str__()
52*288bf522SAndroid Build Coastguard Worker
53*288bf522SAndroid Build Coastguard Worker    def __str__(self):
54*288bf522SAndroid Build Coastguard Worker        return '[path: %s, name %s, score %s]' % (self.path, self.name, self.score)
55*288bf522SAndroid Build Coastguard Worker
56*288bf522SAndroid Build Coastguard Worker
57*288bf522SAndroid Build Coastguard Workerclass NativeLibDownloader(object):
58*288bf522SAndroid Build Coastguard Worker    """Download native libs on device.
59*288bf522SAndroid Build Coastguard Worker
60*288bf522SAndroid Build Coastguard Worker    1. Collect info of all native libs in the native_lib_dir on host.
61*288bf522SAndroid Build Coastguard Worker    2. Check the available native libs in /data/local/tmp/native_libs on device.
62*288bf522SAndroid Build Coastguard Worker    3. Sync native libs on device.
63*288bf522SAndroid Build Coastguard Worker    """
64*288bf522SAndroid Build Coastguard Worker
65*288bf522SAndroid Build Coastguard Worker    def __init__(self, ndk_path, device_arch, adb):
66*288bf522SAndroid Build Coastguard Worker        self.adb = adb
67*288bf522SAndroid Build Coastguard Worker        self.readelf = ReadElf(ndk_path)
68*288bf522SAndroid Build Coastguard Worker        self.device_arch = device_arch
69*288bf522SAndroid Build Coastguard Worker        self.need_archs = self._get_need_archs()
70*288bf522SAndroid Build Coastguard Worker        self.host_build_id_map = {}  # Map from build_id to HostElfEntry.
71*288bf522SAndroid Build Coastguard Worker        self.device_build_id_map = {}  # Map from build_id to relative_path on device.
72*288bf522SAndroid Build Coastguard Worker        # Map from filename to HostElfEntry for elf files without build id.
73*288bf522SAndroid Build Coastguard Worker        self.no_build_id_file_map = {}
74*288bf522SAndroid Build Coastguard Worker        self.name_count_map = {}  # Used to give a unique name for each library.
75*288bf522SAndroid Build Coastguard Worker        self.dir_on_device = NATIVE_LIBS_DIR_ON_DEVICE
76*288bf522SAndroid Build Coastguard Worker        self.build_id_list_file = 'build_id_list'
77*288bf522SAndroid Build Coastguard Worker
78*288bf522SAndroid Build Coastguard Worker    def _get_need_archs(self):
79*288bf522SAndroid Build Coastguard Worker        """Return the archs of binaries needed on device."""
80*288bf522SAndroid Build Coastguard Worker        if self.device_arch == 'arm64':
81*288bf522SAndroid Build Coastguard Worker            return ['arm', 'arm64']
82*288bf522SAndroid Build Coastguard Worker        if self.device_arch == 'arm':
83*288bf522SAndroid Build Coastguard Worker            return ['arm']
84*288bf522SAndroid Build Coastguard Worker        if self.device_arch == 'x86_64':
85*288bf522SAndroid Build Coastguard Worker            return ['x86', 'x86_64']
86*288bf522SAndroid Build Coastguard Worker        if self.device_arch == 'x86':
87*288bf522SAndroid Build Coastguard Worker            return ['x86']
88*288bf522SAndroid Build Coastguard Worker        return []
89*288bf522SAndroid Build Coastguard Worker
90*288bf522SAndroid Build Coastguard Worker    def collect_native_libs_on_host(self, native_lib_dir):
91*288bf522SAndroid Build Coastguard Worker        self.host_build_id_map.clear()
92*288bf522SAndroid Build Coastguard Worker        for root, _, files in os.walk(native_lib_dir):
93*288bf522SAndroid Build Coastguard Worker            for name in files:
94*288bf522SAndroid Build Coastguard Worker                if not name.endswith('.so'):
95*288bf522SAndroid Build Coastguard Worker                    continue
96*288bf522SAndroid Build Coastguard Worker                self.add_native_lib_on_host(os.path.join(root, name), name)
97*288bf522SAndroid Build Coastguard Worker
98*288bf522SAndroid Build Coastguard Worker    def add_native_lib_on_host(self, path, name):
99*288bf522SAndroid Build Coastguard Worker        arch = self.readelf.get_arch(path)
100*288bf522SAndroid Build Coastguard Worker        if arch not in self.need_archs:
101*288bf522SAndroid Build Coastguard Worker            return
102*288bf522SAndroid Build Coastguard Worker        sections = self.readelf.get_sections(path)
103*288bf522SAndroid Build Coastguard Worker        score = 0
104*288bf522SAndroid Build Coastguard Worker        if '.debug_info' in sections:
105*288bf522SAndroid Build Coastguard Worker            score = 3
106*288bf522SAndroid Build Coastguard Worker        elif '.gnu_debugdata' in sections:
107*288bf522SAndroid Build Coastguard Worker            score = 2
108*288bf522SAndroid Build Coastguard Worker        elif '.symtab' in sections:
109*288bf522SAndroid Build Coastguard Worker            score = 1
110*288bf522SAndroid Build Coastguard Worker        build_id = self.readelf.get_build_id(path)
111*288bf522SAndroid Build Coastguard Worker        if build_id:
112*288bf522SAndroid Build Coastguard Worker            entry = self.host_build_id_map.get(build_id)
113*288bf522SAndroid Build Coastguard Worker            if entry:
114*288bf522SAndroid Build Coastguard Worker                if entry.score < score:
115*288bf522SAndroid Build Coastguard Worker                    entry.path = path
116*288bf522SAndroid Build Coastguard Worker                    entry.score = score
117*288bf522SAndroid Build Coastguard Worker            else:
118*288bf522SAndroid Build Coastguard Worker                repeat_count = self.name_count_map.get(name, 0)
119*288bf522SAndroid Build Coastguard Worker                self.name_count_map[name] = repeat_count + 1
120*288bf522SAndroid Build Coastguard Worker                unique_name = name if repeat_count == 0 else name + '_' + str(repeat_count)
121*288bf522SAndroid Build Coastguard Worker                self.host_build_id_map[build_id] = HostElfEntry(path, unique_name, score)
122*288bf522SAndroid Build Coastguard Worker        else:
123*288bf522SAndroid Build Coastguard Worker            entry = self.no_build_id_file_map.get(name)
124*288bf522SAndroid Build Coastguard Worker            if entry:
125*288bf522SAndroid Build Coastguard Worker                if entry.score < score:
126*288bf522SAndroid Build Coastguard Worker                    entry.path = path
127*288bf522SAndroid Build Coastguard Worker                    entry.score = score
128*288bf522SAndroid Build Coastguard Worker            else:
129*288bf522SAndroid Build Coastguard Worker                self.no_build_id_file_map[name] = HostElfEntry(path, name, score)
130*288bf522SAndroid Build Coastguard Worker
131*288bf522SAndroid Build Coastguard Worker    def collect_native_libs_on_device(self):
132*288bf522SAndroid Build Coastguard Worker        self.device_build_id_map.clear()
133*288bf522SAndroid Build Coastguard Worker        self.adb.check_run(['shell', 'mkdir', '-p', self.dir_on_device])
134*288bf522SAndroid Build Coastguard Worker        if os.path.exists(self.build_id_list_file):
135*288bf522SAndroid Build Coastguard Worker            os.remove(self.build_id_list_file)
136*288bf522SAndroid Build Coastguard Worker        result, output = self.adb.run_and_return_output(['shell', 'ls', self.dir_on_device])
137*288bf522SAndroid Build Coastguard Worker        if not result:
138*288bf522SAndroid Build Coastguard Worker            return
139*288bf522SAndroid Build Coastguard Worker        file_set = set(output.strip().split())
140*288bf522SAndroid Build Coastguard Worker        if self.build_id_list_file not in file_set:
141*288bf522SAndroid Build Coastguard Worker            return
142*288bf522SAndroid Build Coastguard Worker        self.adb.run(['pull', self.dir_on_device + self.build_id_list_file])
143*288bf522SAndroid Build Coastguard Worker        if os.path.exists(self.build_id_list_file):
144*288bf522SAndroid Build Coastguard Worker            with open(self.build_id_list_file, 'rb') as fh:
145*288bf522SAndroid Build Coastguard Worker                for line in fh.readlines():
146*288bf522SAndroid Build Coastguard Worker                    line = bytes_to_str(line).strip()
147*288bf522SAndroid Build Coastguard Worker                    items = line.split('=')
148*288bf522SAndroid Build Coastguard Worker                    if len(items) == 2:
149*288bf522SAndroid Build Coastguard Worker                        build_id, filename = items
150*288bf522SAndroid Build Coastguard Worker                        if filename in file_set:
151*288bf522SAndroid Build Coastguard Worker                            self.device_build_id_map[build_id] = filename
152*288bf522SAndroid Build Coastguard Worker            remove(self.build_id_list_file)
153*288bf522SAndroid Build Coastguard Worker
154*288bf522SAndroid Build Coastguard Worker    def sync_native_libs_on_device(self):
155*288bf522SAndroid Build Coastguard Worker        # Push missing native libs on device.
156*288bf522SAndroid Build Coastguard Worker        for build_id in self.host_build_id_map:
157*288bf522SAndroid Build Coastguard Worker            if build_id not in self.device_build_id_map:
158*288bf522SAndroid Build Coastguard Worker                entry = self.host_build_id_map[build_id]
159*288bf522SAndroid Build Coastguard Worker                self.adb.check_run(['push', entry.path, self.dir_on_device + entry.name])
160*288bf522SAndroid Build Coastguard Worker        # Remove native libs not exist on host.
161*288bf522SAndroid Build Coastguard Worker        for build_id in self.device_build_id_map:
162*288bf522SAndroid Build Coastguard Worker            if build_id not in self.host_build_id_map:
163*288bf522SAndroid Build Coastguard Worker                name = self.device_build_id_map[build_id]
164*288bf522SAndroid Build Coastguard Worker                self.adb.run(['shell', 'rm', self.dir_on_device + name])
165*288bf522SAndroid Build Coastguard Worker        # Push new build_id_list on device.
166*288bf522SAndroid Build Coastguard Worker        with open(self.build_id_list_file, 'wb') as fh:
167*288bf522SAndroid Build Coastguard Worker            for build_id in self.host_build_id_map:
168*288bf522SAndroid Build Coastguard Worker                s = str_to_bytes('%s=%s\n' % (build_id, self.host_build_id_map[build_id].name))
169*288bf522SAndroid Build Coastguard Worker                fh.write(s)
170*288bf522SAndroid Build Coastguard Worker        self.adb.check_run(['push', self.build_id_list_file,
171*288bf522SAndroid Build Coastguard Worker                            self.dir_on_device + self.build_id_list_file])
172*288bf522SAndroid Build Coastguard Worker        os.remove(self.build_id_list_file)
173*288bf522SAndroid Build Coastguard Worker
174*288bf522SAndroid Build Coastguard Worker        # Push elf files without build id on device.
175*288bf522SAndroid Build Coastguard Worker        for entry in self.no_build_id_file_map.values():
176*288bf522SAndroid Build Coastguard Worker            target = self.dir_on_device + entry.name
177*288bf522SAndroid Build Coastguard Worker
178*288bf522SAndroid Build Coastguard Worker            # Skip download if we have a file with the same name and size on device.
179*288bf522SAndroid Build Coastguard Worker            result, output = self.adb.run_and_return_output(['shell', 'ls', '-l', target])
180*288bf522SAndroid Build Coastguard Worker            if result:
181*288bf522SAndroid Build Coastguard Worker                items = output.split()
182*288bf522SAndroid Build Coastguard Worker                if len(items) > 5:
183*288bf522SAndroid Build Coastguard Worker                    try:
184*288bf522SAndroid Build Coastguard Worker                        file_size = int(items[4])
185*288bf522SAndroid Build Coastguard Worker                    except ValueError:
186*288bf522SAndroid Build Coastguard Worker                        file_size = 0
187*288bf522SAndroid Build Coastguard Worker                    if file_size == os.path.getsize(entry.path):
188*288bf522SAndroid Build Coastguard Worker                        continue
189*288bf522SAndroid Build Coastguard Worker            self.adb.check_run(['push', entry.path, target])
190*288bf522SAndroid Build Coastguard Worker
191*288bf522SAndroid Build Coastguard Worker
192*288bf522SAndroid Build Coastguard Workerclass ProfilerBase(object):
193*288bf522SAndroid Build Coastguard Worker    """Base class of all Profilers."""
194*288bf522SAndroid Build Coastguard Worker
195*288bf522SAndroid Build Coastguard Worker    def __init__(self, args):
196*288bf522SAndroid Build Coastguard Worker        self.args = args
197*288bf522SAndroid Build Coastguard Worker        self.adb = AdbHelper(enable_switch_to_root=not args.disable_adb_root)
198*288bf522SAndroid Build Coastguard Worker        if not self.adb.is_device_available():
199*288bf522SAndroid Build Coastguard Worker            log_exit('No Android device is connected via ADB.')
200*288bf522SAndroid Build Coastguard Worker        self.is_root_device = self.adb.switch_to_root()
201*288bf522SAndroid Build Coastguard Worker        self.android_version = self.adb.get_android_version()
202*288bf522SAndroid Build Coastguard Worker        if self.android_version < 7:
203*288bf522SAndroid Build Coastguard Worker            log_exit("""app_profiler.py isn't supported on Android < N, please switch to use
204*288bf522SAndroid Build Coastguard Worker                        simpleperf binary directly.""")
205*288bf522SAndroid Build Coastguard Worker        self.device_arch = self.adb.get_device_arch()
206*288bf522SAndroid Build Coastguard Worker        self.record_subproc = None
207*288bf522SAndroid Build Coastguard Worker
208*288bf522SAndroid Build Coastguard Worker    def profile(self):
209*288bf522SAndroid Build Coastguard Worker        logging.info('prepare profiling')
210*288bf522SAndroid Build Coastguard Worker        self.prepare()
211*288bf522SAndroid Build Coastguard Worker        logging.info('start profiling')
212*288bf522SAndroid Build Coastguard Worker        self.start()
213*288bf522SAndroid Build Coastguard Worker        self.wait_profiling()
214*288bf522SAndroid Build Coastguard Worker        logging.info('collect profiling data')
215*288bf522SAndroid Build Coastguard Worker        self.collect_profiling_data()
216*288bf522SAndroid Build Coastguard Worker        logging.info('profiling is finished.')
217*288bf522SAndroid Build Coastguard Worker
218*288bf522SAndroid Build Coastguard Worker    def prepare(self):
219*288bf522SAndroid Build Coastguard Worker        """Prepare recording. """
220*288bf522SAndroid Build Coastguard Worker        self.download_simpleperf()
221*288bf522SAndroid Build Coastguard Worker        if self.args.native_lib_dir:
222*288bf522SAndroid Build Coastguard Worker            self.download_libs()
223*288bf522SAndroid Build Coastguard Worker
224*288bf522SAndroid Build Coastguard Worker    def download_simpleperf(self):
225*288bf522SAndroid Build Coastguard Worker        simpleperf_binary = get_target_binary_path(self.device_arch, 'simpleperf')
226*288bf522SAndroid Build Coastguard Worker        self.adb.check_run(['push', simpleperf_binary, '/data/local/tmp'])
227*288bf522SAndroid Build Coastguard Worker        self.adb.check_run(['shell', 'chmod', 'a+x', '/data/local/tmp/simpleperf'])
228*288bf522SAndroid Build Coastguard Worker
229*288bf522SAndroid Build Coastguard Worker    def download_libs(self):
230*288bf522SAndroid Build Coastguard Worker        downloader = NativeLibDownloader(self.args.ndk_path, self.device_arch, self.adb)
231*288bf522SAndroid Build Coastguard Worker        downloader.collect_native_libs_on_host(self.args.native_lib_dir)
232*288bf522SAndroid Build Coastguard Worker        downloader.collect_native_libs_on_device()
233*288bf522SAndroid Build Coastguard Worker        downloader.sync_native_libs_on_device()
234*288bf522SAndroid Build Coastguard Worker
235*288bf522SAndroid Build Coastguard Worker    def start(self):
236*288bf522SAndroid Build Coastguard Worker        raise NotImplementedError
237*288bf522SAndroid Build Coastguard Worker
238*288bf522SAndroid Build Coastguard Worker    def start_profiling(self, target_args):
239*288bf522SAndroid Build Coastguard Worker        """Start simpleperf record process on device."""
240*288bf522SAndroid Build Coastguard Worker        args = ['/data/local/tmp/simpleperf', 'record', '-o', '/data/local/tmp/perf.data',
241*288bf522SAndroid Build Coastguard Worker                self.args.record_options]
242*288bf522SAndroid Build Coastguard Worker        if self.adb.run(['shell', 'ls', NATIVE_LIBS_DIR_ON_DEVICE]):
243*288bf522SAndroid Build Coastguard Worker            args += ['--symfs', NATIVE_LIBS_DIR_ON_DEVICE]
244*288bf522SAndroid Build Coastguard Worker        args += ['--log', self.args.log]
245*288bf522SAndroid Build Coastguard Worker        args += target_args
246*288bf522SAndroid Build Coastguard Worker        adb_args = [self.adb.adb_path, 'shell'] + args
247*288bf522SAndroid Build Coastguard Worker        logging.info('run adb cmd: %s' % adb_args)
248*288bf522SAndroid Build Coastguard Worker        self.record_subproc = subprocess.Popen(adb_args)
249*288bf522SAndroid Build Coastguard Worker
250*288bf522SAndroid Build Coastguard Worker    def wait_profiling(self):
251*288bf522SAndroid Build Coastguard Worker        """Wait until profiling finishes, or stop profiling when user presses Ctrl-C."""
252*288bf522SAndroid Build Coastguard Worker        returncode = None
253*288bf522SAndroid Build Coastguard Worker        try:
254*288bf522SAndroid Build Coastguard Worker            returncode = self.record_subproc.wait()
255*288bf522SAndroid Build Coastguard Worker        except KeyboardInterrupt:
256*288bf522SAndroid Build Coastguard Worker            self.stop_profiling()
257*288bf522SAndroid Build Coastguard Worker            self.record_subproc = None
258*288bf522SAndroid Build Coastguard Worker            # Don't check return value of record_subproc. Because record_subproc also
259*288bf522SAndroid Build Coastguard Worker            # receives Ctrl-C, and always returns non-zero.
260*288bf522SAndroid Build Coastguard Worker            returncode = 0
261*288bf522SAndroid Build Coastguard Worker        logging.debug('profiling result [%s]' % (returncode == 0))
262*288bf522SAndroid Build Coastguard Worker        if returncode != 0:
263*288bf522SAndroid Build Coastguard Worker            log_exit('Failed to record profiling data.')
264*288bf522SAndroid Build Coastguard Worker
265*288bf522SAndroid Build Coastguard Worker    def stop_profiling(self):
266*288bf522SAndroid Build Coastguard Worker        """Stop profiling by sending SIGINT to simpleperf, and wait until it exits
267*288bf522SAndroid Build Coastguard Worker           to make sure perf.data is completely generated."""
268*288bf522SAndroid Build Coastguard Worker        has_killed = False
269*288bf522SAndroid Build Coastguard Worker        while True:
270*288bf522SAndroid Build Coastguard Worker            (result, _) = self.adb.run_and_return_output(['shell', 'pidof', 'simpleperf'])
271*288bf522SAndroid Build Coastguard Worker            if not result:
272*288bf522SAndroid Build Coastguard Worker                break
273*288bf522SAndroid Build Coastguard Worker            if not has_killed:
274*288bf522SAndroid Build Coastguard Worker                has_killed = True
275*288bf522SAndroid Build Coastguard Worker                self.adb.run_and_return_output(['shell', 'pkill', '-l', '2', 'simpleperf'])
276*288bf522SAndroid Build Coastguard Worker            time.sleep(1)
277*288bf522SAndroid Build Coastguard Worker
278*288bf522SAndroid Build Coastguard Worker    def collect_profiling_data(self):
279*288bf522SAndroid Build Coastguard Worker        self.adb.check_run_and_return_output(['pull', '/data/local/tmp/perf.data',
280*288bf522SAndroid Build Coastguard Worker                                              self.args.perf_data_path])
281*288bf522SAndroid Build Coastguard Worker        if not self.args.skip_collect_binaries:
282*288bf522SAndroid Build Coastguard Worker            binary_cache_args = [sys.executable,
283*288bf522SAndroid Build Coastguard Worker                                 os.path.join(get_script_dir(), 'binary_cache_builder.py')]
284*288bf522SAndroid Build Coastguard Worker            binary_cache_args += ['-i', self.args.perf_data_path, '--log', self.args.log]
285*288bf522SAndroid Build Coastguard Worker            if self.args.native_lib_dir:
286*288bf522SAndroid Build Coastguard Worker                binary_cache_args += ['-lib', self.args.native_lib_dir]
287*288bf522SAndroid Build Coastguard Worker            if self.args.disable_adb_root:
288*288bf522SAndroid Build Coastguard Worker                binary_cache_args += ['--disable_adb_root']
289*288bf522SAndroid Build Coastguard Worker            if self.args.ndk_path:
290*288bf522SAndroid Build Coastguard Worker                binary_cache_args += ['--ndk_path', self.args.ndk_path]
291*288bf522SAndroid Build Coastguard Worker            subprocess.check_call(binary_cache_args)
292*288bf522SAndroid Build Coastguard Worker
293*288bf522SAndroid Build Coastguard Worker
294*288bf522SAndroid Build Coastguard Workerclass AppProfiler(ProfilerBase):
295*288bf522SAndroid Build Coastguard Worker    """Profile an Android app."""
296*288bf522SAndroid Build Coastguard Worker
297*288bf522SAndroid Build Coastguard Worker    def prepare(self):
298*288bf522SAndroid Build Coastguard Worker        super(AppProfiler, self).prepare()
299*288bf522SAndroid Build Coastguard Worker        self.app_versioncode = self.get_app_versioncode()
300*288bf522SAndroid Build Coastguard Worker        if self.args.compile_java_code:
301*288bf522SAndroid Build Coastguard Worker            self.compile_java_code()
302*288bf522SAndroid Build Coastguard Worker
303*288bf522SAndroid Build Coastguard Worker    def get_app_versioncode(self) -> Optional[str]:
304*288bf522SAndroid Build Coastguard Worker        result, output = self.adb.run_and_return_output(
305*288bf522SAndroid Build Coastguard Worker            ['shell', 'pm', 'list', 'packages', '--show-versioncode'])
306*288bf522SAndroid Build Coastguard Worker        if not result:
307*288bf522SAndroid Build Coastguard Worker            return None
308*288bf522SAndroid Build Coastguard Worker        prefix = f'package:{self.args.app} '
309*288bf522SAndroid Build Coastguard Worker        for line in output.splitlines():
310*288bf522SAndroid Build Coastguard Worker            if line.startswith(prefix):
311*288bf522SAndroid Build Coastguard Worker                pos = line.find('versionCode:')
312*288bf522SAndroid Build Coastguard Worker                if pos != -1:
313*288bf522SAndroid Build Coastguard Worker                    return line[pos + len('versionCode:'):].strip()
314*288bf522SAndroid Build Coastguard Worker        return None
315*288bf522SAndroid Build Coastguard Worker
316*288bf522SAndroid Build Coastguard Worker    def compile_java_code(self):
317*288bf522SAndroid Build Coastguard Worker        self.kill_app_process()
318*288bf522SAndroid Build Coastguard Worker        # Fully compile Java code on Android >= N.
319*288bf522SAndroid Build Coastguard Worker        self.adb.set_property('debug.generate-debug-info', 'true')
320*288bf522SAndroid Build Coastguard Worker        self.adb.check_run(['shell', 'cmd', 'package', 'compile', '-f', '-m', 'speed',
321*288bf522SAndroid Build Coastguard Worker                            self.args.app])
322*288bf522SAndroid Build Coastguard Worker
323*288bf522SAndroid Build Coastguard Worker    def kill_app_process(self):
324*288bf522SAndroid Build Coastguard Worker        if self.find_app_process():
325*288bf522SAndroid Build Coastguard Worker            self.adb.check_run(['shell', 'am', 'force-stop', self.args.app])
326*288bf522SAndroid Build Coastguard Worker            count = 0
327*288bf522SAndroid Build Coastguard Worker            while True:
328*288bf522SAndroid Build Coastguard Worker                time.sleep(1)
329*288bf522SAndroid Build Coastguard Worker                pid = self.find_app_process()
330*288bf522SAndroid Build Coastguard Worker                if not pid:
331*288bf522SAndroid Build Coastguard Worker                    break
332*288bf522SAndroid Build Coastguard Worker                count += 1
333*288bf522SAndroid Build Coastguard Worker                if count >= 5:
334*288bf522SAndroid Build Coastguard Worker                    logging.info('unable to kill %s, skipping...' % self.args.app)
335*288bf522SAndroid Build Coastguard Worker                    break
336*288bf522SAndroid Build Coastguard Worker                # When testing on Android N, `am force-stop` sometimes can't kill
337*288bf522SAndroid Build Coastguard Worker                # com.example.simpleperf.simpleperfexampleofkotlin. So use kill when this happens.
338*288bf522SAndroid Build Coastguard Worker                if count >= 3:
339*288bf522SAndroid Build Coastguard Worker                    self.run_in_app_dir(['kill', '-9', str(pid)])
340*288bf522SAndroid Build Coastguard Worker
341*288bf522SAndroid Build Coastguard Worker    def find_app_process(self):
342*288bf522SAndroid Build Coastguard Worker        result, pidof_output = self.adb.run_and_return_output(
343*288bf522SAndroid Build Coastguard Worker            ['shell', 'pidof', self.args.app])
344*288bf522SAndroid Build Coastguard Worker        if not result:
345*288bf522SAndroid Build Coastguard Worker            return None
346*288bf522SAndroid Build Coastguard Worker        result, current_user = self.adb.run_and_return_output(
347*288bf522SAndroid Build Coastguard Worker            ['shell', 'am', 'get-current-user'])
348*288bf522SAndroid Build Coastguard Worker        if not result:
349*288bf522SAndroid Build Coastguard Worker            return None
350*288bf522SAndroid Build Coastguard Worker        pids = pidof_output.split()
351*288bf522SAndroid Build Coastguard Worker        for pid in pids:
352*288bf522SAndroid Build Coastguard Worker            result, ps_output = self.adb.run_and_return_output(
353*288bf522SAndroid Build Coastguard Worker                ['shell', 'ps', '-p', pid, '-o', 'USER'])
354*288bf522SAndroid Build Coastguard Worker            if not result:
355*288bf522SAndroid Build Coastguard Worker                return None
356*288bf522SAndroid Build Coastguard Worker            uid = SHELL_PS_UID_PATTERN.search(ps_output).group(1)
357*288bf522SAndroid Build Coastguard Worker            if uid == current_user.strip():
358*288bf522SAndroid Build Coastguard Worker                return int(pid)
359*288bf522SAndroid Build Coastguard Worker        return None
360*288bf522SAndroid Build Coastguard Worker
361*288bf522SAndroid Build Coastguard Worker    def run_in_app_dir(self, args):
362*288bf522SAndroid Build Coastguard Worker        if self.is_root_device:
363*288bf522SAndroid Build Coastguard Worker            adb_args = ['shell', 'cd /data/data/' + self.args.app + ' && ' + (' '.join(args))]
364*288bf522SAndroid Build Coastguard Worker        else:
365*288bf522SAndroid Build Coastguard Worker            adb_args = ['shell', 'run-as', self.args.app] + args
366*288bf522SAndroid Build Coastguard Worker        return self.adb.run_and_return_output(adb_args)
367*288bf522SAndroid Build Coastguard Worker
368*288bf522SAndroid Build Coastguard Worker    def start(self):
369*288bf522SAndroid Build Coastguard Worker        if self.args.launch or self.args.activity or self.args.test:
370*288bf522SAndroid Build Coastguard Worker            self.kill_app_process()
371*288bf522SAndroid Build Coastguard Worker        args = ['--app', self.args.app]
372*288bf522SAndroid Build Coastguard Worker        if self.app_versioncode:
373*288bf522SAndroid Build Coastguard Worker            args += ['--add-meta-info', f'app_versioncode={self.app_versioncode}']
374*288bf522SAndroid Build Coastguard Worker        self.start_profiling(args)
375*288bf522SAndroid Build Coastguard Worker        if self.args.launch:
376*288bf522SAndroid Build Coastguard Worker            self.start_app()
377*288bf522SAndroid Build Coastguard Worker        if self.args.activity:
378*288bf522SAndroid Build Coastguard Worker            self.start_activity()
379*288bf522SAndroid Build Coastguard Worker        elif self.args.test:
380*288bf522SAndroid Build Coastguard Worker            self.start_test()
381*288bf522SAndroid Build Coastguard Worker        # else: no need to start an activity or test.
382*288bf522SAndroid Build Coastguard Worker
383*288bf522SAndroid Build Coastguard Worker    def start_app(self):
384*288bf522SAndroid Build Coastguard Worker        result = self.adb.run(['shell', 'monkey', '-p', self.args.app, '1'])
385*288bf522SAndroid Build Coastguard Worker        if not result:
386*288bf522SAndroid Build Coastguard Worker            self.record_subproc.terminate()
387*288bf522SAndroid Build Coastguard Worker            log_exit(f"Can't start {self.args.app}")
388*288bf522SAndroid Build Coastguard Worker
389*288bf522SAndroid Build Coastguard Worker    def start_activity(self):
390*288bf522SAndroid Build Coastguard Worker        activity = self.args.app + '/' + self.args.activity
391*288bf522SAndroid Build Coastguard Worker        result = self.adb.run(['shell', 'am', 'start', '-n', activity])
392*288bf522SAndroid Build Coastguard Worker        if not result:
393*288bf522SAndroid Build Coastguard Worker            self.record_subproc.terminate()
394*288bf522SAndroid Build Coastguard Worker            log_exit("Can't start activity %s" % activity)
395*288bf522SAndroid Build Coastguard Worker
396*288bf522SAndroid Build Coastguard Worker    def start_test(self):
397*288bf522SAndroid Build Coastguard Worker        runner = self.args.app + '/androidx.test.runner.AndroidJUnitRunner'
398*288bf522SAndroid Build Coastguard Worker        result = self.adb.run(['shell', 'am', 'instrument', '-e', 'class',
399*288bf522SAndroid Build Coastguard Worker                               self.args.test, runner])
400*288bf522SAndroid Build Coastguard Worker        if not result:
401*288bf522SAndroid Build Coastguard Worker            self.record_subproc.terminate()
402*288bf522SAndroid Build Coastguard Worker            log_exit("Can't start instrumentation test  %s" % self.args.test)
403*288bf522SAndroid Build Coastguard Worker
404*288bf522SAndroid Build Coastguard Worker
405*288bf522SAndroid Build Coastguard Workerclass NativeProgramProfiler(ProfilerBase):
406*288bf522SAndroid Build Coastguard Worker    """Profile a native program."""
407*288bf522SAndroid Build Coastguard Worker
408*288bf522SAndroid Build Coastguard Worker    def start(self):
409*288bf522SAndroid Build Coastguard Worker        logging.info('Waiting for native process %s' % self.args.native_program)
410*288bf522SAndroid Build Coastguard Worker        while True:
411*288bf522SAndroid Build Coastguard Worker            (result, pid) = self.adb.run_and_return_output(['shell', 'pidof',
412*288bf522SAndroid Build Coastguard Worker                                                            self.args.native_program])
413*288bf522SAndroid Build Coastguard Worker            if not result:
414*288bf522SAndroid Build Coastguard Worker                # Wait for 1 millisecond.
415*288bf522SAndroid Build Coastguard Worker                time.sleep(0.001)
416*288bf522SAndroid Build Coastguard Worker            else:
417*288bf522SAndroid Build Coastguard Worker                self.start_profiling(['-p', str(int(pid))])
418*288bf522SAndroid Build Coastguard Worker                break
419*288bf522SAndroid Build Coastguard Worker
420*288bf522SAndroid Build Coastguard Worker
421*288bf522SAndroid Build Coastguard Workerclass NativeCommandProfiler(ProfilerBase):
422*288bf522SAndroid Build Coastguard Worker    """Profile running a native command."""
423*288bf522SAndroid Build Coastguard Worker
424*288bf522SAndroid Build Coastguard Worker    def start(self):
425*288bf522SAndroid Build Coastguard Worker        self.start_profiling([self.args.cmd])
426*288bf522SAndroid Build Coastguard Worker
427*288bf522SAndroid Build Coastguard Worker
428*288bf522SAndroid Build Coastguard Workerclass NativeProcessProfiler(ProfilerBase):
429*288bf522SAndroid Build Coastguard Worker    """Profile processes given their pids."""
430*288bf522SAndroid Build Coastguard Worker
431*288bf522SAndroid Build Coastguard Worker    def start(self):
432*288bf522SAndroid Build Coastguard Worker        self.start_profiling(['-p', ','.join(self.args.pid)])
433*288bf522SAndroid Build Coastguard Worker
434*288bf522SAndroid Build Coastguard Worker
435*288bf522SAndroid Build Coastguard Workerclass NativeThreadProfiler(ProfilerBase):
436*288bf522SAndroid Build Coastguard Worker    """Profile threads given their tids."""
437*288bf522SAndroid Build Coastguard Worker
438*288bf522SAndroid Build Coastguard Worker    def start(self):
439*288bf522SAndroid Build Coastguard Worker        self.start_profiling(['-t', ','.join(self.args.tid)])
440*288bf522SAndroid Build Coastguard Worker
441*288bf522SAndroid Build Coastguard Worker
442*288bf522SAndroid Build Coastguard Workerclass SystemWideProfiler(ProfilerBase):
443*288bf522SAndroid Build Coastguard Worker    """Profile system wide."""
444*288bf522SAndroid Build Coastguard Worker
445*288bf522SAndroid Build Coastguard Worker    def start(self):
446*288bf522SAndroid Build Coastguard Worker        self.start_profiling(['-a'])
447*288bf522SAndroid Build Coastguard Worker
448*288bf522SAndroid Build Coastguard Worker
449*288bf522SAndroid Build Coastguard Workerdef main():
450*288bf522SAndroid Build Coastguard Worker    parser = BaseArgumentParser(description=__doc__)
451*288bf522SAndroid Build Coastguard Worker
452*288bf522SAndroid Build Coastguard Worker    target_group = parser.add_argument_group(title='Select profiling target'
453*288bf522SAndroid Build Coastguard Worker                                             ).add_mutually_exclusive_group(required=True)
454*288bf522SAndroid Build Coastguard Worker    target_group.add_argument('-p', '--app', help="""Profile an Android app, given the package name.
455*288bf522SAndroid Build Coastguard Worker                              Like `-p com.example.android.myapp`.""")
456*288bf522SAndroid Build Coastguard Worker
457*288bf522SAndroid Build Coastguard Worker    target_group.add_argument('-np', '--native_program', help="""Profile a native program running on
458*288bf522SAndroid Build Coastguard Worker                              the Android device. Like `-np surfaceflinger`.""")
459*288bf522SAndroid Build Coastguard Worker
460*288bf522SAndroid Build Coastguard Worker    target_group.add_argument('-cmd', help="""Profile running a command on the Android device.
461*288bf522SAndroid Build Coastguard Worker                              Like `-cmd "pm -l"`.""")
462*288bf522SAndroid Build Coastguard Worker
463*288bf522SAndroid Build Coastguard Worker    target_group.add_argument('--pid', nargs='+', help="""Profile native processes running on device
464*288bf522SAndroid Build Coastguard Worker                              given their process ids.""")
465*288bf522SAndroid Build Coastguard Worker
466*288bf522SAndroid Build Coastguard Worker    target_group.add_argument('--tid', nargs='+', help="""Profile native threads running on device
467*288bf522SAndroid Build Coastguard Worker                              given their thread ids.""")
468*288bf522SAndroid Build Coastguard Worker
469*288bf522SAndroid Build Coastguard Worker    target_group.add_argument('--system_wide', action='store_true', help="""Profile system wide.""")
470*288bf522SAndroid Build Coastguard Worker
471*288bf522SAndroid Build Coastguard Worker    app_target_group = parser.add_argument_group(title='Extra options for profiling an app')
472*288bf522SAndroid Build Coastguard Worker    app_target_group.add_argument('--compile_java_code', action='store_true', help="""Used with -p.
473*288bf522SAndroid Build Coastguard Worker                                  On Android N and Android O, we need to compile Java code into
474*288bf522SAndroid Build Coastguard Worker                                  native instructions to profile Java code. Android O also needs
475*288bf522SAndroid Build Coastguard Worker                                  wrap.sh in the apk to use the native instructions.""")
476*288bf522SAndroid Build Coastguard Worker
477*288bf522SAndroid Build Coastguard Worker    app_start_group = app_target_group.add_mutually_exclusive_group()
478*288bf522SAndroid Build Coastguard Worker    app_start_group.add_argument('--launch', action='store_true', help="""Used with -p. Profile the
479*288bf522SAndroid Build Coastguard Worker                                 launch time of an Android app. The app will be started or
480*288bf522SAndroid Build Coastguard Worker                                 restarted.""")
481*288bf522SAndroid Build Coastguard Worker    app_start_group.add_argument('-a', '--activity', help="""Used with -p. Profile the launch time
482*288bf522SAndroid Build Coastguard Worker                                 of an activity in an Android app. The app will be started or
483*288bf522SAndroid Build Coastguard Worker                                 restarted to run the activity. Like `-a .MainActivity`.""")
484*288bf522SAndroid Build Coastguard Worker
485*288bf522SAndroid Build Coastguard Worker    app_start_group.add_argument('-t', '--test', help="""Used with -p. Profile the launch time of an
486*288bf522SAndroid Build Coastguard Worker                                 instrumentation test in an Android app. The app will be started or
487*288bf522SAndroid Build Coastguard Worker                                 restarted to run the instrumentation test. Like
488*288bf522SAndroid Build Coastguard Worker                                 `-t test_class_name`.""")
489*288bf522SAndroid Build Coastguard Worker
490*288bf522SAndroid Build Coastguard Worker    record_group = parser.add_argument_group('Select recording options')
491*288bf522SAndroid Build Coastguard Worker    record_group.add_argument('-r', '--record_options',
492*288bf522SAndroid Build Coastguard Worker                              default='-e task-clock:u -f 1000 -g --duration 10', help="""Set
493*288bf522SAndroid Build Coastguard Worker                              recording options for `simpleperf record` command. Use
494*288bf522SAndroid Build Coastguard Worker                              `run_simpleperf_on_device.py record -h` to see all accepted options.
495*288bf522SAndroid Build Coastguard Worker                              Default is "-e task-clock:u -f 1000 -g --duration 10".""")
496*288bf522SAndroid Build Coastguard Worker
497*288bf522SAndroid Build Coastguard Worker    record_group.add_argument('-lib', '--native_lib_dir', type=extant_dir,
498*288bf522SAndroid Build Coastguard Worker                              help="""When profiling an Android app containing native libraries,
499*288bf522SAndroid Build Coastguard Worker                                      the native libraries are usually stripped and lake of symbols
500*288bf522SAndroid Build Coastguard Worker                                      and debug information to provide good profiling result. By
501*288bf522SAndroid Build Coastguard Worker                                      using -lib, you tell app_profiler.py the path storing
502*288bf522SAndroid Build Coastguard Worker                                      unstripped native libraries, and app_profiler.py will search
503*288bf522SAndroid Build Coastguard Worker                                      all shared libraries with suffix .so in the directory. Then
504*288bf522SAndroid Build Coastguard Worker                                      the native libraries will be downloaded on device and
505*288bf522SAndroid Build Coastguard Worker                                      collected in build_cache.""")
506*288bf522SAndroid Build Coastguard Worker
507*288bf522SAndroid Build Coastguard Worker    record_group.add_argument('-o', '--perf_data_path', default='perf.data',
508*288bf522SAndroid Build Coastguard Worker                              help='The path to store profiling data. Default is perf.data.')
509*288bf522SAndroid Build Coastguard Worker
510*288bf522SAndroid Build Coastguard Worker    record_group.add_argument('-nb', '--skip_collect_binaries', action='store_true',
511*288bf522SAndroid Build Coastguard Worker                              help="""By default we collect binaries used in profiling data from
512*288bf522SAndroid Build Coastguard Worker                                      device to binary_cache directory. It can be used to annotate
513*288bf522SAndroid Build Coastguard Worker                                      source code and disassembly. This option skips it.""")
514*288bf522SAndroid Build Coastguard Worker
515*288bf522SAndroid Build Coastguard Worker    other_group = parser.add_argument_group('Other options')
516*288bf522SAndroid Build Coastguard Worker    other_group.add_argument('--ndk_path', type=extant_dir,
517*288bf522SAndroid Build Coastguard Worker                             help="""Set the path of a ndk release. app_profiler.py needs some
518*288bf522SAndroid Build Coastguard Worker                                     tools in ndk, like readelf.""")
519*288bf522SAndroid Build Coastguard Worker
520*288bf522SAndroid Build Coastguard Worker    other_group.add_argument('--disable_adb_root', action='store_true',
521*288bf522SAndroid Build Coastguard Worker                             help="""Force adb to run in non root mode. By default, app_profiler.py
522*288bf522SAndroid Build Coastguard Worker                                     will try to switch to root mode to be able to profile released
523*288bf522SAndroid Build Coastguard Worker                                     Android apps.""")
524*288bf522SAndroid Build Coastguard Worker
525*288bf522SAndroid Build Coastguard Worker    def check_args(args):
526*288bf522SAndroid Build Coastguard Worker        if (not args.app) and (args.compile_java_code or args.activity or args.test):
527*288bf522SAndroid Build Coastguard Worker            log_exit('--compile_java_code, -a, -t can only be used when profiling an Android app.')
528*288bf522SAndroid Build Coastguard Worker
529*288bf522SAndroid Build Coastguard Worker    args = parser.parse_args()
530*288bf522SAndroid Build Coastguard Worker    check_args(args)
531*288bf522SAndroid Build Coastguard Worker    if args.app:
532*288bf522SAndroid Build Coastguard Worker        profiler = AppProfiler(args)
533*288bf522SAndroid Build Coastguard Worker    elif args.native_program:
534*288bf522SAndroid Build Coastguard Worker        profiler = NativeProgramProfiler(args)
535*288bf522SAndroid Build Coastguard Worker    elif args.cmd:
536*288bf522SAndroid Build Coastguard Worker        profiler = NativeCommandProfiler(args)
537*288bf522SAndroid Build Coastguard Worker    elif args.pid:
538*288bf522SAndroid Build Coastguard Worker        profiler = NativeProcessProfiler(args)
539*288bf522SAndroid Build Coastguard Worker    elif args.tid:
540*288bf522SAndroid Build Coastguard Worker        profiler = NativeThreadProfiler(args)
541*288bf522SAndroid Build Coastguard Worker    elif args.system_wide:
542*288bf522SAndroid Build Coastguard Worker        profiler = SystemWideProfiler(args)
543*288bf522SAndroid Build Coastguard Worker    profiler.profile()
544*288bf522SAndroid Build Coastguard Worker
545*288bf522SAndroid Build Coastguard Worker
546*288bf522SAndroid Build Coastguard Workerif __name__ == '__main__':
547*288bf522SAndroid Build Coastguard Worker    main()
548