1#!/usr/bin/env python3 2 3# Copyright (C) 2022 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16"""Runs tracing with CPU profiling enabled, and symbolizes traces if requested. 17 18For usage instructions, please see: 19https://perfetto.dev/docs/quickstart/callstack-sampling 20 21Adapted in large part from `heap_profile`. 22""" 23 24import argparse 25import os 26import shutil 27import signal 28import subprocess 29import sys 30import tempfile 31import time 32import uuid 33 34# Used for creating directories, etc. 35UUID = str(uuid.uuid4())[-6:] 36 37# See `sigint_handler` below. 38IS_INTERRUPTED = False 39 40 41def sigint_handler(signal, frame): 42 """Useful for cleanly interrupting tracing.""" 43 global IS_INTERRUPTED 44 IS_INTERRUPTED = True 45 46 47def exit_with_no_profile(): 48 sys.exit("No profiles generated.") 49 50 51def exit_with_bug_report(error): 52 sys.exit( 53 "{}\n\n If this is unexpected, please consider filing a bug at: \n" 54 "https://perfetto.dev/docs/contributing/getting-started#bugs.".format( 55 error)) 56 57 58def adb_check_output(command): 59 """Runs an `adb` command and returns its output.""" 60 try: 61 return subprocess.check_output(command).decode('utf-8') 62 except FileNotFoundError: 63 sys.exit("`adb` not found: Is it installed or on PATH?") 64 except subprocess.CalledProcessError as error: 65 sys.exit("`adb` error: Are any (or multiple) devices connected?\n" 66 "If multiple devices are connected, please select one by " 67 "setting `ANDROID_SERIAL=device_id`.\n" 68 "{}".format(error)) 69 except Exception as error: 70 exit_with_bug_report(error) 71 72 73def parse_and_validate_args(): 74 """Parses, validates, and returns command-line arguments for this script.""" 75 DESCRIPTION = """Runs tracing with CPU profiling enabled, and symbolizes 76 traces if requested. 77 78 For usage instructions, please see: 79 https://perfetto.dev/docs/quickstart/cpu-profiling 80 """ 81 parser = argparse.ArgumentParser(description=DESCRIPTION) 82 parser.add_argument( 83 "-f", 84 "--frequency", 85 help="Sampling frequency (Hz). " 86 "Default: 100 Hz.", 87 metavar="FREQUENCY", 88 type=int, 89 default=100) 90 parser.add_argument( 91 "-d", 92 "--duration", 93 help="Duration of profile (ms). 0 to run until interrupted. " 94 "Default: until interrupted by user.", 95 metavar="DURATION", 96 type=int, 97 default=0) 98 parser.add_argument( 99 "-n", 100 "--name", 101 help="Comma-separated list of names of processes to be profiled.", 102 metavar="NAMES", 103 default=None) 104 parser.add_argument( 105 "-p", 106 "--partial-matching", 107 help="If set, enables \"partial matching\" on the strings in --names/-n." 108 "Processes that are already running when profiling is started, and whose " 109 "names include any of the values in --names/-n as substrings will be profiled.", 110 action="store_true") 111 parser.add_argument( 112 "-c", 113 "--config", 114 help="A custom configuration file, if any, to be used for profiling. " 115 "If provided, --frequency/-f, --duration/-d, and --name/-n are not used.", 116 metavar="CONFIG", 117 default=None) 118 parser.add_argument( 119 "-o", 120 "--output", 121 help="Output directory for recorded trace.", 122 metavar="DIRECTORY", 123 default=None) 124 125 args = parser.parse_args() 126 if args.config is not None and args.name is not None: 127 sys.exit("--name/-n should not be provided when --config/-c is provided.") 128 elif args.config is None and args.name is None: 129 sys.exit("One of --names/-n or --config/-c is required.") 130 131 return args 132 133 134def get_matching_processes(args, names_to_match): 135 """Returns a list of currently-running processes whose names match `names_to_match`. 136 137 Args: 138 args: The command-line arguments provided to this script. 139 names_to_match: The list of process names provided by the user. 140 """ 141 # Returns names as they are. 142 if not args.partial_matching: 143 return names_to_match 144 145 # Attempt to match names to names of currently running processes. 146 PS_PROCESS_OFFSET = 8 147 matching_processes = [] 148 for line in adb_check_output(['adb', 'shell', 'ps', '-A']).splitlines(): 149 line_split = line.split() 150 if len(line_split) <= PS_PROCESS_OFFSET: 151 continue 152 process = line_split[PS_PROCESS_OFFSET] 153 for name in names_to_match: 154 if name in process: 155 matching_processes.append(process) 156 break 157 158 return matching_processes 159 160 161def get_perfetto_config(args): 162 """Returns a Perfetto config with CPU profiling enabled for the selected processes. 163 164 Args: 165 args: The command-line arguments provided to this script. 166 """ 167 if args.config is not None: 168 try: 169 with open(args.config, 'r') as config_file: 170 return config_file.read() 171 except IOError as error: 172 sys.exit("Unable to read config file: {}".format(error)) 173 174 CONFIG_INDENT = ' ' 175 CONFIG = '''buffers {{ 176 size_kb: 2048 177 }} 178 179 buffers {{ 180 size_kb: 63488 181 }} 182 183 data_sources {{ 184 config {{ 185 name: "linux.process_stats" 186 target_buffer: 0 187 process_stats_config {{ 188 proc_stats_poll_ms: 100 189 }} 190 }} 191 }} 192 193 data_sources {{ 194 config {{ 195 name: "linux.perf" 196 target_buffer: 1 197 perf_event_config {{ 198 all_cpus: true 199 sampling_frequency: {frequency} 200{target_config} 201 }} 202 }} 203 }} 204 205 duration_ms: {duration} 206 write_into_file: true 207 flush_timeout_ms: 30000 208 flush_period_ms: 604800000 209 ''' 210 211 target_config = "" 212 matching_processes = [] 213 if args.name is not None: 214 names_to_match = [name.strip() for name in args.name.split(',')] 215 matching_processes = get_matching_processes(args, names_to_match) 216 217 if not matching_processes: 218 sys.exit("No running processes matched for profiling.") 219 220 for process in matching_processes: 221 target_config += CONFIG_INDENT + 'target_cmdline: "{}"\n'.format(process) 222 223 print("Configured profiling for these processes:\n") 224 for matching_process in matching_processes: 225 print(matching_process) 226 print() 227 228 config = CONFIG.format( 229 frequency=args.frequency, 230 duration=args.duration, 231 target_config=target_config) 232 233 return config 234 235 236def release_or_newer(release): 237 """Returns whether a new enough Android release is being used.""" 238 SDK = {'R': 30} 239 sdk = int( 240 adb_check_output( 241 ['adb', 'shell', 'getprop', 'ro.system.build.version.sdk']).strip()) 242 if sdk >= SDK[release]: 243 return True 244 245 codename = adb_check_output( 246 ['adb', 'shell', 'getprop', 'ro.build.version.codename']).strip() 247 return codename == release 248 249 250def get_and_prepare_profile_target(args): 251 """Returns the target where the trace/profile will be output. Creates a new directory if necessary. 252 253 Args: 254 args: The command-line arguments provided to this script. 255 """ 256 profile_target = os.path.join(tempfile.gettempdir(), UUID) 257 if args.output is not None: 258 profile_target = args.output 259 else: 260 os.makedirs(profile_target, exist_ok=True) 261 if not os.path.isdir(profile_target): 262 sys.exit("Output directory {} not found.".format(profile_target)) 263 if os.listdir(profile_target): 264 sys.exit("Output directory {} not empty.".format(profile_target)) 265 266 return profile_target 267 268 269def record_trace(config, profile_target): 270 """Runs Perfetto with the provided configuration to record a trace. 271 272 Args: 273 config: The Perfetto config to be used for tracing/profiling. 274 profile_target: The directory where the recorded trace is output. 275 """ 276 NULL = open(os.devnull) 277 NO_OUT = { 278 'stdout': NULL, 279 'stderr': NULL, 280 } 281 if not release_or_newer('R'): 282 sys.exit("This tool requires Android R+ to run.") 283 profile_device_path = '/data/misc/perfetto-traces/profile-' + UUID 284 perfetto_command = ('CONFIG=\'{}\'; echo ${{CONFIG}} | ' 285 'perfetto --txt -c - -o {} -d') 286 try: 287 perfetto_pid = int( 288 adb_check_output([ 289 'adb', 'exec-out', 290 perfetto_command.format(config, profile_device_path) 291 ]).strip()) 292 except ValueError as error: 293 sys.exit("Unable to start profiling: {}".format(error)) 294 295 print("Profiling active. Press Ctrl+C to terminate.") 296 297 old_handler = signal.signal(signal.SIGINT, sigint_handler) 298 299 perfetto_alive = True 300 while perfetto_alive and not IS_INTERRUPTED: 301 perfetto_alive = subprocess.call( 302 ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)], **NO_OUT) == 0 303 time.sleep(0.25) 304 305 print("Finishing profiling and symbolization...") 306 307 if IS_INTERRUPTED: 308 adb_check_output(['adb', 'shell', 'kill', '-INT', str(perfetto_pid)]) 309 310 # Restore old handler. 311 signal.signal(signal.SIGINT, old_handler) 312 313 while perfetto_alive: 314 perfetto_alive = subprocess.call( 315 ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)]) == 0 316 time.sleep(0.25) 317 318 profile_host_path = os.path.join(profile_target, 'raw-trace') 319 adb_check_output(['adb', 'pull', profile_device_path, profile_host_path]) 320 adb_check_output(['adb', 'shell', 'rm', profile_device_path]) 321 322 323def get_trace_to_text(): 324 """Sets up and returns the path to `trace_to_text`.""" 325 try: 326 trace_to_text = get_perfetto_prebuilt('trace_to_text', soft_fail=True) 327 except Exception as error: 328 exit_with_bug_report(error) 329 if trace_to_text is None: 330 exit_with_bug_report( 331 "Unable to download `trace_to_text` for symbolizing profiles.") 332 333 return trace_to_text 334 335 336def concatenate_files(files_to_concatenate, output_file): 337 """Concatenates files. 338 339 Args: 340 files_to_concatenate: Paths for input files to concatenate. 341 output_file: Path to the resultant output file. 342 """ 343 with open(output_file, 'wb') as output: 344 for file in files_to_concatenate: 345 with open(file, 'rb') as input: 346 shutil.copyfileobj(input, output) 347 348 349def symbolize_trace(trace_to_text, profile_target): 350 """Attempts symbolization of the recorded trace/profile, if symbols are available. 351 352 Args: 353 trace_to_text: The path to the `trace_to_text` binary used for symbolization. 354 profile_target: The directory where the recorded trace was output. 355 356 Returns: 357 The path to the symbolized trace file if symbolization was completed, 358 and the original trace file, if it was not. 359 """ 360 binary_path = os.getenv('PERFETTO_BINARY_PATH') 361 trace_file = os.path.join(profile_target, 'raw-trace') 362 files_to_concatenate = [trace_file] 363 364 if binary_path is not None: 365 try: 366 with open(os.path.join(profile_target, 'symbols'), 'w') as symbols_file: 367 return_code = subprocess.call([trace_to_text, 'symbolize', trace_file], 368 env=dict( 369 os.environ, 370 PERFETTO_BINARY_PATH=binary_path), 371 stdout=symbols_file) 372 except IOError as error: 373 sys.exit("Unable to write symbols to disk: {}".format(error)) 374 if return_code == 0: 375 files_to_concatenate.append(os.path.join(profile_target, 'symbols')) 376 else: 377 print("Failed to symbolize. Continuing without symbols.", file=sys.stderr) 378 379 if len(files_to_concatenate) > 1: 380 trace_file = os.path.join(profile_target, 'symbolized-trace') 381 try: 382 concatenate_files(files_to_concatenate, trace_file) 383 except Exception as error: 384 sys.exit("Unable to write symbolized profile to disk: {}".format(error)) 385 386 return trace_file 387 388 389def generate_pprof_profiles(trace_to_text, trace_file): 390 """Generates pprof profiles from the recorded trace. 391 392 Args: 393 trace_to_text: The path to the `trace_to_text` binary used for generating profiles. 394 trace_file: The oath to the recorded and potentially symbolized trace file. 395 396 Returns: 397 The directory where pprof profiles are output. 398 """ 399 try: 400 trace_to_text_output = subprocess.check_output( 401 [trace_to_text, 'profile', '--perf', trace_file]) 402 except Exception as error: 403 exit_with_bug_report( 404 "Unable to extract profiles from trace: {}".format(error)) 405 406 profiles_output_directory = None 407 for word in trace_to_text_output.decode('utf-8').split(): 408 if 'perf_profile-' in word: 409 profiles_output_directory = word 410 if profiles_output_directory is None: 411 exit_with_no_profile() 412 return profiles_output_directory 413 414 415def copy_profiles_to_destination(profile_target, profile_path): 416 """Copies recorded profiles to `profile_target` from `profile_path`.""" 417 profile_files = os.listdir(profile_path) 418 if not profile_files: 419 exit_with_no_profile() 420 421 try: 422 for profile_file in profile_files: 423 shutil.copy(os.path.join(profile_path, profile_file), profile_target) 424 except Exception as error: 425 sys.exit("Unable to copy profiles to {}: {}".format(profile_target, error)) 426 427 print("Wrote profiles to {}".format(profile_target)) 428 429 430def main(argv): 431 args = parse_and_validate_args() 432 profile_target = get_and_prepare_profile_target(args) 433 record_trace(get_perfetto_config(args), profile_target) 434 trace_to_text = get_trace_to_text() 435 trace_file = symbolize_trace(trace_to_text, profile_target) 436 copy_profiles_to_destination( 437 profile_target, generate_pprof_profiles(trace_to_text, trace_file)) 438 return 0 439 440 441# BEGIN_SECTION_GENERATED_BY(roll-prebuilts) 442# Revision: v25.0 443PERFETTO_PREBUILT_MANIFEST = [{ 444 'tool': 445 'trace_to_text', 446 'arch': 447 'mac-amd64', 448 'file_name': 449 'trace_to_text', 450 'file_size': 451 6525752, 452 'url': 453 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v25.0/mac-amd64/trace_to_text', 454 'sha256': 455 '64ccf6bac87825145691c6533412e514891f82300d68ff7ce69e8d2ca69aaf62', 456 'platform': 457 'darwin', 458 'machine': ['x86_64'] 459}, { 460 'tool': 461 'trace_to_text', 462 'arch': 463 'windows-amd64', 464 'file_name': 465 'trace_to_text.exe', 466 'file_size': 467 5925888, 468 'url': 469 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v25.0/windows-amd64/trace_to_text.exe', 470 'sha256': 471 '29e50ec4d8e28c7c322ba13273afcce80c63fe7d9f182b83af0e2077b4d2b952', 472 'platform': 473 'win32', 474 'machine': ['amd64'] 475}, { 476 'tool': 477 'trace_to_text', 478 'arch': 479 'linux-amd64', 480 'file_name': 481 'trace_to_text', 482 'file_size': 483 6939560, 484 'url': 485 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v25.0/linux-amd64/trace_to_text', 486 'sha256': 487 '109f4ff3bbd47633b0c08a338f1230e69d529ddf1584656ed45d8a59acaaabeb', 488 'platform': 489 'linux', 490 'machine': ['x86_64'] 491}] 492 493 494# DO NOT EDIT. If you wish to make edits to this code, you need to change only 495# //tools/get_perfetto_prebuilt.py and run /tools/roll-prebuilts to regenerate 496# all the others scripts this is embedded into. 497def get_perfetto_prebuilt(tool_name, soft_fail=False, arch=None): 498 """ Downloads the prebuilt, if necessary, and returns its path on disk. """ 499 500 # The first time this is invoked, it downloads the |url| and caches it into 501 # ~/.perfetto/prebuilts/$tool_name. On subsequent invocations it just runs the 502 # cached version. 503 def download_or_get_cached(file_name, url, sha256): 504 import os, hashlib, subprocess 505 dir = os.path.join( 506 os.path.expanduser('~'), '.local', 'share', 'perfetto', 'prebuilts') 507 os.makedirs(dir, exist_ok=True) 508 bin_path = os.path.join(dir, file_name) 509 sha256_path = os.path.join(dir, file_name + '.sha256') 510 needs_download = True 511 512 # Avoid recomputing the SHA-256 on each invocation. The SHA-256 of the last 513 # download is cached into file_name.sha256, just check if that matches. 514 if os.path.exists(bin_path) and os.path.exists(sha256_path): 515 with open(sha256_path, 'rb') as f: 516 digest = f.read().decode() 517 if digest == sha256: 518 needs_download = False 519 520 if needs_download: 521 # Either the filed doesn't exist or the SHA256 doesn't match. 522 tmp_path = bin_path + '.tmp' 523 print('Downloading ' + url) 524 subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url]) 525 with open(tmp_path, 'rb') as fd: 526 actual_sha256 = hashlib.sha256(fd.read()).hexdigest() 527 if actual_sha256 != sha256: 528 raise Exception('Checksum mismatch for %s (actual: %s, expected: %s)' % 529 (url, actual_sha256, sha256)) 530 os.chmod(tmp_path, 0o755) 531 os.rename(tmp_path, bin_path) 532 with open(sha256_path, 'w') as f: 533 f.write(sha256) 534 return bin_path 535 # --- end of download_or_get_cached() --- 536 537 # --- get_perfetto_prebuilt() function starts here. --- 538 import os, platform, sys 539 plat = sys.platform.lower() 540 machine = platform.machine().lower() 541 manifest_entry = None 542 for entry in PERFETTO_PREBUILT_MANIFEST: 543 # If the caller overrides the arch, just match that (for Android prebuilts). 544 if arch and entry.get('arch') == arch: 545 manifest_entry = entry 546 break 547 # Otherwise guess the local machine arch. 548 if entry.get('tool') == tool_name and entry.get( 549 'platform') == plat and machine in entry.get('machine', []): 550 manifest_entry = entry 551 break 552 if manifest_entry is None: 553 if soft_fail: 554 return None 555 raise Exception( 556 ('No prebuilts available for %s-%s\n' % (plat, machine)) + 557 'See https://perfetto.dev/docs/contributing/build-instructions') 558 559 return download_or_get_cached( 560 file_name=manifest_entry['file_name'], 561 url=manifest_entry['url'], 562 sha256=manifest_entry['sha256']) 563 564 565# END_SECTION_GENERATED_BY(roll-prebuilts) 566 567if __name__ == '__main__': 568 sys.exit(main(sys.argv)) 569