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