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