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 optparse
7import os
8import py_utils
9import signal
10import subprocess
11import sys
12import tempfile
13
14from devil.android import device_temp_file
15from devil.android.perf import perf_control
16
17from profile_chrome import ui
18from systrace import trace_result
19from systrace import tracing_agents
20
21_CATAPULT_DIR = os.path.join(
22    os.path.dirname(os.path.abspath(__file__)), '..', '..')
23sys.path.append(os.path.join(_CATAPULT_DIR, 'telemetry'))
24try:
25  # pylint: disable=F0401
26  from telemetry.internal.platform.profiler import android_profiling_helper
27  from telemetry.internal.util import binary_manager
28except ImportError:
29  android_profiling_helper = None
30  binary_manager = None
31
32
33_PERF_OPTIONS = [
34    # Sample across all processes and CPUs to so that the current CPU gets
35    # recorded to each sample.
36    '--all-cpus',
37    # In perf 3.13 --call-graph requires an argument, so use the -g short-hand
38    # which does not.
39    '-g',
40    # Increase priority to avoid dropping samples. Requires root.
41    '--realtime', '80',
42    # Record raw samples to get CPU information.
43    '--raw-samples',
44    # Increase sampling frequency for better coverage.
45    '--freq', '2000',
46]
47
48
49class _PerfProfiler(object):
50  def __init__(self, device, perf_binary, categories):
51    self._device = device
52    self._output_file = device_temp_file.DeviceTempFile(
53        self._device.adb, prefix='perf_output')
54    self._log_file = tempfile.TemporaryFile()
55
56    # TODO(jbudorick) Look at providing a way to unhandroll this once the
57    #                 adb rewrite has fully landed.
58    device_param = (['-s', str(self._device)] if str(self._device) else [])
59    cmd = ['adb'] + device_param + \
60          ['shell', perf_binary, 'record',
61           '--output', self._output_file.name] + _PERF_OPTIONS
62    if categories:
63      cmd += ['--event', ','.join(categories)]
64    self._perf_control = perf_control.PerfControl(self._device)
65    self._perf_control.SetPerfProfilingMode()
66    self._perf_process = subprocess.Popen(cmd,
67                                          stdout=self._log_file,
68                                          stderr=subprocess.STDOUT)
69
70  def SignalAndWait(self):
71    self._device.KillAll('perf', signum=signal.SIGINT)
72    self._perf_process.wait()
73    self._perf_control.SetDefaultPerfMode()
74
75  def _FailWithLog(self, msg):
76    self._log_file.seek(0)
77    log = self._log_file.read()
78    raise RuntimeError('%s. Log output:\n%s' % (msg, log))
79
80  def PullResult(self, output_path):
81    if not self._device.FileExists(self._output_file.name):
82      self._FailWithLog('Perf recorded no data')
83
84    perf_profile = os.path.join(output_path,
85                                os.path.basename(self._output_file.name))
86    self._device.PullFile(self._output_file.name, perf_profile)
87    if not os.stat(perf_profile).st_size:
88      os.remove(perf_profile)
89      self._FailWithLog('Perf recorded a zero-sized file')
90
91    self._log_file.close()
92    self._output_file.close()
93    return perf_profile
94
95
96class PerfProfilerAgent(tracing_agents.TracingAgent):
97  def __init__(self, device):
98    tracing_agents.TracingAgent.__init__(self)
99    self._device = device
100    self._perf_binary = self._PrepareDevice(device)
101    self._perf_instance = None
102    self._categories = None
103
104  def __repr__(self):
105    return 'perf profile'
106
107  @staticmethod
108  def IsSupported():
109    return bool(android_profiling_helper)
110
111  @staticmethod
112  def _PrepareDevice(device):
113    if not 'BUILDTYPE' in os.environ:
114      os.environ['BUILDTYPE'] = 'Release'
115    if binary_manager.NeedsInit():
116      binary_manager.InitDependencyManager(None)
117    return android_profiling_helper.PrepareDeviceForPerf(device)
118
119  @classmethod
120  def GetCategories(cls, device):
121    perf_binary = cls._PrepareDevice(device)
122    # Perf binary returns non-zero exit status on "list" command.
123    return device.RunShellCommand([perf_binary, 'list'], check_return=False)
124
125  @py_utils.Timeout(tracing_agents.START_STOP_TIMEOUT)
126  def StartAgentTracing(self, config, timeout=None):
127    self._categories = _ComputePerfCategories(config)
128    self._perf_instance = _PerfProfiler(self._device,
129                                        self._perf_binary,
130                                        self._categories)
131    return True
132
133  @py_utils.Timeout(tracing_agents.START_STOP_TIMEOUT)
134  def StopAgentTracing(self, timeout=None):
135    if not self._perf_instance:
136      return
137    self._perf_instance.SignalAndWait()
138    return True
139
140  @py_utils.Timeout(tracing_agents.GET_RESULTS_TIMEOUT)
141  def GetResults(self, timeout=None):
142    with open(self._PullTrace(), 'r') as f:
143      trace_data = f.read()
144    return trace_result.TraceResult('perf', trace_data)
145
146  @staticmethod
147  def _GetInteractivePerfCommand(perfhost_path, perf_profile, symfs_dir,
148                                 required_libs, kallsyms):
149    cmd = '%s report -n -i %s --symfs %s --kallsyms %s' % (
150        os.path.relpath(perfhost_path, '.'), perf_profile, symfs_dir, kallsyms)
151    for lib in required_libs:
152      lib = os.path.join(symfs_dir, lib[1:])
153      if not os.path.exists(lib):
154        continue
155      objdump_path = android_profiling_helper.GetToolchainBinaryPath(
156          lib, 'objdump')
157      if objdump_path:
158        cmd += ' --objdump %s' % os.path.relpath(objdump_path, '.')
159        break
160    return cmd
161
162  def _PullTrace(self):
163    symfs_dir = os.path.join(tempfile.gettempdir(),
164                             os.path.expandvars('$USER-perf-symfs'))
165    if not os.path.exists(symfs_dir):
166      os.makedirs(symfs_dir)
167    required_libs = set()
168
169    # Download the recorded perf profile.
170    perf_profile = self._perf_instance.PullResult(symfs_dir)
171    required_libs = \
172        android_profiling_helper.GetRequiredLibrariesForPerfProfile(
173            perf_profile)
174    if not required_libs:
175      logging.warning('No libraries required by perf trace. Most likely there '
176                      'are no samples in the trace.')
177
178    # Build a symfs with all the necessary libraries.
179    kallsyms = android_profiling_helper.CreateSymFs(self._device,
180                                                    symfs_dir,
181                                                    required_libs,
182                                                    use_symlinks=False)
183    perfhost_path = binary_manager.FetchPath(
184        android_profiling_helper.GetPerfhostName(), 'x86_64', 'linux')
185
186    ui.PrintMessage('\nNote: to view the profile in perf, run:')
187    ui.PrintMessage('  ' + self._GetInteractivePerfCommand(perfhost_path,
188        perf_profile, symfs_dir, required_libs, kallsyms))
189
190    # Convert the perf profile into JSON.
191    perf_script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
192                                    'third_party', 'perf_to_tracing.py')
193    json_file_name = os.path.basename(perf_profile)
194    with open(os.devnull, 'w') as dev_null, \
195        open(json_file_name, 'w') as json_file:
196      cmd = [perfhost_path, 'script', '-s', perf_script_path, '-i',
197             perf_profile, '--symfs', symfs_dir, '--kallsyms', kallsyms]
198      if subprocess.call(cmd, stdout=json_file, stderr=dev_null):
199        logging.warning('Perf data to JSON conversion failed. The result will '
200                        'not contain any perf samples. You can still view the '
201                        'perf data manually as shown above.')
202        return None
203
204    return json_file_name
205
206  def SupportsExplicitClockSync(self):
207    return False
208
209  def RecordClockSyncMarker(self, sync_id, did_record_sync_marker_callback):
210    # pylint: disable=unused-argument
211    assert self.SupportsExplicitClockSync(), ('Clock sync marker cannot be '
212        'recorded since explicit clock sync is not supported.')
213
214def _OptionalValueCallback(default_value):
215  def callback(option, _, __, parser):  # pylint: disable=unused-argument
216    value = default_value
217    if parser.rargs and not parser.rargs[0].startswith('-'):
218      value = parser.rargs.pop(0)
219    setattr(parser.values, option.dest, value)
220  return callback
221
222
223class PerfConfig(tracing_agents.TracingConfig):
224  def __init__(self, perf_categories, device):
225    tracing_agents.TracingConfig.__init__(self)
226    self.perf_categories = perf_categories
227    self.device = device
228
229
230def try_create_agent(config):
231  if config.perf_categories:
232    return PerfProfilerAgent(config.device)
233  return None
234
235def add_options(parser):
236  options = optparse.OptionGroup(parser, 'Perf profiling options')
237  options.add_option('-p', '--perf', help='Capture a perf profile with '
238                     'the chosen comma-delimited event categories. '
239                     'Samples CPU cycles by default. Use "list" to see '
240                     'the available sample types.', action='callback',
241                     default='', callback=_OptionalValueCallback('cycles'),
242                     metavar='PERF_CATEGORIES', dest='perf_categories')
243  return options
244
245def get_config(options):
246  return PerfConfig(options.perf_categories, options.device)
247
248def _ComputePerfCategories(config):
249  if not PerfProfilerAgent.IsSupported():
250    return []
251  if not config.perf_categories:
252    return []
253  return config.perf_categories.split(',')
254