255 lines
9.1 KiB
Python
255 lines
9.1 KiB
Python
# Copyright 2014 The Chromium Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
import logging
|
|
import optparse
|
|
import os
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
import py_utils
|
|
|
|
from devil.android import device_temp_file
|
|
from devil.android.perf import perf_control
|
|
|
|
from profile_chrome import ui
|
|
from systrace import trace_result
|
|
from systrace import tracing_agents
|
|
|
|
_CATAPULT_DIR = os.path.join(
|
|
os.path.dirname(os.path.abspath(__file__)), '..', '..')
|
|
sys.path.append(os.path.join(_CATAPULT_DIR, 'telemetry'))
|
|
try:
|
|
# pylint: disable=F0401,no-name-in-module,wrong-import-position
|
|
from telemetry.internal.platform.profiler import android_profiling_helper
|
|
from telemetry.internal.util import binary_manager
|
|
except ImportError:
|
|
android_profiling_helper = None
|
|
binary_manager = None
|
|
|
|
|
|
_PERF_OPTIONS = [
|
|
# Sample across all processes and CPUs to so that the current CPU gets
|
|
# recorded to each sample.
|
|
'--all-cpus',
|
|
# In perf 3.13 --call-graph requires an argument, so use the -g short-hand
|
|
# which does not.
|
|
'-g',
|
|
# Increase priority to avoid dropping samples. Requires root.
|
|
'--realtime', '80',
|
|
# Record raw samples to get CPU information.
|
|
'--raw-samples',
|
|
# Increase sampling frequency for better coverage.
|
|
'--freq', '2000',
|
|
]
|
|
|
|
|
|
class _PerfProfiler(object):
|
|
def __init__(self, device, perf_binary, categories):
|
|
self._device = device
|
|
self._output_file = device_temp_file.DeviceTempFile(
|
|
self._device.adb, prefix='perf_output')
|
|
self._log_file = tempfile.TemporaryFile()
|
|
|
|
# TODO(jbudorick) Look at providing a way to unhandroll this once the
|
|
# adb rewrite has fully landed.
|
|
device_param = (['-s', str(self._device)] if str(self._device) else [])
|
|
cmd = ['adb'] + device_param + \
|
|
['shell', perf_binary, 'record',
|
|
'--output', self._output_file.name] + _PERF_OPTIONS
|
|
if categories:
|
|
cmd += ['--event', ','.join(categories)]
|
|
self._perf_control = perf_control.PerfControl(self._device)
|
|
self._perf_control.SetPerfProfilingMode()
|
|
self._perf_process = subprocess.Popen(cmd,
|
|
stdout=self._log_file,
|
|
stderr=subprocess.STDOUT)
|
|
|
|
def SignalAndWait(self):
|
|
self._device.KillAll('perf', signum=signal.SIGINT)
|
|
self._perf_process.wait()
|
|
self._perf_control.SetDefaultPerfMode()
|
|
|
|
def _FailWithLog(self, msg):
|
|
self._log_file.seek(0)
|
|
log = self._log_file.read()
|
|
raise RuntimeError('%s. Log output:\n%s' % (msg, log))
|
|
|
|
def PullResult(self, output_path):
|
|
if not self._device.FileExists(self._output_file.name):
|
|
self._FailWithLog('Perf recorded no data')
|
|
|
|
perf_profile = os.path.join(output_path,
|
|
os.path.basename(self._output_file.name))
|
|
self._device.PullFile(self._output_file.name, perf_profile)
|
|
if not os.stat(perf_profile).st_size:
|
|
os.remove(perf_profile)
|
|
self._FailWithLog('Perf recorded a zero-sized file')
|
|
|
|
self._log_file.close()
|
|
self._output_file.close()
|
|
return perf_profile
|
|
|
|
|
|
class PerfProfilerAgent(tracing_agents.TracingAgent):
|
|
def __init__(self, device):
|
|
tracing_agents.TracingAgent.__init__(self)
|
|
self._device = device
|
|
self._perf_binary = self._PrepareDevice(device)
|
|
self._perf_instance = None
|
|
self._categories = None
|
|
|
|
def __repr__(self):
|
|
return 'perf profile'
|
|
|
|
@staticmethod
|
|
def IsSupported():
|
|
return bool(android_profiling_helper)
|
|
|
|
@staticmethod
|
|
def _PrepareDevice(device):
|
|
if not 'BUILDTYPE' in os.environ:
|
|
os.environ['BUILDTYPE'] = 'Release'
|
|
if binary_manager.NeedsInit():
|
|
binary_manager.InitDependencyManager(None)
|
|
return android_profiling_helper.PrepareDeviceForPerf(device)
|
|
|
|
@classmethod
|
|
def GetCategories(cls, device):
|
|
perf_binary = cls._PrepareDevice(device)
|
|
# Perf binary returns non-zero exit status on "list" command.
|
|
return device.RunShellCommand([perf_binary, 'list'], check_return=False)
|
|
|
|
@py_utils.Timeout(tracing_agents.START_STOP_TIMEOUT)
|
|
def StartAgentTracing(self, config, timeout=None):
|
|
self._categories = _ComputePerfCategories(config)
|
|
self._perf_instance = _PerfProfiler(self._device,
|
|
self._perf_binary,
|
|
self._categories)
|
|
return True
|
|
|
|
@py_utils.Timeout(tracing_agents.START_STOP_TIMEOUT)
|
|
def StopAgentTracing(self, timeout=None):
|
|
if not self._perf_instance:
|
|
return
|
|
self._perf_instance.SignalAndWait()
|
|
return True
|
|
|
|
@py_utils.Timeout(tracing_agents.GET_RESULTS_TIMEOUT)
|
|
def GetResults(self, timeout=None):
|
|
with open(self._PullTrace(), 'r') as f:
|
|
trace_data = f.read()
|
|
return trace_result.TraceResult('perf', trace_data)
|
|
|
|
@staticmethod
|
|
def _GetInteractivePerfCommand(perfhost_path, perf_profile, symfs_dir,
|
|
required_libs, kallsyms):
|
|
cmd = '%s report -n -i %s --symfs %s --kallsyms %s' % (
|
|
os.path.relpath(perfhost_path, '.'), perf_profile, symfs_dir, kallsyms)
|
|
for lib in required_libs:
|
|
lib = os.path.join(symfs_dir, lib[1:])
|
|
if not os.path.exists(lib):
|
|
continue
|
|
objdump_path = android_profiling_helper.GetToolchainBinaryPath(
|
|
lib, 'objdump')
|
|
if objdump_path:
|
|
cmd += ' --objdump %s' % os.path.relpath(objdump_path, '.')
|
|
break
|
|
return cmd
|
|
|
|
def _PullTrace(self):
|
|
symfs_dir = os.path.join(tempfile.gettempdir(),
|
|
os.path.expandvars('$USER-perf-symfs'))
|
|
if not os.path.exists(symfs_dir):
|
|
os.makedirs(symfs_dir)
|
|
required_libs = set()
|
|
|
|
# Download the recorded perf profile.
|
|
perf_profile = self._perf_instance.PullResult(symfs_dir)
|
|
required_libs = \
|
|
android_profiling_helper.GetRequiredLibrariesForPerfProfile(
|
|
perf_profile)
|
|
if not required_libs:
|
|
logging.warning('No libraries required by perf trace. Most likely there '
|
|
'are no samples in the trace.')
|
|
|
|
# Build a symfs with all the necessary libraries.
|
|
kallsyms = android_profiling_helper.CreateSymFs(self._device,
|
|
symfs_dir,
|
|
required_libs,
|
|
use_symlinks=False)
|
|
perfhost_path = binary_manager.FetchPath(
|
|
android_profiling_helper.GetPerfhostName(), 'linux', 'x86_64')
|
|
|
|
ui.PrintMessage('\nNote: to view the profile in perf, run:')
|
|
ui.PrintMessage(' ' + self._GetInteractivePerfCommand(perfhost_path,
|
|
perf_profile, symfs_dir, required_libs, kallsyms))
|
|
|
|
# Convert the perf profile into JSON.
|
|
perf_script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
'third_party', 'perf_to_tracing.py')
|
|
json_file_name = os.path.basename(perf_profile)
|
|
with open(os.devnull, 'w') as dev_null, \
|
|
open(json_file_name, 'w') as json_file:
|
|
cmd = [perfhost_path, 'script', '-s', perf_script_path, '-i',
|
|
perf_profile, '--symfs', symfs_dir, '--kallsyms', kallsyms]
|
|
if subprocess.call(cmd, stdout=json_file, stderr=dev_null):
|
|
logging.warning('Perf data to JSON conversion failed. The result will '
|
|
'not contain any perf samples. You can still view the '
|
|
'perf data manually as shown above.')
|
|
return None
|
|
|
|
return json_file_name
|
|
|
|
def SupportsExplicitClockSync(self):
|
|
return False
|
|
|
|
def RecordClockSyncMarker(self, sync_id, did_record_sync_marker_callback):
|
|
# pylint: disable=unused-argument
|
|
assert self.SupportsExplicitClockSync(), ('Clock sync marker cannot be '
|
|
'recorded since explicit clock sync is not supported.')
|
|
|
|
def _OptionalValueCallback(default_value):
|
|
def callback(option, _, __, parser): # pylint: disable=unused-argument
|
|
value = default_value
|
|
if parser.rargs and not parser.rargs[0].startswith('-'):
|
|
value = parser.rargs.pop(0)
|
|
setattr(parser.values, option.dest, value)
|
|
return callback
|
|
|
|
|
|
class PerfConfig(tracing_agents.TracingConfig):
|
|
def __init__(self, perf_categories, device):
|
|
tracing_agents.TracingConfig.__init__(self)
|
|
self.perf_categories = perf_categories
|
|
self.device = device
|
|
|
|
|
|
def try_create_agent(config):
|
|
if config.perf_categories:
|
|
return PerfProfilerAgent(config.device)
|
|
return None
|
|
|
|
def add_options(parser):
|
|
options = optparse.OptionGroup(parser, 'Perf profiling options')
|
|
options.add_option('-p', '--perf', help='Capture a perf profile with '
|
|
'the chosen comma-delimited event categories. '
|
|
'Samples CPU cycles by default. Use "list" to see '
|
|
'the available sample types.', action='callback',
|
|
default='', callback=_OptionalValueCallback('cycles'),
|
|
metavar='PERF_CATEGORIES', dest='perf_categories')
|
|
return options
|
|
|
|
def get_config(options):
|
|
return PerfConfig(options.perf_categories, options.device)
|
|
|
|
def _ComputePerfCategories(config):
|
|
if not PerfProfilerAgent.IsSupported():
|
|
return []
|
|
if not config.perf_categories:
|
|
return []
|
|
return config.perf_categories.split(',')
|