Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / tools / telemetry / telemetry / core / platform / profiler / perf_profiler.py
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
5 import logging
6 import os
7 import re
8 import signal
9 import subprocess
10 import sys
11 import tempfile
12
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
18
19 util.AddDirToPythonPath(util.GetChromiumSrcDir(), 'build', 'android')
20 from pylib.perf import perf_control  # pylint: disable=F0401
21
22
23 _PERF_OPTIONS = [
24     # In perf 3.13 --call-graph requires an argument, so use the -g short-hand
25     # which does not.
26     '-g',
27     # Increase sampling frequency for better coverage.
28     '--freq', '2000',
29 ]
30
31 _PERF_OPTIONS_ANDROID = [
32     # Increase priority to avoid dropping samples. Requires root.
33     '--realtime', '80',
34 ]
35
36
37 def _NicePath(path):
38   rel_path = os.path.relpath(path, os.curdir)
39   return rel_path if len(rel_path) < len(path) else path
40
41
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:
49         zero.write('0')
50         zero.flush()
51         subprocess.call(['sudo', 'cp', zero.name, kptr_file])
52
53
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())
59
60
61 class _SingleProcessPerfProfiler(object):
62   """An internal class for using perf for a given process.
63
64   On android, this profiler uses pre-built binaries from AOSP.
65   See more details in prebuilt/android/README.txt.
66   """
67   def __init__(self, pid, output_file, browser_backend, platform_backend,
68                perf_binary, perfhost_binary):
69     self._pid = pid
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
76     cmd_prefix = []
77     perf_args = ['record', '--pid', str(pid)]
78     if self._is_android:
79       cmd_prefix = ['adb', '-s', browser_backend.adb.device_serial(), 'shell',
80                     perf_binary]
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)
88     else:
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)
93
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"')
101     if self._is_android:
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()
106     try:
107       if exit_code == 128:
108         raise Exception(
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""" %
112             self._GetStdOut())
113       elif exit_code not in (0, -2):
114         raise Exception(
115             'perf failed with exit code %d. Output:\n%s' % (exit_code,
116                                                             self._GetStdOut()))
117     finally:
118       self._tmp_output_file.close()
119     cmd = '%s report -n -i %s' % (_NicePath(self._perfhost_binary),
120                                   self._output_file)
121     if self._is_android:
122       device = self._browser_backend.adb.device()
123       device.old_interface.Adb().Pull(self._device_output_file,
124                                       self._output_file)
125       required_libs = \
126           android_profiling_helper.GetRequiredLibrariesForPerfProfile(
127               self._output_file)
128       symfs_root = os.path.dirname(self._output_file)
129       kallsyms = android_profiling_helper.CreateSymFs(device,
130                                                       symfs_root,
131                                                       required_libs,
132                                                       use_symlinks=True)
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):
137           continue
138         objdump_path = android_profiling_helper.GetToolchainBinaryPath(
139             lib, 'objdump')
140         if objdump_path:
141           cmd += ' --objdump %s' % _NicePath(objdump_path)
142           break
143
144     print 'To view the profile, run:'
145     print ' ', cmd
146     return self._output_file
147
148   def _GetStdOut(self):
149     self._tmp_output_file.flush()
150     try:
151       with open(self._tmp_output_file.name) as f:
152         return f.read()
153     except IOError:
154       return ''
155
156
157 class PerfProfiler(profiler.Profiler):
158
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
165
166     perf_binary = perfhost_binary = _InstallPerfHost()
167     try:
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()
173       else:
174         _PrepareHostForPerf()
175
176       for pid, output_file in process_output_file_map.iteritems():
177         if 'zygote' in output_file:
178           continue
179         self._process_profilers.append(
180             _SingleProcessPerfProfiler(
181                 pid, output_file, browser_backend, platform_backend,
182                 perf_binary, perfhost_binary))
183     except:
184       if self._perf_control:
185         self._perf_control.SetDefaultPerfMode()
186       raise
187
188   @classmethod
189   def name(cls):
190     return 'perf'
191
192   @classmethod
193   def is_supported(cls, browser_type):
194     if sys.platform != 'linux2':
195       return False
196     if browser_type.startswith('cros'):
197       return False
198     return True
199
200   @classmethod
201   def CustomizeBrowserOptions(cls, browser_type, options):
202     options.AppendExtraBrowserArgs([
203         '--no-sandbox',
204         '--allow-sandbox-debugging',
205     ])
206
207   def CollectProfile(self):
208     if self._perf_control:
209       self._perf_control.SetDefaultPerfMode()
210     output_files = []
211     for single_process in self._process_profilers:
212       output_files.append(single_process.CollectProfile())
213     return output_files
214
215   @classmethod
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.
219     """
220     assert os.path.exists(file_name)
221     with open(os.devnull, 'w') as devnull:
222       _InstallPerfHost()
223       report = subprocess.Popen(
224           ['perfhost', 'report', '--show-total-period', '-U', '-t', '^', '-i',
225            file_name],
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('#'):
230         continue
231       fields = line.split('^')
232       if len(fields) != 5:
233         continue
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:
240         break
241     return period_by_function