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