Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / tools / telemetry / telemetry / core / platform / profiler / perf_profiler.py
1 # Copyright (c) 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 util
14 from telemetry.core.platform import profiler
15 from telemetry.core.platform.profiler import android_prebuilt_profiler_helper
16
17
18 class _SingleProcessPerfProfiler(object):
19   """An internal class for using perf for a given process.
20
21   On android, this profiler uses pre-built binaries from AOSP.
22   See more details in prebuilt/android/README.txt.
23   """
24   def __init__(self, pid, output_file, browser_backend, platform_backend):
25     self._pid = pid
26     self._browser_backend = browser_backend
27     self._platform_backend = platform_backend
28     self._output_file = output_file
29     self._tmp_output_file = tempfile.NamedTemporaryFile('w', 0)
30     self._is_android = platform_backend.GetOSName() == 'android'
31     cmd_prefix = []
32     if self._is_android:
33       perf_binary = android_prebuilt_profiler_helper.GetDevicePath(
34           'perf')
35       cmd_prefix = ['adb', '-s', browser_backend.adb.device_serial(), 'shell',
36                     perf_binary]
37       output_file = os.path.join('/sdcard', 'perf_profiles',
38                                  os.path.basename(output_file))
39       self._device_output_file = output_file
40       browser_backend.adb.RunShellCommand(
41           'mkdir -p ' + os.path.dirname(self._device_output_file))
42     else:
43       cmd_prefix = ['perf']
44     # In perf 3.13 --call-graph requires an argument, so use
45     # the -g short-hand which does not.
46     self._proc = subprocess.Popen(cmd_prefix +
47         ['record', '-g',
48          '--pid', str(pid), '--output', output_file],
49         stdout=self._tmp_output_file, stderr=subprocess.STDOUT)
50
51   def CollectProfile(self):
52     if ('renderer' in self._output_file and
53         not self._is_android and
54         not self._platform_backend.GetCommandLine(self._pid)):
55       logging.warning('Renderer was swapped out during profiling. '
56                       'To collect a full profile rerun with '
57                       '"--extra-browser-args=--single-process"')
58     if self._is_android:
59       device = self._browser_backend.adb.device()
60       perf_pids = device.old_interface.ExtractPid('perf')
61       device.old_interface.RunShellCommand(
62           'kill -SIGINT ' + ' '.join(perf_pids))
63       util.WaitFor(lambda: not device.old_interface.ExtractPid('perf'),
64                    timeout=2)
65     self._proc.send_signal(signal.SIGINT)
66     exit_code = self._proc.wait()
67     try:
68       if exit_code == 128:
69         raise Exception(
70             """perf failed with exit code 128.
71 Try rerunning this script under sudo or setting
72 /proc/sys/kernel/perf_event_paranoid to "-1".\nOutput:\n%s""" %
73             self._GetStdOut())
74       elif exit_code not in (0, -2):
75         raise Exception(
76             'perf failed with exit code %d. Output:\n%s' % (exit_code,
77                                                             self._GetStdOut()))
78     finally:
79       self._tmp_output_file.close()
80     cmd = 'perf report -n -i %s' % self._output_file
81     if self._is_android:
82       print 'On Android, assuming $CHROMIUM_OUT_DIR/Release/lib has a fresh'
83       print 'symbolized library matching the one on device.'
84       objdump_path = os.path.join(os.environ.get('ANDROID_TOOLCHAIN',
85                                                  '$ANDROID_TOOLCHAIN'),
86                                   'arm-linux-androideabi-objdump')
87       print 'If you have recent version of perf (3.10+), append the following '
88       print 'to see annotated source code (by pressing the \'a\' key): '
89       print '  --objdump %s' % objdump_path
90       cmd += ' ' + ' '.join(self._PrepareAndroidSymfs())
91     print 'To view the profile, run:'
92     print ' ', cmd
93     return self._output_file
94
95   def _PrepareAndroidSymfs(self):
96     """Create a symfs directory using an Android device.
97
98     Create a symfs directory by pulling the necessary files from an Android
99     device.
100
101     Returns:
102       List of arguments to be passed to perf to point it to the created symfs.
103     """
104     assert self._is_android
105     device = self._browser_backend.adb.device()
106     device.old_interface.Adb().Pull(self._device_output_file, self._output_file)
107     symfs_dir = os.path.dirname(self._output_file)
108     host_app_symfs = os.path.join(symfs_dir, 'data', 'app-lib')
109     if not os.path.exists(host_app_symfs):
110       os.makedirs(host_app_symfs)
111       # On Android, the --symfs parameter needs to map a directory structure
112       # similar to the device, that is:
113       # --symfs=/tmp/foobar and then inside foobar there'll be something like
114       # /tmp/foobar/data/app-lib/$PACKAGE/libname.so
115       # Assume the symbolized library under out/Release/lib is equivalent to
116       # the one in the device, and symlink it in the host to match --symfs.
117       device_dir = filter(
118           lambda app_lib: app_lib.startswith(self._browser_backend.package),
119           device.old_interface.RunShellCommand('ls /data/app-lib'))
120       os.symlink(os.path.abspath(
121                     os.path.join(util.GetChromiumSrcDir(),
122                                  os.environ.get('CHROMIUM_OUT_DIR', 'out'),
123                                  'Release', 'lib')),
124                  os.path.join(host_app_symfs, device_dir[0]))
125
126     # Also pull copies of common system libraries from the device so perf can
127     # resolve their symbols. Only copy a subset of libraries to make this
128     # faster.
129     # TODO(skyostil): Find a way to pull in all system libraries without being
130     # too slow.
131     host_system_symfs = os.path.join(symfs_dir, 'system', 'lib')
132     if not os.path.exists(host_system_symfs):
133       os.makedirs(host_system_symfs)
134       common_system_libs = [
135         'libandroid*.so',
136         'libart.so',
137         'libc.so',
138         'libdvm.so',
139         'libEGL*.so',
140         'libGL*.so',
141         'libm.so',
142         'libRS.so',
143         'libskia.so',
144         'libstdc++.so',
145         'libstlport.so',
146         'libz.so',
147       ]
148       for lib in common_system_libs:
149         device.old_interface.Adb().Pull('/system/lib/%s' % lib,
150                                         host_system_symfs)
151     # Pull a copy of the kernel symbols.
152     host_kallsyms = os.path.join(symfs_dir, 'kallsyms')
153     if not os.path.exists(host_kallsyms):
154       device.old_interface.Adb().Pull('/proc/kallsyms', host_kallsyms)
155     return ['--kallsyms', host_kallsyms, '--symfs', symfs_dir]
156
157   def _GetStdOut(self):
158     self._tmp_output_file.flush()
159     try:
160       with open(self._tmp_output_file.name) as f:
161         return f.read()
162     except IOError:
163       return ''
164
165
166 class PerfProfiler(profiler.Profiler):
167
168   def __init__(self, browser_backend, platform_backend, output_path, state):
169     super(PerfProfiler, self).__init__(
170         browser_backend, platform_backend, output_path, state)
171     process_output_file_map = self._GetProcessOutputFileMap()
172     self._process_profilers = []
173     if platform_backend.GetOSName() == 'android':
174       android_prebuilt_profiler_helper.InstallOnDevice(
175           browser_backend.adb.device(), 'perf')
176       # Make sure kernel pointers are not hidden.
177       browser_backend.adb.device().old_interface.SetProtectedFileContents(
178           '/proc/sys/kernel/kptr_restrict', '0')
179     for pid, output_file in process_output_file_map.iteritems():
180       if 'zygote' in output_file:
181         continue
182       self._process_profilers.append(
183           _SingleProcessPerfProfiler(
184               pid, output_file, browser_backend, platform_backend))
185
186   @classmethod
187   def name(cls):
188     return 'perf'
189
190   @classmethod
191   def is_supported(cls, browser_type):
192     if sys.platform != 'linux2':
193       return False
194     if browser_type.startswith('cros'):
195       return False
196     return cls._CheckLinuxPerf() or browser_type.startswith('android')
197
198   @classmethod
199   def _CheckLinuxPerf(cls):
200     try:
201       with open(os.devnull, 'w') as devnull:
202         return not subprocess.Popen(['perf', '--version'],
203                                     stderr=devnull,
204                                     stdout=devnull).wait()
205     except OSError:
206       return False
207
208   @classmethod
209   def CustomizeBrowserOptions(cls, browser_type, options):
210     options.AppendExtraBrowserArgs([
211         '--no-sandbox',
212         '--allow-sandbox-debugging',
213     ])
214
215   def CollectProfile(self):
216     output_files = []
217     for single_process in self._process_profilers:
218       output_files.append(single_process.CollectProfile())
219     return output_files
220
221   @classmethod
222   def GetTopSamples(cls, file_name, number):
223     """Parses the perf generated profile in |file_name| and returns a
224     {function: period} dict of the |number| hottests functions.
225     """
226     assert os.path.exists(file_name)
227     with open(os.devnull, 'w') as devnull:
228       report = subprocess.Popen(
229           ['perf', 'report', '--show-total-period', '-U', '-t', '^', '-i',
230            file_name],
231           stdout=subprocess.PIPE, stderr=devnull).communicate()[0]
232     period_by_function = {}
233     for line in report.split('\n'):
234       if not line or line.startswith('#'):
235         continue
236       fields = line.split('^')
237       if len(fields) != 5:
238         continue
239       period = int(fields[1])
240       function = fields[4].partition(' ')[2]
241       function = re.sub('<.*>', '', function)  # Strip template params.
242       function = re.sub('[(].*[)]', '', function)  # Strip function params.
243       period_by_function[function] = period
244       if len(period_by_function) == number:
245         break
246     return period_by_function