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