# 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(), 'x86_64', 'linux') 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(',')