Home | History | Annotate | Download | only in system
      1 # Copyright (C) 2012 Google Inc. All rights reserved.
      2 #
      3 # Redistribution and use in source and binary forms, with or without
      4 # modification, are permitted provided that the following conditions are
      5 # met:
      6 #
      7 #     * Redistributions of source code must retain the above copyright
      8 # notice, this list of conditions and the following disclaimer.
      9 #     * Redistributions in binary form must reproduce the above
     10 # copyright notice, this list of conditions and the following disclaimer
     11 # in the documentation and/or other materials provided with the
     12 # distribution.
     13 #     * Neither the Google name nor the names of its
     14 # contributors may be used to endorse or promote products derived from
     15 # this software without specific prior written permission.
     16 #
     17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     18 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     19 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     20 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     21 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     28 
     29 import logging
     30 import re
     31 import itertools
     32 
     33 _log = logging.getLogger(__name__)
     34 
     35 
     36 class ProfilerFactory(object):
     37     @classmethod
     38     def create_profiler(cls, host, executable_path, output_dir, profiler_name=None, identifier=None):
     39         profilers = cls.profilers_for_platform(host.platform)
     40         if not profilers:
     41             return None
     42         profiler_name = profiler_name or cls.default_profiler_name(host.platform)
     43         profiler_class = next(itertools.ifilter(lambda profiler: profiler.name == profiler_name, profilers), None)
     44         if not profiler_class:
     45             return None
     46         return profilers[0](host, executable_path, output_dir, identifier)
     47 
     48     @classmethod
     49     def default_profiler_name(cls, platform):
     50         profilers = cls.profilers_for_platform(platform)
     51         return profilers[0].name if profilers else None
     52 
     53     @classmethod
     54     def profilers_for_platform(cls, platform):
     55         # GooglePProf requires TCMalloc/google-perftools, but is available everywhere.
     56         profilers_by_os_name = {
     57             'mac': [IProfiler, Sample, GooglePProf],
     58             'linux': [Perf, GooglePProf],
     59             # Note: freebsd, win32 have no profilers defined yet, thus --profile will be ignored
     60             # by default, but a profiler can be selected with --profiler=PROFILER explicitly.
     61         }
     62         return profilers_by_os_name.get(platform.os_name, [])
     63 
     64 
     65 class Profiler(object):
     66     # Used by ProfilerFactory to lookup a profiler from the --profiler=NAME option.
     67     name = None
     68 
     69     def __init__(self, host, executable_path, output_dir, identifier=None):
     70         self._host = host
     71         self._executable_path = executable_path
     72         self._output_dir = output_dir
     73         self._identifier = "test"
     74         self._host.filesystem.maybe_make_directory(self._output_dir)
     75 
     76     def adjusted_environment(self, env):
     77         return env
     78 
     79     def attach_to_pid(self, pid):
     80         pass
     81 
     82     def profile_after_exit(self):
     83         pass
     84 
     85 
     86 class SingleFileOutputProfiler(Profiler):
     87     def __init__(self, host, executable_path, output_dir, output_suffix, identifier=None):
     88         super(SingleFileOutputProfiler, self).__init__(host, executable_path, output_dir, identifier)
     89         # FIXME: Currently all reports are kept as test.*, until we fix that, search up to 1000 names before giving up.
     90         self._output_path = self._host.workspace.find_unused_filename(self._output_dir, self._identifier, output_suffix, search_limit=1000)
     91         assert(self._output_path)
     92 
     93 
     94 class GooglePProf(SingleFileOutputProfiler):
     95     name = 'pprof'
     96 
     97     def __init__(self, host, executable_path, output_dir, identifier=None):
     98         super(GooglePProf, self).__init__(host, executable_path, output_dir, "pprof", identifier)
     99 
    100     def adjusted_environment(self, env):
    101         env['CPUPROFILE'] = self._output_path
    102         return env
    103 
    104     def _first_ten_lines_of_profile(self, pprof_output):
    105         match = re.search("^Total:[^\n]*\n((?:[^\n]*\n){0,10})", pprof_output, re.MULTILINE)
    106         return match.group(1) if match else None
    107 
    108     def _pprof_path(self):
    109         # FIXME: We should have code to find the right google-pprof executable, some Googlers have
    110         # google-pprof installed as "pprof" on their machines for them.
    111         return '/usr/bin/google-pprof'
    112 
    113     def profile_after_exit(self):
    114         # google-pprof doesn't check its arguments, so we have to.
    115         if not (self._host.filesystem.exists(self._output_path)):
    116             print "Failed to gather profile, %s does not exist." % self._output_path
    117             return
    118 
    119         pprof_args = [self._pprof_path(), '--text', self._executable_path, self._output_path]
    120         profile_text = self._host.executive.run_command(pprof_args)
    121         print "First 10 lines of pprof --text:"
    122         print self._first_ten_lines_of_profile(profile_text)
    123         print "http://google-perftools.googlecode.com/svn/trunk/doc/cpuprofile.html documents output."
    124         print
    125         print "To interact with the the full profile, including produce graphs:"
    126         print ' '.join([self._pprof_path(), self._executable_path, self._output_path])
    127 
    128 
    129 class Perf(SingleFileOutputProfiler):
    130     name = 'perf'
    131 
    132     def __init__(self, host, executable_path, output_dir, identifier=None):
    133         super(Perf, self).__init__(host, executable_path, output_dir, "data", identifier)
    134         self._perf_process = None
    135         self._pid_being_profiled = None
    136 
    137     def _perf_path(self):
    138         # FIXME: We may need to support finding the perf binary in other locations.
    139         return 'perf'
    140 
    141     def attach_to_pid(self, pid):
    142         assert(not self._perf_process and not self._pid_being_profiled)
    143         self._pid_being_profiled = pid
    144         cmd = [self._perf_path(), "record", "--call-graph", "--pid", pid, "--output", self._output_path]
    145         self._perf_process = self._host.executive.popen(cmd)
    146 
    147     def _first_ten_lines_of_profile(self, perf_output):
    148         match = re.search("^#[^\n]*\n((?: [^\n]*\n){1,10})", perf_output, re.MULTILINE)
    149         return match.group(1) if match else None
    150 
    151     def profile_after_exit(self):
    152         # Perf doesn't automatically watch the attached pid for death notifications,
    153         # so we have to do it for it, and then tell it its time to stop sampling. :(
    154         self._host.executive.wait_limited(self._pid_being_profiled, limit_in_seconds=10)
    155         perf_exitcode = self._perf_process.poll()
    156         if perf_exitcode is None:  # This should always be the case, unless perf error'd out early.
    157             self._host.executive.interrupt(self._perf_process.pid)
    158 
    159         perf_exitcode = self._perf_process.wait()
    160         if perf_exitcode not in (0, -2):  # The exit code should always be -2, as we're always interrupting perf.
    161             print "'perf record' failed (exit code: %i), can't process results:" % perf_exitcode
    162             return
    163 
    164         perf_args = [self._perf_path(), 'report', '--call-graph', 'none', '--input', self._output_path]
    165         print "First 10 lines of 'perf report --call-graph=none':"
    166 
    167         print " ".join(perf_args)
    168         perf_output = self._host.executive.run_command(perf_args)
    169         print self._first_ten_lines_of_profile(perf_output)
    170 
    171         print "To view the full profile, run:"
    172         print ' '.join([self._perf_path(), 'report', '-i', self._output_path])
    173         print  # An extra line between tests looks nicer.
    174 
    175 
    176 class Sample(SingleFileOutputProfiler):
    177     name = 'sample'
    178 
    179     def __init__(self, host, executable_path, output_dir, identifier=None):
    180         super(Sample, self).__init__(host, executable_path, output_dir, "txt", identifier)
    181         self._profiler_process = None
    182 
    183     def attach_to_pid(self, pid):
    184         cmd = ["sample", pid, "-mayDie", "-file", self._output_path]
    185         self._profiler_process = self._host.executive.popen(cmd)
    186 
    187     def profile_after_exit(self):
    188         self._profiler_process.wait()
    189 
    190 
    191 class IProfiler(SingleFileOutputProfiler):
    192     name = 'iprofiler'
    193 
    194     def __init__(self, host, executable_path, output_dir, identifier=None):
    195         super(IProfiler, self).__init__(host, executable_path, output_dir, "dtps", identifier)
    196         self._profiler_process = None
    197 
    198     def attach_to_pid(self, pid):
    199         # FIXME: iprofiler requires us to pass the directory separately
    200         # from the basename of the file, with no control over the extension.
    201         fs = self._host.filesystem
    202         cmd = ["iprofiler", "-timeprofiler", "-a", pid,
    203                 "-d", fs.dirname(self._output_path), "-o", fs.splitext(fs.basename(self._output_path))[0]]
    204         # FIXME: Consider capturing instead of letting instruments spam to stderr directly.
    205         self._profiler_process = self._host.executive.popen(cmd)
    206 
    207     def profile_after_exit(self):
    208         # It seems like a nicer user experiance to wait on the profiler to exit to prevent
    209         # it from spewing to stderr at odd times.
    210         self._profiler_process.wait()
    211