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