# 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. import logging import re import itertools _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(itertools.ifilter(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 is not 0: print "'perf record' failed, ", 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()