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.
13 from telemetry.core import platform
14 from telemetry.core import util
15 from telemetry.core.platform import profiler
16 from telemetry.core.platform.profiler import android_profiling_helper
17 from telemetry.util import support_binaries
19 util.AddDirToPythonPath(util.GetChromiumSrcDir(), 'build', 'android')
20 from pylib.perf import perf_control # pylint: disable=F0401
24 # In perf 3.13 --call-graph requires an argument, so use the -g short-hand
27 # Increase sampling frequency for better coverage.
31 _PERF_OPTIONS_ANDROID = [
32 # Increase priority to avoid dropping samples. Requires root.
38 rel_path = os.path.relpath(path, os.curdir)
39 return rel_path if len(rel_path) < len(path) else path
42 def _PrepareHostForPerf():
43 kptr_file = '/proc/sys/kernel/kptr_restrict'
44 with open(kptr_file) as f:
45 if f.read().strip() != '0':
46 logging.warning('Making kernel symbols unrestricted. You might have to '
47 'enter your password for "sudo".')
48 with tempfile.NamedTemporaryFile() as zero:
51 subprocess.call(['sudo', 'cp', zero.name, kptr_file])
54 def _InstallPerfHost():
55 host = platform.GetHostPlatform()
56 if not host.CanLaunchApplication('perfhost'):
57 host.InstallApplication('perfhost')
58 return support_binaries.FindPath('perfhost', host.GetOSName())
61 class _SingleProcessPerfProfiler(object):
62 """An internal class for using perf for a given process.
64 On android, this profiler uses pre-built binaries from AOSP.
65 See more details in prebuilt/android/README.txt.
67 def __init__(self, pid, output_file, browser_backend, platform_backend,
68 perf_binary, perfhost_binary):
70 self._browser_backend = browser_backend
71 self._platform_backend = platform_backend
72 self._output_file = output_file
73 self._tmp_output_file = tempfile.NamedTemporaryFile('w', 0)
74 self._is_android = platform_backend.GetOSName() == 'android'
75 self._perfhost_binary = perfhost_binary
77 perf_args = ['record', '--pid', str(pid)]
79 cmd_prefix = ['adb', '-s', browser_backend.adb.device_serial(), 'shell',
81 perf_args += _PERF_OPTIONS_ANDROID
82 output_file = os.path.join('/sdcard', 'perf_profiles',
83 os.path.basename(output_file))
84 self._device_output_file = output_file
85 browser_backend.adb.RunShellCommand(
86 'mkdir -p ' + os.path.dirname(self._device_output_file))
87 browser_backend.adb.RunShellCommand('rm -f ' + self._device_output_file)
89 cmd_prefix = [perf_binary]
90 perf_args += ['--output', output_file] + _PERF_OPTIONS
91 self._proc = subprocess.Popen(cmd_prefix + perf_args,
92 stdout=self._tmp_output_file, stderr=subprocess.STDOUT)
94 def CollectProfile(self):
95 if ('renderer' in self._output_file and
96 not self._is_android and
97 not self._platform_backend.GetCommandLine(self._pid)):
98 logging.warning('Renderer was swapped out during profiling. '
99 'To collect a full profile rerun with '
100 '"--extra-browser-args=--single-process"')
102 device = self._browser_backend.adb.device()
103 device.KillAll('perf', signum=signal.SIGINT, blocking=True)
104 self._proc.send_signal(signal.SIGINT)
105 exit_code = self._proc.wait()
109 """perf failed with exit code 128.
110 Try rerunning this script under sudo or setting
111 /proc/sys/kernel/perf_event_paranoid to "-1".\nOutput:\n%s""" %
113 elif exit_code not in (0, -2):
115 'perf failed with exit code %d. Output:\n%s' % (exit_code,
118 self._tmp_output_file.close()
119 cmd = '%s report -n -i %s' % (_NicePath(self._perfhost_binary),
122 device = self._browser_backend.adb.device()
123 device.old_interface.Adb().Pull(self._device_output_file,
126 android_profiling_helper.GetRequiredLibrariesForPerfProfile(
128 symfs_root = os.path.dirname(self._output_file)
129 kallsyms = android_profiling_helper.CreateSymFs(device,
133 cmd += ' --symfs %s --kallsyms %s' % (symfs_root, kallsyms)
134 for lib in required_libs:
135 lib = os.path.join(symfs_root, lib[1:])
136 if not os.path.exists(lib):
138 objdump_path = android_profiling_helper.GetToolchainBinaryPath(
141 cmd += ' --objdump %s' % _NicePath(objdump_path)
144 print 'To view the profile, run:'
146 return self._output_file
148 def _GetStdOut(self):
149 self._tmp_output_file.flush()
151 with open(self._tmp_output_file.name) as f:
157 class PerfProfiler(profiler.Profiler):
159 def __init__(self, browser_backend, platform_backend, output_path, state):
160 super(PerfProfiler, self).__init__(
161 browser_backend, platform_backend, output_path, state)
162 process_output_file_map = self._GetProcessOutputFileMap()
163 self._process_profilers = []
164 self._perf_control = None
166 perf_binary = perfhost_binary = _InstallPerfHost()
168 if platform_backend.GetOSName() == 'android':
169 device = browser_backend.adb.device()
170 perf_binary = android_profiling_helper.PrepareDeviceForPerf(device)
171 self._perf_control = perf_control.PerfControl(device)
172 self._perf_control.SetPerfProfilingMode()
174 _PrepareHostForPerf()
176 for pid, output_file in process_output_file_map.iteritems():
177 if 'zygote' in output_file:
179 self._process_profilers.append(
180 _SingleProcessPerfProfiler(
181 pid, output_file, browser_backend, platform_backend,
182 perf_binary, perfhost_binary))
184 if self._perf_control:
185 self._perf_control.SetDefaultPerfMode()
193 def is_supported(cls, browser_type):
194 if sys.platform != 'linux2':
196 if browser_type.startswith('cros'):
201 def CustomizeBrowserOptions(cls, browser_type, options):
202 options.AppendExtraBrowserArgs([
204 '--allow-sandbox-debugging',
207 def CollectProfile(self):
208 if self._perf_control:
209 self._perf_control.SetDefaultPerfMode()
211 for single_process in self._process_profilers:
212 output_files.append(single_process.CollectProfile())
216 def GetTopSamples(cls, file_name, number):
217 """Parses the perf generated profile in |file_name| and returns a
218 {function: period} dict of the |number| hottests functions.
220 assert os.path.exists(file_name)
221 with open(os.devnull, 'w') as devnull:
223 report = subprocess.Popen(
224 ['perfhost', 'report', '--show-total-period', '-U', '-t', '^', '-i',
226 stdout=subprocess.PIPE, stderr=devnull).communicate()[0]
227 period_by_function = {}
228 for line in report.split('\n'):
229 if not line or line.startswith('#'):
231 fields = line.split('^')
234 period = int(fields[1])
235 function = fields[4].partition(' ')[2]
236 function = re.sub('<.*>', '', function) # Strip template params.
237 function = re.sub('[(].*[)]', '', function) # Strip function params.
238 period_by_function[function] = period
239 if len(period_by_function) == number:
241 return period_by_function