1# Copyright 2014 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 signal
8import subprocess
9import sys
10import tempfile
11
12from devil.android import device_temp_file
13from devil.android.perf import perf_control
14
15from profile_chrome import controllers
16from profile_chrome import ui
17
18_CATAPULT_DIR = os.path.join(
19    os.path.dirname(os.path.abspath(__file__)), '..', '..')
20sys.path.append(os.path.join(_CATAPULT_DIR, 'telemetry'))
21try:
22  # pylint: disable=F0401
23  from telemetry.internal.platform.profiler import android_profiling_helper
24  from telemetry.internal.util import binary_manager
25except ImportError:
26  android_profiling_helper = None
27  binary_manager = None
28
29
30_PERF_OPTIONS = [
31    # Sample across all processes and CPUs to so that the current CPU gets
32    # recorded to each sample.
33    '--all-cpus',
34    # In perf 3.13 --call-graph requires an argument, so use the -g short-hand
35    # which does not.
36    '-g',
37    # Increase priority to avoid dropping samples. Requires root.
38    '--realtime', '80',
39    # Record raw samples to get CPU information.
40    '--raw-samples',
41    # Increase sampling frequency for better coverage.
42    '--freq', '2000',
43]
44
45
46class _PerfProfiler(object):
47  def __init__(self, device, perf_binary, categories):
48    self._device = device
49    self._output_file = device_temp_file.DeviceTempFile(
50        self._device.adb, prefix='perf_output')
51    self._log_file = tempfile.TemporaryFile()
52
53    # TODO(jbudorick) Look at providing a way to unhandroll this once the
54    #                 adb rewrite has fully landed.
55    device_param = (['-s', str(self._device)] if str(self._device) else [])
56    cmd = ['adb'] + device_param + \
57          ['shell', perf_binary, 'record',
58           '--output', self._output_file.name] + _PERF_OPTIONS
59    if categories:
60      cmd += ['--event', ','.join(categories)]
61    self._perf_control = perf_control.PerfControl(self._device)
62    self._perf_control.SetPerfProfilingMode()
63    self._perf_process = subprocess.Popen(cmd,
64                                          stdout=self._log_file,
65                                          stderr=subprocess.STDOUT)
66
67  def SignalAndWait(self):
68    self._device.KillAll('perf', signum=signal.SIGINT)
69    self._perf_process.wait()
70    self._perf_control.SetDefaultPerfMode()
71
72  def _FailWithLog(self, msg):
73    self._log_file.seek(0)
74    log = self._log_file.read()
75    raise RuntimeError('%s. Log output:\n%s' % (msg, log))
76
77  def PullResult(self, output_path):
78    if not self._device.FileExists(self._output_file.name):
79      self._FailWithLog('Perf recorded no data')
80
81    perf_profile = os.path.join(output_path,
82                                os.path.basename(self._output_file.name))
83    self._device.PullFile(self._output_file.name, perf_profile)
84    if not os.stat(perf_profile).st_size:
85      os.remove(perf_profile)
86      self._FailWithLog('Perf recorded a zero-sized file')
87
88    self._log_file.close()
89    self._output_file.close()
90    return perf_profile
91
92
93class PerfProfilerController(controllers.BaseController):
94  def __init__(self, device, categories):
95    controllers.BaseController.__init__(self)
96    self._device = device
97    self._categories = categories
98    self._perf_binary = self._PrepareDevice(device)
99    self._perf_instance = None
100
101  def __repr__(self):
102    return 'perf profile'
103
104  @staticmethod
105  def IsSupported():
106    return bool(android_profiling_helper)
107
108  @staticmethod
109  def _PrepareDevice(device):
110    if not 'BUILDTYPE' in os.environ:
111      os.environ['BUILDTYPE'] = 'Release'
112    if binary_manager.NeedsInit():
113      binary_manager.InitDependencyManager(None)
114    return android_profiling_helper.PrepareDeviceForPerf(device)
115
116  @classmethod
117  def GetCategories(cls, device):
118    perf_binary = cls._PrepareDevice(device)
119    return device.RunShellCommand('%s list' % perf_binary)
120
121  def StartTracing(self, _):
122    self._perf_instance = _PerfProfiler(self._device,
123                                        self._perf_binary,
124                                        self._categories)
125
126  def StopTracing(self):
127    if not self._perf_instance:
128      return
129    self._perf_instance.SignalAndWait()
130
131  @staticmethod
132  def _GetInteractivePerfCommand(perfhost_path, perf_profile, symfs_dir,
133                                 required_libs, kallsyms):
134    cmd = '%s report -n -i %s --symfs %s --kallsyms %s' % (
135        os.path.relpath(perfhost_path, '.'), perf_profile, symfs_dir, kallsyms)
136    for lib in required_libs:
137      lib = os.path.join(symfs_dir, lib[1:])
138      if not os.path.exists(lib):
139        continue
140      objdump_path = android_profiling_helper.GetToolchainBinaryPath(
141          lib, 'objdump')
142      if objdump_path:
143        cmd += ' --objdump %s' % os.path.relpath(objdump_path, '.')
144        break
145    return cmd
146
147  def PullTrace(self):
148    symfs_dir = os.path.join(tempfile.gettempdir(),
149                             os.path.expandvars('$USER-perf-symfs'))
150    if not os.path.exists(symfs_dir):
151      os.makedirs(symfs_dir)
152    required_libs = set()
153
154    # Download the recorded perf profile.
155    perf_profile = self._perf_instance.PullResult(symfs_dir)
156    required_libs = \
157        android_profiling_helper.GetRequiredLibrariesForPerfProfile(
158            perf_profile)
159    if not required_libs:
160      logging.warning('No libraries required by perf trace. Most likely there '
161                      'are no samples in the trace.')
162
163    # Build a symfs with all the necessary libraries.
164    kallsyms = android_profiling_helper.CreateSymFs(self._device,
165                                                    symfs_dir,
166                                                    required_libs,
167                                                    use_symlinks=False)
168    perfhost_path = binary_manager.FetchPath(
169        android_profiling_helper.GetPerfhostName(), 'x86_64', 'linux')
170
171    ui.PrintMessage('\nNote: to view the profile in perf, run:')
172    ui.PrintMessage('  ' + self._GetInteractivePerfCommand(perfhost_path,
173        perf_profile, symfs_dir, required_libs, kallsyms))
174
175    # Convert the perf profile into JSON.
176    perf_script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
177                                    'third_party', 'perf_to_tracing.py')
178    json_file_name = os.path.basename(perf_profile)
179    with open(os.devnull, 'w') as dev_null, \
180        open(json_file_name, 'w') as json_file:
181      cmd = [perfhost_path, 'script', '-s', perf_script_path, '-i',
182             perf_profile, '--symfs', symfs_dir, '--kallsyms', kallsyms]
183      if subprocess.call(cmd, stdout=json_file, stderr=dev_null):
184        logging.warning('Perf data to JSON conversion failed. The result will '
185                        'not contain any perf samples. You can still view the '
186                        'perf data manually as shown above.')
187        return None
188
189    return json_file_name
190