1# Copyright 2013 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 os 7import re 8import signal 9import subprocess 10import sys 11import tempfile 12 13from devil.android import device_errors # pylint: disable=import-error 14 15from telemetry.internal.util import binary_manager 16from telemetry.core import platform 17from telemetry.internal.platform import profiler 18from telemetry.internal.platform.profiler import android_profiling_helper 19 20from devil.android.perf import perf_control # pylint: disable=import-error 21 22 23_PERF_OPTIONS = [ 24 # Sample across all processes and CPUs to so that the current CPU gets 25 # recorded to each sample. 26 '--all-cpus', 27 # In perf 3.13 --call-graph requires an argument, so use the -g short-hand 28 # which does not. 29 '-g', 30 # Record raw samples to get CPU information. 31 '--raw-samples', 32 # Increase sampling frequency for better coverage. 33 '--freq', '2000', 34] 35 36_PERF_OPTIONS_ANDROID = [ 37 # Increase priority to avoid dropping samples. Requires root. 38 '--realtime', '80', 39] 40 41 42def _NicePath(path): 43 rel_path = os.path.relpath(path, os.curdir) 44 return rel_path if len(rel_path) < len(path) else path 45 46 47def _PrepareHostForPerf(): 48 kptr_file = '/proc/sys/kernel/kptr_restrict' 49 with open(kptr_file) as f: 50 if f.read().strip() != '0': 51 logging.warning('Making kernel symbols unrestricted. You might have to ' 52 'enter your password for "sudo".') 53 with tempfile.NamedTemporaryFile() as zero: 54 zero.write('0') 55 zero.flush() 56 subprocess.call(['/usr/bin/sudo', 'cp', zero.name, kptr_file]) 57 58 59def _InstallPerfHost(): 60 perfhost_name = android_profiling_helper.GetPerfhostName() 61 host = platform.GetHostPlatform() 62 if not host.CanLaunchApplication(perfhost_name): 63 host.InstallApplication(perfhost_name) 64 return binary_manager.FetchPath(perfhost_name, 'x86_64', 'linux') 65 66 67class _SingleProcessPerfProfiler(object): 68 """An internal class for using perf for a given process. 69 70 On android, this profiler uses pre-built binaries from AOSP. 71 See more details in prebuilt/android/README.txt. 72 """ 73 def __init__(self, pid, output_file, browser_backend, platform_backend, 74 perf_binary, perfhost_binary): 75 self._pid = pid 76 self._browser_backend = browser_backend 77 self._platform_backend = platform_backend 78 self._output_file = output_file 79 self._tmp_output_file = tempfile.NamedTemporaryFile('w', 0) 80 self._is_android = platform_backend.GetOSName() == 'android' 81 self._perf_binary = perf_binary 82 self._perfhost_binary = perfhost_binary 83 cmd_prefix = [] 84 perf_args = ['record', '--pid', str(pid)] 85 if self._is_android: 86 cmd_prefix = ['adb', '-s', browser_backend.device.adb.GetDeviceSerial(), 87 'shell', perf_binary] 88 perf_args += _PERF_OPTIONS_ANDROID 89 output_file = os.path.join('/sdcard', 'perf_profiles', 90 os.path.basename(output_file)) 91 self._device_output_file = output_file 92 browser_backend.device.RunShellCommand( 93 'mkdir -p ' + os.path.dirname(self._device_output_file)) 94 browser_backend.device.RunShellCommand( 95 'rm -f ' + self._device_output_file) 96 else: 97 cmd_prefix = [perf_binary] 98 perf_args += ['--output', output_file] + _PERF_OPTIONS 99 self._proc = subprocess.Popen(cmd_prefix + perf_args, 100 stdout=self._tmp_output_file, stderr=subprocess.STDOUT) 101 102 def CollectProfile(self): 103 if ('renderer' in self._output_file and 104 not self._is_android and 105 not self._platform_backend.GetCommandLine(self._pid)): 106 logging.warning('Renderer was swapped out during profiling. ' 107 'To collect a full profile rerun with ' 108 '"--extra-browser-args=--single-process"') 109 if self._is_android: 110 device = self._browser_backend.device 111 try: 112 binary_name = os.path.basename(self._perf_binary) 113 device.KillAll(binary_name, signum=signal.SIGINT, blocking=True, 114 quiet=True) 115 except device_errors.CommandFailedError: 116 logging.warning('The perf process could not be killed on the device.') 117 self._proc.send_signal(signal.SIGINT) 118 exit_code = self._proc.wait() 119 try: 120 if exit_code == 128: 121 raise Exception( 122 """perf failed with exit code 128. 123Try rerunning this script under sudo or setting 124/proc/sys/kernel/perf_event_paranoid to "-1".\nOutput:\n%s""" % 125 self._GetStdOut()) 126 elif exit_code not in (0, -2): 127 raise Exception( 128 'perf failed with exit code %d. Output:\n%s' % (exit_code, 129 self._GetStdOut())) 130 finally: 131 self._tmp_output_file.close() 132 cmd = '%s report -n -i %s' % (_NicePath(self._perfhost_binary), 133 self._output_file) 134 if self._is_android: 135 device = self._browser_backend.device 136 try: 137 device.PullFile(self._device_output_file, self._output_file) 138 except: 139 logging.exception('New exception caused by DeviceUtils conversion') 140 raise 141 required_libs = \ 142 android_profiling_helper.GetRequiredLibrariesForPerfProfile( 143 self._output_file) 144 symfs_root = os.path.dirname(self._output_file) 145 kallsyms = android_profiling_helper.CreateSymFs(device, 146 symfs_root, 147 required_libs, 148 use_symlinks=True) 149 cmd += ' --symfs %s --kallsyms %s' % (symfs_root, kallsyms) 150 for lib in required_libs: 151 lib = os.path.join(symfs_root, lib[1:]) 152 if not os.path.exists(lib): 153 continue 154 objdump_path = android_profiling_helper.GetToolchainBinaryPath( 155 lib, 'objdump') 156 if objdump_path: 157 cmd += ' --objdump %s' % _NicePath(objdump_path) 158 break 159 160 print 'To view the profile, run:' 161 print ' ', cmd 162 return self._output_file 163 164 def _GetStdOut(self): 165 self._tmp_output_file.flush() 166 try: 167 with open(self._tmp_output_file.name) as f: 168 return f.read() 169 except IOError: 170 return '' 171 172 173class PerfProfiler(profiler.Profiler): 174 175 def __init__(self, browser_backend, platform_backend, output_path, state): 176 super(PerfProfiler, self).__init__( 177 browser_backend, platform_backend, output_path, state) 178 process_output_file_map = self._GetProcessOutputFileMap() 179 self._process_profilers = [] 180 self._perf_control = None 181 182 perf_binary = perfhost_binary = _InstallPerfHost() 183 try: 184 if platform_backend.GetOSName() == 'android': 185 device = browser_backend.device 186 perf_binary = android_profiling_helper.PrepareDeviceForPerf(device) 187 self._perf_control = perf_control.PerfControl(device) 188 self._perf_control.SetPerfProfilingMode() 189 else: 190 _PrepareHostForPerf() 191 192 for pid, output_file in process_output_file_map.iteritems(): 193 if 'zygote' in output_file: 194 continue 195 self._process_profilers.append( 196 _SingleProcessPerfProfiler( 197 pid, output_file, browser_backend, platform_backend, 198 perf_binary, perfhost_binary)) 199 except: 200 if self._perf_control: 201 self._perf_control.SetDefaultPerfMode() 202 raise 203 204 @classmethod 205 def name(cls): 206 return 'perf' 207 208 @classmethod 209 def is_supported(cls, browser_type): 210 if sys.platform != 'linux2': 211 return False 212 if platform.GetHostPlatform().GetOSName() == 'chromeos': 213 return False 214 return True 215 216 @classmethod 217 def CustomizeBrowserOptions(cls, browser_type, options): 218 options.AppendExtraBrowserArgs([ 219 '--no-sandbox', 220 '--allow-sandbox-debugging', 221 ]) 222 223 def CollectProfile(self): 224 if self._perf_control: 225 self._perf_control.SetDefaultPerfMode() 226 output_files = [] 227 for single_process in self._process_profilers: 228 output_files.append(single_process.CollectProfile()) 229 return output_files 230 231 @classmethod 232 def GetTopSamples(cls, file_name, number): 233 """Parses the perf generated profile in |file_name| and returns a 234 {function: period} dict of the |number| hottests functions. 235 """ 236 assert os.path.exists(file_name) 237 with open(os.devnull, 'w') as devnull: 238 _InstallPerfHost() 239 report = subprocess.Popen( 240 [android_profiling_helper.GetPerfhostName(), 241 'report', '--show-total-period', '-U', '-t', '^', '-i', file_name], 242 stdout=subprocess.PIPE, stderr=devnull).communicate()[0] 243 period_by_function = {} 244 for line in report.split('\n'): 245 if not line or line.startswith('#'): 246 continue 247 fields = line.split('^') 248 if len(fields) != 5: 249 continue 250 period = int(fields[1]) 251 function = fields[4].partition(' ')[2] 252 function = re.sub('<.*>', '', function) # Strip template params. 253 function = re.sub('[(].*[)]', '', function) # Strip function params. 254 period_by_function[function] = period 255 if len(period_by_function) == number: 256 break 257 return period_by_function 258