blob: 7dbca530e618e0825d65c9571932de38bb444583 [file] [log] [blame]
Primiano Tucci11d94e12022-08-02 17:44:33 +01001#!/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.
Christopher Phlipot53330f72022-09-29 10:19:07 -070036HERMETIC_ADB_PATH = repo_dir('buildtools/android_sdk/platform-tools/adb')
Primiano Tucci11d94e12022-08-02 17:44:33 +010037
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):
Ryan Zuklie288eb4d2023-05-26 15:22:19 -070067 self.send_header('Access-Control-Allow-Origin', self.server.allow_origin)
68 self.send_header('Cache-Control', 'no-cache')
69 super().end_headers()
Primiano Tucci11d94e12022-08-02 17:44:33 +010070
71 def do_GET(self):
Ryan Zuklie288eb4d2023-05-26 15:22:19 -070072 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()
Primiano Tucci11d94e12022-08-02 17:44:33 +010078
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
Ryan Zuklie288eb4d2023-05-26 15:22:19 -0700103 help = 'Don\'t open or serve the trace'
Primiano Tucci11d94e12022-08-02 17:44:33 +0100104 parser.add_argument('-n', '--no-open', action='store_true', help=help)
105
Ryan Zuklie288eb4d2023-05-26 15:22:19 -0700106 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
Primiano Tucci11d94e12022-08-02 17:44:33 +0100112 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
Lalit Magantid7031a92023-09-26 12:50:01 +0100119 help = 'Ignores any tracing guardrails which might be used'
120 parser.add_argument('--no-guardrails', action='store_true', help=help)
121
Primiano Tucci11d94e12022-08-02 17:44:33 +0100122 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
Ryan Zuklieefb63072023-05-26 16:23:52 -0700165 help = 'Parse input from --config as binary proto (default: parse as text)'
166 grp.add_argument('--bin', action='store_true', help=help)
167
Lalit Magantid7031a92023-09-26 12:50:01 +0100168 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
Primiano Tucci11d94e12022-08-02 17:44:33 +0100176 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
Lalit Magantid7031a92023-09-26 12:50:01 +0100207 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
Primiano Tucci11d94e12022-08-02 17:44:33 +0100212 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
Lalit Magantid7031a92023-09-26 12:50:01 +0100253 cmd = [perfetto_cmd, '--background']
Ryan Zuklieefb63072023-05-26 16:23:52 -0700254 if not args.bin:
255 cmd.append('--txt')
Lalit Magantid7031a92023-09-26 12:50:01 +0100256
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
Primiano Tucci11d94e12022-08-02 17:44:33 +0100269 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
Primiano Tucci11d94e12022-08-02 17:44:33 +0100299 # 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
Lalit Magantid7031a92023-09-26 12:50:01 +0100377 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
Primiano Tucci11d94e12022-08-02 17:44:33 +0100389 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)
Ryan Zuklie288eb4d2023-05-26 15:22:19 -0700397 open_browser = not args.no_open_browser
398 open_trace_in_browser(host_file, open_browser, args.origin)
Primiano Tucci11d94e12022-08-02 17:44:33 +0100399
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
Ryan Zuklie288eb4d2023-05-26 15:22:19 -0700426def open_trace_in_browser(path, open_browser, origin):
Primiano Tucci11d94e12022-08-02 17:44:33 +0100427 # We reuse the HTTP+RPC port because it's the only one allowed by the CSP.
428 PORT = 9001
Ryan Zuklie288eb4d2023-05-26 15:22:19 -0700429 path = os.path.abspath(path)
Primiano Tucci11d94e12022-08-02 17:44:33 +0100430 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:
Hector Dearman5a6608e2023-12-05 19:48:09 +0000434 address = f'{origin}/#!/?url=http://127.0.0.1:{PORT}/{fname}&referrer=record_android_trace'
Ryan Zuklie288eb4d2023-05-26 15:22:19 -0700435 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:
Primiano Tucci11d94e12022-08-02 17:44:33 +0100444 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())