blob: a9e413ff3865fa2d91ae47986d5012e91d8bf193 [file] [log] [blame]
# Copyright (C) 2012 Google Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the Google name nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import print_function
import logging
import re
import sys
if sys.version_info < (3, 0):
from itertools import ifilter as filter
_log = logging.getLogger(__name__)
class ProfilerFactory(object):
@classmethod
def create_profiler(cls, host, executable_path, output_dir, profiler_name=None, identifier=None):
profilers = cls.profilers_for_platform(host.platform)
if not profilers:
return None
profiler_name = profiler_name or cls.default_profiler_name(host.platform)
profiler_class = next(filter(lambda profiler: profiler.name == profiler_name, profilers), None)
if not profiler_class:
return None
return profilers[0](host, executable_path, output_dir, identifier)
@classmethod
def default_profiler_name(cls, platform):
profilers = cls.profilers_for_platform(platform)
return profilers[0].name if profilers else None
@classmethod
def profilers_for_platform(cls, platform):
# GooglePProf requires TCMalloc/google-perftools, but is available everywhere.
profilers_by_os_name = {
'mac': [IProfiler, Sample, GooglePProf],
'linux': [Perf, GooglePProf],
# Note: freebsd, win32 have no profilers defined yet, thus --profile will be ignored
# by default, but a profiler can be selected with --profiler=PROFILER explicitly.
}
return profilers_by_os_name.get(platform.os_name, [])
class Profiler(object):
# Used by ProfilerFactory to lookup a profiler from the --profiler=NAME option.
name = None
def __init__(self, host, executable_path, output_dir, identifier=None):
self._host = host
self._executable_path = executable_path
self._output_dir = output_dir
self._identifier = "test"
self._host.filesystem.maybe_make_directory(self._output_dir)
def adjusted_environment(self, env):
return env
def attach_to_pid(self, pid):
pass
def wrapper_arguments(self):
return []
def profile_after_exit(self):
pass
class SingleFileOutputProfiler(Profiler):
def __init__(self, host, executable_path, output_dir, output_suffix, identifier=None):
super(SingleFileOutputProfiler, self).__init__(host, executable_path, output_dir, identifier)
# FIXME: Currently all reports are kept as test.*, until we fix that, search up to 1000 names before giving up.
self._output_path = self._host.workspace.find_unused_filename(self._output_dir, self._identifier, output_suffix, search_limit=1000)
assert(self._output_path)
class GooglePProf(SingleFileOutputProfiler):
name = 'pprof'
def __init__(self, host, executable_path, output_dir, identifier=None):
super(GooglePProf, self).__init__(host, executable_path, output_dir, "pprof", identifier)
def adjusted_environment(self, env):
env['CPUPROFILE'] = self._output_path
return env
def _first_ten_lines_of_profile(self, pprof_output):
match = re.search("^Total:[^\n]*\n((?:[^\n]*\n){0,10})", pprof_output, re.MULTILINE)
return match.group(1) if match else None
def _pprof_path(self):
# FIXME: We should have code to find the right google-pprof executable, some Googlers have
# google-pprof installed as "pprof" on their machines for them.
return '/usr/bin/google-pprof'
def profile_after_exit(self):
# google-pprof doesn't check its arguments, so we have to.
if not (self._host.filesystem.exists(self._output_path)):
print("Failed to gather profile, %s does not exist." % self._output_path)
return
pprof_args = [self._pprof_path(), '--text', self._executable_path, self._output_path]
profile_text = self._host.executive.run_command(pprof_args)
print("First 10 lines of pprof --text:")
print(self._first_ten_lines_of_profile(profile_text))
print("http://google-perftools.googlecode.com/svn/trunk/doc/cpuprofile.html documents output.")
print()
print("To interact with the the full profile, including produce graphs:")
print(' '.join([self._pprof_path(), self._executable_path, self._output_path]))
class Perf(SingleFileOutputProfiler):
name = 'perf'
def __init__(self, host, executable_path, output_dir, identifier=None):
super(Perf, self).__init__(host, executable_path, output_dir, "data", identifier)
self._watched_pid = None
self._wait_process = None
def _perf_path(self):
# FIXME: We may need to support finding the perf binary in other locations.
return 'perf'
def attach_to_pid(self, pid):
# The passed-in pid here is the pid of the profiler. A wait process is launched here that
# watches that pid and returns the same return code as the profiler process.
self._watched_pid = pid
self._wait_process = self._host.executive.popen(["wait", "%d" % pid], shell=True)
def wrapper_arguments(self):
return [self._perf_path(), "record", "-g", "--output", self._output_path]
def _first_ten_lines_of_profile(self, perf_output):
output_lines = re.finditer(r"^(?:( [^\n]*?)\s*\n)", perf_output, re.MULTILINE)
prettified_lines = []
for i in range(0, 10):
line = next(output_lines, None)
if not line:
break
prettified_lines.append(line.group(1))
return prettified_lines
def profile_after_exit(self):
# Kill the watched process if it's still running.
if self._wait_process.poll() is None:
self._host.executive.kill_process(self._watched_pid)
# Return early if the process produced non-zero exit code or is still running (if it couldn't be killed).
exit_code = self._wait_process.poll()
if exit_code != 0:
print("'perf record' failed, ", end=' ')
if exit_code:
print("exit code was %i." % exit_code)
else:
print("the profiled process with pid %i is still running." % self._watched_pid)
return
perf_report_args = [self._perf_path(), 'report', '--call-graph', 'none', '--input', self._output_path]
perf_report_output = self._host.executive.run_command(perf_report_args)
print("First 10 lines of 'perf report --call-graph=none':")
print(" ".join(perf_report_args))
print("\n".join(self._first_ten_lines_of_profile(perf_report_output)))
print("To view the full profile, run:")
print(' '.join([self._perf_path(), 'report', '-i', self._output_path]))
print() # An extra line between tests looks nicer.
class Sample(SingleFileOutputProfiler):
name = 'sample'
def __init__(self, host, executable_path, output_dir, identifier=None):
super(Sample, self).__init__(host, executable_path, output_dir, "txt", identifier)
self._profiler_process = None
def attach_to_pid(self, pid):
cmd = ["sample", pid, "-mayDie", "-file", self._output_path]
self._profiler_process = self._host.executive.popen(cmd)
def profile_after_exit(self):
self._profiler_process.wait()
class IProfiler(SingleFileOutputProfiler):
name = 'iprofiler'
def __init__(self, host, executable_path, output_dir, identifier=None):
super(IProfiler, self).__init__(host, executable_path, output_dir, "dtps", identifier)
self._profiler_process = None
def attach_to_pid(self, pid):
# FIXME: iprofiler requires us to pass the directory separately
# from the basename of the file, with no control over the extension.
fs = self._host.filesystem
cmd = ["iprofiler", "-T", "0", "-timeprofiler", "-a", pid,
"-d", fs.dirname(self._output_path), "-o", fs.splitext(fs.basename(self._output_path))[0]]
# FIXME: Consider capturing instead of letting instruments spam to stderr directly.
self._profiler_process = self._host.executive.popen(cmd)
def profile_after_exit(self):
# It seems like a nicer user experiance to wait on the profiler to exit to prevent
# it from spewing to stderr at odd times.
self._profiler_process.wait()