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