• 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 tempfile
28import time
29import webbrowser
30
31ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
32
33# This is not required. It's only used as a fallback if no adb is found on the
34# PATH. It's fine if it doesn't exist so this script can be copied elsewhere.
35HERMETIC_ADB_PATH = ROOT_DIR + '/buildtools/android_sdk/platform-tools/adb'
36
37# Translates the Android ro.product.cpu.abi into the GN's target_cpu.
38ABI_TO_ARCH = {
39    'armeabi-v7a': 'arm',
40    'arm64-v8a': 'arm64',
41    'x86': 'x86',
42    'x86_64': 'x64',
43}
44
45MAX_ADB_FAILURES = 15  # 2 seconds between retries, 30 seconds total.
46
47devnull = open(os.devnull, 'rb')
48adb_path = None
49procs = []
50
51
52class ANSI:
53  END = '\033[0m'
54  BOLD = '\033[1m'
55  RED = '\033[91m'
56  BLACK = '\033[30m'
57  BLUE = '\033[94m'
58  BG_YELLOW = '\033[43m'
59  BG_BLUE = '\033[44m'
60
61
62# HTTP Server used to open the trace in the browser.
63class HttpHandler(http.server.SimpleHTTPRequestHandler):
64
65  def end_headers(self):
66    self.send_header('Access-Control-Allow-Origin', '*')
67    return super().end_headers()
68
69  def do_GET(self):
70    self.server.last_request = self.path
71    return super().do_GET()
72
73  def do_POST(self):
74    self.send_error(404, "File not found")
75
76
77def main():
78  atexit.register(kill_all_subprocs_on_exit)
79  default_out_dir_str = '~/traces/'
80  default_out_dir = os.path.expanduser(default_out_dir_str)
81
82  examples = '\n'.join([
83      ANSI.BOLD + 'Examples' + ANSI.END, '  -t 10s -b 32mb sched gfx wm -a*',
84      '  -t 5s sched/sched_switch raw_syscalls/sys_enter raw_syscalls/sys_exit',
85      '  -c /path/to/full-textual-trace.config', '',
86      ANSI.BOLD + 'Long traces' + ANSI.END,
87      'If you want to record a hours long trace and stream it into a file ',
88      'you need to pass a full trace config and set write_into_file = true.',
89      'See https://perfetto.dev/docs/concepts/config#long-traces .'
90  ])
91  parser = argparse.ArgumentParser(
92      epilog=examples, formatter_class=argparse.RawTextHelpFormatter)
93
94  help = 'Output file or directory (default: %s)' % default_out_dir_str
95  parser.add_argument('-o', '--out', default=default_out_dir, help=help)
96
97  help = 'Don\'t open in the browser'
98  parser.add_argument('-n', '--no-open', action='store_true', help=help)
99
100  help = 'Force the use of the sideloaded binaries rather than system daemons'
101  parser.add_argument('--sideload', action='store_true', help=help)
102
103  help = ('Sideload the given binary rather than downloading it. ' +
104          'Implies --sideload')
105  parser.add_argument('--sideload-path', default=None, help=help)
106
107  help = 'Don\'t run `adb root` run as user (only when sideloading)'
108  parser.add_argument('-u', '--user', action='store_true', help=help)
109
110  grp = parser.add_argument_group(
111      'Short options: (only when not using -c/--config)')
112
113  help = 'Trace duration N[s,m,h] (default: trace until stopped)'
114  grp.add_argument('-t', '--time', default='0s', help=help)
115
116  help = 'Ring buffer size N[mb,gb] (default: 32mb)'
117  grp.add_argument('-b', '--buffer', default='32mb', help=help)
118
119  help = ('Android (atrace) app names. Can be specified multiple times.\n-a*' +
120          'for all apps (without space between a and * or bash will expand it)')
121  grp.add_argument(
122      '-a',
123      '--app',
124      metavar='com.myapp',
125      action='append',
126      default=[],
127      help=help)
128
129  help = 'sched, gfx, am, wm (see --list)'
130  grp.add_argument('events', metavar='Atrace events', nargs='*', help=help)
131
132  help = 'sched/sched_switch kmem/kmem (see --list-ftrace)'
133  grp.add_argument('_', metavar='Ftrace events', nargs='*', help=help)
134
135  help = 'Lists all the categories available'
136  grp.add_argument('--list', action='store_true', help=help)
137
138  help = 'Lists all the ftrace events available'
139  grp.add_argument('--list-ftrace', action='store_true', help=help)
140
141  section = ('Full trace config (only when not using short options)')
142  grp = parser.add_argument_group(section)
143
144  help = 'Can be generated with https://ui.perfetto.dev/#!/record'
145  grp.add_argument('-c', '--config', default=None, help=help)
146
147  help = 'Specify the ADB device serial'
148  grp.add_argument('--serial', '-s', default=None, help=help)
149
150  args = parser.parse_args()
151  args.sideload = args.sideload or args.sideload_path is not None
152
153  if args.serial:
154    os.environ["ANDROID_SERIAL"] = args.serial
155
156  find_adb()
157
158  if args.list:
159    adb('shell', 'atrace', '--list_categories').wait()
160    sys.exit(0)
161
162  if args.list_ftrace:
163    adb('shell', 'cat /d/tracing/available_events | tr : /').wait()
164    sys.exit(0)
165
166  if args.config is not None and not os.path.exists(args.config):
167    prt('Config file not found: %s' % args.config, ANSI.RED)
168    sys.exit(1)
169
170  if len(args.events) == 0 and args.config is None:
171    prt('Must either pass short options (e.g. -t 10s sched) or a --config file',
172        ANSI.RED)
173    parser.print_help()
174    sys.exit(1)
175
176  if args.config is None and args.events and os.path.exists(args.events[0]):
177    prt(('The passed event name "%s" is a local file. ' % args.events[0] +
178         'Did you mean to pass -c / --config ?'), ANSI.RED)
179    sys.exit(1)
180
181  perfetto_cmd = 'perfetto'
182  device_dir = '/data/misc/perfetto-traces/'
183
184  # Check the version of android. If too old (< Q) sideload tracebox. Also use
185  # use /data/local/tmp as /data/misc/perfetto-traces was introduced only later.
186  probe_cmd = 'getprop ro.build.version.sdk; getprop ro.product.cpu.abi; whoami'
187  probe = adb('shell', probe_cmd, stdout=subprocess.PIPE)
188  lines = probe.communicate()[0].decode().strip().split('\n')
189  lines = [x.strip() for x in lines]  # To strip \r(s) on Windows.
190  if probe.returncode != 0:
191    prt('ADB connection failed', ANSI.RED)
192    sys.exit(1)
193  api_level = int(lines[0])
194  abi = lines[1]
195  arch = ABI_TO_ARCH.get(abi)
196  if arch is None:
197    prt('Unsupported ABI: ' + abi)
198    sys.exit(1)
199  shell_user = lines[2]
200  if api_level < 29 or args.sideload:  # 29: Android Q.
201    tracebox_bin = args.sideload_path
202    if tracebox_bin is None:
203      tracebox_bin = get_perfetto_prebuilt('tracebox', arch='android-' + arch)
204    perfetto_cmd = '/data/local/tmp/tracebox'
205    exit_code = adb('push', '--sync', tracebox_bin, perfetto_cmd).wait()
206    exit_code |= adb('shell', 'chmod 755 ' + perfetto_cmd).wait()
207    if exit_code != 0:
208      prt('ADB push failed', ANSI.RED)
209      sys.exit(1)
210    device_dir = '/data/local/tmp/'
211    if shell_user != 'root' and not args.user:
212      # Run as root if possible as that will give access to more tracing
213      # capabilities. Non-root still works, but some ftrace events might not be
214      # available.
215      adb('root').wait()
216
217  tstamp = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M')
218  fname = '%s-%s.pftrace' % (tstamp, os.urandom(3).hex())
219  device_file = device_dir + fname
220
221  cmd = [perfetto_cmd, '--background', '--txt', '-o', device_file]
222  on_device_config = None
223  on_host_config = None
224  if args.config is not None:
225    cmd += ['-c', '-']
226    if api_level < 24:
227      # adb shell does not redirect stdin. Push the config on a temporary file
228      # on the device.
229      mktmp = adb(
230          'shell',
231          'mktemp',
232          '--tmpdir',
233          '/data/local/tmp',
234          stdout=subprocess.PIPE)
235      on_device_config = mktmp.communicate()[0].decode().strip().strip()
236      if mktmp.returncode != 0:
237        prt('Failed to create config on device', ANSI.RED)
238        sys.exit(1)
239      exit_code = adb('push', '--sync', args.config, on_device_config).wait()
240      if exit_code != 0:
241        prt('Failed to push config on device', ANSI.RED)
242        sys.exit(1)
243      cmd = ['cat', on_device_config, '|'] + cmd
244    else:
245      on_host_config = args.config
246  else:
247    cmd += ['-t', args.time, '-b', args.buffer]
248    for app in args.app:
249      cmd += ['--app', '\'' + app + '\'']
250    cmd += args.events
251
252  # Perfetto will error out with a proper message if both a config file and
253  # short options are specified. No need to replicate that logic.
254
255  # Work out the output file or directory.
256  if args.out.endswith('/') or os.path.isdir(args.out):
257    host_dir = args.out
258    host_file = os.path.join(args.out, fname)
259  else:
260    host_file = args.out
261    host_dir = os.path.dirname(host_file)
262    if host_dir == '':
263      host_dir = '.'
264      host_file = './' + host_file
265  if not os.path.exists(host_dir):
266    shutil.os.makedirs(host_dir)
267
268  with open(on_host_config or os.devnull, 'rb') as f:
269    print('Running ' + ' '.join(cmd))
270    proc = adb('shell', *cmd, stdin=f, stdout=subprocess.PIPE)
271    proc_out = proc.communicate()[0].decode().strip()
272    if on_device_config is not None:
273      adb('shell', 'rm', on_device_config).wait()
274    # On older versions of Android (x86_64 emulator running API 22) the output
275    # looks like:
276    #   WARNING: linker: /data/local/tmp/tracebox: unused DT entry: ...
277    #   WARNING: ... (other 2 WARNING: linker: lines)
278    #   1234  <-- The actual pid we want.
279    match = re.search(r'^(\d+)$', proc_out, re.M)
280    if match is None:
281      prt('Failed to read the pid from perfetto --background', ANSI.RED)
282      prt(proc_out)
283      sys.exit(1)
284    bg_pid = match.group(1)
285    exit_code = proc.wait()
286
287  if exit_code != 0:
288    prt('Perfetto invocation failed', ANSI.RED)
289    sys.exit(1)
290
291  prt('Trace started. Press CTRL+C to stop', ANSI.BLACK + ANSI.BG_BLUE)
292  logcat = adb('logcat', '-v', 'brief', '-s', 'perfetto', '-b', 'main', '-T',
293               '1')
294
295  ctrl_c_count = 0
296  adb_failure_count = 0
297  while ctrl_c_count < 2:
298    try:
299      # On older Android devices adbd doesn't propagate the exit code. Hence
300      # the RUN/TERM parts.
301      poll = adb(
302          'shell',
303          'test -d /proc/%s && echo RUN || echo TERM' % bg_pid,
304          stdout=subprocess.PIPE)
305      poll_res = poll.communicate()[0].decode().strip()
306      if poll_res == 'TERM':
307        break  # Process terminated
308      if poll_res == 'RUN':
309        # The 'perfetto' cmdline client is still running. If previously we had
310        # an ADB error, tell the user now it's all right again.
311        if adb_failure_count > 0:
312          adb_failure_count = 0
313          prt('ADB connection re-established, the trace is still ongoing',
314              ANSI.BLUE)
315        time.sleep(0.5)
316        continue
317      # Some ADB error happened. This can happen when tracing soon after boot,
318      # before logging in, when adb gets restarted.
319      adb_failure_count += 1
320      if adb_failure_count >= MAX_ADB_FAILURES:
321        prt('Too many unrecoverable ADB failures, bailing out', ANSI.RED)
322        sys.exit(1)
323      time.sleep(2)
324    except KeyboardInterrupt:
325      sig = 'TERM' if ctrl_c_count == 0 else 'KILL'
326      ctrl_c_count += 1
327      prt('Stopping the trace (SIG%s)' % sig, ANSI.BLACK + ANSI.BG_YELLOW)
328      adb('shell', 'kill -%s %s' % (sig, bg_pid)).wait()
329
330  logcat.kill()
331  logcat.wait()
332
333  prt('\n')
334  prt('Pulling into %s' % host_file, ANSI.BOLD)
335  adb('pull', device_file, host_file).wait()
336  adb('shell', 'rm -f ' + device_file).wait()
337
338  if not args.no_open:
339    prt('\n')
340    prt('Opening the trace (%s) in the browser' % host_file)
341    open_trace_in_browser(host_file)
342
343
344def prt(msg, colors=ANSI.END):
345  print(colors + msg + ANSI.END)
346
347
348def find_adb():
349  """ Locate the "right" adb path
350
351  If adb is in the PATH use that (likely what the user wants) otherwise use the
352  hermetic one in our SDK copy.
353  """
354  global adb_path
355  for path in ['adb', HERMETIC_ADB_PATH]:
356    try:
357      subprocess.call([path, '--version'], stdout=devnull, stderr=devnull)
358      adb_path = path
359      break
360    except OSError:
361      continue
362  if adb_path is None:
363    sdk_url = 'https://developer.android.com/studio/releases/platform-tools'
364    prt('Could not find a suitable adb binary in the PATH. ', ANSI.RED)
365    prt('You can download adb from %s' % sdk_url, ANSI.RED)
366    sys.exit(1)
367
368
369def open_trace_in_browser(path):
370  # We reuse the HTTP+RPC port because it's the only one allowed by the CSP.
371  PORT = 9001
372  os.chdir(os.path.dirname(path))
373  fname = os.path.basename(path)
374  socketserver.TCPServer.allow_reuse_address = True
375  with socketserver.TCPServer(('127.0.0.1', PORT), HttpHandler) as httpd:
376    webbrowser.open_new_tab(
377        'https://ui.perfetto.dev/#!/?url=http://127.0.0.1:%d/%s' %
378        (PORT, fname))
379    while httpd.__dict__.get('last_request') != '/' + fname:
380      httpd.handle_request()
381
382
383def adb(*args, stdin=devnull, stdout=None):
384  cmd = [adb_path, *args]
385  setpgrp = None
386  if os.name != 'nt':
387    # On Linux/Mac, start a new process group so all child processes are killed
388    # on exit. Unsupported on Windows.
389    setpgrp = lambda: os.setpgrp()
390  proc = subprocess.Popen(cmd, stdin=stdin, stdout=stdout, preexec_fn=setpgrp)
391  procs.append(proc)
392  return proc
393
394
395def kill_all_subprocs_on_exit():
396  for p in [p for p in procs if p.poll() is None]:
397    p.kill()
398
399
400def check_hash(file_name, sha_value):
401  with open(file_name, 'rb') as fd:
402    file_hash = hashlib.sha1(fd.read()).hexdigest()
403    return file_hash == sha_value
404
405
406# BEGIN_SECTION_GENERATED_BY(roll-prebuilts)
407# Revision: v25.0
408PERFETTO_PREBUILT_MANIFEST = [{
409    'tool':
410        'tracebox',
411    'arch':
412        'android-arm',
413    'file_name':
414        'tracebox',
415    'file_size':
416        1067004,
417    'url':
418        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v25.0/android-arm/tracebox',
419    'sha256':
420        '6fff40fc02d154b187187fe70069cc93bde00d3be5b3582ba6120b0cfa83d379'
421}, {
422    'tool':
423        'tracebox',
424    'arch':
425        'android-arm64',
426    'file_name':
427        'tracebox',
428    'file_size':
429        1620704,
430    'url':
431        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v25.0/android-arm64/tracebox',
432    'sha256':
433        'd482c86eccd7dc0ff4f57bd5d841d90e97ca5e31b881fcea442d4ad03ecfd4f4'
434}, {
435    'tool':
436        'tracebox',
437    'arch':
438        'android-x86',
439    'file_name':
440        'tracebox',
441    'file_size':
442        1644500,
443    'url':
444        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v25.0/android-x86/tracebox',
445    'sha256':
446        '48de1d10959b6130d2d6d68ae3ba0d8bb3f3aa6fe526d77ea9abad058a631d8f'
447}, {
448    'tool':
449        'tracebox',
450    'arch':
451        'android-x64',
452    'file_name':
453        'tracebox',
454    'file_size':
455        1870560,
456    'url':
457        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v25.0/android-x64/tracebox',
458    'sha256':
459        '333c17912a143300ca6afa3b27d93497821615028e612e5d6132889427c10599'
460}]
461
462
463# DO NOT EDIT. If you wish to make edits to this code, you need to change only
464# //tools/get_perfetto_prebuilt.py and run /tools/roll-prebuilts to regenerate
465# all the others scripts this is embedded into.
466def get_perfetto_prebuilt(tool_name, soft_fail=False, arch=None):
467  """ Downloads the prebuilt, if necessary, and returns its path on disk. """
468
469  # The first time this is invoked, it downloads the |url| and caches it into
470  # ~/.perfetto/prebuilts/$tool_name. On subsequent invocations it just runs the
471  # cached version.
472  def download_or_get_cached(file_name, url, sha256):
473    import os, hashlib, subprocess
474    dir = os.path.join(
475        os.path.expanduser('~'), '.local', 'share', 'perfetto', 'prebuilts')
476    os.makedirs(dir, exist_ok=True)
477    bin_path = os.path.join(dir, file_name)
478    sha256_path = os.path.join(dir, file_name + '.sha256')
479    needs_download = True
480
481    # Avoid recomputing the SHA-256 on each invocation. The SHA-256 of the last
482    # download is cached into file_name.sha256, just check if that matches.
483    if os.path.exists(bin_path) and os.path.exists(sha256_path):
484      with open(sha256_path, 'rb') as f:
485        digest = f.read().decode()
486        if digest == sha256:
487          needs_download = False
488
489    if needs_download:
490      # Either the filed doesn't exist or the SHA256 doesn't match.
491      tmp_path = bin_path + '.tmp'
492      print('Downloading ' + url)
493      subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url])
494      with open(tmp_path, 'rb') as fd:
495        actual_sha256 = hashlib.sha256(fd.read()).hexdigest()
496      if actual_sha256 != sha256:
497        raise Exception('Checksum mismatch for %s (actual: %s, expected: %s)' %
498                        (url, actual_sha256, sha256))
499      os.chmod(tmp_path, 0o755)
500      os.rename(tmp_path, bin_path)
501      with open(sha256_path, 'w') as f:
502        f.write(sha256)
503    return bin_path
504    # --- end of download_or_get_cached() ---
505
506  # --- get_perfetto_prebuilt() function starts here. ---
507  import os, platform, sys
508  plat = sys.platform.lower()
509  machine = platform.machine().lower()
510  manifest_entry = None
511  for entry in PERFETTO_PREBUILT_MANIFEST:
512    # If the caller overrides the arch, just match that (for Android prebuilts).
513    if arch and entry.get('arch') == arch:
514      manifest_entry = entry
515      break
516    # Otherwise guess the local machine arch.
517    if entry.get('tool') == tool_name and entry.get(
518        'platform') == plat and machine in entry.get('machine', []):
519      manifest_entry = entry
520      break
521  if manifest_entry is None:
522    if soft_fail:
523      return None
524    raise Exception(
525        ('No prebuilts available for %s-%s\n' % (plat, machine)) +
526        'See https://perfetto.dev/docs/contributing/build-instructions')
527
528  return download_or_get_cached(
529      file_name=manifest_entry['file_name'],
530      url=manifest_entry['url'],
531      sha256=manifest_entry['sha256'])
532
533
534# END_SECTION_GENERATED_BY(roll-prebuilts)
535
536if __name__ == '__main__':
537  sys.exit(main())
538