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