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