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 os
      7 import signal
      8 import subprocess
      9 import sys
     10 import tempfile
     11 
     12 from profile_chrome import controllers
     13 from profile_chrome import ui
     14 
     15 from pylib import android_commands
     16 from pylib import constants
     17 from pylib.perf import perf_control
     18 
     19 sys.path.append(os.path.join(constants.DIR_SOURCE_ROOT,
     20                              'tools',
     21                              'telemetry'))
     22 try:
     23   # pylint: disable=F0401
     24   from telemetry.core.platform.profiler import android_profiling_helper
     25   from telemetry.util import support_binaries
     26 except ImportError:
     27   android_profiling_helper = None
     28   support_binaries = None
     29 
     30 
     31 _PERF_OPTIONS = [
     32     # Sample across all processes and CPUs to so that the current CPU gets
     33     # recorded to each sample.
     34     '--all-cpus',
     35     # In perf 3.13 --call-graph requires an argument, so use the -g short-hand
     36     # which does not.
     37     '-g',
     38     # Increase priority to avoid dropping samples. Requires root.
     39     '--realtime', '80',
     40     # Record raw samples to get CPU information.
     41     '--raw-samples',
     42     # Increase sampling frequency for better coverage.
     43     '--freq', '2000',
     44 ]
     45 
     46 
     47 class _PerfProfiler(object):
     48   def __init__(self, device, perf_binary, categories):
     49     self._device = device
     50     self._output_file = android_commands.DeviceTempFile(
     51         self._device.old_interface, prefix='perf_output')
     52     self._log_file = tempfile.TemporaryFile()
     53 
     54     # TODO(jbudorick) Look at providing a way to unhandroll this once the
     55     #                 adb rewrite has fully landed.
     56     device_param = (['-s', str(self._device)] if str(self._device) else [])
     57     cmd = ['adb'] + device_param + \
     58           ['shell', perf_binary, 'record',
     59            '--output', self._output_file.name] + _PERF_OPTIONS
     60     if categories:
     61       cmd += ['--event', ','.join(categories)]
     62     self._perf_control = perf_control.PerfControl(self._device)
     63     self._perf_control.SetPerfProfilingMode()
     64     self._perf_process = subprocess.Popen(cmd,
     65                                           stdout=self._log_file,
     66                                           stderr=subprocess.STDOUT)
     67 
     68   def SignalAndWait(self):
     69     self._device.KillAll('perf', signum=signal.SIGINT)
     70     self._perf_process.wait()
     71     self._perf_control.SetDefaultPerfMode()
     72 
     73   def _FailWithLog(self, msg):
     74     self._log_file.seek(0)
     75     log = self._log_file.read()
     76     raise RuntimeError('%s. Log output:\n%s' % (msg, log))
     77 
     78   def PullResult(self, output_path):
     79     if not self._device.FileExists(self._output_file.name):
     80       self._FailWithLog('Perf recorded no data')
     81 
     82     perf_profile = os.path.join(output_path,
     83                                 os.path.basename(self._output_file.name))
     84     self._device.PullFile(self._output_file.name, perf_profile)
     85     if not os.stat(perf_profile).st_size:
     86       os.remove(perf_profile)
     87       self._FailWithLog('Perf recorded a zero-sized file')
     88 
     89     self._log_file.close()
     90     self._output_file.close()
     91     return perf_profile
     92 
     93 
     94 class PerfProfilerController(controllers.BaseController):
     95   def __init__(self, device, categories):
     96     controllers.BaseController.__init__(self)
     97     self._device = device
     98     self._categories = categories
     99     self._perf_binary = self._PrepareDevice(device)
    100     self._perf_instance = None
    101 
    102   def __repr__(self):
    103     return 'perf profile'
    104 
    105   @staticmethod
    106   def IsSupported():
    107     return bool(android_profiling_helper)
    108 
    109   @staticmethod
    110   def _PrepareDevice(device):
    111     if not 'BUILDTYPE' in os.environ:
    112       os.environ['BUILDTYPE'] = 'Release'
    113     return android_profiling_helper.PrepareDeviceForPerf(device)
    114 
    115   @classmethod
    116   def GetCategories(cls, device):
    117     perf_binary = cls._PrepareDevice(device)
    118     return device.RunShellCommand('%s list' % perf_binary)
    119 
    120   def StartTracing(self, _):
    121     self._perf_instance = _PerfProfiler(self._device,
    122                                         self._perf_binary,
    123                                         self._categories)
    124 
    125   def StopTracing(self):
    126     if not self._perf_instance:
    127       return
    128     self._perf_instance.SignalAndWait()
    129 
    130   @staticmethod
    131   def _GetInteractivePerfCommand(perfhost_path, perf_profile, symfs_dir,
    132                                  required_libs, kallsyms):
    133     cmd = '%s report -n -i %s --symfs %s --kallsyms %s' % (
    134         os.path.relpath(perfhost_path, '.'), perf_profile, symfs_dir, kallsyms)
    135     for lib in required_libs:
    136       lib = os.path.join(symfs_dir, lib[1:])
    137       if not os.path.exists(lib):
    138         continue
    139       objdump_path = android_profiling_helper.GetToolchainBinaryPath(
    140           lib, 'objdump')
    141       if objdump_path:
    142         cmd += ' --objdump %s' % os.path.relpath(objdump_path, '.')
    143         break
    144     return cmd
    145 
    146   def PullTrace(self):
    147     symfs_dir = os.path.join(tempfile.gettempdir(),
    148                              os.path.expandvars('$USER-perf-symfs'))
    149     if not os.path.exists(symfs_dir):
    150       os.makedirs(symfs_dir)
    151     required_libs = set()
    152 
    153     # Download the recorded perf profile.
    154     perf_profile = self._perf_instance.PullResult(symfs_dir)
    155     required_libs = \
    156         android_profiling_helper.GetRequiredLibrariesForPerfProfile(
    157             perf_profile)
    158     if not required_libs:
    159       logging.warning('No libraries required by perf trace. Most likely there '
    160                       'are no samples in the trace.')
    161 
    162     # Build a symfs with all the necessary libraries.
    163     kallsyms = android_profiling_helper.CreateSymFs(self._device,
    164                                                     symfs_dir,
    165                                                     required_libs,
    166                                                     use_symlinks=False)
    167     perfhost_path = support_binaries.FindPath(
    168         android_profiling_helper.GetPerfhostName(), 'linux')
    169 
    170     ui.PrintMessage('\nNote: to view the profile in perf, run:')
    171     ui.PrintMessage('  ' + self._GetInteractivePerfCommand(perfhost_path,
    172         perf_profile, symfs_dir, required_libs, kallsyms))
    173 
    174     # Convert the perf profile into JSON.
    175     perf_script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
    176                                     'third_party', 'perf_to_tracing.py')
    177     json_file_name = os.path.basename(perf_profile)
    178     with open(os.devnull, 'w') as dev_null, \
    179         open(json_file_name, 'w') as json_file:
    180       cmd = [perfhost_path, 'script', '-s', perf_script_path, '-i',
    181              perf_profile, '--symfs', symfs_dir, '--kallsyms', kallsyms]
    182       if subprocess.call(cmd, stdout=json_file, stderr=dev_null):
    183         logging.warning('Perf data to JSON conversion failed. The result will '
    184                         'not contain any perf samples. You can still view the '
    185                         'perf data manually as shown above.')
    186         return None
    187 
    188     return json_file_name
    189