1#!/usr/bin/env python3 2# Copyright (C) 2022 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"""Runs tracing with CPU profiling enabled, and symbolizes traces if requested. 16 17For usage instructions, please see: 18https://perfetto.dev/docs/quickstart/callstack-sampling 19 20Adapted in large part from `heap_profile`. 21""" 22 23import argparse 24import os 25import shutil 26import signal 27import subprocess 28import sys 29import tempfile 30import textwrap 31import time 32import uuid 33 34from perfetto.prebuilts.manifests.traceconv import * 35from perfetto.prebuilts.perfetto_prebuilts import * 36 37# Used for creating directories, etc. 38UUID = str(uuid.uuid4())[-6:] 39 40# See `sigint_handler` below. 41IS_INTERRUPTED = False 42 43 44def sigint_handler(signal, frame): 45 """Useful for cleanly interrupting tracing.""" 46 global IS_INTERRUPTED 47 IS_INTERRUPTED = True 48 49 50def exit_with_no_profile(): 51 sys.exit("No profiles generated.") 52 53 54def exit_with_bug_report(error): 55 sys.exit( 56 "{}\n\n If this is unexpected, please consider filing a bug at: \n" 57 "https://perfetto.dev/docs/contributing/getting-started#bugs.".format( 58 error)) 59 60 61def adb_check_output(command): 62 """Runs an `adb` command and returns its output.""" 63 try: 64 return subprocess.check_output(command).decode('utf-8') 65 except FileNotFoundError: 66 sys.exit("`adb` not found: Is it installed or on PATH?") 67 except subprocess.CalledProcessError as error: 68 sys.exit("`adb` error: Are any (or multiple) devices connected?\n" 69 "If multiple devices are connected, please select one by " 70 "setting `ANDROID_SERIAL=device_id`.\n" 71 "{}".format(error)) 72 except Exception as error: 73 exit_with_bug_report(error) 74 75 76def parse_and_validate_args(): 77 """Parses, validates, and returns command-line arguments for this script.""" 78 DESCRIPTION = """Runs tracing with CPU profiling enabled, and symbolizes 79 traces if requested. 80 81 For usage instructions, please see: 82 https://perfetto.dev/docs/quickstart/callstack-sampling 83 """ 84 parser = argparse.ArgumentParser(description=DESCRIPTION) 85 parser.add_argument( 86 "-f", 87 "--frequency", 88 help="Sampling frequency (Hz). " 89 "Default: 100 Hz.", 90 metavar="FREQUENCY", 91 type=int, 92 default=100) 93 parser.add_argument( 94 "-d", 95 "--duration", 96 help="Duration of profile (ms). 0 to run until interrupted. " 97 "Default: until interrupted by user.", 98 metavar="DURATION", 99 type=int, 100 default=0) 101 # Profiling using hardware counters. 102 parser.add_argument( 103 "-e", 104 "--event", 105 help="Use the specified hardware counter event for sampling.", 106 metavar="EVENT", 107 action="append", 108 # See: '//perfetto/protos/perfetto/trace/perfetto_trace.proto'. 109 choices=['HW_CPU_CYCLES', 'HW_INSTRUCTIONS', 'HW_CACHE_REFERENCES', 110 'HW_CACHE_MISSES', 'HW_BRANCH_INSTRUCTIONS', 'HW_BRANCH_MISSES', 111 'HW_BUS_CYCLES', 'HW_STALLED_CYCLES_FRONTEND', 112 'HW_STALLED_CYCLES_BACKEND'], 113 default=[]) 114 parser.add_argument( 115 "-k", 116 "--kernel-frames", 117 help="Collect kernel frames. Default: false.", 118 action="store_true", 119 default=False) 120 parser.add_argument( 121 "-n", 122 "--name", 123 help="Comma-separated list of names of processes to be profiled.", 124 metavar="NAMES", 125 default=None) 126 parser.add_argument( 127 "-p", 128 "--partial-matching", 129 help="If set, enables \"partial matching\" on the strings in --names/-n." 130 "Processes that are already running when profiling is started, and whose " 131 "names include any of the values in --names/-n as substrings will be " 132 "profiled.", 133 action="store_true") 134 parser.add_argument( 135 "-c", 136 "--config", 137 help="A custom configuration file, if any, to be used for profiling. " 138 "If provided, --frequency/-f, --duration/-d, and --name/-n are not used.", 139 metavar="CONFIG", 140 default=None) 141 parser.add_argument( 142 "--no-annotations", 143 help="Do not suffix the pprof function names with Android ART mode " 144 "annotations such as [jit].", 145 action="store_true") 146 parser.add_argument( 147 "--print-config", 148 action="store_true", 149 help="Print config instead of running. For debugging.") 150 parser.add_argument( 151 "-o", 152 "--output", 153 help="Output directory for recorded trace.", 154 metavar="DIRECTORY", 155 default=None) 156 157 args = parser.parse_args() 158 if args.config is not None: 159 if args.name is not None: 160 sys.exit("--name/-n should not be specified with --config/-c.") 161 elif args.event: 162 sys.exit("-e/--event should not be specified with --config/-c.") 163 elif args.config is None and args.name is None: 164 sys.exit("One of --names/-n or --config/-c is required.") 165 166 return args 167 168 169def get_matching_processes(args, names_to_match): 170 """Returns a list of currently-running processes whose names match 171 `names_to_match`. 172 173 Args: 174 args: The command-line arguments provided to this script. 175 names_to_match: The list of process names provided by the user. 176 """ 177 # Returns names as they are. 178 if not args.partial_matching: 179 return names_to_match 180 181 # Attempt to match names to names of currently running processes. 182 PS_PROCESS_OFFSET = 8 183 matching_processes = [] 184 for line in adb_check_output(['adb', 'shell', 'ps', '-A']).splitlines(): 185 line_split = line.split() 186 if len(line_split) <= PS_PROCESS_OFFSET: 187 continue 188 process = line_split[PS_PROCESS_OFFSET] 189 for name in names_to_match: 190 if name in process: 191 matching_processes.append(process) 192 break 193 194 return matching_processes 195 196 197def get_perfetto_config(args): 198 """Returns a Perfetto config with CPU profiling enabled for the selected 199 processes. 200 201 Args: 202 args: The command-line arguments provided to this script. 203 """ 204 if args.config is not None: 205 try: 206 with open(args.config, 'r') as config_file: 207 return config_file.read() 208 except IOError as error: 209 sys.exit("Unable to read config file: {}".format(error)) 210 211 CONFIG_INDENT = ' ' 212 CONFIG = textwrap.dedent('''\ 213 buffers {{ 214 size_kb: 2048 215 }} 216 217 buffers {{ 218 size_kb: 63488 219 }} 220 221 data_sources {{ 222 config {{ 223 name: "linux.process_stats" 224 target_buffer: 0 225 process_stats_config {{ 226 proc_stats_poll_ms: 100 227 }} 228 }} 229 }} 230 231 duration_ms: {duration} 232 write_into_file: true 233 flush_timeout_ms: 30000 234 flush_period_ms: 604800000 235 ''') 236 237 matching_processes = [] 238 if args.name is not None: 239 names_to_match = [name.strip() for name in args.name.split(',')] 240 matching_processes = get_matching_processes(args, names_to_match) 241 242 if not matching_processes: 243 sys.exit("No running processes matched for profiling.") 244 245 target_config = "\n".join( 246 [f'{CONFIG_INDENT}target_cmdline: "{p}"' for p in matching_processes]) 247 248 events = args.event or ['SW_CPU_CLOCK'] 249 for event in events: 250 CONFIG += (textwrap.dedent(''' 251 data_sources {{ 252 config {{ 253 name: "linux.perf" 254 target_buffer: 1 255 perf_event_config {{ 256 timebase {{ 257 counter: %s 258 frequency: {frequency} 259 timestamp_clock: PERF_CLOCK_MONOTONIC 260 }} 261 callstack_sampling {{ 262 scope {{ 263 {target_config} 264 }} 265 kernel_frames: {kernel_config} 266 }} 267 }} 268 }} 269 }} 270 ''') % (event)) 271 272 if args.kernel_frames: 273 kernel_config = "true" 274 else: 275 kernel_config = "false" 276 277 if not args.print_config: 278 print("Configured profiling for these processes:\n") 279 for matching_process in matching_processes: 280 print(matching_process) 281 print() 282 283 config = CONFIG.format( 284 frequency=args.frequency, 285 duration=args.duration, 286 target_config=target_config, 287 kernel_config=kernel_config) 288 289 return config 290 291 292def release_or_newer(release): 293 """Returns whether a new enough Android release is being used.""" 294 SDK = {'T': 33} 295 sdk = int( 296 adb_check_output( 297 ['adb', 'shell', 'getprop', 'ro.system.build.version.sdk']).strip()) 298 if sdk >= SDK[release]: 299 return True 300 301 codename = adb_check_output( 302 ['adb', 'shell', 'getprop', 'ro.build.version.codename']).strip() 303 return codename == release 304 305 306def get_and_prepare_profile_target(args): 307 """Returns the target where the trace/profile will be output. Creates a 308 new directory if necessary. 309 310 Args: 311 args: The command-line arguments provided to this script. 312 """ 313 profile_target = os.path.join(tempfile.gettempdir(), UUID) 314 if args.output is not None: 315 profile_target = args.output 316 else: 317 os.makedirs(profile_target, exist_ok=True) 318 if not os.path.isdir(profile_target): 319 sys.exit("Output directory {} not found.".format(profile_target)) 320 if os.listdir(profile_target): 321 sys.exit("Output directory {} not empty.".format(profile_target)) 322 323 return profile_target 324 325 326def record_trace(config, profile_target): 327 """Runs Perfetto with the provided configuration to record a trace. 328 329 Args: 330 config: The Perfetto config to be used for tracing/profiling. 331 profile_target: The directory where the recorded trace is output. 332 """ 333 NULL = open(os.devnull) 334 NO_OUT = { 335 'stdout': NULL, 336 'stderr': NULL, 337 } 338 if not release_or_newer('T'): 339 sys.exit("This tool requires Android T+ to run.") 340 341 # Push configuration to the device. 342 tf = tempfile.NamedTemporaryFile() 343 tf.file.write(config.encode('utf-8')) 344 tf.file.flush() 345 profile_config_path = '/data/misc/perfetto-configs/config-' + UUID 346 adb_check_output(['adb', 'push', tf.name, profile_config_path]) 347 tf.close() 348 349 350 profile_device_path = '/data/misc/perfetto-traces/profile-' + UUID 351 perfetto_command = ('perfetto --txt -c {} -o {} -d') 352 try: 353 perfetto_pid = int( 354 adb_check_output([ 355 'adb', 'exec-out', 356 perfetto_command.format(profile_config_path, profile_device_path) 357 ]).strip()) 358 except ValueError as error: 359 sys.exit("Unable to start profiling: {}".format(error)) 360 361 print("Profiling active. Press Ctrl+C to terminate.") 362 363 old_handler = signal.signal(signal.SIGINT, sigint_handler) 364 365 perfetto_alive = True 366 while perfetto_alive and not IS_INTERRUPTED: 367 perfetto_alive = subprocess.call( 368 ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)], **NO_OUT) == 0 369 time.sleep(0.25) 370 371 print("Finishing profiling and symbolization...") 372 373 if IS_INTERRUPTED: 374 adb_check_output(['adb', 'shell', 'kill', '-INT', str(perfetto_pid)]) 375 376 # Restore old handler. 377 signal.signal(signal.SIGINT, old_handler) 378 379 while perfetto_alive: 380 perfetto_alive = subprocess.call( 381 ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)]) == 0 382 time.sleep(0.25) 383 384 profile_host_path = os.path.join(profile_target, 'raw-trace') 385 adb_check_output(['adb', 'pull', profile_device_path, profile_host_path]) 386 adb_check_output(['adb', 'shell', 'rm', profile_config_path]) 387 adb_check_output(['adb', 'shell', 'rm', profile_device_path]) 388 389 390def get_traceconv(): 391 """Sets up and returns the path to `traceconv`.""" 392 try: 393 traceconv = get_perfetto_prebuilt(TRACECONV_MANIFEST, soft_fail=True) 394 except Exception as error: 395 exit_with_bug_report(error) 396 if traceconv is None: 397 exit_with_bug_report( 398 "Unable to download `traceconv` for symbolizing profiles.") 399 400 return traceconv 401 402 403def concatenate_files(files_to_concatenate, output_file): 404 """Concatenates files. 405 406 Args: 407 files_to_concatenate: Paths for input files to concatenate. 408 output_file: Path to the resultant output file. 409 """ 410 with open(output_file, 'wb') as output: 411 for file in files_to_concatenate: 412 with open(file, 'rb') as input: 413 shutil.copyfileobj(input, output) 414 415 416def symbolize_trace(traceconv, profile_target): 417 """Attempts symbolization of the recorded trace/profile, if symbols are 418 available. 419 420 Args: 421 traceconv: The path to the `traceconv` binary used for symbolization. 422 profile_target: The directory where the recorded trace was output. 423 424 Returns: 425 The path to the symbolized trace file if symbolization was completed, 426 and the original trace file, if it was not. 427 """ 428 binary_path = os.getenv('PERFETTO_BINARY_PATH') 429 trace_file = os.path.join(profile_target, 'raw-trace') 430 files_to_concatenate = [trace_file] 431 432 if binary_path is not None: 433 try: 434 with open(os.path.join(profile_target, 'symbols'), 'w') as symbols_file: 435 return_code = subprocess.call([traceconv, 'symbolize', trace_file], 436 env=dict( 437 os.environ, 438 PERFETTO_BINARY_PATH=binary_path), 439 stdout=symbols_file) 440 except IOError as error: 441 sys.exit("Unable to write symbols to disk: {}".format(error)) 442 if return_code == 0: 443 files_to_concatenate.append(os.path.join(profile_target, 'symbols')) 444 else: 445 print("Failed to symbolize. Continuing without symbols.", file=sys.stderr) 446 447 if len(files_to_concatenate) > 1: 448 trace_file = os.path.join(profile_target, 'symbolized-trace') 449 try: 450 concatenate_files(files_to_concatenate, trace_file) 451 except Exception as error: 452 sys.exit("Unable to write symbolized profile to disk: {}".format(error)) 453 454 return trace_file 455 456 457def generate_pprof_profiles(traceconv, trace_file, args): 458 """Generates pprof profiles from the recorded trace. 459 460 Args: 461 traceconv: The path to the `traceconv` binary used for generating profiles. 462 trace_file: The oath to the recorded and potentially symbolized trace file. 463 464 Returns: 465 The directory where pprof profiles are output. 466 """ 467 try: 468 conversion_args = [traceconv, 'profile', '--perf'] + ( 469 ['--no-annotations'] if args.no_annotations else []) + [trace_file] 470 traceconv_output = subprocess.check_output(conversion_args) 471 except Exception as error: 472 exit_with_bug_report( 473 "Unable to extract profiles from trace: {}".format(error)) 474 475 profiles_output_directory = None 476 for word in traceconv_output.decode('utf-8').split(): 477 if 'perf_profile-' in word: 478 profiles_output_directory = word 479 if profiles_output_directory is None: 480 exit_with_no_profile() 481 return profiles_output_directory 482 483 484def copy_profiles_to_destination(profile_target, profile_path): 485 """Copies recorded profiles to `profile_target` from `profile_path`.""" 486 profile_files = os.listdir(profile_path) 487 if not profile_files: 488 exit_with_no_profile() 489 490 try: 491 for profile_file in profile_files: 492 shutil.copy(os.path.join(profile_path, profile_file), profile_target) 493 except Exception as error: 494 sys.exit("Unable to copy profiles to {}: {}".format(profile_target, error)) 495 496 print("Wrote profiles to {}".format(profile_target)) 497 498 499def main(argv): 500 args = parse_and_validate_args() 501 profile_target = get_and_prepare_profile_target(args) 502 trace_config = get_perfetto_config(args) 503 if args.print_config: 504 print(trace_config) 505 return 0 506 record_trace(trace_config, profile_target) 507 traceconv = get_traceconv() 508 trace_file = symbolize_trace(traceconv, profile_target) 509 copy_profiles_to_destination( 510 profile_target, generate_pprof_profiles(traceconv, trace_file, args)) 511 return 0 512 513 514if __name__ == '__main__': 515 sys.exit(main(sys.argv)) 516