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