- add sources.
[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 stat
10 import subprocess
11 import sys
12 import tempfile
13
14 from telemetry.core import util
15 from telemetry.core.platform import profiler
16 from telemetry.core.platform.profiler import android_prebuilt_profiler_helper
17
18
19 class _SingleProcessPerfProfiler(object):
20   """An internal class for using perf for a given process.
21
22   On android, this profiler uses pre-built binaries from AOSP.
23   See more details in prebuilt/android/README.txt.
24   """
25   def __init__(self, pid, output_file, browser_backend, platform_backend):
26     self._pid = pid
27     self._browser_backend = browser_backend
28     self._platform_backend = platform_backend
29     self._output_file = output_file
30     self._tmp_output_file = tempfile.NamedTemporaryFile('w', 0)
31     self._is_android = platform_backend.GetOSName() == 'android'
32     cmd_prefix = []
33     if self._is_android:
34       perf_binary = android_prebuilt_profiler_helper.GetDevicePath(
35           'perf')
36       cmd_prefix = ['adb', '-s', browser_backend.adb.device(), 'shell',
37                     perf_binary]
38       output_file = os.path.join('/sdcard', 'perf_profiles',
39                                  os.path.basename(output_file))
40       self._device_output_file = output_file
41       browser_backend.adb.RunShellCommand(
42           'mkdir -p ' + os.path.dirname(self._device_output_file))
43     else:
44       cmd_prefix = ['perf']
45     self._proc = subprocess.Popen(cmd_prefix +
46         ['record', '--call-graph',
47          '--pid', str(pid), '--output', output_file],
48         stdout=self._tmp_output_file, stderr=subprocess.STDOUT)
49
50   def CollectProfile(self):
51     if ('renderer' in self._output_file and
52         not self._is_android and
53         not self._platform_backend.GetCommandLine(self._pid)):
54       logging.warning('Renderer was swapped out during profiling. '
55                       'To collect a full profile rerun with '
56                       '"--extra-browser-args=--single-process"')
57     if self._is_android:
58       perf_pids = self._browser_backend.adb.Adb().ExtractPid('perf')
59       self._browser_backend.adb.Adb().RunShellCommand(
60           'kill -SIGINT ' + ' '.join(perf_pids))
61       util.WaitFor(
62           lambda: not self._browser_backend.adb.Adb().ExtractPid('perf'),
63           timeout=2)
64     self._proc.send_signal(signal.SIGINT)
65     exit_code = self._proc.wait()
66     try:
67       if exit_code == 128:
68         raise Exception(
69             """perf failed with exit code 128.
70 Try rerunning this script under sudo or setting
71 /proc/sys/kernel/perf_event_paranoid to "-1".\nOutput:\n%s""" %
72             self._GetStdOut())
73       elif exit_code not in (0, -2):
74         raise Exception(
75             'perf failed with exit code %d. Output:\n%s' % (exit_code,
76                                                             self._GetStdOut()))
77     finally:
78       self._tmp_output_file.close()
79     cmd = 'perf report'
80     if self._is_android:
81       self._browser_backend.adb.Adb().Adb().Pull(
82           self._device_output_file,
83           self._output_file)
84       host_symfs = os.path.join(os.path.dirname(self._output_file),
85                                 'data', 'app-lib')
86       if not os.path.exists(host_symfs):
87         os.makedirs(host_symfs)
88         # On Android, the --symfs parameter needs to map a directory structure
89         # similar to the device, that is:
90         # --symfs=/tmp/foobar and then inside foobar there'll be something like
91         # /tmp/foobar/data/app-lib/$PACKAGE/libname.so
92         # Assume the symbolized library under out/Release/lib is equivalent to
93         # the one in the device, and symlink it in the host to match --symfs.
94         device_dir = filter(
95             lambda app_lib: app_lib.startswith(self._browser_backend.package),
96             self._browser_backend.adb.Adb().RunShellCommand('ls /data/app-lib'))
97         os.symlink(os.path.abspath(
98                       os.path.join(util.GetChromiumSrcDir(),
99                                    os.environ.get('CHROMIUM_OUT_DIR', 'out'),
100                                    'Release', 'lib')),
101                    os.path.join(host_symfs, device_dir[0]))
102       print 'On Android, assuming $CHROMIUM_OUT_DIR/Release/lib has a fresh'
103       print 'symbolized library matching the one on device.'
104       cmd = (android_prebuilt_profiler_helper.GetHostPath('perfhost') +
105              ' report --symfs %s' % os.path.dirname(self._output_file))
106     print 'To view the profile, run:'
107     print '  %s -n -i %s' % (cmd, self._output_file)
108     return self._output_file
109
110   def _GetStdOut(self):
111     self._tmp_output_file.flush()
112     try:
113       with open(self._tmp_output_file.name) as f:
114         return f.read()
115     except IOError:
116       return ''
117
118
119 class PerfProfiler(profiler.Profiler):
120
121   def __init__(self, browser_backend, platform_backend, output_path, state):
122     super(PerfProfiler, self).__init__(
123         browser_backend, platform_backend, output_path, state)
124     process_output_file_map = self._GetProcessOutputFileMap()
125     self._process_profilers = []
126     if platform_backend.GetOSName() == 'android':
127       android_prebuilt_profiler_helper.GetIfChanged('perfhost')
128       os.chmod(android_prebuilt_profiler_helper.GetHostPath('perfhost'),
129                stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
130       android_prebuilt_profiler_helper.InstallOnDevice(
131           browser_backend.adb, 'perf')
132     for pid, output_file in process_output_file_map.iteritems():
133       if 'zygote' in output_file:
134         continue
135       self._process_profilers.append(
136           _SingleProcessPerfProfiler(
137               pid, output_file, browser_backend, platform_backend))
138
139   @classmethod
140   def name(cls):
141     return 'perf'
142
143   @classmethod
144   def is_supported(cls, browser_type):
145     if sys.platform != 'linux2':
146       return False
147     if browser_type.startswith('cros'):
148       return False
149     return cls._CheckLinuxPerf() or browser_type.startswith('android')
150
151   @classmethod
152   def _CheckLinuxPerf(cls):
153     try:
154       return not subprocess.Popen(['perf', '--version'],
155                                   stderr=subprocess.STDOUT,
156                                   stdout=subprocess.PIPE).wait()
157     except OSError:
158       return False
159
160   @classmethod
161   def CustomizeBrowserOptions(cls, browser_type, options):
162     options.AppendExtraBrowserArgs([
163         '--no-sandbox',
164         '--allow-sandbox-debugging',
165     ])
166
167   def CollectProfile(self):
168     output_files = []
169     for single_process in self._process_profilers:
170       output_files.append(single_process.CollectProfile())
171     return output_files
172
173   @classmethod
174   def GetTopSamples(cls, os_name, file_name, number):
175     """Parses the perf generated profile in |file_name| and returns a
176     {function: period} dict of the |number| hottests functions.
177     """
178     assert os.path.exists(file_name)
179     cmd = 'perf'
180     if os_name == 'android':
181       cmd = 'perfhost'
182     report = subprocess.Popen(
183         [cmd, 'report', '--show-total-period', '-U', '-t', '^', '-i',
184          file_name],
185         stdout=subprocess.PIPE, stderr=open(os.devnull, 'w')).communicate()[0]
186     period_by_function = {}
187     for line in report.split('\n'):
188       if not line or line.startswith('#'):
189         continue
190       fields = line.split('^')
191       if len(fields) != 5:
192         continue
193       period = int(fields[1])
194       function = fields[4].partition(' ')[2]
195       function = re.sub('<.*>', '', function)  # Strip template params.
196       function = re.sub('[(].*[)]', '', function)  # Strip function params.
197       period_by_function[function] = period
198       if len(period_by_function) == number:
199         break
200     return period_by_function