• 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', self.server.allow_origin)
68    self.send_header('Cache-Control', 'no-cache')
69    super().end_headers()
70
71  def do_GET(self):
72    if self.path != '/' + self.server.expected_fname:
73      self.send_error(404, "File not found")
74      return
75
76    self.server.fname_get_completed = True
77    super().do_GET()
78
79  def do_POST(self):
80    self.send_error(404, "File not found")
81
82
83def main():
84  atexit.register(kill_all_subprocs_on_exit)
85  default_out_dir_str = '~/traces/'
86  default_out_dir = os.path.expanduser(default_out_dir_str)
87
88  examples = '\n'.join([
89      ANSI.BOLD + 'Examples' + ANSI.END, '  -t 10s -b 32mb sched gfx wm -a*',
90      '  -t 5s sched/sched_switch raw_syscalls/sys_enter raw_syscalls/sys_exit',
91      '  -c /path/to/full-textual-trace.config', '',
92      ANSI.BOLD + 'Long traces' + ANSI.END,
93      'If you want to record a hours long trace and stream it into a file ',
94      'you need to pass a full trace config and set write_into_file = true.',
95      'See https://perfetto.dev/docs/concepts/config#long-traces .'
96  ])
97  parser = argparse.ArgumentParser(
98      epilog=examples, formatter_class=argparse.RawTextHelpFormatter)
99
100  help = 'Output file or directory (default: %s)' % default_out_dir_str
101  parser.add_argument('-o', '--out', default=default_out_dir, help=help)
102
103  help = 'Don\'t open or serve the trace'
104  parser.add_argument('-n', '--no-open', action='store_true', help=help)
105
106  help = 'Don\'t open in browser, but still serve trace (good for remote use)'
107  parser.add_argument('--no-open-browser', action='store_true', help=help)
108
109  help = 'The web address used to open trace files'
110  parser.add_argument('--origin', default='https://ui.perfetto.dev', help=help)
111
112  help = 'Force the use of the sideloaded binaries rather than system daemons'
113  parser.add_argument('--sideload', action='store_true', help=help)
114
115  help = ('Sideload the given binary rather than downloading it. ' +
116          'Implies --sideload')
117  parser.add_argument('--sideload-path', default=None, help=help)
118
119  help = 'Ignores any tracing guardrails which might be used'
120  parser.add_argument('--no-guardrails', action='store_true', help=help)
121
122  help = 'Don\'t run `adb root` run as user (only when sideloading)'
123  parser.add_argument('-u', '--user', action='store_true', help=help)
124
125  help = 'Specify the ADB device serial'
126  parser.add_argument('--serial', '-s', default=None, help=help)
127
128  grp = parser.add_argument_group(
129      'Short options: (only when not using -c/--config)')
130
131  help = 'Trace duration N[s,m,h] (default: trace until stopped)'
132  grp.add_argument('-t', '--time', default='0s', help=help)
133
134  help = 'Ring buffer size N[mb,gb] (default: 32mb)'
135  grp.add_argument('-b', '--buffer', default='32mb', help=help)
136
137  help = ('Android (atrace) app names. Can be specified multiple times.\n-a*' +
138          'for all apps (without space between a and * or bash will expand it)')
139  grp.add_argument(
140      '-a',
141      '--app',
142      metavar='com.myapp',
143      action='append',
144      default=[],
145      help=help)
146
147  help = 'sched, gfx, am, wm (see --list)'
148  grp.add_argument('events', metavar='Atrace events', nargs='*', help=help)
149
150  help = 'sched/sched_switch kmem/kmem (see --list-ftrace)'
151  grp.add_argument('_', metavar='Ftrace events', nargs='*', help=help)
152
153  help = 'Lists all the categories available'
154  grp.add_argument('--list', action='store_true', help=help)
155
156  help = 'Lists all the ftrace events available'
157  grp.add_argument('--list-ftrace', action='store_true', help=help)
158
159  section = ('Full trace config (only when not using short options)')
160  grp = parser.add_argument_group(section)
161
162  help = 'Can be generated with https://ui.perfetto.dev/#!/record'
163  grp.add_argument('-c', '--config', default=None, help=help)
164
165  help = 'Parse input from --config as binary proto (default: parse as text)'
166  grp.add_argument('--bin', action='store_true', help=help)
167
168  help = ('Pass the trace through the trace reporter API. Only works when '
169          'using the full trace config (-c) with the reporter package name '
170          "'android.perfetto.cts.reporter' and the reporter class name "
171          "'android.perfetto.cts.reporter.PerfettoReportService' with the "
172          'reporter installed on the device (see '
173          'tools/install_test_reporter_app.py).')
174  grp.add_argument('--reporter-api', action='store_true', help=help)
175
176  args = parser.parse_args()
177  args.sideload = args.sideload or args.sideload_path is not None
178
179  if args.serial:
180    os.environ["ANDROID_SERIAL"] = args.serial
181
182  find_adb()
183
184  if args.list:
185    adb('shell', 'atrace', '--list_categories').wait()
186    sys.exit(0)
187
188  if args.list_ftrace:
189    adb('shell', 'cat /d/tracing/available_events | tr : /').wait()
190    sys.exit(0)
191
192  if args.config is not None and not os.path.exists(args.config):
193    prt('Config file not found: %s' % args.config, ANSI.RED)
194    sys.exit(1)
195
196  if len(args.events) == 0 and args.config is None:
197    prt('Must either pass short options (e.g. -t 10s sched) or a --config file',
198        ANSI.RED)
199    parser.print_help()
200    sys.exit(1)
201
202  if args.config is None and args.events and os.path.exists(args.events[0]):
203    prt(('The passed event name "%s" is a local file. ' % args.events[0] +
204         'Did you mean to pass -c / --config ?'), ANSI.RED)
205    sys.exit(1)
206
207  if args.reporter_api and not args.config:
208    prt('Must pass --config when using --reporter-api', ANSI.RED)
209    parser.print_help()
210    sys.exit(1)
211
212  perfetto_cmd = 'perfetto'
213  device_dir = '/data/misc/perfetto-traces/'
214
215  # Check the version of android. If too old (< Q) sideload tracebox. Also use
216  # use /data/local/tmp as /data/misc/perfetto-traces was introduced only later.
217  probe_cmd = 'getprop ro.build.version.sdk; getprop ro.product.cpu.abi; whoami'
218  probe = adb('shell', probe_cmd, stdout=subprocess.PIPE)
219  lines = probe.communicate()[0].decode().strip().split('\n')
220  lines = [x.strip() for x in lines]  # To strip \r(s) on Windows.
221  if probe.returncode != 0:
222    prt('ADB connection failed', ANSI.RED)
223    sys.exit(1)
224  api_level = int(lines[0])
225  abi = lines[1]
226  arch = ABI_TO_ARCH.get(abi)
227  if arch is None:
228    prt('Unsupported ABI: ' + abi)
229    sys.exit(1)
230  shell_user = lines[2]
231  if api_level < 29 or args.sideload:  # 29: Android Q.
232    tracebox_bin = args.sideload_path
233    if tracebox_bin is None:
234      tracebox_bin = get_perfetto_prebuilt(
235          TRACEBOX_MANIFEST, arch='android-' + arch)
236    perfetto_cmd = '/data/local/tmp/tracebox'
237    exit_code = adb('push', '--sync', tracebox_bin, perfetto_cmd).wait()
238    exit_code |= adb('shell', 'chmod 755 ' + perfetto_cmd).wait()
239    if exit_code != 0:
240      prt('ADB push failed', ANSI.RED)
241      sys.exit(1)
242    device_dir = '/data/local/tmp/'
243    if shell_user != 'root' and not args.user:
244      # Run as root if possible as that will give access to more tracing
245      # capabilities. Non-root still works, but some ftrace events might not be
246      # available.
247      adb('root').wait()
248
249  tstamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M')
250  fname = '%s-%s.pftrace' % (tstamp, os.urandom(3).hex())
251  device_file = device_dir + fname
252
253  cmd = [perfetto_cmd, '--background']
254  if not args.bin:
255    cmd.append('--txt')
256
257  if args.no_guardrails:
258    cmd.append('--no-guardrails')
259
260  if args.reporter_api:
261    # Remove all old reporter files to avoid polluting the file we will extract
262    # later.
263    adb('shell',
264        'rm /sdcard/Android/data/android.perfetto.cts.reporter/files/*').wait()
265    cmd.append('--upload')
266  else:
267    cmd.extend(['-o', device_file])
268
269  on_device_config = None
270  on_host_config = None
271  if args.config is not None:
272    cmd += ['-c', '-']
273    if api_level < 24:
274      # adb shell does not redirect stdin. Push the config on a temporary file
275      # on the device.
276      mktmp = adb(
277          'shell',
278          'mktemp',
279          '--tmpdir',
280          '/data/local/tmp',
281          stdout=subprocess.PIPE)
282      on_device_config = mktmp.communicate()[0].decode().strip().strip()
283      if mktmp.returncode != 0:
284        prt('Failed to create config on device', ANSI.RED)
285        sys.exit(1)
286      exit_code = adb('push', '--sync', args.config, on_device_config).wait()
287      if exit_code != 0:
288        prt('Failed to push config on device', ANSI.RED)
289        sys.exit(1)
290      cmd = ['cat', on_device_config, '|'] + cmd
291    else:
292      on_host_config = args.config
293  else:
294    cmd += ['-t', args.time, '-b', args.buffer]
295    for app in args.app:
296      cmd += ['--app', '\'' + app + '\'']
297    cmd += args.events
298
299  # Work out the output file or directory.
300  if args.out.endswith('/') or os.path.isdir(args.out):
301    host_dir = args.out
302    host_file = os.path.join(args.out, fname)
303  else:
304    host_file = args.out
305    host_dir = os.path.dirname(host_file)
306    if host_dir == '':
307      host_dir = '.'
308      host_file = './' + host_file
309  if not os.path.exists(host_dir):
310    shutil.os.makedirs(host_dir)
311
312  with open(on_host_config or os.devnull, 'rb') as f:
313    print('Running ' + ' '.join(cmd))
314    proc = adb('shell', *cmd, stdin=f, stdout=subprocess.PIPE)
315    proc_out = proc.communicate()[0].decode().strip()
316    if on_device_config is not None:
317      adb('shell', 'rm', on_device_config).wait()
318    # On older versions of Android (x86_64 emulator running API 22) the output
319    # looks like:
320    #   WARNING: linker: /data/local/tmp/tracebox: unused DT entry: ...
321    #   WARNING: ... (other 2 WARNING: linker: lines)
322    #   1234  <-- The actual pid we want.
323    match = re.search(r'^(\d+)$', proc_out, re.M)
324    if match is None:
325      prt('Failed to read the pid from perfetto --background', ANSI.RED)
326      prt(proc_out)
327      sys.exit(1)
328    bg_pid = match.group(1)
329    exit_code = proc.wait()
330
331  if exit_code != 0:
332    prt('Perfetto invocation failed', ANSI.RED)
333    sys.exit(1)
334
335  prt('Trace started. Press CTRL+C to stop', ANSI.BLACK + ANSI.BG_BLUE)
336  logcat = adb('logcat', '-v', 'brief', '-s', 'perfetto', '-b', 'main', '-T',
337               '1')
338
339  ctrl_c_count = 0
340  adb_failure_count = 0
341  while ctrl_c_count < 2:
342    try:
343      # On older Android devices adbd doesn't propagate the exit code. Hence
344      # the RUN/TERM parts.
345      poll = adb(
346          'shell',
347          'test -d /proc/%s && echo RUN || echo TERM' % bg_pid,
348          stdout=subprocess.PIPE)
349      poll_res = poll.communicate()[0].decode().strip()
350      if poll_res == 'TERM':
351        break  # Process terminated
352      if poll_res == 'RUN':
353        # The 'perfetto' cmdline client is still running. If previously we had
354        # an ADB error, tell the user now it's all right again.
355        if adb_failure_count > 0:
356          adb_failure_count = 0
357          prt('ADB connection re-established, the trace is still ongoing',
358              ANSI.BLUE)
359        time.sleep(0.5)
360        continue
361      # Some ADB error happened. This can happen when tracing soon after boot,
362      # before logging in, when adb gets restarted.
363      adb_failure_count += 1
364      if adb_failure_count >= MAX_ADB_FAILURES:
365        prt('Too many unrecoverable ADB failures, bailing out', ANSI.RED)
366        sys.exit(1)
367      time.sleep(2)
368    except KeyboardInterrupt:
369      sig = 'TERM' if ctrl_c_count == 0 else 'KILL'
370      ctrl_c_count += 1
371      prt('Stopping the trace (SIG%s)' % sig, ANSI.BLACK + ANSI.BG_YELLOW)
372      adb('shell', 'kill -%s %s' % (sig, bg_pid)).wait()
373
374  logcat.kill()
375  logcat.wait()
376
377  if args.reporter_api:
378    prt('Waiting a few seconds to allow reporter to copy trace')
379    time.sleep(5)
380
381    ret = adb(
382        'shell',
383        'cp /sdcard/Android/data/android.perfetto.cts.reporter/files/* ' +
384        device_file).wait()
385    if ret != 0:
386      prt('Failed to extract reporter trace', ANSI.RED)
387      sys.exit(1)
388
389  prt('\n')
390  prt('Pulling into %s' % host_file, ANSI.BOLD)
391  adb('pull', device_file, host_file).wait()
392  adb('shell', 'rm -f ' + device_file).wait()
393
394  if not args.no_open:
395    prt('\n')
396    prt('Opening the trace (%s) in the browser' % host_file)
397    open_browser = not args.no_open_browser
398    open_trace_in_browser(host_file, open_browser, args.origin)
399
400
401def prt(msg, colors=ANSI.END):
402  print(colors + msg + ANSI.END)
403
404
405def find_adb():
406  """ Locate the "right" adb path
407
408  If adb is in the PATH use that (likely what the user wants) otherwise use the
409  hermetic one in our SDK copy.
410  """
411  global adb_path
412  for path in ['adb', HERMETIC_ADB_PATH]:
413    try:
414      subprocess.call([path, '--version'], stdout=devnull, stderr=devnull)
415      adb_path = path
416      break
417    except OSError:
418      continue
419  if adb_path is None:
420    sdk_url = 'https://developer.android.com/studio/releases/platform-tools'
421    prt('Could not find a suitable adb binary in the PATH. ', ANSI.RED)
422    prt('You can download adb from %s' % sdk_url, ANSI.RED)
423    sys.exit(1)
424
425
426def open_trace_in_browser(path, open_browser, origin):
427  # We reuse the HTTP+RPC port because it's the only one allowed by the CSP.
428  PORT = 9001
429  path = os.path.abspath(path)
430  os.chdir(os.path.dirname(path))
431  fname = os.path.basename(path)
432  socketserver.TCPServer.allow_reuse_address = True
433  with socketserver.TCPServer(('127.0.0.1', PORT), HttpHandler) as httpd:
434    address = f'{origin}/#!/?url=http://127.0.0.1:{PORT}/{fname}&referrer=record_android_trace'
435    if open_browser:
436      webbrowser.open_new_tab(address)
437    else:
438      print(f'Open URL in browser: {address}')
439
440    httpd.expected_fname = fname
441    httpd.fname_get_completed = None
442    httpd.allow_origin = origin
443    while httpd.fname_get_completed is None:
444      httpd.handle_request()
445
446
447def adb(*args, stdin=devnull, stdout=None):
448  cmd = [adb_path, *args]
449  setpgrp = None
450  if os.name != 'nt':
451    # On Linux/Mac, start a new process group so all child processes are killed
452    # on exit. Unsupported on Windows.
453    setpgrp = lambda: os.setpgrp()
454  proc = subprocess.Popen(cmd, stdin=stdin, stdout=stdout, preexec_fn=setpgrp)
455  procs.append(proc)
456  return proc
457
458
459def kill_all_subprocs_on_exit():
460  for p in [p for p in procs if p.poll() is None]:
461    p.kill()
462
463
464def check_hash(file_name, sha_value):
465  with open(file_name, 'rb') as fd:
466    file_hash = hashlib.sha1(fd.read()).hexdigest()
467    return file_hash == sha_value
468
469
470if __name__ == '__main__':
471  sys.exit(main())
472