| # Copyright 2018 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import contextlib |
| import logging |
| import os |
| import shutil |
| import subprocess |
| import sys |
| import tempfile |
| |
| from devil import devil_env |
| from devil.android import device_signal, device_errors |
| from devil.android.sdk import version_codes |
| from pylib import constants |
| |
| |
| def _ProcessType(proc): |
| _, _, suffix = proc.name.partition(':') |
| if not suffix: |
| return 'browser' |
| if suffix.startswith('sandboxed_process'): |
| return 'renderer' |
| if suffix.startswith('privileged_process'): |
| return 'gpu' |
| return None |
| |
| |
| def _GetSpecifiedPID(device, package_name, process_specifier): |
| if process_specifier is None: |
| return None |
| |
| # Check for numeric PID |
| try: |
| pid = int(process_specifier) |
| return pid |
| except ValueError: |
| pass |
| |
| # Check for exact process name; can be any of these formats: |
| # <package>:<process name>, i.e. 'org.chromium.chrome:sandboxed_process0' |
| # :<process name>, i.e. ':sandboxed_process0' |
| # <process name>, i.e. 'sandboxed_process0' |
| full_process_name = process_specifier |
| if process_specifier.startswith(':'): |
| full_process_name = package_name + process_specifier |
| elif ':' not in process_specifier: |
| full_process_name = '%s:%s' % (package_name, process_specifier) |
| matching_processes = device.ListProcesses(full_process_name) |
| if len(matching_processes) == 1: |
| return matching_processes[0].pid |
| if len(matching_processes) > 1: |
| raise RuntimeError('Found %d processes with name "%s".' % ( |
| len(matching_processes), process_specifier)) |
| |
| # Check for process type (i.e. 'renderer') |
| package_processes = device.ListProcesses(package_name) |
| matching_processes = [p for p in package_processes if ( |
| _ProcessType(p) == process_specifier)] |
| if process_specifier == 'renderer' and len(matching_processes) > 1: |
| raise RuntimeError('Found %d renderer processes; please re-run with only ' |
| 'one open tab.' % len(matching_processes)) |
| if len(matching_processes) != 1: |
| raise RuntimeError('Found %d processes of type "%s".' % ( |
| len(matching_processes), process_specifier)) |
| return matching_processes[0].pid |
| |
| |
| def _ThreadsForProcess(device, pid): |
| # The thread list output format for 'ps' is the same regardless of version. |
| # Here's the column headers, and a sample line for a thread belonging to |
| # pid 12345 (note that the last few columns are not aligned with headers): |
| # |
| # USER PID TID PPID VSZ RSS WCHAN ADDR S CMD |
| # u0_i101 12345 24680 567 1357902 97531 futex_wait_queue_me e85acd9c S \ |
| # CrRendererMain |
| if device.build_version_sdk >= version_codes.OREO: |
| pid_regex = ( |
| r'^[[:graph:]]\{1,\}[[:blank:]]\{1,\}%d[[:blank:]]\{1,\}' % pid) |
| ps_cmd = "ps -T -e | grep '%s'" % pid_regex |
| ps_output_lines = device.RunShellCommand( |
| ps_cmd, shell=True, check_return=True) |
| else: |
| ps_cmd = ['ps', '-p', str(pid), '-t'] |
| ps_output_lines = device.RunShellCommand(ps_cmd, check_return=True) |
| result = [] |
| for l in ps_output_lines: |
| fields = l.split() |
| # fields[2] is tid, fields[-1] is thread name. Output may include an entry |
| # for the process itself with tid=pid; omit that one. |
| if fields[2] == str(pid): |
| continue |
| result.append((int(fields[2]), fields[-1])) |
| return result |
| |
| |
| def _ThreadType(thread_name): |
| if not thread_name: |
| return 'unknown' |
| if (thread_name.startswith('Chrome_ChildIO') or |
| thread_name.startswith('Chrome_IO')): |
| return 'io' |
| if thread_name.startswith('Compositor'): |
| return 'compositor' |
| if (thread_name.startswith('ChildProcessMai') or |
| thread_name.startswith('CrGpuMain') or |
| thread_name.startswith('CrRendererMain')): |
| return 'main' |
| if thread_name.startswith('RenderThread'): |
| return 'render' |
| raise ValueError('got no matching thread_name') |
| |
| |
| def _GetSpecifiedTID(device, pid, thread_specifier): |
| if thread_specifier is None: |
| return None |
| |
| # Check for numeric TID |
| try: |
| tid = int(thread_specifier) |
| return tid |
| except ValueError: |
| pass |
| |
| # Check for thread type |
| if pid is not None: |
| matching_threads = [t for t in _ThreadsForProcess(device, pid) if ( |
| _ThreadType(t[1]) == thread_specifier)] |
| if len(matching_threads) != 1: |
| raise RuntimeError('Found %d threads of type "%s".' % ( |
| len(matching_threads), thread_specifier)) |
| return matching_threads[0][0] |
| |
| return None |
| |
| |
| def PrepareDevice(device): |
| if device.build_version_sdk < version_codes.NOUGAT: |
| raise RuntimeError('Simpleperf profiling is only supported on Android N ' |
| 'and later.') |
| |
| # Necessary for profiling |
| # https://android-review.googlesource.com/c/platform/system/sepolicy/+/234400 |
| device.SetProp('security.perf_harden', '0') |
| |
| |
| def InstallSimpleperf(device, package_name): |
| package_arch = device.GetPackageArchitecture(package_name) or 'armeabi-v7a' |
| host_simpleperf_path = devil_env.config.LocalPath('simpleperf', package_arch) |
| if not host_simpleperf_path: |
| raise Exception('Could not get path to simpleperf executable on host.') |
| device_simpleperf_path = '/'.join( |
| ('/data/local/tmp/profilers', package_arch, 'simpleperf')) |
| device.PushChangedFiles([(host_simpleperf_path, device_simpleperf_path)]) |
| return device_simpleperf_path |
| |
| |
| @contextlib.contextmanager |
| def RunSimpleperf(device, device_simpleperf_path, package_name, |
| process_specifier, thread_specifier, events, |
| profiler_args, host_out_path): |
| pid = _GetSpecifiedPID(device, package_name, process_specifier) |
| tid = _GetSpecifiedTID(device, pid, thread_specifier) |
| if pid is None and tid is None: |
| raise RuntimeError('Could not find specified process/thread running on ' |
| 'device. Make sure the apk is already running before ' |
| 'attempting to profile.') |
| profiler_args = list(profiler_args) |
| if profiler_args and profiler_args[0] == 'record': |
| profiler_args.pop(0) |
| profiler_args.extend(('-e', events)) |
| if '--call-graph' not in profiler_args and '-g' not in profiler_args: |
| profiler_args.append('-g') |
| if '-f' not in profiler_args: |
| profiler_args.extend(('-f', '1000')) |
| |
| device_out_path = '/data/local/tmp/perf.data' |
| should_remove_device_out_path = True |
| if '-o' in profiler_args: |
| device_out_path = profiler_args[profiler_args.index('-o') + 1] |
| should_remove_device_out_path = False |
| else: |
| profiler_args.extend(('-o', device_out_path)) |
| |
| # Remove the default output to avoid confusion if simpleperf opts not |
| # to update the file. |
| file_exists = True |
| try: |
| device.adb.Shell('readlink -e ' + device_out_path) |
| except device_errors.AdbCommandFailedError: |
| file_exists = False |
| if file_exists: |
| logging.warning('%s output file already exists on device', device_out_path) |
| if not should_remove_device_out_path: |
| raise RuntimeError('Specified output file \'{}\' already exists, not ' |
| 'continuing'.format(device_out_path)) |
| device.adb.Shell('rm -f ' + device_out_path) |
| |
| if tid: |
| profiler_args.extend(('-t', str(tid))) |
| else: |
| profiler_args.extend(('-p', str(pid))) |
| |
| adb_shell_simpleperf_process = device.adb.StartShell( |
| [device_simpleperf_path, 'record'] + profiler_args) |
| |
| completed = False |
| try: |
| yield |
| completed = True |
| |
| finally: |
| device.KillAll('simpleperf', signum=device_signal.SIGINT, blocking=True, |
| quiet=True) |
| if completed: |
| adb_shell_simpleperf_process.wait() |
| ret = adb_shell_simpleperf_process.returncode |
| if ret == 0: |
| # Successfully gathered a profile |
| device.PullFile(device_out_path, host_out_path) |
| else: |
| logging.warning( |
| 'simpleperf exited unusually, expected exit 0, got %d', ret |
| ) |
| stdout, stderr = adb_shell_simpleperf_process.communicate() |
| logging.info('stdout: \'%s\', stderr: \'%s\'', stdout, stderr) |
| raise RuntimeError('simpleperf exited with unexpected code {} ' |
| '(run with -vv for full stdout/stderr)'.format(ret)) |
| |
| |
| def ConvertSimpleperfToPprof(simpleperf_out_path, build_directory, |
| pprof_out_path): |
| # The simpleperf scripts require the unstripped libs to be installed in the |
| # same directory structure as the libs on the device. Much of the logic here |
| # is just figuring out and creating the necessary directory structure, and |
| # symlinking the unstripped shared libs. |
| |
| # Get the set of libs that we can symbolize |
| unstripped_lib_dir = os.path.join(build_directory, 'lib.unstripped') |
| unstripped_libs = set( |
| f for f in os.listdir(unstripped_lib_dir) if f.endswith('.so')) |
| |
| # report.py will show the directory structure above the shared libs; |
| # that is the directory structure we need to recreate on the host. |
| script_dir = devil_env.config.LocalPath('simpleperf_scripts') |
| report_path = os.path.join(script_dir, 'report.py') |
| report_cmd = [sys.executable, report_path, '-i', simpleperf_out_path] |
| device_lib_path = None |
| output = subprocess.check_output(report_cmd, stderr=subprocess.STDOUT) |
| if isinstance(output, bytes): |
| output = output.decode() |
| for line in output.splitlines(): |
| fields = line.split() |
| if len(fields) < 5: |
| continue |
| shlib_path = fields[4] |
| shlib_dirname, shlib_basename = shlib_path.rpartition('/')[::2] |
| if shlib_basename in unstripped_libs: |
| device_lib_path = shlib_dirname |
| break |
| if not device_lib_path: |
| raise RuntimeError('No chrome-related symbols in profiling data in %s. ' |
| 'Either the process was idle for the entire profiling ' |
| 'period, or something went very wrong (and you should ' |
| 'file a bug at crbug.com/new with component ' |
| 'Speed>Tracing, and assign it to szager@chromium.org).' |
| % simpleperf_out_path) |
| |
| # Recreate the directory structure locally, and symlink unstripped libs. |
| processing_dir = tempfile.mkdtemp() |
| try: |
| processing_lib_dir = os.path.join( |
| processing_dir, 'binary_cache', device_lib_path.lstrip('/')) |
| os.makedirs(processing_lib_dir) |
| for lib in unstripped_libs: |
| unstripped_lib_path = os.path.join(unstripped_lib_dir, lib) |
| processing_lib_path = os.path.join(processing_lib_dir, lib) |
| os.symlink(unstripped_lib_path, processing_lib_path) |
| |
| # Run the script to annotate symbols and convert from simpleperf format to |
| # pprof format. |
| pprof_converter_script = os.path.join( |
| script_dir, 'pprof_proto_generator.py') |
| pprof_converter_cmd = [ |
| sys.executable, pprof_converter_script, '-i', simpleperf_out_path, '-o', |
| os.path.abspath(pprof_out_path), '--ndk_path', |
| constants.ANDROID_NDK_ROOT |
| ] |
| subprocess.check_output(pprof_converter_cmd, stderr=subprocess.STDOUT, |
| cwd=processing_dir) |
| finally: |
| shutil.rmtree(processing_dir, ignore_errors=True) |