• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# Copyright (C) 2021 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16import atexit
17import argparse
18import datetime
19import hashlib
20import http.server
21import os
22import re
23import shutil
24import socketserver
25import subprocess
26import sys
27import time
28import webbrowser
29
30from perfetto.prebuilts.manifests.tracebox import *
31from perfetto.prebuilts.perfetto_prebuilts import *
32from perfetto.common.repo_utils import *
33
34# This is not required. It's only used as a fallback if no adb is found on the
35# PATH. It's fine if it doesn't exist so this script can be copied elsewhere.
36HERMETIC_ADB_PATH = repo_dir('buildtools/android_sdk/platform-tools/adb')
37
38# Translates the Android ro.product.cpu.abi into the GN's target_cpu.
39ABI_TO_ARCH = {
40    'armeabi-v7a': 'arm',
41    'arm64-v8a': 'arm64',
42    'x86': 'x86',
43    'x86_64': 'x64',
44}
45
46MAX_ADB_FAILURES = 15  # 2 seconds between retries, 30 seconds total.
47
48devnull = open(os.devnull, 'rb')
49adb_path = None
50procs = []
51
52
53class ANSI:
54  END = '\033[0m'
55  BOLD = '\033[1m'
56  RED = '\033[91m'
57  BLACK = '\033[30m'
58  BLUE = '\033[94m'
59  BG_YELLOW = '\033[43m'
60  BG_BLUE = '\033[44m'
61
62
63# HTTP Server used to open the trace in the browser.
64class HttpHandler(http.server.SimpleHTTPRequestHandler):
65
66  def end_headers(self):
67    self.send_header('Access-Control-Allow-Origin', '*')
68    return super().end_headers()
69
70  def do_GET(self):
71    self.server.last_request = self.path
72    return super().do_GET()
73
74  def do_POST(self):
75    self.send_error(404, "File not found")
76
77
78def main():
79  atexit.register(kill_all_subprocs_on_exit)
80  default_out_dir_str = '~/traces/'
81  default_out_dir = os.path.expanduser(default_out_dir_str)
82
83  examples = '\n'.join([
84      ANSI.BOLD + 'Examples' + ANSI.END, '  -t 10s -b 32mb sched gfx wm -a*',
85      '  -t 5s sched/sched_switch raw_syscalls/sys_enter raw_syscalls/sys_exit',
86      '  -c /path/to/full-textual-trace.config', '',
87      ANSI.BOLD + 'Long traces' + ANSI.END,
88      'If you want to record a hours long trace and stream it into a file ',
89      'you need to pass a full trace config and set write_into_file = true.',
90      'See https://perfetto.dev/docs/concepts/config#long-traces .'
91  ])
92  parser = argparse.ArgumentParser(
93      epilog=examples, formatter_class=argparse.RawTextHelpFormatter)
94
95  help = 'Output file or directory (default: %s)' % default_out_dir_str
96  parser.add_argument('-o', '--out', default=default_out_dir, help=help)
97
98  help = 'Don\'t open in the browser'
99  parser.add_argument('-n', '--no-open', action='store_true', help=help)
100
101  help = 'Force the use of the sideloaded binaries rather than system daemons'
102  parser.add_argument('--sideload', action='store_true', help=help)
103
104  help = ('Sideload the given binary rather than downloading it. ' +
105          'Implies --sideload')
106  parser.add_argument('--sideload-path', default=None, help=help)
107
108  help = 'Don\'t run `adb root` run as user (only when sideloading)'
109  parser.add_argument('-u', '--user', action='store_true', help=help)
110
111  help = 'Specify the ADB device serial'
112  parser.add_argument('--serial', '-s', default=None, help=help)
113
114  grp = parser.add_argument_group(
115      'Short options: (only when not using -c/--config)')
116
117  help = 'Trace duration N[s,m,h] (default: trace until stopped)'
118  grp.add_argument('-t', '--time', default='0s', help=help)
119
120  help = 'Ring buffer size N[mb,gb] (default: 32mb)'
121  grp.add_argument('-b', '--buffer', default='32mb', help=help)
122
123  help = ('Android (atrace) app names. Can be specified multiple times.\n-a*' +
124          'for all apps (without space between a and * or bash will expand it)')
125  grp.add_argument(
126      '-a',
127      '--app',
128      metavar='com.myapp',
129      action='append',
130      default=[],
131      help=help)
132
133  help = 'sched, gfx, am, wm (see --list)'
134  grp.add_argument('events', metavar='Atrace events', nargs='*', help=help)
135
136  help = 'sched/sched_switch kmem/kmem (see --list-ftrace)'
137  grp.add_argument('_', metavar='Ftrace events', nargs='*', help=help)
138
139  help = 'Lists all the categories available'
140  grp.add_argument('--list', action='store_true', help=help)
141
142  help = 'Lists all the ftrace events available'
143  grp.add_argument('--list-ftrace', action='store_true', help=help)
144
145  section = ('Full trace config (only when not using short options)')
146  grp = parser.add_argument_group(section)
147
148  help = 'Can be generated with https://ui.perfetto.dev/#!/record'
149  grp.add_argument('-c', '--config', default=None, help=help)
150
151  args = parser.parse_args()
152  args.sideload = args.sideload or args.sideload_path is not None
153
154  if args.serial:
155    os.environ["ANDROID_SERIAL"] = args.serial
156
157  find_adb()
158
159  if args.list:
160    adb('shell', 'atrace', '--list_categories').wait()
161    sys.exit(0)
162
163  if args.list_ftrace:
164    adb('shell', 'cat /d/tracing/available_events | tr : /').wait()
165    sys.exit(0)
166
167  if args.config is not None and not os.path.exists(args.config):
168    prt('Config file not found: %s' % args.config, ANSI.RED)
169    sys.exit(1)
170
171  if len(args.events) == 0 and args.config is None:
172    prt('Must either pass short options (e.g. -t 10s sched) or a --config file',
173        ANSI.RED)
174    parser.print_help()
175    sys.exit(1)
176
177  if args.config is None and args.events and os.path.exists(args.events[0]):
178    prt(('The passed event name "%s" is a local file. ' % args.events[0] +
179         'Did you mean to pass -c / --config ?'), ANSI.RED)
180    sys.exit(1)
181
182  perfetto_cmd = 'perfetto'
183  device_dir = '/data/misc/perfetto-traces/'
184
185  # Check the version of android. If too old (< Q) sideload tracebox. Also use
186  # use /data/local/tmp as /data/misc/perfetto-traces was introduced only later.
187  probe_cmd = 'getprop ro.build.version.sdk; getprop ro.product.cpu.abi; whoami'
188  probe = adb('shell', probe_cmd, stdout=subprocess.PIPE)
189  lines = probe.communicate()[0].decode().strip().split('\n')
190  lines = [x.strip() for x in lines]  # To strip \r(s) on Windows.
191  if probe.returncode != 0:
192    prt('ADB connection failed', ANSI.RED)
193    sys.exit(1)
194  api_level = int(lines[0])
195  abi = lines[1]
196  arch = ABI_TO_ARCH.get(abi)
197  if arch is None:
198    prt('Unsupported ABI: ' + abi)
199    sys.exit(1)
200  shell_user = lines[2]
201  if api_level < 29 or args.sideload:  # 29: Android Q.
202    tracebox_bin = args.sideload_path
203    if tracebox_bin is None:
204      tracebox_bin = get_perfetto_prebuilt(
205          TRACEBOX_MANIFEST, arch='android-' + arch)
206    perfetto_cmd = '/data/local/tmp/tracebox'
207    exit_code = adb('push', '--sync', tracebox_bin, perfetto_cmd).wait()
208    exit_code |= adb('shell', 'chmod 755 ' + perfetto_cmd).wait()
209    if exit_code != 0:
210      prt('ADB push failed', ANSI.RED)
211      sys.exit(1)
212    device_dir = '/data/local/tmp/'
213    if shell_user != 'root' and not args.user:
214      # Run as root if possible as that will give access to more tracing
215      # capabilities. Non-root still works, but some ftrace events might not be
216      # available.
217      adb('root').wait()
218
219  tstamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M')
220  fname = '%s-%s.pftrace' % (tstamp, os.urandom(3).hex())
221  device_file = device_dir + fname
222
223  cmd = [perfetto_cmd, '--background', '--txt', '-o', device_file]
224  on_device_config = None
225  on_host_config = None
226  if args.config is not None:
227    cmd += ['-c', '-']
228    if api_level < 24:
229      # adb shell does not redirect stdin. Push the config on a temporary file
230      # on the device.
231      mktmp = adb(
232          'shell',
233          'mktemp',
234          '--tmpdir',
235          '/data/local/tmp',
236          stdout=subprocess.PIPE)
237      on_device_config = mktmp.communicate()[0].decode().strip().strip()
238      if mktmp.returncode != 0:
239        prt('Failed to create config on device', ANSI.RED)
240        sys.exit(1)
241      exit_code = adb('push', '--sync', args.config, on_device_config).wait()
242      if exit_code != 0:
243        prt('Failed to push config on device', ANSI.RED)
244        sys.exit(1)
245      cmd = ['cat', on_device_config, '|'] + cmd
246    else:
247      on_host_config = args.config
248  else:
249    cmd += ['-t', args.time, '-b', args.buffer]
250    for app in args.app:
251      cmd += ['--app', '\'' + app + '\'']
252    cmd += args.events
253
254  # Perfetto will error out with a proper message if both a config file and
255  # short options are specified. No need to replicate that logic.
256
257  # Work out the output file or directory.
258  if args.out.endswith('/') or os.path.isdir(args.out):
259    host_dir = args.out
260    host_file = os.path.join(args.out, fname)
261  else:
262    host_file = args.out
263    host_dir = os.path.dirname(host_file)
264    if host_dir == '':
265      host_dir = '.'
266      host_file = './' + host_file
267  if not os.path.exists(host_dir):
268    shutil.os.makedirs(host_dir)
269
270  with open(on_host_config or os.devnull, 'rb') as f:
271    print('Running ' + ' '.join(cmd))
272    proc = adb('shell', *cmd, stdin=f, stdout=subprocess.PIPE)
273    proc_out = proc.communicate()[0].decode().strip()
274    if on_device_config is not None:
275      adb('shell', 'rm', on_device_config).wait()
276    # On older versions of Android (x86_64 emulator running API 22) the output
277    # looks like:
278    #   WARNING: linker: /data/local/tmp/tracebox: unused DT entry: ...
279    #   WARNING: ... (other 2 WARNING: linker: lines)
280    #   1234  <-- The actual pid we want.
281    match = re.search(r'^(\d+)$', proc_out, re.M)
282    if match is None:
283      prt('Failed to read the pid from perfetto --background', ANSI.RED)
284      prt(proc_out)
285      sys.exit(1)
286    bg_pid = match.group(1)
287    exit_code = proc.wait()
288
289  if exit_code != 0:
290    prt('Perfetto invocation failed', ANSI.RED)
291    sys.exit(1)
292
293  prt('Trace started. Press CTRL+C to stop', ANSI.BLACK + ANSI.BG_BLUE)
294  logcat = adb('logcat', '-v', 'brief', '-s', 'perfetto', '-b', 'main', '-T',
295               '1')
296
297  ctrl_c_count = 0
298  adb_failure_count = 0
299  while ctrl_c_count < 2:
300    try:
301      # On older Android devices adbd doesn't propagate the exit code. Hence
302      # the RUN/TERM parts.
303      poll = adb(
304          'shell',
305          'test -d /proc/%s && echo RUN || echo TERM' % bg_pid,
306          stdout=subprocess.PIPE)
307      poll_res = poll.communicate()[0].decode().strip()
308      if poll_res == 'TERM':
309        break  # Process terminated
310      if poll_res == 'RUN':
311        # The 'perfetto' cmdline client is still running. If previously we had
312        # an ADB error, tell the user now it's all right again.
313        if adb_failure_count > 0:
314          adb_failure_count = 0
315          prt('ADB connection re-established, the trace is still ongoing',
316              ANSI.BLUE)
317        time.sleep(0.5)
318        continue
319      # Some ADB error happened. This can happen when tracing soon after boot,
320      # before logging in, when adb gets restarted.
321      adb_failure_count += 1
322      if adb_failure_count >= MAX_ADB_FAILURES:
323        prt('Too many unrecoverable ADB failures, bailing out', ANSI.RED)
324        sys.exit(1)
325      time.sleep(2)
326    except KeyboardInterrupt:
327      sig = 'TERM' if ctrl_c_count == 0 else 'KILL'
328      ctrl_c_count += 1
329      prt('Stopping the trace (SIG%s)' % sig, ANSI.BLACK + ANSI.BG_YELLOW)
330      adb('shell', 'kill -%s %s' % (sig, bg_pid)).wait()
331
332  logcat.kill()
333  logcat.wait()
334
335  prt('\n')
336  prt('Pulling into %s' % host_file, ANSI.BOLD)
337  adb('pull', device_file, host_file).wait()
338  adb('shell', 'rm -f ' + device_file).wait()
339
340  if not args.no_open:
341    prt('\n')
342    prt('Opening the trace (%s) in the browser' % host_file)
343    open_trace_in_browser(host_file)
344
345
346def prt(msg, colors=ANSI.END):
347  print(colors + msg + ANSI.END)
348
349
350def find_adb():
351  """ Locate the "right" adb path
352
353  If adb is in the PATH use that (likely what the user wants) otherwise use the
354  hermetic one in our SDK copy.
355  """
356  global adb_path
357  for path in ['adb', HERMETIC_ADB_PATH]:
358    try:
359      subprocess.call([path, '--version'], stdout=devnull, stderr=devnull)
360      adb_path = path
361      break
362    except OSError:
363      continue
364  if adb_path is None:
365    sdk_url = 'https://developer.android.com/studio/releases/platform-tools'
366    prt('Could not find a suitable adb binary in the PATH. ', ANSI.RED)
367    prt('You can download adb from %s' % sdk_url, ANSI.RED)
368    sys.exit(1)
369
370
371def open_trace_in_browser(path):
372  # We reuse the HTTP+RPC port because it's the only one allowed by the CSP.
373  PORT = 9001
374  os.chdir(os.path.dirname(path))
375  fname = os.path.basename(path)
376  socketserver.TCPServer.allow_reuse_address = True
377  with socketserver.TCPServer(('127.0.0.1', PORT), HttpHandler) as httpd:
378    webbrowser.open_new_tab(
379        'https://ui.perfetto.dev/#!/?url=http://127.0.0.1:%d/%s' %
380        (PORT, fname))
381    while httpd.__dict__.get('last_request') != '/' + fname:
382      httpd.handle_request()
383
384
385def adb(*args, stdin=devnull, stdout=None):
386  cmd = [adb_path, *args]
387  setpgrp = None
388  if os.name != 'nt':
389    # On Linux/Mac, start a new process group so all child processes are killed
390    # on exit. Unsupported on Windows.
391    setpgrp = lambda: os.setpgrp()
392  proc = subprocess.Popen(cmd, stdin=stdin, stdout=stdout, preexec_fn=setpgrp)
393  procs.append(proc)
394  return proc
395
396
397def kill_all_subprocs_on_exit():
398  for p in [p for p in procs if p.poll() is None]:
399    p.kill()
400
401
402def check_hash(file_name, sha_value):
403  with open(file_name, 'rb') as fd:
404    file_hash = hashlib.sha1(fd.read()).hexdigest()
405    return file_hash == sha_value
406
407
408if __name__ == '__main__':
409  sys.exit(main())
410