tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

simpleperf.py (11107B)


      1 # Copyright 2018 The Chromium Authors
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import contextlib
      6 import logging
      7 import os
      8 import shutil
      9 import subprocess
     10 import sys
     11 import tempfile
     12 
     13 from devil import devil_env
     14 from devil.android import device_signal, device_errors
     15 from devil.android.sdk import version_codes
     16 from pylib import constants
     17 
     18 
     19 def _ProcessType(proc):
     20  _, _, suffix = proc.name.partition(':')
     21  if not suffix:
     22    return 'browser'
     23  if suffix.startswith('sandboxed_process'):
     24    return 'renderer'
     25  if suffix.startswith('privileged_process'):
     26    return 'gpu'
     27  return None
     28 
     29 
     30 def _GetSpecifiedPID(device, package_name, process_specifier):
     31  if process_specifier is None:
     32    return None
     33 
     34  # Check for numeric PID
     35  try:
     36    pid = int(process_specifier)
     37    return pid
     38  except ValueError:
     39    pass
     40 
     41  # Check for exact process name; can be any of these formats:
     42  #   <package>:<process name>, i.e. 'org.chromium.chrome:sandboxed_process0'
     43  #   :<process name>, i.e. ':sandboxed_process0'
     44  #   <process name>, i.e. 'sandboxed_process0'
     45  full_process_name = process_specifier
     46  if process_specifier.startswith(':'):
     47    full_process_name = package_name + process_specifier
     48  elif ':' not in process_specifier:
     49    full_process_name = '%s:%s' % (package_name, process_specifier)
     50  matching_processes = device.ListProcesses(full_process_name)
     51  if len(matching_processes) == 1:
     52    return matching_processes[0].pid
     53  if len(matching_processes) > 1:
     54    raise RuntimeError('Found %d processes with name "%s".' % (
     55        len(matching_processes), process_specifier))
     56 
     57  # Check for process type (i.e. 'renderer')
     58  package_processes = device.ListProcesses(package_name)
     59  matching_processes = [p for p in package_processes if (
     60      _ProcessType(p) == process_specifier)]
     61  if process_specifier == 'renderer' and len(matching_processes) > 1:
     62    raise RuntimeError('Found %d renderer processes; please re-run with only '
     63                       'one open tab.' % len(matching_processes))
     64  if len(matching_processes) != 1:
     65    raise RuntimeError('Found %d processes of type "%s".' % (
     66        len(matching_processes), process_specifier))
     67  return matching_processes[0].pid
     68 
     69 
     70 def _ThreadsForProcess(device, pid):
     71  # The thread list output format for 'ps' is the same regardless of version.
     72  # Here's the column headers, and a sample line for a thread belonging to
     73  # pid 12345 (note that the last few columns are not aligned with headers):
     74  #
     75  # USER        PID   TID  PPID     VSZ    RSS WCHAN            ADDR S CMD
     76  # u0_i101   12345 24680   567 1357902  97531 futex_wait_queue_me e85acd9c S \
     77  #     CrRendererMain
     78  if device.build_version_sdk >= version_codes.OREO:
     79    pid_regex = (
     80        r'^[[:graph:]]\{1,\}[[:blank:]]\{1,\}%d[[:blank:]]\{1,\}' % pid)
     81    ps_cmd = "ps -T -e | grep '%s'" % pid_regex
     82    ps_output_lines = device.RunShellCommand(
     83        ps_cmd, shell=True, check_return=True)
     84  else:
     85    ps_cmd = ['ps', '-p', str(pid), '-t']
     86    ps_output_lines = device.RunShellCommand(ps_cmd, check_return=True)
     87  result = []
     88  for l in ps_output_lines:
     89    fields = l.split()
     90    # fields[2] is tid, fields[-1] is thread name. Output may include an entry
     91    # for the process itself with tid=pid; omit that one.
     92    if fields[2] == str(pid):
     93      continue
     94    result.append((int(fields[2]), fields[-1]))
     95  return result
     96 
     97 
     98 def _ThreadType(thread_name):
     99  if not thread_name:
    100    return 'unknown'
    101  if (thread_name.startswith('Chrome_ChildIO') or
    102      thread_name.startswith('Chrome_IO')):
    103    return 'io'
    104  if thread_name.startswith('Compositor'):
    105    return 'compositor'
    106  if (thread_name.startswith('ChildProcessMai') or
    107      thread_name.startswith('CrGpuMain') or
    108      thread_name.startswith('CrRendererMain')):
    109    return 'main'
    110  if thread_name.startswith('RenderThread'):
    111    return 'render'
    112  raise ValueError('got no matching thread_name')
    113 
    114 
    115 def _GetSpecifiedTID(device, pid, thread_specifier):
    116  if thread_specifier is None:
    117    return None
    118 
    119  # Check for numeric TID
    120  try:
    121    tid = int(thread_specifier)
    122    return tid
    123  except ValueError:
    124    pass
    125 
    126  # Check for thread type
    127  if pid is not None:
    128    matching_threads = [t for t in _ThreadsForProcess(device, pid) if (
    129        _ThreadType(t[1]) == thread_specifier)]
    130    if len(matching_threads) != 1:
    131      raise RuntimeError('Found %d threads of type "%s".' % (
    132          len(matching_threads), thread_specifier))
    133    return matching_threads[0][0]
    134 
    135  return None
    136 
    137 
    138 def PrepareDevice(device):
    139  if device.build_version_sdk < version_codes.NOUGAT:
    140    raise RuntimeError('Simpleperf profiling is only supported on Android N '
    141                       'and later.')
    142 
    143  # Necessary for profiling
    144  # https://android-review.googlesource.com/c/platform/system/sepolicy/+/234400
    145  device.SetProp('security.perf_harden', '0')
    146 
    147 
    148 def InstallSimpleperf(device, package_name):
    149  package_arch = device.GetPackageArchitecture(package_name) or 'armeabi-v7a'
    150  host_simpleperf_path = devil_env.config.LocalPath('simpleperf', package_arch)
    151  if not host_simpleperf_path:
    152    raise Exception('Could not get path to simpleperf executable on host.')
    153  device_simpleperf_path = '/'.join(
    154      ('/data/local/tmp/profilers', package_arch, 'simpleperf'))
    155  device.PushChangedFiles([(host_simpleperf_path, device_simpleperf_path)])
    156  return device_simpleperf_path
    157 
    158 
    159 @contextlib.contextmanager
    160 def RunSimpleperf(device, device_simpleperf_path, package_name,
    161                  process_specifier, thread_specifier, events,
    162                  profiler_args, host_out_path):
    163  pid = _GetSpecifiedPID(device, package_name, process_specifier)
    164  tid = _GetSpecifiedTID(device, pid, thread_specifier)
    165  if pid is None and tid is None:
    166    raise RuntimeError('Could not find specified process/thread running on '
    167                       'device. Make sure the apk is already running before '
    168                       'attempting to profile.')
    169  profiler_args = list(profiler_args)
    170  if profiler_args and profiler_args[0] == 'record':
    171    profiler_args.pop(0)
    172  profiler_args.extend(('-e', events))
    173  if '--call-graph' not in profiler_args and '-g' not in profiler_args:
    174    profiler_args.append('-g')
    175  if '-f' not in profiler_args:
    176    profiler_args.extend(('-f', '1000'))
    177 
    178  device_out_path = '/data/local/tmp/perf.data'
    179  should_remove_device_out_path = True
    180  if '-o' in profiler_args:
    181    device_out_path = profiler_args[profiler_args.index('-o') + 1]
    182    should_remove_device_out_path = False
    183  else:
    184    profiler_args.extend(('-o', device_out_path))
    185 
    186  # Remove the default output to avoid confusion if simpleperf opts not
    187  # to update the file.
    188  file_exists = True
    189  try:
    190      device.adb.Shell('readlink -e ' + device_out_path)
    191  except device_errors.AdbCommandFailedError:
    192    file_exists = False
    193  if file_exists:
    194    logging.warning('%s output file already exists on device', device_out_path)
    195    if not should_remove_device_out_path:
    196      raise RuntimeError('Specified output file \'{}\' already exists, not '
    197                         'continuing'.format(device_out_path))
    198  device.adb.Shell('rm -f ' + device_out_path)
    199 
    200  if tid:
    201    profiler_args.extend(('-t', str(tid)))
    202  else:
    203    profiler_args.extend(('-p', str(pid)))
    204 
    205  adb_shell_simpleperf_process = device.adb.StartShell(
    206      [device_simpleperf_path, 'record'] + profiler_args)
    207 
    208  completed = False
    209  try:
    210    yield
    211    completed = True
    212 
    213  finally:
    214    device.KillAll('simpleperf', signum=device_signal.SIGINT, blocking=True,
    215                   quiet=True)
    216    if completed:
    217      adb_shell_simpleperf_process.wait()
    218      ret = adb_shell_simpleperf_process.returncode
    219      if ret == 0:
    220        # Successfully gathered a profile
    221        device.PullFile(device_out_path, host_out_path)
    222      else:
    223        logging.warning(
    224            'simpleperf exited unusually, expected exit 0, got %d', ret
    225        )
    226        stdout, stderr = adb_shell_simpleperf_process.communicate()
    227        logging.info('stdout: \'%s\', stderr: \'%s\'', stdout, stderr)
    228        raise RuntimeError('simpleperf exited with unexpected code {} '
    229                           '(run with -vv for full stdout/stderr)'.format(ret))
    230 
    231 
    232 def ConvertSimpleperfToPprof(simpleperf_out_path, build_directory,
    233                             pprof_out_path):
    234  # The simpleperf scripts require the unstripped libs to be installed in the
    235  # same directory structure as the libs on the device. Much of the logic here
    236  # is just figuring out and creating the necessary directory structure, and
    237  # symlinking the unstripped shared libs.
    238 
    239  # Get the set of libs that we can symbolize
    240  unstripped_lib_dir = os.path.join(build_directory, 'lib.unstripped')
    241  unstripped_libs = set(
    242      f for f in os.listdir(unstripped_lib_dir) if f.endswith('.so'))
    243 
    244  # report.py will show the directory structure above the shared libs;
    245  # that is the directory structure we need to recreate on the host.
    246  script_dir = devil_env.config.LocalPath('simpleperf_scripts')
    247  report_path = os.path.join(script_dir, 'report.py')
    248  report_cmd = [sys.executable, report_path, '-i', simpleperf_out_path]
    249  device_lib_path = None
    250  output = subprocess.check_output(report_cmd, stderr=subprocess.STDOUT)
    251  if isinstance(output, bytes):
    252    output = output.decode()
    253  for line in output.splitlines():
    254    fields = line.split()
    255    if len(fields) < 5:
    256      continue
    257    shlib_path = fields[4]
    258    shlib_dirname, shlib_basename = shlib_path.rpartition('/')[::2]
    259    if shlib_basename in unstripped_libs:
    260      device_lib_path = shlib_dirname
    261      break
    262  if not device_lib_path:
    263    raise RuntimeError('No chrome-related symbols in profiling data in %s. '
    264                       'Either the process was idle for the entire profiling '
    265                       'period, or something went very wrong (and you should '
    266                       'file a bug at crbug.com/new with component '
    267                       'Speed>Tracing, and assign it to szager@chromium.org).'
    268                       % simpleperf_out_path)
    269 
    270  # Recreate the directory structure locally, and symlink unstripped libs.
    271  processing_dir = tempfile.mkdtemp()
    272  try:
    273    processing_lib_dir = os.path.join(
    274        processing_dir, 'binary_cache', device_lib_path.lstrip('/'))
    275    os.makedirs(processing_lib_dir)
    276    for lib in unstripped_libs:
    277      unstripped_lib_path = os.path.join(unstripped_lib_dir, lib)
    278      processing_lib_path = os.path.join(processing_lib_dir, lib)
    279      os.symlink(unstripped_lib_path, processing_lib_path)
    280 
    281    # Run the script to annotate symbols and convert from simpleperf format to
    282    # pprof format.
    283    pprof_converter_script = os.path.join(
    284        script_dir, 'pprof_proto_generator.py')
    285    pprof_converter_cmd = [
    286        sys.executable, pprof_converter_script, '-i', simpleperf_out_path, '-o',
    287        os.path.abspath(pprof_out_path), '--ndk_path',
    288        constants.ANDROID_NDK_ROOT
    289    ]
    290    subprocess.check_output(pprof_converter_cmd, stderr=subprocess.STDOUT,
    291                            cwd=processing_dir)
    292  finally:
    293    shutil.rmtree(processing_dir, ignore_errors=True)