Home | History | Annotate | Download | only in profile_chrome
      1 # Copyright 2014 The Chromium Authors. All rights reserved.
      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 logging
      6 import optparse
      7 import os
      8 import signal
      9 import subprocess
     10 import sys
     11 import tempfile
     12 
     13 import py_utils
     14 
     15 from devil.android import device_temp_file
     16 from devil.android.perf import perf_control
     17 
     18 from profile_chrome import ui
     19 from systrace import trace_result
     20 from systrace import tracing_agents
     21 
     22 _CATAPULT_DIR = os.path.join(
     23     os.path.dirname(os.path.abspath(__file__)), '..', '..')
     24 sys.path.append(os.path.join(_CATAPULT_DIR, 'telemetry'))
     25 try:
     26   # pylint: disable=F0401,no-name-in-module,wrong-import-position
     27   from telemetry.internal.platform.profiler import android_profiling_helper
     28   from telemetry.internal.util import binary_manager
     29   # pylint: enable=wrong-import-position
     30 except ImportError:
     31   android_profiling_helper = None
     32   binary_manager = None
     33 
     34 
     35 _PERF_OPTIONS = [
     36     # Sample across all processes and CPUs to so that the current CPU gets
     37     # recorded to each sample.
     38     '--all-cpus',
     39     # In perf 3.13 --call-graph requires an argument, so use the -g short-hand
     40     # which does not.
     41     '-g',
     42     # Increase priority to avoid dropping samples. Requires root.
     43     '--realtime', '80',
     44     # Record raw samples to get CPU information.
     45     '--raw-samples',
     46     # Increase sampling frequency for better coverage.
     47     '--freq', '2000',
     48 ]
     49 
     50 
     51 class _PerfProfiler(object):
     52   def __init__(self, device, perf_binary, categories):
     53     self._device = device
     54     self._output_file = device_temp_file.DeviceTempFile(
     55         self._device.adb, prefix='perf_output')
     56     self._log_file = tempfile.TemporaryFile()
     57 
     58     # TODO(jbudorick) Look at providing a way to unhandroll this once the
     59     #                 adb rewrite has fully landed.
     60     device_param = (['-s', str(self._device)] if str(self._device) else [])
     61     cmd = ['adb'] + device_param + \
     62           ['shell', perf_binary, 'record',
     63            '--output', self._output_file.name] + _PERF_OPTIONS
     64     if categories:
     65       cmd += ['--event', ','.join(categories)]
     66     self._perf_control = perf_control.PerfControl(self._device)
     67     self._perf_control.SetPerfProfilingMode()
     68     self._perf_process = subprocess.Popen(cmd,
     69                                           stdout=self._log_file,
     70                                           stderr=subprocess.STDOUT)
     71 
     72   def SignalAndWait(self):
     73     self._device.KillAll('perf', signum=signal.SIGINT)
     74     self._perf_process.wait()
     75     self._perf_control.SetDefaultPerfMode()
     76 
     77   def _FailWithLog(self, msg):
     78     self._log_file.seek(0)
     79     log = self._log_file.read()
     80     raise RuntimeError('%s. Log output:\n%s' % (msg, log))
     81 
     82   def PullResult(self, output_path):
     83     if not self._device.FileExists(self._output_file.name):
     84       self._FailWithLog('Perf recorded no data')
     85 
     86     perf_profile = os.path.join(output_path,
     87                                 os.path.basename(self._output_file.name))
     88     self._device.PullFile(self._output_file.name, perf_profile)
     89     if not os.stat(perf_profile).st_size:
     90       os.remove(perf_profile)
     91       self._FailWithLog('Perf recorded a zero-sized file')
     92 
     93     self._log_file.close()
     94     self._output_file.close()
     95     return perf_profile
     96 
     97 
     98 class PerfProfilerAgent(tracing_agents.TracingAgent):
     99   def __init__(self, device):
    100     tracing_agents.TracingAgent.__init__(self)
    101     self._device = device
    102     self._perf_binary = self._PrepareDevice(device)
    103     self._perf_instance = None
    104     self._categories = None
    105 
    106   def __repr__(self):
    107     return 'perf profile'
    108 
    109   @staticmethod
    110   def IsSupported():
    111     return bool(android_profiling_helper)
    112 
    113   @staticmethod
    114   def _PrepareDevice(device):
    115     if not 'BUILDTYPE' in os.environ:
    116       os.environ['BUILDTYPE'] = 'Release'
    117     if binary_manager.NeedsInit():
    118       binary_manager.InitDependencyManager(None)
    119     return android_profiling_helper.PrepareDeviceForPerf(device)
    120 
    121   @classmethod
    122   def GetCategories(cls, device):
    123     perf_binary = cls._PrepareDevice(device)
    124     # Perf binary returns non-zero exit status on "list" command.
    125     return device.RunShellCommand([perf_binary, 'list'], check_return=False)
    126 
    127   @py_utils.Timeout(tracing_agents.START_STOP_TIMEOUT)
    128   def StartAgentTracing(self, config, timeout=None):
    129     self._categories = _ComputePerfCategories(config)
    130     self._perf_instance = _PerfProfiler(self._device,
    131                                         self._perf_binary,
    132                                         self._categories)
    133     return True
    134 
    135   @py_utils.Timeout(tracing_agents.START_STOP_TIMEOUT)
    136   def StopAgentTracing(self, timeout=None):
    137     if not self._perf_instance:
    138       return
    139     self._perf_instance.SignalAndWait()
    140     return True
    141 
    142   @py_utils.Timeout(tracing_agents.GET_RESULTS_TIMEOUT)
    143   def GetResults(self, timeout=None):
    144     with open(self._PullTrace(), 'r') as f:
    145       trace_data = f.read()
    146     return trace_result.TraceResult('perf', trace_data)
    147 
    148   @staticmethod
    149   def _GetInteractivePerfCommand(perfhost_path, perf_profile, symfs_dir,
    150                                  required_libs, kallsyms):
    151     cmd = '%s report -n -i %s --symfs %s --kallsyms %s' % (
    152         os.path.relpath(perfhost_path, '.'), perf_profile, symfs_dir, kallsyms)
    153     for lib in required_libs:
    154       lib = os.path.join(symfs_dir, lib[1:])
    155       if not os.path.exists(lib):
    156         continue
    157       objdump_path = android_profiling_helper.GetToolchainBinaryPath(
    158           lib, 'objdump')
    159       if objdump_path:
    160         cmd += ' --objdump %s' % os.path.relpath(objdump_path, '.')
    161         break
    162     return cmd
    163 
    164   def _PullTrace(self):
    165     symfs_dir = os.path.join(tempfile.gettempdir(),
    166                              os.path.expandvars('$USER-perf-symfs'))
    167     if not os.path.exists(symfs_dir):
    168       os.makedirs(symfs_dir)
    169     required_libs = set()
    170 
    171     # Download the recorded perf profile.
    172     perf_profile = self._perf_instance.PullResult(symfs_dir)
    173     required_libs = \
    174         android_profiling_helper.GetRequiredLibrariesForPerfProfile(
    175             perf_profile)
    176     if not required_libs:
    177       logging.warning('No libraries required by perf trace. Most likely there '
    178                       'are no samples in the trace.')
    179 
    180     # Build a symfs with all the necessary libraries.
    181     kallsyms = android_profiling_helper.CreateSymFs(self._device,
    182                                                     symfs_dir,
    183                                                     required_libs,
    184                                                     use_symlinks=False)
    185     perfhost_path = binary_manager.FetchPath(
    186         android_profiling_helper.GetPerfhostName(), 'x86_64', 'linux')
    187 
    188     ui.PrintMessage('\nNote: to view the profile in perf, run:')
    189     ui.PrintMessage('  ' + self._GetInteractivePerfCommand(perfhost_path,
    190         perf_profile, symfs_dir, required_libs, kallsyms))
    191 
    192     # Convert the perf profile into JSON.
    193     perf_script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
    194                                     'third_party', 'perf_to_tracing.py')
    195     json_file_name = os.path.basename(perf_profile)
    196     with open(os.devnull, 'w') as dev_null, \
    197         open(json_file_name, 'w') as json_file:
    198       cmd = [perfhost_path, 'script', '-s', perf_script_path, '-i',
    199              perf_profile, '--symfs', symfs_dir, '--kallsyms', kallsyms]
    200       if subprocess.call(cmd, stdout=json_file, stderr=dev_null):
    201         logging.warning('Perf data to JSON conversion failed. The result will '
    202                         'not contain any perf samples. You can still view the '
    203                         'perf data manually as shown above.')
    204         return None
    205 
    206     return json_file_name
    207 
    208   def SupportsExplicitClockSync(self):
    209     return False
    210 
    211   def RecordClockSyncMarker(self, sync_id, did_record_sync_marker_callback):
    212     # pylint: disable=unused-argument
    213     assert self.SupportsExplicitClockSync(), ('Clock sync marker cannot be '
    214         'recorded since explicit clock sync is not supported.')
    215 
    216 def _OptionalValueCallback(default_value):
    217   def callback(option, _, __, parser):  # pylint: disable=unused-argument
    218     value = default_value
    219     if parser.rargs and not parser.rargs[0].startswith('-'):
    220       value = parser.rargs.pop(0)
    221     setattr(parser.values, option.dest, value)
    222   return callback
    223 
    224 
    225 class PerfConfig(tracing_agents.TracingConfig):
    226   def __init__(self, perf_categories, device):
    227     tracing_agents.TracingConfig.__init__(self)
    228     self.perf_categories = perf_categories
    229     self.device = device
    230 
    231 
    232 def try_create_agent(config):
    233   if config.perf_categories:
    234     return PerfProfilerAgent(config.device)
    235   return None
    236 
    237 def add_options(parser):
    238   options = optparse.OptionGroup(parser, 'Perf profiling options')
    239   options.add_option('-p', '--perf', help='Capture a perf profile with '
    240                      'the chosen comma-delimited event categories. '
    241                      'Samples CPU cycles by default. Use "list" to see '
    242                      'the available sample types.', action='callback',
    243                      default='', callback=_OptionalValueCallback('cycles'),
    244                      metavar='PERF_CATEGORIES', dest='perf_categories')
    245   return options
    246 
    247 def get_config(options):
    248   return PerfConfig(options.perf_categories, options.device)
    249 
    250 def _ComputePerfCategories(config):
    251   if not PerfProfilerAgent.IsSupported():
    252     return []
    253   if not config.perf_categories:
    254     return []
    255   return config.perf_categories.split(',')
    256