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